This recipe showcases Quine's ability to identify fraudulent authentication attempts (called password spraying attacks) and issue alerts in real time. Password spraying attacks are designed to avoid drawing attention to themselves. Attempts are spread out over days , weeks, or months. The password spraying recipe ingests password-based authentication logs modeled on the top IAM providers (hosted and on-prem) and generates a graph manifesting the following nodes:
The recipe uses one standing query to link authentication attempts attributed to a user temporally with the “NEXT” edge, while the second standing query creates an alert when four failed attempts followed by a successful login occur, suggesting that a low velocity password spraying attack may have been successfully executed.
In the sample data provided, the attempts are spread out over three months. In a production situation, because it does not impose time windows, Quine could detect an attack spread out over that long a period.
Download to same directory as Quine. Then try it on your own IAM log data.
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 attempts.json file is in the same directory as Quine and issue the following command to begin:
java -jar quine-x.x.x.jar -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, "count")
CALL incrementCounter(client, toLower($that.outcome.result))
CALL incrementCounter(user, "count")
CALL incrementCounter(user, toLower($that.outcome.result))
CALL incrementCounter(asset, "count")
CALL incrementCounter(asset, toLower($that.outcome.result))
// Create timeNode node to provide time bucketing and counting of attempts at the second, minute, hour, day, month and year levels //
CALL reify.time(datetime($that.timestamp), ["year", "month", "day", "hour", "minute", "second"]) YIELD node AS timeNode
CALL incrementCounter(timeNode, "count")
//////////////////////////////
// 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.userSequence = coalesce(user.count,0),
attempt.clientSequence = coalesce(client.count,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)
// To minimize supernodes while still enabling a good datetime hierarchy //
WITH attempt, collect(timeNode) as tNodes
WITH attempt, tNodes[5] as tNode
CREATE (attempt)-[:AT]->(tNode)
standingQueries:
- pattern:
type: Cypher
query: |-
////////////////////////////////////////////////////////
// Subquery to sequence attempts (attempt)-[:NEXT]->(attempt)
////////////////////////////////////////////////////////
MATCH (attempt1)<-[:ORIGINATED]-(user)-[:ORIGINATED]->(attempt2)
RETURN DISTINCT id(attempt2) AS attempt2
mode: DistinctId
outputs:
sequence:
type: CypherQuery
query: |-
MATCH (attempt2)<-[:ORIGINATED]-(user)-[:ORIGINATED]->(attempt1 {userSequence: (attempt2.userSequence-1)})
WHERE id(attempt2) = $that.data.attempt2
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"})
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/#MATCH%20(user)-[:ORIGINATED]-%3E(attempt1%20%7BoutcomeResult:%22FAILURE%22%7D)-[:NEXT]-%3E(attempt2%20%7BoutcomeResult:%22FAILURE%22%7D)-[:NEXT]-%3E(attempt3%20%7BoutcomeResult:%22FAILURE%22%7D)-[:NEXT]-%3E(attempt4%20%7BoutcomeResult:%22FAILURE%22%7D)-[:NEXT]-%3E(attempt5%20%7BoutcomeResult:%22SUCCESS%22%7D)%20WHERE%20id(attempt1)=%22' + toString(strId(attempt1)) + '%22%20RETURN%20DISTINCT%20user%2Cattempt1%2Cattempt2%2Cattempt3%2Cattempt4%2Cattempt5' 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: 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
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