Search filters
Selecting nodes by kind
is(instance)
In order to select nodes by a specific type, the search syntax supports the is(<kind>)
function. The term is(instance)
would select the EC2 instance above, but also all other instances, e.g. Google Cloud instances, while the term is(aws_ec2_instance)
would select only EC2 instances from AWS.
You can get a list of all available kinds via the kind
CLI command.
Selecting nodes by ID
id(id2345)
Fix Security will add a synthetic id
property to each node that is globally unique, no matter which cloud provider you are using. Nodes can be selected by their internal ID via the id(xyz)
function.
Selecting nodes by predicate
name=~"my-.*" and age>30d
In order to filter for specific attributes of a node, it is possible to define predicates. A predicate always has the syntax <property_path> <operation> <value>
(e.g., answer != 42
).
Property Path
The property_path
is the path to the property in the JSON structure.
A nested property can be accessed by defining its dot-delimited path in the object structure. For example, some.nested.prop
has the value 42
in the following JSON structure:
{ "some": { "nested": { "prop": 42 } } }
If the path contains special characters (e.g., .
) or is otherwise noncompliant, the affected portion(s) of the path should be surrounded by backticks.
For example, the path some.`non.compliant.key`.prop
is used to access the property with value 42
in the below JSON structure: some.`non.compliant.key`.prop
.
{ "some": { "non.compliant.key": { "prop": 42 } } }
Arrays
If a property is an array, specific elements can be accessed using index notation.
In the below JSON, the path some.nested[0]
references first element of the array with path some.nested
, and some.nested[2]
points to the third (and last) element:
{ "some": { "nested": [1, 2, 3] } }
In arrays of objects, object elements can also be accessed using index notation.
The path some.nested[0].prop
has a value of 1
here:
{ "some": { "nested": [{ "prop": 1 }, { "prop": 2 }, { "prop": 3 }] } }
It is also possible to define filters based on object element properties. For example, the path some.nested[0].prop > 2
matches if the first array element has a prop
value greater than 2.
We can also check if any array element has a prop
value greater than 2. To do so, we would use the path some.nested[*].prop > 2
. The *
wildcard symbol indicates that all elements of the array should be checked, and that any single element satisfying the defined condition is sufficient for a match.
Take the following example:
{
"some": {
"nested": [
{ "prop": 1 },
{ "prop": 2 },
{ "prop": 3 }
]
}
}
some.nested[*].prop==2
matches because the second element has aprop
value of 2.some.nested[*].prop>1
matches because one or more elements has aprop
value greater than 1.some.nested[*].prop>3
does not match because there is no element with aprop
value greater than 3.
Operation
The operation
is one of the following:
Operation | Description |
---|---|
= or == | Property is equal to the provided value. |
!= | Property is not equal to the provided value. |
<= | Property is less than or equal to the provided value. |
>= | Property is greater than or equal to the provided value. |
> | Property is greater than the provided value. |
< | Property is less than the provided value. |
~ or =~ | Property conforms to the given regexp. Only applicable to strings. |
!~ | Property is not conform to the given regexp. Only applicable to strings. |
in | Property is one of the following values. The value has to be an array. |
not in | Property is not one of the following values. The value has to be an array. |
Value
The value
can be any JSON literal or JSON conform value.
A JSON conform value is:
-
string
Examples:
"hello world"
,"test"
noteThe query parser is gracious with quotes. If there are no white space and no special characters, it is possible to omit quotes. In case you see parse errors, try adding quotes to your strings.
-
number
(integers and floating-point numbers)Examples:
23
,12.123
The model itself clearly defines if a number is
int32
,int64
,float
ordouble
. From the query point of view, all numbers are treated the same way. -
boolean
Examples:
true
,false
-
array
Example:
[1, true, "test"]
-
object
Example:
{"a": 1, "b": 2}
-
null
This can be useful to query for properties that are unset or do not exist.
Arrays
If the filtered property is an array, it is also possible to define a criteria based on elements of the array using one of the operator modifier: all
, any
or none
in front of the operation.
Let us assume following document: {"reported": { "test": [1, 2, 3, 4]}}
, we could define a query like
test all >= 1
all elements in the test array need to be greater or equals than 1test any > 2
at least one element needs to be greater than 2test none > 100
no element is greater than 100
In case no operator modifier is defined, the default is any
.
name == "sunset"
name == sunset
instance_cores > 2
name =~ "sun.*"
name in ["sunset", "sunrise"]
Combining selections
All listed selections can be combined with and
and or
clauses. In order to define precedence, enclose terms with parentheses. If no brackets are defined, the terms are evaluated from left to right.
It is possible to negate a simple predicate or more complex term with not
.
name in [sunset, sunrise]
is(instance) and name == sunrise
is(aws_ec2_instance) and (instance_type == "m5a.large" or instance_cores>2)
is(instance) and /ancestors.account.reported.name=~engineering
is(instance) and not /ancestors.account.reported.name==engineering
Nested predicates
There are entities that contain complex, deeply nested structures. As explained above, you can reference a nested property using its path and access specific array elements using index notation. But what if we don't know the position of the target element in the array, or want to match multiple properties of an array element?
If you need to combine several predicates for nested array elements, it is not sufficient to combine multiple predicates using [*]
since there is no way to define the same position for all predicates. However, this is achievable using context notation.
{
"id": "sg-123",
"tags": {},
"name": "redirus-master",
"description": "redirus Master Security Group",
"group_ip_permissions": [
{
"from_port": 80,
"to_port": 80,
"ip_protocol": "tcp",
"ip_ranges": [
{ "cidr_ip": "0.0.0.0/0" }
]
},
{
"from_port": 22,
"to_port": 22,
"ip_protocol": "tcp",
"ip_ranges": [
{ "cidr_ip": "3.3.175.2/32" },
{ "cidr_ip": "4.4.119.97/32", "description": "Office access" }
]
}
]
}
We want to select resources that allow SSH access (port 22) from everywhere on the internet by matching:
from_port >= 22
,to_port <= 22
, and- at least one
cidr
that contains0.0.0.0
.
group_ip_permissions[*].from_port>=22 and group_ip_permissions[*].to_port<=22 and group_ip_permissions[*].ip_ranges[*].cidr_ip~"0.0.0.0"
A simple combination of the same properties is not sufficient. It would eventually match more items than expected, since every filter criteria has to be fulfilled in at least one of the group permissions. What we actually wanted to express: all the criteria should match in any of the nested complex objects.
group_ip_permissions[*].{from_port>=22, to_port<=22, ip_ranges[*].cidr_ip~"0.0.0.0"}
Fix Security supports context notation, which allows us to reference a single position in all predicates. Context notation combines several predicates into a single term which is matched to a nested structure. The path to this structure is defined by a path which can contain arrays that use the wildcard index [*]
.
The syntax of context notation is:
path.to[*].nested.structure[*].{predicate1, predicate2, ....}
It is also possible to nest a context within another:
path[*].{predicate1, nested[*].{predicate2, ...}, ...}
Here is another example of context notation using the example JSON document above:
group_ip_permissions[*].{from_port>=22, to_port<=22, ip_ranges[*].{cidr_ip~"0.0.0.0" or description~"Office access"}}
With context notation you can match deeply nested objects by describing the structure and the predicates that should be matched in a single term. The nested context notation is not limited to arrays, but they are useful in combination with the wildcard index [*]
.
Property sections
Every resource in Fix Security is represented by a JSON document with the following structure:
{
"id": "abc",
"reported": {
"kind": "node",
"name": "some-name",
"ctime": "2022-12-16T09:12:12Z",
"tags": {
...
}
...
},
"ancestors": {
"cloud": { "reported": { "name": "aws", ... } },
"account": { "reported": { "name": "abc", ... }},
...
}
"metadata": { ... },
"desired": { ... },
}
Values coming from the cloud provider are available in the reported
section, and the CLI interprets property paths relative to the reported
section by default. Thus, the path to property reported.name
can simply be written as name
. To target properties not in the reported
section, prefix the property path with a slash (/
) for it to be interpreted as an absolute path:
cpu_count > 3 and /ancestors.account.reported.name=="123"
Selecting nodes by property key
Nodes can be selected based on the fact that a specific property exists, no matter which value this property has. The has_key(parent, property)
function will select all nodes where the property parent
has property property
.
We want to find all volumes that are tagged with the tag owner
.
is(volume) and has_key(tags, owner)
(Note: We could have used the search search is(volume) and tags.owner!=null
instead. This would select all volumes with an owner tag, where the owner tag is not null. The has_key
function ignores the value of the property and only returns if the property exists.)
Selecting nodes by IPv4 address
For resources with IPv4 addresses, it is possible to select nodes in a specific subnet range. The function in_subnet(ipv4_property, cidr)
will select all nodes where the property ipv4_property
is in the subnet range cidr
.
We want to find all load balancer, where the public IPv4 address is in the subnet range 167.123.0.0/16
.
is(load_balancer) and in_subnet(public_ip_address, "167.123.0.0/16")