version: 1 title: Password Spraying Detection contributor: https://github.com/thatdot summary: Ingests JSON-formatted IAM-style password authentication log file and creates relationships to detect Password Spraying attacks. description: |- Ingests password-based authentication logs modeled on the top IAM providers (hosted and on-prem) and generates a graph manifesting the following nodes: attempt - transaction representing a password authentication attempt user - user that originated the attempt client - client (computer/mobile/unknown) from which user originated the attempt asn - ASN from which user originated the attempt asset - asset (server, service, etc.) that the user targeted time - time of attempt The first standing query uses the manifested graph structure to generate synthetic edges between sequential attempts for a user: (attempt1)-[:NEXT]->(attempt2)-[:NEXT]->(attempt3) The second standing query looks for four consecutive failed attempts followed by a successful attempt from a user to trigger an alert with a link to the subgraph that represents a potential password spraying attack. Ensure that the attemps.json file is in the same directory as Quine and issue the following command to begin: java -jar {{ quine }} -r password_spraying.yml where x.x.x represents the version (e.g., quine-1.3.2.jar represents Quine version 1.3.2). ingestStreams: - type: FileIngest path: attempts.json format: type: CypherJson query: |- ////////////////////////////// // Set IDs for nodes ////////////////////////////// MATCH (attempt), (client), (asn), (user), (asset) WHERE id(attempt) = idFrom('attempt', $that.eventId, $that.timestamp) AND id(client) = idFrom('client', $that.user.id, $that.client.ipAddress) AND id(asn) = idFrom('asn', $that.client.asn) AND id(user) = idFrom('user', $that.user.id) AND id(asset) = idFrom('asset', $that.transaction.entityId) ////////////////////////////// // Bucketing for counters ////////////////////////////// CALL incrementCounter(client, "clientCount", 1) YIELD count AS clientCount CALL incrementCounter(client, toLower($that.outcome.result), 1) YIELD count AS clientOutcomeCount CALL incrementCounter(user, "userCount", 1) YIELD count AS userCount CALL incrementCounter(user, toLower($that.outcome.result), 1) YIELD count AS userOutcomeCount CALL incrementCounter(asset, "assetCount", 1) YIELD count AS assetCount CALL incrementCounter(asset, toLower($that.outcome.result), 1) YIELD count AS assetOutcomeCount ////////////////////////////// // Client ////////////////////////////// SET client.device = $that.client.device, client.ipAddress = $that.client.ipAddress, client.userAgent = $that.client.userAgent, client: client // Identify last time client seen across clients // SET client.lastseen = coll.max([$that.timestamp, coalesce(client.lastseen, $that.timestamp)]) // Percentage of success vs. failure // SET client.successPercent = ceil(coalesce((client.success*1.0)/(client.count*1.0)*100.0, 0.0)) SET client.failurePercent = floor(coalesce((client.failure*1.0)/(client.count*1.0)*100.0, 0.0)) SET client.state = CASE // Set threshold ratios below for each of three cases // WHEN client.successPercent >= 90 THEN 'good' WHEN client.successPercent >= 75 AND client.successPercent < 90 THEN 'warn' WHEN client.successPercent < 75 THEN 'alarm' ELSE 'alarm' END ////////////////////////////// // User ////////////////////////////// SET user.id = $that.user.id, user.alternateId = $that.user.alternateId, user.displayName = $that.user.displayName, user.type = $that.user.type, user: user // Identify last time user seen across users // SET user.lastseen = coll.max([$that.timestamp, coalesce(user.lastseen, $that.timestamp)]) // Percentage of success vs. failure // SET user.successPercent = ceil(coalesce((user.success*1.0)/(user.count*1.0)*100.0, 0.0)) SET user.failurePercent = floor(coalesce((user.failure*1.0)/(user.count*1.0)*100.0, 0.0)) SET user.state = CASE // Set threshold ratios below for each of three cases // WHEN user.successPercent >= 90 THEN 'good' WHEN user.successPercent >= 75 AND user.successPercent < 90 THEN 'warn' WHEN user.successPercent < 75 THEN 'alarm' ELSE 'alarm' END ////////////////////////////// // Attempts ////////////////////////////// SET attempt.schemaVersion = $that.schemaVersion, attempt.eventId = $that.eventId, attempt.transactionId = $that.transaction.id, attempt.timestamp = $that.timestamp, attempt.entityId = $that.transaction.entityId, attempt.eventType = $that.eventType, attempt.transactionType = $that.transaction.type, attempt.eventCode = $that.eventCode, attempt.displayMessage = $that.displayMessage, attempt.outcomeResult = $that.outcome.result, attempt.logLevel = $that.level, attempt.zone = $that.client.zone, attempt.client = $that.client.ipAddress, attempt.userSequence = coalesce(userCount,0), attempt.clientSequence = coalesce(clientCount,0), attempt: attempt ////////////////////////////// // ASN ////////////////////////////// SET asn.id = $that.client.asn, asn: asn ////////////////////////////// // Asset ////////////////////////////// SET asset.id = $that.transaction.entityId, asset.detail = $that.client.requestUri, asset: asset // Percentage of success vs. failure // SET asset.successPercent = ceil(coalesce((asset.success*1.0)/(asset.count*1.0)*100.0, 0.0)) SET asset.failurePercent = floor(coalesce((asset.failure*1.0)/(asset.count*1.0)*100.0, 0.0)) SET asset.state = CASE // Set threshold ratios below for each of three cases // WHEN asset.successPercent >= 90 THEN 'good' WHEN asset.successPercent >= 75 AND asset.successPercent < 90 THEN 'warn' WHEN asset.successPercent < 75 THEN 'alarm' ELSE 'alarm' END ////////////////////////////// // Create relationship between nodes ////////////////////////////// CREATE (user)-[:ORIGINATED]->(attempt)-[:USING]->(client), (client)<-[:USING]-(attempt)-[:TARGETED]->(asset), (user)-[:ORIGINATED]->(attempt)-[:TARGETED]->(asset), (attempt)-[:OVER]->(asn) standingQueries: - pattern: type: Cypher parallelism: 32 query: |- //////////////////////////////////////////////////////// // Subquery to sequence attempts (attempt)-[:NEXT]->(attempt) //////////////////////////////////////////////////////// MATCH (client2)<-[:USING]-(attempt1)<-[:ORIGINATED]-(user)-[:ORIGINATED]->(attempt2)-[:USING]->(client1) RETURN DISTINCT id(attempt2) AS attempt2 mode: DistinctId outputs: sequence: type: CypherQuery query: |- MATCH (client2)<-[:USING]-(attempt2)<-[:ORIGINATED]-(user)-[:ORIGINATED]->(attempt1 {clientSequence: (attempt2.clientSequence-1)})-[:USING]->(client1) WHERE id(attempt2) = $that.data.attempt2 AND id(client1) = id(client2) CREATE (attempt2)<-[:NEXT]-(attempt1) shouldRetry: false - pattern: type: Cypher query: |- /////////////////////////////////////////////////////////////////////////////////// // Subquery to match 4 consecutive failed attempts followed by a successful attempt /////////////////////////////////////////////////////////////////////////////////// MATCH (attempt1 {outcomeResult:"FAILURE"})-[:NEXT]->(attempt2 {outcomeResult:"FAILURE"})-[:NEXT]->(attempt3 {outcomeResult:"FAILURE"})-[:NEXT]->(attempt4 {outcomeResult:"FAILURE"})-[:NEXT]->(attempt5 {outcomeResult:"SUCCESS"})-[:USING]->(client4) RETURN DISTINCT id(attempt1) AS attempt1 mode: DistinctId outputs: alert: type: CypherQuery query: |- MATCH (attempt1 {outcomeResult:"FAILURE"})-[:NEXT]->(attempt2 {outcomeResult:"FAILURE"})-[:NEXT]->(attempt3 {outcomeResult:"FAILURE"})-[:NEXT]->(attempt4 {outcomeResult:"FAILURE"})-[:NEXT]->(attempt5 {outcomeResult:"SUCCESS"}) WHERE id(attempt1)=$that.data.attempt1 RETURN 'Password Spraying Attack: ' + 'http://localhost:8080/#' + text.urlencode('MATCH (user)-[:ORIGINATED]->(attempt1 {outcomeResult:"FAILURE"})-[:NEXT]->(attempt2 {outcomeResult:"FAILURE"})-[:NEXT]->(attempt3 {outcomeResult:"FAILURE"})-[:NEXT]->(attempt4 {outcomeResult:"FAILURE"})-[:NEXT]->(attempt5 {outcomeResult:"SUCCESS"})-[:USING]->(client) WHERE id(attempt1)="' + toString(strId(attempt1)) + '" RETURN DISTINCT user,attempt1,attempt2,attempt3,attempt4,attempt5,client') AS QuineUILink andThen: type: PrintToStandardOut nodeAppearances: # Attempt Appearance for FAILURE ******************* - predicate: propertyKeys: - outcomeResult knownValues: outcomeResult: "FAILURE" dbLabel: attempt icon: ion-log-in color: "#F44336" size: label: type: Property key: timestamp prefix: "attempt: " # Attempt Appearance for SUCCESS ******************* - predicate: propertyKeys: - outcomeResult knownValues: outcomeResult: "SUCCESS" dbLabel: attempt icon: ion-log-in color: "#32a852" size: label: type: Property key: timestamp prefix: "attempt: " # Client Type (computer/mobile/unknown) Appearance ******************* - predicate: propertyKeys: - device - state knownValues: device: "computer" state: "good" dbLabel: client icon: ion-android-laptop color: "#32a852" size: label: type: Property key: ipAddress prefix: "client: " - predicate: propertyKeys: - device - state knownValues: device: "computer" state: "warn" dbLabel: client icon: ion-android-laptop color: "#d68400" size: label: type: Property key: ipAddress prefix: "client: " - predicate: propertyKeys: - device - state knownValues: device: "computer" state: "alarm" dbLabel: client icon: ion-android-laptop color: "#cf151e" size: label: type: Property key: ipAddress prefix: "client: " - predicate: propertyKeys: - device - state knownValues: device: "mobile" state: "good" dbLabel: client icon: ion-iphone color: "#32a852" size: label: type: Property key: ipAddress prefix: "client: " - predicate: propertyKeys: - device - state knownValues: device: "mobile" state: "warn" dbLabel: client icon: ion-iphone color: "#d68400" size: label: type: Property key: ipAddress prefix: "client: " - predicate: propertyKeys: - device - state knownValues: device: "mobile" state: "alarm" dbLabel: client icon: ion-iphone color: "#cf151e" size: label: type: Property key: ipAddress prefix: "client: " - predicate: propertyKeys: - device - state knownValues: device: "unknown" state: "good" dbLabel: client icon: ion-help color: "#32a852" size: label: type: Property key: ipAddress prefix: "client: " - predicate: propertyKeys: - device - state knownValues: device: "unknown" state: "warn" dbLabel: client icon: ion-help color: "#d68400" size: label: type: Property key: ipAddress prefix: "client: " - predicate: propertyKeys: - device - state knownValues: device: "unknown" state: "alarm" dbLabel: client icon: ion-help color: "#cf151e" size: label: type: Property key: ipAddress prefix: "client: " # ASN Appearance ******************* - predicate: propertyKeys: [] knownValues: {} dbLabel: asn icon: radio-waves color: size: 48 label: type: Property key: id prefix: "asn: " # User (USER/ADMIN/GUEST/CONTRACTOR) Appearance ******************* - predicate: propertyKeys: - type - state knownValues: type: "User" state: "good" dbLabel: user icon: ion-ios-contact-outline color: "#32a852" size: label: type: Property key: id prefix: "user: " - predicate: propertyKeys: - type - state knownValues: type: "User" state: "warn" dbLabel: user icon: ion-ios-contact-outline color: "#d68400" size: label: type: Property key: id prefix: "user: " - predicate: propertyKeys: - type - state knownValues: type: "User" state: "alarm" dbLabel: user icon: ion-ios-contact-outline color: "#cf151e" size: label: type: Property key: id prefix: "user: " - predicate: propertyKeys: - type - state knownValues: type: "Admin" state: "good" dbLabel: user icon: ion-at color: "#32a852" size: label: type: Property key: id prefix: "admin: " - predicate: propertyKeys: - type - state knownValues: type: "Admin" state: "warn" dbLabel: user icon: ion-at color: "#d68400" size: label: type: Property key: id prefix: "admin: " - predicate: propertyKeys: - type - state knownValues: type: "Admin" state: "alarm" dbLabel: user icon: ion-at color: "#cf151e" size: label: type: Property key: id prefix: "admin: " - predicate: propertyKeys: - type - state knownValues: type: "Contractor" state: "good" dbLabel: user icon: ion-hammer color: "#32a852" size: label: type: Property key: id prefix: "contractor: " - predicate: propertyKeys: - type - state knownValues: type: "Contractor" state: "warn" dbLabel: user icon: ion-hammer color: "#d68400" size: label: type: Property key: id prefix: "contractor: " - predicate: propertyKeys: - type - state knownValues: type: "Contractor" state: "alarm" dbLabel: user icon: ion-hammer color: "#cf151e" size: label: type: Property key: id prefix: "contractor: " - predicate: propertyKeys: - type - state knownValues: type: "Guest" state: "good" dbLabel: user icon: ion-social-octocat color: "#32a852" size: label: type: Property key: id prefix: "guest: " - predicate: propertyKeys: - type - state knownValues: type: "Guest" state: "warn" dbLabel: user icon: ion-social-octocat color: "#d68400" size: label: type: Property key: id prefix: "guest: " - predicate: propertyKeys: - type - state knownValues: type: "Guest" state: "alarm" dbLabel: user icon: ion-social-octocat color: "#cf151e" size: label: type: Property key: id prefix: "guest: " # Zone (ONNET/OFFNET/VPN) Appearance ******************* - predicate: propertyKeys: - id knownValues: id: "VPN" dbLabel: zone icon: ion-locked color: size: label: type: Property key: id prefix: "zone: " - predicate: propertyKeys: - id knownValues: id: "ONNET" dbLabel: zone icon: ion-network color: size: label: type: Property key: id prefix: "zone: " - predicate: propertyKeys: - id knownValues: id: "OFFNET" dbLabel: zone icon: ion-android-globe color: size: label: type: Property key: id prefix: "zone: " # Asset Appearance ******************* - predicate: propertyKeys: - state knownValues: state: "good" dbLabel: asset icon: ion-ios-briefcase color: "#32a852" size: label: type: Property key: id prefix: "asset: " - predicate: propertyKeys: - state knownValues: state: "warn" dbLabel: asset icon: ion-ios-briefcase color: "#d68400" size: label: type: Property key: id prefix: "asset: " - predicate: propertyKeys: - state knownValues: state: "alarm" dbLabel: asset icon: ion-ios-briefcase color: "#cf151e" size: label: type: Property key: id prefix: "asset: " # Period (year/month/day/hour/minute/second) Appearance ******************* - predicate: propertyKeys: - period knownValues: period: "second" dbLabel: icon: ion-clock color: size: 22 label: type: Property key: start prefix: "timestamp: " - predicate: propertyKeys: - period knownValues: period: "minute" dbLabel: icon: ion-clock color: size: 24 label: type: Property key: start prefix: "timestamp: " - predicate: propertyKeys: - period knownValues: period: "hour" dbLabel: icon: ion-clock color: size: 32 - predicate: propertyKeys: - period knownValues: period: "day" dbLabel: icon: ion-android-calendar color: size: 24 - predicate: propertyKeys: - period knownValues: period: "month" dbLabel: icon: ion-android-calendar color: size: 32 - predicate: propertyKeys: - period knownValues: period: "year" dbLabel: icon: ion-android-calendar color: size: 40 sampleQueries: # Provide easy access to node types in the Exploration UI - name: Last 10 Nodes query: CALL recentNodes(10) - name: Legend query: MATCH (n) WHERE labels(n) IS NOT NULL WITH labels(n) AS kind, collect(n) AS legend RETURN legend[0] - name: One User Node query: MATCH (user:user) RETURN user LIMIT 1 - name: One Asset Node query: MATCH (asset:asset) RETURN asset LIMIT 1 - name: One ASN Node query: MATCH (asn:asn) RETURN asn LIMIT 1 - name: One Attempt Node query: MATCH (attempt:attempt) RETURN attempt LIMIT 1 - name: One Client Node query: MATCH (client:client) RETURN client LIMIT 1 - name: Table of logins showing spraying attack (run with SHIFT/RETURN) query: MATCH (n) WHERE id(n) = idFrom('user', '8b2d78e3-6d4d-42f4-9221-4d91111fe62d') MATCH (n)-[:ORIGINATED]->(m)-[:USING]->(o) MATCH (n)-[:ORIGINATED]->(m) RETURN m.timestamp AS Timestamp, m.eventId AS Attempt, o.ipAddress AS Source, m.zone AS Zone, m.entityId AS Entity, m.outcomeResult AS Outcome ORDER BY m.timestamp - name: Table of logins showing spraying attack with only attacker (run with SHIFT/RETURN) query: MATCH (n) WHERE id(n) = idFrom('user', '8b2d78e3-6d4d-42f4-9221-4d91111fe62d') MATCH (n)-[:ORIGINATED]->(m)-[:USING]->(o) MATCH (n)-[:ORIGINATED]->(m) WHERE o.ipAddress="217.21.4.61" RETURN m.timestamp AS Timestamp, m.userSequence AS Sequence, m.eventId AS Attempt, o.ipAddress AS Source, m.zone AS Zone, m.entityId AS Entity, m.outcomeResult AS Outcome ORDER BY m.timestamp - name: Find Incoming NEXT Loop query: MATCH (attempt1:attempt)-[:NEXT]->(attempt0:attempt)<-[:NEXT]-(attempt2:attempt) RETURN attempt0,attempt1,attempt2 LIMIT 1 - name: Find Outgoing NEXT Loop query: MATCH (attempt1:attempt)<-[:NEXT]-(attempt0:attempt)-[:NEXT]->(attempt2:attempt) RETURN attempt0,attempt1,attempt2 LIMIT 1 - name: Dirty Attempts (SHIFT-ENTER) query: MATCH (attempt:attempt)-[r]-() WITH attempt, count(r) AS edgeCount WHERE edgeCount>7 RETURN count(attempt) AS OOGIE_NODES quickQueries: - predicate: propertyKeys: [] knownValues: {} quickQuery: name: "[Node] Adjacent Nodes" querySuffix: MATCH (n)--(m) RETURN DISTINCT m queryLanguage: Cypher sort: Node - predicate: propertyKeys: [] knownValues: {} quickQuery: name: "[Node] Refresh" querySuffix: RETURN n queryLanguage: Cypher sort: Node - predicate: propertyKeys: [] knownValues: {} quickQuery: name: "[Text] Local Properties" querySuffix: RETURN id(n), properties(n) queryLanguage: Cypher sort: Text - predicate: propertyKeys: [] knownValues: {} dbLabel: asset quickQuery: name: "[Node] All User Types that Targeted Asset" querySuffix: MATCH (user)-[:ORIGINATED]->(attempt)-[:TARGETED]->(n) RETURN user queryLanguage: Cypher sort: Node edgeLabel: TARGETED_BY - predicate: propertyKeys: [] knownValues: {} dbLabel: asset quickQuery: name: "[Node] Admins that Targeted Asset" querySuffix: MATCH (user)-[:ORIGINATED]->(attempt)-[:TARGETED]->(n) WHERE user.type = "Admin" RETURN user queryLanguage: Cypher sort: Node edgeLabel: TARGETED_BY_ADMIN - predicate: propertyKeys: [] knownValues: {} dbLabel: asset quickQuery: name: "[Node] Contractors that Targeted Asset" querySuffix: MATCH (user)-[:ORIGINATED]->(attempt)-[:TARGETED]->(n) WHERE user.type = "Contractor" RETURN user queryLanguage: Cypher sort: Node edgeLabel: TARGETED_BY_CONTRACTOR - predicate: propertyKeys: [] knownValues: {} dbLabel: asset quickQuery: name: "[Node] Contractors that Failed Authentication for Asset" querySuffix: MATCH (user)-[:ORIGINATED]->(attempt)-[:TARGETED]->(n) WHERE user.type = "Contractor" AND attempt.outcomeResult = "FAILURE" RETURN user queryLanguage: Cypher sort: Node edgeLabel: FAILED_AUTH_BY_CONTRACTOR - predicate: propertyKeys: [] knownValues: {} dbLabel: asset quickQuery: name: "[Node] Guests that Targeted Asset" querySuffix: MATCH (user)-[:ORIGINATED]->(attempt)-[:TARGETED]->(n) WHERE user.type = "Guest" RETURN user queryLanguage: Cypher sort: Node edgeLabel: TARGETED_BY_GUEST - predicate: propertyKeys: [] knownValues: {} dbLabel: asset quickQuery: name: "[Node] Users that Targeted Asset" querySuffix: MATCH (user)-[:ORIGINATED]->(attempt)-[:TARGETED]->(n) WHERE user.type = "User" RETURN user queryLanguage: Cypher sort: Node edgeLabel: TARGETED_BY_USER - predicate: propertyKeys: [] knownValues: {} dbLabel: attempt quickQuery: name: "[Node] Previous Attempt" querySuffix: MATCH (n)<-[:NEXT]-(attempt) RETURN attempt queryLanguage: Cypher sort: Node - predicate: propertyKeys: [] knownValues: {} dbLabel: attempt quickQuery: name: "[Node] Next Attempt" querySuffix: MATCH (n)-[:NEXT]->(attempt) RETURN attempt queryLanguage: Cypher sort: Node - predicate: propertyKeys: [] knownValues: {} dbLabel: attempt quickQuery: name: "[Node] Show Client and ASN" querySuffix: MATCH (n)-[:USING]->(m) MATCH (n)-[:OVER]->(o) RETURN DISTINCT n,m,o queryLanguage: Cypher sort: Node - predicate: propertyKeys: [] knownValues: {} dbLabel: client quickQuery: name: "[Node] Targeted Assets" querySuffix: MATCH (n)<-[:USING]-(attempt)-[:TARGETED]->(asset:asset) RETURN asset queryLanguage: Cypher sort: Node edgeLabel: TARGETED - predicate: propertyKeys: [] knownValues: {} dbLabel: client quickQuery: name: "[Text] Authentication attempts in chronological order" querySuffix: MATCH (n)<-[:USING]->(m) RETURN m.timestamp AS Timestamp, m.eventId AS Attempt, n.ipAddress AS Source, m.zone AS Zone, m.entityId AS Entity, m.outcomeResult AS Outcome ORDER BY m.timestamp queryLanguage: Cypher sort: Text - predicate: propertyKeys: [] knownValues: {} dbLabel: user quickQuery: name: "[Node] Failed Password Authentication Attempts" querySuffix: MATCH (n)-[:ORIGINATED]->(attempt {outcomeResult:"FAILURE"}) RETURN attempt queryLanguage: Cypher sort: Node - predicate: propertyKeys: [] knownValues: {} dbLabel: user quickQuery: name: "[Node] Targeted Assets" querySuffix: MATCH (n)-[:ORIGINATED]->(attempt)-[:TARGETED]->(asset:asset) RETURN asset queryLanguage: Cypher sort: Node edgeLabel: TARGETED - predicate: propertyKeys: [] knownValues: {} dbLabel: user quickQuery: name: "[Text] Authentication attempts in chronological order" querySuffix: MATCH (n)-[:ORIGINATED]->(m)-[:USING]->(o) MATCH (n)-[:ORIGINATED]->(m) RETURN m.timestamp AS Timestamp, m.eventId AS Attempt, o.ipAddress AS Source, m.zone AS Zone, m.entityId AS Entity, m.outcomeResult AS Outcome ORDER BY m.timestamp queryLanguage: Cypher sort: Text - predicate: propertyKeys: [] knownValues: {} dbLabel: user quickQuery: name: "[Node] Attempts Timeline" querySuffix: MATCH (n)-[:ORIGINATED]->(event)-[:NEXT]->(m) RETURN DISTINCT m queryLanguage: Cypher sort: Node