New to Claude Skills? Learn how to install them →

prowler-cloud on GitHub

Prowler Attack Paths Query

Free

Efficiently create security queries for cloud graphs.

Get this skill

Free · Opens the source repo

What Prowler Attack Paths Query does

The Prowler Attack Paths Query skill simplifies the process of generating openCypher queries tailored for cloud security analysis. By leveraging the Cartography schema, this skill enables users to construct queries that identify privilege escalation paths, network vulnerabilities, and other security risks within cloud environments. It is designed for use with both Neo4j and Amazon Neptune databases, ensuring compatibility across different graph database platforms. The queries generated are read-only and focus on detecting complex relationships and risks within cloud architecture.

This skill is particularly useful for security analysts and cloud architects who need to assess their cloud infrastructure for potential vulnerabilities. With predefined queries and the ability to create custom queries, users can quickly adapt their security assessments to specific needs. The skill supports the automatic injection of provider-specific labels, ensuring that queries are scoped correctly to the relevant AWS accounts while maintaining a focus on the relationships and properties defined in the Cartography model.

The input methods for creating queries include both pathfinding.cloud IDs and natural language descriptions, making it accessible for users with varying levels of expertise in Cypher. The structured format of the queries allows for clear and concise definitions, including attributes like descriptions and short summaries, which aid in understanding the purpose of each query. This skill not only enhances the efficiency of security assessments but also promotes best practices in query construction by guiding users on how to utilize the Cartography schema effectively.

Overall, the Prowler Attack Paths Query skill is an essential tool for anyone involved in cloud security, providing a robust framework for identifying and mitigating risks in cloud environments.

When to use it

Use this skill when you need to create or update security queries for cloud environments using the Cartography schema.

When not to use it

This skill is not suitable for users who do not require openCypher queries or those working outside of cloud security contexts.

What you can build with it

Identifying Privilege Escalation Paths

Use the skill to generate queries that reveal potential privilege escalation paths within your AWS accounts.

Assessing Network Exposure

Leverage predefined queries to quickly assess network exposure and security risks in your cloud infrastructure.

Custom Query Development

Utilize the skill to create tailored queries based on specific security requirements or natural language descriptions.

How to install Prowler Attack Paths Query

View source

1. Install with the skills CLI

npx skills add prowler-cloud/prowler/prowler-attack-paths-query --agent claude-code

2. Or install it manually

Download the skill folder and drop it into ~/.claude/skills/ for all projects, or .claude/skills/ to scope it to one repo. Restart Claude Code so it picks up the new skill.

Anthropic's agentic coding CLI, and the reference implementation of Agent Skills. Drop a skill folder into ~/.claude/skills and Claude Code loads it automatically whenever a task matches the skill's description. Claude Code docs

Inside SKILL.md

Written by prowler-cloud

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:

    # 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

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

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:

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:

// 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:

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:

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:

WITH aws, collect(DISTINCT path_principal) AS principal_paths
MATCH path_target = (aws)--(target)
WITH principal_paths, collect(DISTINCT path_target) AS target_paths
WITH principal_paths + target_paths AS paths

Statements that constrain a target are still checked via HAS_RESOURCE traversals (res, res2). See IAM-015 or EC2-001 in aws.py.


Network exposure pattern

The Internet node is reached via CAN_ACCESS through an already-scoped resource, never as a standalone lookup:

cypher=f"""
    // Resource scoped through the account anchor
    MATCH path = (aws:AWSAccount {{id: $provider_uid}})--(resource:EC2Instance)
    WHERE resource.exposed_internet = true

    // Internet node reached via path connectivity through the resource
    OPTIONAL MATCH (internet:Internet)-[can_access:CAN_ACCESS]->(resource)

    WITH collect(path) AS paths, head(collect(internet)) AS internet, collect(can_access) AS can_access
    UNWIND paths AS p
    UNWIND nodes(p) AS n

    WITH paths, internet, can_access, 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,
        internet, can_access
"""

The CAN_ACCESS edge stays typed and directed (-[:CAN_ACCESS]->); that is its canonical sync-time orientation.


List-typed properties as child nodes

Some Cartography node properties carry a list of values: AWSPolicyStatement.action, AWSPolicyStatement.resource, KMSKey.encryption_algorithms, CloudFrontDistribution.aliases, and many others. The graph models each such property as a set of child item nodes connected to the parent by a typed edge. Queries reach the values by traversing the edge; the parent does not carry the list as a single field.

