SET n.owned = "LLMNR", n.wave = 1
RETURN '[email protected]','1','LLMNR'
---
{
"results": [
{
"columns": [
"'[email protected]'",
"'1'",
"'LLMNR'"
],
"data": [
{
"row": [
"[email protected]",
"1",
"LLMNR"
],
"meta": [
null,
null,
null
]
}
],
"stats": {
"contains_updates": true,
"nodes_created": 0,
"nodes_deleted": 0,
"properties_set": 2,
"relationships_created": 0,
"relationship_deleted": 0,
"labels_added": 0,
"labels_removed": 0,
"indexes_added": 0,
"indexes_removed": 0,
"constraints_added": 0,
"constraints_removed": 0
}
}
],
"errors": [
]
}
Note that “properties_set” is 2. So this query sets the properties that I want set, but what happens if I run it again? It’ll overwrite properties that already exist. I don’t want that. I could fix this by adding an AND
to filter out the nodes that I’d like to skip:
Great, this sets the two properties I want and it doesn’t overwrite existing ones. Notice that “properties_set” was 0 because this node already had the custom properties set.
But what happens if we don’t find the node? For example, if ‘name’ was misspelled or if a node with ‘name’ was never added?
It’s the exact same response (except for the name being misspelled). There’s no way to differentiate between a “node not found” error and a “properties already exists” scenario. That’s not helpful to a user.
I want to conditionally set properties, but I need a way to discern between the three scenarios. Here’s where we get creative.
Cypher doesn’t support full-blown conditional statements. We can’t directly express something like if a.x > 0, then SET a.y=1, else SET a.y=0, a.z=1
. We can get close with the CASE
statement, which acts a lot like it does in the SQL world (example from here):
The problem is that CASE
is limited to returning a literal expression. We can’t put clauses like SET
or MATCH
inside a THEN
. That makes it difficult to do something like setting a property conditionally.
What we can do though is nest a CASE
statement inside a FOREACH
clause (FOREACH documentation here). FOREACH
will loop through a list or a path and pass each matching element to a clause (like SET
):
This is nifty, but we only want to perform clauses under certain conditions, like when a property does or doesn’t exist.
This is the hack: we’ll use a CASE
statement to either return an empty list or a list with one element. That result is passed to a FOREACH
loop. If the result is an empty list, then the FOREACH
clause won’t execute (because there’s nothing to iterate over.) If the result is a list with one element, then the FOREACH
clause will execute once (because it iterated over one element.)
Let’s see it in context. This is how I ultimately ended up crafting the Cypher query (and here it is in bh-owned.rb):
A lot going on here, but let’s take it line-by-line:
1. MATCH (n) WHERE (n.name = '[email protected]')
: Match nodes where the ‘name’ property is equal to ‘[email protected]’, store node in variable named ‘n’.
2. FOREACH (ignoreMe in CASE
: FOREACH will loop through a list or path. We’re not passing in a list or path directly though – we’re passing in the result of a CASE statement. The result will either be a list with one element (if true) or an empty list (if false). As the name suggests, we can ignore the ‘ignoreMe’ variable because we’ll never use it, we just need it there to satisfy syntax.
3. WHEN exists(n.wave) THEN [1]
: Here’s where we can specify the “if” conditional. If the ‘wave’ property already exists for our node, then pass a list with one element ([1]
) back to the FOREACH clause. This means we want to execute the clause (| SET n.wave=n.wave
) once. This is our “true” condition.
4. ELSE [] END | SET n.wave=n.wave)
: If the ‘wave’ property doesn’t exist for our node, then pass an empty list ([]
) to the FOREACH clause. This is our “false” condition. With an empty list, no iteration happens. That means the clause | SET n.wave=n.wave
won’t execute. However, if the list is not empty ([1]
) then we execute the clause once and set ‘n.wave’ equal to itself (I’ll explain why later, just remember that we only set one property.)
5. FOREACH (ignoreMe in CASE
: Again, we’ll loop through the result of a CASE statement (which will return an empty list or list with one element).
6. WHEN not(exists(n.wave)) THEN [1]
: If the ‘wave’ property doesn’t exist for our node, then we’ll return a list with one element and ensure we execute the clause.
7. ELSE [] END | SET n.owned = 'LLMNR', n.wave = 1)
: If the ‘wave’ property does exist, then return an empty list ([]
) and skip execution of the clause. However if we return a one-element list ([1]
) then the ‘wave’ property doesn’t exist and we’ll execute | SET n.owned = 'LLMNR', n.wave = 1
. In this example, we set two properties.
8. RETURN '[email protected]','1','LLMNR'
: The primary goal of conditionally setting properties is already done by this point. We RETURN the inputs (name, method, wave) so we can make parsing the API response easier.
With this query, we can now differentiate between the three different potential outcomes by inspecting “properties_set” in the API response:
Here’s what each scenario looks like when using bh-owned.rb
. Take note of the “properties_set” key in the JSON responses.
Scenario 1 – Misspelled node name. “properties_set” is 0. Don’t run the follow-up query to find the spread of compromise for a node:
Scenario 2 – Node found and ‘wave’ property already exists. “properties_set” is 1:
Scenario 3 – Node found and ‘wave’ property doesn’t exist. “properties_set” is 2:
And that’s one way to do conditional statements with Cypher :D
(Sorry. I couldn’t write about hacking, Cypher, and Neo4j without making at least one Matrix reference. #CypherIsReallyTheOne)
Thank you for reading!
porterhau5 BLOG
BloodHound Neo4j Cypher