Skip to main content

prowler-attack-paths-query

Creates Prowler Attack Paths openCypher queries using the Cartography schema as the source of truth for node labels, properties, and relationships. Covers Prowler-specific additions (Internet node, ProwlerFinding, internal isolation labels), $provider_uid scoping, and list-property item nodes with typed `HAS_*` edges that run efficiently on both Neo4j and Amazon Neptune sinks. Trigger: When creating or updating Attack Paths queries.

Aller à l'installation

Informations de source

Dépôt
prowler-cloud/prowler
Dernière activité de la source
28 juillet 2026 à 16:28
Langue détectée de SKILL.md
anglais
Étoiles
14 842
Forks
2 391

Options d'installation

Le prompt qui vérifie d'abord la source est sélectionné par défaut. Vous pouvez passer à une commande directe ou télécharger une copie locale.

Vérifiez les fichiers source

Lisez SKILL.md et les fichiers associés affichés par SkillsMP avant de décider de l'installer.

Affichage de SKILL.md

SKILL.md
Instructions source · Aperçu en lecture seule
name
prowler-attack-paths-query
description
Creates Prowler Attack Paths openCypher queries using the Cartography schema as the source of truth for node labels, properties, and relationships. Covers Prowler-specific additions (Internet node, ProwlerFinding, internal isolation labels), $provider_uid scoping, and list-property item nodes with typed `HAS_*` edges that run efficiently on both Neo4j and Amazon Neptune sinks. Trigger: When creating or updating Attack Paths queries.
license
Apache-2.0
metadata
{"author":"prowler-cloud","version":"3.1","scope":["root","api"],"auto_invoke":["Creating Attack Paths queries","Updating existing Attack Paths queries","Adding privilege escalation detection queries"]}
allowed-tools
Read, Edit, Write, Glob, Grep, Bash, WebFetch, Task
## Overview 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 | | Predefined queries | Custom queries | | ------------------ | ----------------------------------------------------------- | --------------------------------------------------------------------- | | Where they live | `api/src/backend/api/attack_paths/queries/{provider}.py` | User-supplied via the custom query API endpoint | | Provider isolation | `AWSAccount {id: $provider_uid}` anchor + path connectivity | Automatic `_Provider_{uuid}` label injection by `cypher_sanitizer.py` | | What to write | Chain every MATCH from the `aws` variable | Plain Cypher, no isolation boilerplate | | Internal labels | Never use | Never use (system-injected) | **Predefined queries**: every node must be reachable from the `AWSAccount` root via graph traversal. That is the isolation boundary. **Custom queries**: write natural Cypher. The runner injects a `_Provider_{uuid}` label into every node pattern, and a post-query filter handles edge cases. --- ## Input sources Two sources for new queries: 1. **pathfinding.cloud ID** (e.g. `ECS-001`, `GLUE-001`), the Datadog research catalogue. The aggregated `paths.json` is too large for WebFetch: ```bash # Fetch a single path by ID curl -s https://raw.githubusercontent.com/DataDog/pathfinding.cloud/main/docs/paths.json \ | jq '.[] | select(.id == "ecs-002")' # List all path IDs and names curl -s https://raw.githubusercontent.com/DataDog/pathfinding.cloud/main/docs/paths.json \ | jq -r '.[] | "\(.id): \(.name)"' # Filter by service prefix curl -s https://raw.githubusercontent.com/DataDog/pathfinding.cloud/main/docs/paths.json \ | jq -r '.[] | select(.id | startswith("ecs")) | "\(.id): \(.name)"' ``` If `jq` is unavailable, use `python3 -c "import json,sys; ..."`. 2. **Natural language description** from the requester. --- ## Query structure ### Provider scoping parameter | Parameter | Property | Used on | Purpose | | --------------- | -------- | ------------ | -------------------------------------- | | `$provider_uid` | `id` | `AWSAccount` | Scopes the query to a specific account | The runner binds `$provider_uid` automatically. Every other node is isolated by path connectivity from the `AWSAccount` anchor. ### Imports ```python from api.attack_paths.queries.types import ( AttackPathsQueryAttribution, AttackPathsQueryDefinition, AttackPathsQueryParameterDefinition, ) from tasks.jobs.attack_paths.config import PROWLER_FINDING_LABEL ``` Always use `PROWLER_FINDING_LABEL` via f-string interpolation, never hardcode `"ProwlerFinding"`. ### Definition fields - **id**: kebab-case `{provider}-{description}`, e.g. `aws-ec2-privesc-passrole-iam`. - **name**: short, human-friendly label. Sourced queries append the reference ID: `"EC2 Instance Launch with Privileged Role (EC2-001)"`. - **short_description**: one sentence, no technical permissions. - **description**: full technical explanation, plain text. - **provider**: `aws`, `azure`, `gcp`, `kubernetes`, or `github`. - **cypher**: f-string Cypher body. Literal `{` / `}` are escaped as `{{` / `}}`. - **parameters**: `parameters=[]` if none. - **attribution**: optional `AttackPathsQueryAttribution(text, link)` for sourced queries. `link` uses the lowercase ID. Append the constant to the `{PROVIDER}_QUERIES` list at the bottom of the provider file. --- ## Predefined query template The canonical shape combines a principal walk, an optional target walk, deduplicated nodes, and a typed finding overlay: ```python AWS_{QUERY_NAME} = AttackPathsQueryDefinition( id="aws-{kebab-case-name}", name="{Label} ({REFERENCE_ID})", short_description="{One sentence.}", description="{Full technical explanation.}", attribution=AttackPathsQueryAttribution( text="pathfinding.cloud - {REFERENCE_ID} - {permission}", link="https://pathfinding.cloud/paths/{reference_id_lowercase}", ), provider="aws", cypher=f""" // Find principals with {permission} MATCH path_principal = (aws:AWSAccount {{id: $provider_uid}})--(principal:AWSPrincipal)-[:POLICY]->(policy:AWSPolicy)-[:STATEMENT]->(stmt:AWSPolicyStatement {{effect: 'Allow'}}) MATCH (stmt)-[:HAS_ACTION]->(act:AWSPolicyStatementActionItem) WHERE toLower(act.value) IN ['{permission_lowercase}', '{service}:*'] OR act.value = '*' WITH DISTINCT aws, principal, stmt, path_principal // Pre-aggregate the statement's resource values (see "Avoiding cartesian products") MATCH (stmt)-[:HAS_RESOURCE]->(res:AWSPolicyStatementResourceItem) 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 UNWIND paths AS p UNWIND nodes(p) AS n WITH paths, collect(DISTINCT n) AS unique_nodes UNWIND unique_nodes AS n OPTIONAL MATCH (n)-[pfr:HAS_FINDING]-(pf:{PROWLER_FINDING_LABEL} {{status: 'FAIL'}}) RETURN paths, collect(DISTINCT pf) as dpf, collect(DISTINCT pfr) as dpfr """, parameters=[], ) ``` Key points: - The principal walk types the `POLICY` and `STATEMENT` hops. Both are low-fan-out (each principal has a handful of policies; each policy a handful of statements), so the typed edge lets the planner cost a cheap inline filter. - The `(aws)--` hub hops stay anonymous. `AWSAccount` is a high-degree node that fans out to every principal, role, policy, and resource in the account; typing those edges forces the planner to enumerate from the hub and collapses performance on multi-tenant Neptune. - Other relationship types appear only where the file's existing queries already use one (`TRUSTS_AWS_PRINCIPAL`, `STS_ASSUMEROLE_ALLOW`, `MEMBER_AWS_GROUP`, `HAS_EXECUTION_ROLE`). - The finding probe is typed `:HAS_FINDING` and left undirected. The type lets Neptune apply an inline edge filter; the lack of direction matches the convention of the rest of the file. - Collapse duplicate rows after each permission gate with `WITH DISTINCT`, carrying only the variables needed by later clauses. - Each `HAS_*` traversal is its own `MATCH` clause with a `WHERE` on the child item node. `WITH DISTINCT path_principal, path_target` precedes `collect(path...)` to dedupe the row multiplication produced by the joins. - The `RETURN` shape `paths, dpf, dpfr` is the contract the serializer and visualiser depend on. Do not change it. --- ## 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. | Sub-pattern | Target | `path_target` shape | Example | | ------------------- | ------------------------ | ------------------------------------------------------------------------------------------------------- | ------- | | Self-escalation | Principal's own policies | `(aws)--(target_policy:AWSPolicy)--(principal)` | IAM-001 | | Lateral to user | Other IAM users | `(aws)--(target_user:AWSUser)` | IAM-002 | | Assume-role lateral | Assumable roles | `(aws)--(target_role:AWSRole)-[:STS_ASSUMEROLE_ALLOW]-(principal)` | IAM-014 | | PassRole + service | Service-trusting roles | `(aws)--(target_role:AWSRole)-[:TRUSTS_AWS_PRINCIPAL]-(:AWSPrincipal {arn: '{service}.amazonaws.com'})` | EC2-001 | **Multi-permission queries** (e.g. PassRole plus a service-create action) add permission gates before `path_target`. Reuse the per-query counter for new variables (`act2`, `policy2`, `stmt2`) and collapse rows after each gate: ```cypher MATCH (principal)-[:POLICY]->(policy2:AWSPolicy)-[:STATEMENT]->(stmt2:AWSPolicyStatement {effect: 'Allow'}) MATCH (stmt2)-[:HAS_ACTION]->(act2:AWSPolicyStatementActionItem) WHERE toLower(act2.value) IN ['service:*', 'service:createsomething'] OR act2.value = '*' WITH DISTINCT aws, principal, stmt, stmt2, path_principal ``` If a permission is an existence-only gate whose statement resource is not checked later, keep the policy and statement anonymous and carry only the variables still needed: ```cypher MATCH (principal)-[:POLICY]->(:AWSPolicy)-[:STATEMENT]->(:AWSPolicyStatement {effect: 'Allow'})-[:HAS_ACTION]->(act3:AWSPolicyStatementActionItem) WHERE toLower(act3.value) IN ['service:*', 'service:othersomething'] OR act3.value = '*' WITH DISTINCT aws, principal, stmt, path_principal ``` When all matching principals can target the same independent resource set, collect principal paths before expanding targets instead of creating one row per principal-target pair: ```cypher WITH aws, collect(DISTINCT path_principal) AS principal_paths MATCH path_target = (aws)--(target)
Voir sur GitHub
Ce SKILL.md est tres volumineux, SkillsMP affiche donc ici seulement la premiere section. Voir sur GitHub