Naming convention

For a list-typed parent property the sink stores:

  • Child label: <ParentLabel><PropertyPascal>Item. Example: AWSPolicyStatement.resourceAWSPolicyStatementResourceItem.
  • Edge type: HAS_<PROPERTY_UPPER>. Example: resourceHAS_RESOURCE.
  • Child property: value (a single scalar string) for scalar-list properties. For list-of-dict properties (rare; for example SecretsManagerSecretVersion.tags) the child carries the dict keys as named fields per the catalog's field_map.

Variable naming for child-item matches

aws.py uses a per-query counter for each HAS_* traversal so chained matches stay unambiguous:

EdgeFirstSecondThird
HAS_ACTIONactact2act3
HAS_RESOURCEresres2res3
HAS_NOTACTIONnactnact2nact3
HAS_NOTRESOURCEnresnres2nres3

The counter resets at the top of every query.

Example - action match

Find statements that grant iam:PassRole, iam:*, or *. Traverse the HAS_ACTION edge in its own MATCH clause and apply the predicate in the attached WHERE:

MATCH (stmt:AWSPolicyStatement {effect: 'Allow'})
MATCH (stmt)-[:HAS_ACTION]->(act:AWSPolicyStatementActionItem)
WHERE toLower(act.value) IN ['iam:passrole', 'iam:*']
   OR act.value = '*'

The literal-action list is case-folded with toLower(act.value) because IAM authors mix case (iam:PassRole, iam:passrole); the * wildcard never lower-cases.

Example - resource ARN match

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.

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

The provider catalog lives in api/src/backend/tasks/jobs/attack_paths/provider_config.py (AWS_NORMALIZED_LISTS). Beyond policy statements it includes KMS algorithms, ECS container-definition lists (entry_point, command, links, dns_servers, ...), CloudFront aliases, Inspector finding URL and vulnerability lists, RDS event-subscription categories, and others. To query a list property that is not in the catalog, add an entry there first so the sync layer materialises it.


Common openCypher patterns

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

Object-typed Cartography properties (most notably condition on AWSPolicyStatement and S3PolicyStatement) are stored as JSON-encoded strings, e.g. '{"StringEquals":{"aws:SourceAccount":"123456789012"}}'. There is no JSON parser at query time, so use CONTAINS for substring checks:

WHERE stmt.condition CONTAINS '"aws:SourceAccount"'

For structured inspection, fetch the rows and parse in Python. Cypher cannot navigate JSON object keys.

Internet node via path connectivity

OPTIONAL MATCH (internet:Internet)-[can_access:CAN_ACCESS]->(resource)

resource must already be bound by the account-anchored pattern above.

Multi-label OR (multiple resource types)

MATCH path = (aws:AWSAccount {id: $provider_uid})-[r]-(x)-[q]-(y)
WHERE (x:EC2PrivateIp AND x.public_ip = $ip)
   OR (x:EC2Instance AND x.publicipaddress = $ip)
   OR (x:NetworkInterface AND x.public_ip = $ip)
   OR (x:ElasticIPAddress AND x.public_ip = $ip)

Include Prowler findings

Deduplicate nodes before the typed finding probe to avoid one OPTIONAL MATCH per path-occurrence of the same node:

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

For network-exposure queries, aggregate the Internet node and its edge alongside paths:

WITH collect(path) AS paths, head(collect(internet)) AS internet, collect(can_access) AS can_access
UNWIND paths AS p
UNWIND nodes(p) AS n

WITH paths, internet, can_access, 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,
    internet, can_access

Prowler-specific labels and relationships

Added by the sync task, not part of the Cartography schema. For everything else, consult the pinned Cartography schema (see "Creation steps").

Label / RelationshipDescription
ProwlerFindingFinding node (status, severity, check_id)
InternetInternet sentinel node
CAN_ACCESS(Internet)-[:CAN_ACCESS]->(resource) exposure edge
HAS_FINDING(resource)-[:HAS_FINDING]->(:ProwlerFinding) finding link
TRUSTS_AWS_PRINCIPALRole trust relationship
STS_ASSUMEROLE_ALLOWCan assume role

Parameters

For queries that take user input:

