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.

Ir para a instalação

Informações da origem

Repositório
prowler-cloud/prowler
Última atividade na origem
28 de julho de 2026 às 16:28
Idioma detectado do SKILL.md
inglês
Estrelas
14.842
Forks
2.391

Opções de instalação

Por padrão, está selecionado o prompt que primeiro revisa a origem. Você pode mudar para um comando direto ou baixar uma cópia local.

Revise os arquivos de origem

Leia o SKILL.md e os arquivos complementares exibidos pelo SkillsMP antes de decidir se vai instalar.

Exibindo SKILL.md

SKILL.md
Instruções da origem · Visualização somente leitura
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)
Ver no GitHub
Este SKILL.md e muito grande, entao o SkillsMP mostra aqui apenas a primeira secao. Ver no GitHub