The definition of what a sandfly looks for are the rules. Rules are boolean expressions (so they must return a true or false value) written in the expr language (https://expr.medv.io/). A sandfly may have more than one rule in the rules array; they are joined using either "and" or "or" logic depending on the value of the rule_op property. If a sandfly's combined rules evaluate to true, the sandfly will alert on the item found.
Expr Language Definition
Literals | Operators | |||
Comment | /* */ or // | Arithmetic | +, -, *, /, % (modulus), ^ or ** (exponent) | |
Boolean | true, false | Comparison | ==, !=, <, >, <=, >= | |
Integer | 42, 0x2A | Logical | not or !, and or &&, or or || | |
Float | 0.5, .5 | Conditional | ?: (ternary), ?? (nil coalescing) | |
String | "foo", 'bar' | Membership | [], ., ?., in | |
Array | [1, 2, 3] | String | + (concatenation), contains, startsWith, endsWith | |
Map | {a: 1, b: 2, c: 3} | Regex | matches | |
Nil | nil | Range | .. | |
Slice | [:] |
Operator examples taken from actual Sandflies, more samples are provided further below:
log.lastlog.date.created_minutes < 1440 && log.lastlog.username == 'bin'
process.network_ports.operating == true and process.cmdline matches '\\\\btwistd.*--path[[:blank:]]*\\\\.'
Membership Operator
Fields of structs and items of maps can be accessed with . operator or [] operator. Elements of arrays and slices can be accessed with [] operator. Negative indices are supported with -1 being the last element.
The in operator can be used to check if an item is in an array or a map.
file.magic_num.type not in ['elf', 'link']
Optional chaining
The ?. operator can be used to access a field of a struct or an item of a map without checking if the struct or the map is nil. If the struct or the map is nil, the result of the expression is nil.
log?.wtmp?.type_name
Nil coalescing
The ?? operator can be used to return the left-hand side if it is not nil, otherwise the right-hand side is returned.
log?.wtmp?.type_name ?? "user"
Slice Operator
The slice operator [:] can be used to access a slice of an array.
For example, variable array is [1, 2, 3, 4, 5]:
array[1:4] == [2, 3, 4] array[1:-1] == [2, 3, 4] array[:3] == [1, 2, 3] array[3:] == [4, 5] array[:] == array
Built-in Functions
all(array, predicate)
Returns true if all elements satisfies the predicate. If the array is empty, returns true.
all(user.ssh.authorized_keys.file, {.date.accessed_minutes < 120})
any(array, predicate)
Returns true if any elements satisfies the predicate. If the array is empty, returns false.
any(user.group_membership, {# == 'sudo'})
one(array, predicate)
Returns true if exactly one element satisfies the predicate. If the array is empty, returns false.
one(process.stack, {# startsWith 'ptrace'})
none(array, predicate)
Returns true if all elements does not satisfy the predicate. If the array is empty, returns true.
map(array, predicate)
Returns new array by applying the predicate to each element of the array.
filter(array, predicate)
Returns new array by filtering elements of the array by predicate.
count(array, predicate)
Returns the number of elements what satisfies the predicate. Equivalent to:
len(filter(array, predicate))
len(v)
Returns the length of an array, a map or a string.
abs(v)
Returns the absolute value of a number.
int(v)
Returns the integer value of a number or a string.
int("123") == 123
float(v)
Returns the float value of a number or a string.
Predicate
The predicate is an expression that accepts a single argument. To access the argument use the # symbol.
map(0..9, {# / 2})
If items of the array is a struct or a map, it is possible to access fields with omitted # symbol (#.Value becomes .Value).
filter(ports, {len(.Value) > 20})
Braces { } can be omitted:
filter(ports, len(.Value) > 20)
env variable
The env variable is a map of all variables passed to the expression.
Foo.Name == env['Foo'].Name
Expr Rules for Sandfly
An expr rule has access to the result data object(s) that the selected Sandfly forensic engine provides. The user engine will make available the "user" object to the expr rules, the file engine will make available the "file" object, etc.
For example, if you are looking at process recon results in sandfly and see the `{"results": {"process": {"name": "apache"}}}` field in the raw result data, you can write an expr rule that matches that process by referring to the "name" property of the "process" result structure as:
process.name == 'apache'
For result data fields that are arrays which may contain multiple values, the expr language includes functions such as all, any, and none to look for results that do or do not include criteria that you are looking for:
any(process.network_ports.tcp.connections, {.port_local in [1337,4444,31337]})
A different array example that uses the predicate:
any(user.group_membership, {# == 'sudo'})"
Additional Operator Examples
This section contains further rule samples that are used in actual sandflies. Looking at the rules of a similarly functioning sandfly is an effective way to get started.
Comparison: < (less than)
user.password.age_min < 7
Comparison: != (not equals) + Logical: or
user.uid != 65534 or user.gid != 65534
Membership: in + Range: ..
log.wtmp.date.created_minutes in 1..1440
Membership: in + Membership: [ ] (array)
file.username in ['systemd-network','systemd-resolve','systemd-timesync']
Regex: matches + Logical: not
process.name not matches '(apache.*|nginx.*)'
String: startsWith as any part of an array
any(atjob.command, {# startsWith '/dev/shm/'})
rule_op
The rule_op property controls whether all of the rules are combined with "and" or "or" logic, but if you need more complex nesting of logic, individual rules may contain multiple expressions combined with logical operators.
For example, to write a sandfly that matches an old process named "oldproc" or a new process named "newproc", you could use the following rules:
{ "rule_op": "or", "rules": [ "process.name == 'oldproc' && process.date.created_minutes > 1440", "process.name == 'newproc' && process.date.created_minutes < 1440" ] }
Previous Article: | Next Article: | ![]() |