docs: restructure Attack Paths docs and add query developer guide (#12144)

This commit is contained in:
Daniel Barranquero
2026-07-28 18:28:17 +02:00
committed by GitHub
parent 06ea61ffbf
commit 1218b0920f
9 changed files with 573 additions and 136 deletions
+42 -35
View File
@@ -9,7 +9,7 @@ description: >
license: Apache-2.0
metadata:
author: prowler-cloud
version: "3.0"
version: "3.1"
scope: [root, api]
auto_invoke:
- "Creating Attack Paths queries"
@@ -22,6 +22,8 @@ allowed-tools: Read, Edit, Write, Glob, Grep, Bash, WebFetch, Task
Attack Paths queries are read-only openCypher queries over a Cartography-ingested cloud graph that detect privilege escalation chains, network exposure, and other graph-shaped security risks. Queries are written in openCypher Version 9 so they run on both Neo4j and Amazon Neptune sinks.
This skill is the concise, action-oriented reference for building queries. For the complete human-readable reference (graph model, list-typed and JSON-encoded properties, compatibility, and worked examples), see `docs/developer-guide/attack-paths-queries.mdx`.
---
## Two query audiences
@@ -126,12 +128,16 @@ AWS_{QUERY_NAME} = AttackPathsQueryDefinition(
OR act.value = '*'
WITH DISTINCT aws, principal, stmt, path_principal
// Target resources attached to the same principal (sub-patterns below)
MATCH path_target = (aws)--(target_policy:AWSPolicy)--(principal)
WHERE target_policy.arn CONTAINS $provider_uid
// Pre-aggregate the statement's resource values (see "Avoiding cartesian products")
MATCH (stmt)-[:HAS_RESOURCE]->(res:AWSPolicyStatementResourceItem)
WHERE res.value = '*'
OR target_policy.arn CONTAINS res.value
WITH aws, principal, path_principal, collect(DISTINCT res.value) AS res_values
WITH aws, principal, path_principal, res_values, ('*' IN res_values) AS res_wildcard
// Target policies attached to the principal, matched once against the resource list
MATCH path_target = (aws)--(target_policy:AWSPolicy)--(principal)
WITH path_principal, path_target, res_values, res_wildcard, target_policy.arn AS parn
WHERE parn CONTAINS $provider_uid
AND (res_wildcard OR size([rv IN res_values WHERE parn CONTAINS rv]) > 0)
WITH DISTINCT path_principal, path_target
WITH collect(path_principal) + collect(path_target) AS paths
@@ -160,6 +166,33 @@ Key points:
---
## Avoiding cartesian products
Matching a target set (`AWSRole`, `AWSUser`, `AWSGroup`) and then filtering each target against a statement's `HAS_RESOURCE` items in a separate, unconnected `MATCH` builds a cartesian product: every target is paired with every resource item before the filter runs. On accounts with many principals this errors or times out. Pre-aggregate the resource values into a list, then match each target once:
```cypher
// Pre-aggregate the statement's resource values into a list
MATCH (stmt)-[:HAS_RESOURCE]->(res:AWSPolicyStatementResourceItem)
WITH aws, path_principal, collect(DISTINCT res.value) AS res_values
WITH aws, path_principal, res_values, ('*' IN res_values) AS res_wildcard
// Match each target once; bind name/arn to locals so the predicate reads them once
MATCH path_target = (aws)--(target_role:AWSRole)
WITH path_principal, path_target, res_values, res_wildcard,
target_role.name AS rname, target_role.arn AS rarn
WHERE res_wildcard
OR size([rv IN res_values WHERE rv CONTAINS rname OR rarn CONTAINS rv]) > 0
```
- Aggregate resources before matching targets; cost becomes `targets + resources`, not `targets × resources`. This is a pure rewrite, the result set is identical.
- `('*' IN res_values)` short-circuits the wildcard grant so the list scan runs only when needed.
- Bind `target.name` / `target.arn` to locals so the list comprehension reads them once per target, not once per resource value.
- `size([...]) > 0` is the Neptune-compatible form of `any()` (see "openCypher compatibility").
- Two-statement queries aggregate each statement's resources into its own list (`res_values`, `res2_values`) and combine the two `size([...]) > 0` checks with `AND`.
- Targets already constrained by a relationship (`STS_ASSUMEROLE_ALLOW`, `TRUSTS_AWS_PRINCIPAL`) need no aggregation: the relationship already bounds the set.
---
## Privilege escalation sub-patterns
Four `path_target` shapes cover the common attack types. Each shares the canonical template's `path_principal`, deduplication tail, and `RETURN`; only the `path_target` MATCH and its resource predicate differ.
@@ -273,17 +306,9 @@ The literal-action list is case-folded with `toLower(act.value)` because IAM aut
### Example - resource ARN match
Find statements whose resource can target a specific role:
To find statements whose resource can target a specific role, pre-aggregate the resource values and test the target against the list once (see "Avoiding cartesian products"). Do not pair the target set with the `HAS_RESOURCE` items in a separate `MATCH`; that builds a cartesian product.
```cypher
MATCH path_target = (aws)--(target_role:AWSRole)
MATCH (stmt)-[:HAS_RESOURCE]->(res:AWSPolicyStatementResourceItem)
WHERE res.value = '*'
OR res.value CONTAINS target_role.name
OR target_role.arn CONTAINS res.value
```
Three predicates cover the cases: full wildcard (`*`), pattern containing the role name (`arn:aws:iam::*:role/admin*`), and pattern that is a prefix or component of the actual ARN.
Three predicates cover the resource cases: full wildcard (`*`), a pattern containing the target name (`arn:aws:iam::*:role/admin*`), and a pattern that is a prefix or component of the actual ARN.
### Catalog of list properties
@@ -293,25 +318,7 @@ The provider catalog lives in `api/src/backend/tasks/jobs/attack_paths/provider_
## Common openCypher patterns
### Match account and principal
```cypher
MATCH path_principal = (aws:AWSAccount {id: $provider_uid})--(principal:AWSPrincipal)-[:POLICY]->(policy:AWSPolicy)-[:STATEMENT]->(stmt:AWSPolicyStatement {effect: 'Allow'})
```
The `(aws)--(principal)` hop stays anonymous; the `POLICY` and `STATEMENT` hops are typed.
### Roles trusting a service
```cypher
MATCH path_target = (aws)--(target_role:AWSRole)-[:TRUSTS_AWS_PRINCIPAL]-(:AWSPrincipal {arn: 'ec2.amazonaws.com'})
```
### Roles a principal can assume
```cypher
MATCH path_target = (aws)--(target_role:AWSRole)-[:STS_ASSUMEROLE_ALLOW]-(principal)
```
The account/principal match and the service-trust and assume-role target shapes appear in the template and sub-patterns above. Additional reusable patterns:
### JSON-encoded properties