prowler-cloud/prowler/prowler-attack-paths-query

Trust tier 2
  • community
LLM review pending
Source check pending

Checking that the SKILL.md file is still in the repository.

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.

Install

npx vskill@latest install prowler-cloud/prowler/prowler-attack-paths-query

SKILL.md

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 queriesCustom queries
Where they liveapi/src/backend/api/attack_paths/queries/{provider}.pyUser-supplied via the custom query API endpoint
Provider isolationAWSAccount {id: $provider_uid} anchor + path connectivityAutomatic _Provider_{uuid} label injection by cypher_sanitizer.py
What to writeChain every MATCH from the aws variablePlain Cypher, no isolation boilerplate
Internal labelsNever useNever 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 \

# List all path IDs and names curl -s https://raw.githubusercontent.com/DataDog/pathfinding.cloud/main/docs/paths.json \

# Filter by service prefix curl -s https://raw.githubusercontent.com/DataDog/pathfinding.cloud/main/docs/paths.json \

```

If jq is unavailable, use python3 -c "import json,sys; ...".

  1. Natural language description from the requester.

Query structure

Provider scoping parameter
ParameterPropertyUsed onPurpose
$provider_uididAWSAccountScopes 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-patternTargetpath_target shapeExample
Self-escalationPrincipal's own policies(aws)--(target_policy:AWSPolicy)--(principal)IAM-001
Lateral to userOther IAM users(aws)--(target_user:AWSUser)IAM-002
Assume-role lateralAssumable roles(aws)--(target_role:AWSRole)-[:STS_ASSUMEROLE_ALLOW]-(principal)IAM-014
PassRole + serviceService-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:

This is the start of the file. Read the full SKILL.md on GitHub.

Security

  • Security scanPassed, 100 of 100Pattern checks on the skill's files.
  • Model reviewNot runA model review is scheduled for the next rescan.

Quality evaluation

  • Task evaluationNot runNo evaluation yet. Run one in Skill Studio.

Source

Popularity

Repository stars
13,385
Repository forks
2,051
7-day trend score
48

Stars and forks are for the whole GitHub repository, not this skill alone. Refreshed 24 Mar 2026, 7 months ago.

Works with

Any agent vskill installs to: 56 targets in all. These 39 have a page on this site.

  • Claude Code
  • Cursor
  • GitHub Copilot
  • Windsurf
  • Codex
  • Gemini CLI
  • Cline
  • Amp
  • Roo Code
  • Goose
  • Continue
  • Kilo Code
  • Junie
  • Trae
  • OpenHands
  • Qwen Code
  • AI Agent
  • Tabnine Chat
  • Sourcegraph Cody
  • Amazon Q
  • Firebase Genkit
  • Sweep
  • Devin
  • Phind
  • Mentat
  • Claude Desktop
  • Aider
  • GPT Pilot
  • Plandex
  • bolt.new
  • v0
  • Replit Agent
  • Anse
  • TypingMind
  • BetterChatGPT
  • LibreChat
  • Warp AI
  • Pieces
  • Fig AI
Embed badge
vskill VERIFIED badge
[![vskill VERIFIED](https://verified-skill.com/api/v1/skills/prowler-cloud/prowler/prowler-attack-paths-query/badge)](https://verified-skill.com/skills/prowler-cloud/prowler/prowler-attack-paths-query)

Something wrong with this skill or its page? Tell us.