parameters=[
    AttackPathsQueryParameterDefinition(
        name="ip",
        label="IP address",
        # data_type defaults to "string", cast defaults to str.
        # For non-string params, set both: data_type="integer", cast=int
        description="Public IP address, e.g. 192.0.2.0.",
        placeholder="192.0.2.0",
    ),
],

openCypher compatibility

Queries must run on both Neo4j and Amazon Neptune. Avoid these constructs:

FeatureUse instead
APOC procedures (apoc.*)Real nodes and relationships in the graph
Neptune extensionsStandard openCypher
reduce()UNWIND + collect()
FOREACHWITH + UNWIND + SET
Regex =~toLower() + exact match, or STARTS WITH / CONTAINS
CALL () { UNION }Multi-label OR in WHERE (see pattern above)
Carried value plus aggregate expressionProject the aggregate first: WITH principal_paths, collect(...) AS target_paths, then combine lists in the next WITH
any(x IN list ...)size([x IN list WHERE pred]) > 0
all(x IN list ...)size([x IN list WHERE pred]) = size(list)
none(x IN list ...)size([x IN list WHERE pred]) = 0
EXISTS { MATCH (pattern) WHERE pred }Standalone MATCH (pattern) + WHERE pred; precede the downstream collect(path...) with WITH DISTINCT <path-vars> to dedupe the joins

For list-typed properties in the catalog (action, resource, and so on), traverse the HAS_* edges to the child item nodes via the multi-MATCH shape shown in "List-typed properties as child nodes". The parent node does not carry the list as a single field, so split(...) and comma-string predicates do not apply.


Best practices

  1. Chain every MATCH from the account anchor. An unanchored MATCH (role:AWSRole) returns roles from every provider in the graph; MATCH (aws)--(role:AWSRole) is scoped. A second-permission MATCH like MATCH (principal)--(policy2:AWSPolicy)--(stmt2:AWSPolicyStatement) is safe because principal is already bound to the account's subgraph.
  2. Type the finding probe. Always OPTIONAL MATCH (n)-[pfr:HAS_FINDING]-(pf:{PROWLER_FINDING_LABEL} {{status: 'FAIL'}}). The type lets Neptune apply an inline edge filter; an untyped probe scans every incident edge of high-degree nodes.
  3. Comment each MATCH. One inline // ... line per clause explaining its role.
  4. Never use internal labels. _ProviderResource, _AWSResource, _Tenant_*, _Provider_* are system isolation labels and must not appear in query text (predefined or custom).
  5. Reach the Internet node through path connectivity via (internet:Internet)-[:CAN_ACCESS]->(resource), never as a standalone match.
  6. Preserve the RETURN contract. paths, dpf, dpfr for the standard shape; add internet, can_access for network-exposure queries. The serializer and visualiser depend on these names.

Naming conventions

  • ID: kebab-case {provider}-{category}-{description}, e.g. aws-ec2-privesc-passrole-iam.
  • Constant: SHOUTING*SNAKE_CASE {PROVIDER}*{CATEGORY}\_{DESCRIPTION}, e.g. AWS_EC2_PRIVESC_PASSROLE_IAM.

Creation steps

  1. Read the queries module first to match the existing style:

    api/src/backend/api/attack_paths/queries/
    ├── __init__.py
    ├── types.py         # dataclass definitions
    ├── registry.py
    └── {provider}.py
    
  2. Fetch the Cartography schema for the pinned version. Do not guess labels, properties, or relationships. Read the dependency pin:

    grep cartography api/pyproject.toml
    

    Then fetch the schema for that exact tag:

    # Git pin (prowler-cloud/cartography@<TAG>):
    https://raw.githubusercontent.com/prowler-cloud/cartography/refs/tags/<TAG>/docs/root/modules/{provider}/schema.md
    
    # PyPI pin (cartography==<TAG>):
    https://raw.githubusercontent.com/cartography-cncf/cartography/refs/tags/<TAG>/docs/root/modules/{provider}/schema.md
    
  3. Build the query using the canonical predefined template plus the appropriate sub-pattern (privilege escalation or network exposure). For list-typed properties (action/resource/etc.), traverse the exploded child nodes via [:HAS_ACTION]->(:AWSPolicyStatementActionItem) etc. (see "List-typed properties as child nodes" and the AWS_NORMALIZED_LISTS catalog).

  4. Register the constant in the {PROVIDER}_QUERIES list at the bottom of the provider file.


Reference

Frequently asked questions about Prowler Attack Paths Query

Similar skills