mcpField
P
prowler-attack-paths-query

prowler-attack-paths-query

github.com/prowler-cloud安全15.1k60 次安装今天更新
代码仓库

关于 prowler-attack-paths-query

prowler-attack-paths-query 是用于编写 Prowler Attack Paths 查询的开发技能,面向在 Cartography 云图中识别权限提升链和网络暴露等风险的开发者。它提供 openCypher 查询结构、兼容性及性能编写规范。

主要功能

  1. 01说明预定义与自定义查询
  2. 02提供 openCypher 查询模板
  3. 03涵盖权限提升与网络暴露模式
  4. 04指导避免笛卡尔积
  5. 05说明列表属性的图结构映射

适用场景

  1. 01为云图新增攻击路径查询
  2. 02编写权限提升检测查询
  3. 03优化查询以避免性能问题

安装

复制下面的命令,在终端中运行。

$npx skills add https://github.com/prowler-cloud/prowler --skill prowler-attack-paths-query

安装后在 Claude Code 中输入/prowler-attack-paths-query确认已加载。

详细介绍

概述

攻击路径查询是针对 Cartography 摄取的云图谱执行的只读 openCypher 查询,用于检测权限提升链、网络暴露以及其他图结构安全风险。查询使用 openCypher Version 9 编写,因此可在 Neo4j 和 Amazon Neptune 存储端运行。

本技能是用于构建查询的简明、面向行动的参考指南。完整的可读参考(图模型、列表类型和 JSON 编码属性、兼容性及示例)请参阅 docs/developer-guide/attack-paths-queries.mdx。


两类查询受众

预定义查询自定义查询
存放位置api/src/backend/api/attack_paths/queries/{provider}.py用户通过自定义查询 API 端点提供
提供商隔离AWSAccount {id: $provider_uid} 锚点 + 路径连通性由 cypher_sanitizer.py 自动注入 _Provider_{uuid} 标签
编写内容从 aws 变量开始,将每个 MATCH 串联起来普通 Cypher,无需编写隔离样板代码
内部标签切勿使用切勿使用(由系统注入)

预定义查询:每个节点都必须能通过图遍历从 AWSAccount 根节点到达。这就是隔离边界。

自定义查询:编写自然的 Cypher。运行器会向每个节点模式注入 _Provider_{uuid} 标签,并通过查询后过滤器处理边缘情况。


输入来源

新查询有两个来源:

  1. pathfinding.cloud ID(例如 ECS-001、GLUE-001),即 Datadog 研究目录。汇总后的 paths.json 对 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)"'
    

    如果没有 jq,请使用 python3 -c "import json,sys; ..."。

  2. 请求者提供的自然语言描述。


查询结构

提供商范围参数
参数属性用于目的
$provider_uididAWSAccount将查询范围限定到特定账户

运行器会自动绑定 $provider_uid。其他所有节点都通过与 AWSAccount 锚点的路径连通性进行隔离。

导入
from api.attack_paths.queries.types import (
    AttackPathsQueryAttribution,
    AttackPathsQueryDefinition,
    AttackPathsQueryParameterDefinition,
)
from tasks.jobs.attack_paths.config import PROWLER_FINDING_LABEL

始终通过 f 字符串插值使用 PROWLER_FINDING_LABEL,切勿将 "ProwlerFinding" 硬编码。

定义字段
  • id:kebab-case 格式的 {provider}-{description},例如 aws-ec2-privesc-passrole-iam。
  • name:简短、易懂的标签。来源查询会附加参考 ID:"EC2 Instance Launch with Privileged Role (EC2-001)"。
  • short_description:一句话,不包含技术权限。
  • description:完整的技术说明,使用纯文本。
  • provider:aws、azure、gcp、kubernetes 或 github。
  • cypher:f 字符串 Cypher 主体。字面量 { / } 要转义为 {{ / }}。
  • parameters:如果没有参数,则使用 parameters=[]。
  • attribution:来源查询可选的 AttackPathsQueryAttribution(text, link)。link 使用小写 ID。

将常量追加到提供商文件底部的 {PROVIDER}_QUERIES 列表中。


预定义查询模板

规范形态结合了主体遍历、可选的目标遍历、去重后的节点,以及类型化的发现项叠加:

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=[],
)

要点:

  • 主体遍历为 POLICY 和 STATEMENT 跳转指定类型。两者的扇出都很低(每个主体只有少量策略,每个策略只有少量语句),因此,类型化边可让规划器将其作为低成本的内联筛选条件。
  • (aws)-- 枢纽跳转保持匿名。AWSAccount 是一个高度节点,会扇出到帐户中的每个主体、角色、策略和资源;为这些边指定类型会迫使规划器从枢纽开始枚举,并导致多租户 Neptune 上的性能骤降。
  • 其他关系类型仅在文件现有查询已使用的地方出现(TRUSTS_AWS_PRINCIPAL、STS_ASSUMEROLE_ALLOW、MEMBER_AWS_GROUP、HAS_EXECUTION_ROLE)。
  • 发现项探测使用类型 :HAS_FINDING,且不指定方向。类型可让 Neptune 应用内联边筛选;不指定方向则符合文件其余部分的惯例。
  • 每次权限检查后都使用 WITH DISTINCT 折叠重复行,仅保留后续子句所需的变量。
  • 每次 HAS_* 遍历都使用独立的 MATCH 子句,并通过 WHERE 筛选子项节点。在 collect(path...) 前使用 WITH DISTINCT path_principal, path_target,以消除联接产生的行倍增。
  • RETURN 形态 paths, dpf, dpfr 是序列化器和可视化工具所依赖的契约。请勿更改。

避免笛卡尔积

匹配目标集合(AWSRole、AWSUser、AWSGroup),随后在另一个不相关联的 MATCH 中将每个目标与语句的 HAS_RESOURCE 项逐一筛选,会构建笛卡尔积:筛选执行前,每个目标都会与每个资源项配对。在主体较多的帐户上,这会导致报错或超时。请先将资源值预聚合为列表,再对每个目标只进行一次匹配:

// 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
  • 在匹配目标前聚合资源;成本变为 targets + resources,而不是 targets × resources。这只是纯粹的改写,结果集保持不变。
  • ('*' IN res_values) 会短路通配符授权,只有需要时才扫描列表。
  • 将 target.name / target.arn 绑定到局部变量,使列表推导式对每个目标只读取一次,而不是对每个资源值都读取一次。
  • size([...]) > 0 是与 Neptune 兼容的 any() 写法(参见“openCypher 兼容性”)。
  • 双语句查询会将每条语句的资源分别聚合到各自的列表(res_values、res2_values),并用 AND 组合两个 size([...]) > 0 检查。
  • 已由关系(STS_ASSUMEROLE_ALLOW、TRUSTS_AWS_PRINCIPAL)限定的目标无需聚合:该关系已限定了集合范围。

权限提升子模式

四种 path_target 形状涵盖常见的攻击类型。每种都沿用规范模板的 path_principal、去重尾部和 RETURN,只有 path_target 的 MATCH 和资源谓词不同。

子模式目标path_target 形状示例
自我提权主体自身的策略(aws)--(target_policy:AWSPolicy)--(principal)IAM-001
横向访问用户其他 IAM 用户(aws)--(target_user:AWSUser)IAM-002
Assume-role 横向访问可被代入的角色(aws)--(target_role:AWSRole)-[:STS_ASSUMEROLE_ALLOW]-(principal)IAM-014
PassRole + 服务信任服务的角色(aws)--(target_role:AWSRole)-[:TRUSTS_AWS_PRINCIPAL]-(:AWSPrincipal {arn: '{service}.amazonaws.com'})EC2-001

多权限查询(例如 PassRole 加上创建服务的操作)会在 path_target 之前添加权限门控。新变量(act2、policy2、stmt2)沿用每个查询的计数器,并在每个门控之后合并行:

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

如果某个权限仅作为存在性门控,且其语句资源之后不会再检查,则将策略和语句设为匿名,只保留仍需使用的变量:

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

如果所有匹配的主体都能以相同的独立资源集为目标,则应先收集主体路径,再展开目标,而不是为每个主体与目标的组合创建一行:

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

对目标有限制的语句仍会通过 HAS_RESOURCE 遍历(res、res2)进行检查。参见 aws.py 中的 IAM-015 或 EC2-001。


网络暴露模式

通过已限定范围的资源沿 CAN_ACCESS 边到达 Internet 节点,绝不将其作为独立查询对象:

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
"""

CAN_ACCESS 边保持类型明确且有向(-[:CAN_ACCESS]->);这是其规范的同步时方向。


将列表类型属性建模为子节点

某些 Cartography 节点属性包含值列表:AWSPolicyStatement.action、AWSPolicyStatement.resource、KMSKey.encryption_algorithms、CloudFrontDistribution.aliases,以及许多其他属性。图会将每个此类属性建模为一组子项节点,并通过带类型的边连接到父节点。查询通过遍历边来获取这些值;父节点不会将列表作为单个字段保存。

命名约定

对于列表类型的父属性,sink 会存储:

  • 子节点标签:<ParentLabel><PropertyPascal>Item。例如:AWSPolicyStatement.resource → AWSPolicyStatementResourceItem。
  • 边类型:HAS_<PROPERTY_UPPER>。例如:resource → HAS_RESOURCE。
  • 子节点属性:对于标量列表属性,使用 value(单个标量字符串)。对于字典列表属性(较少见,例如 SecretsManagerSecretVersion.tags),子节点会根据目录中的 field_map,将字典键作为具名字段保存。
子项匹配的变量命名

aws.py 对每次 HAS_* 遍历使用每个查询独立的计数器,确保链式匹配清晰无歧义:

边第一次第二次第三次
HAS_ACTIONactact2act3
HAS_RESOURCEresres2res3
HAS_NOTACTIONnactnact2nact3
HAS_NOTRESOURCEnresnres2nres3

每个查询开始时,计数器都会重置。

示例 - action 匹配

查找授予 iam:PassRole、iam:* 或 * 的语句。通过单独的 MATCH 子句遍历 HAS_ACTION 边,并在附加的 WHERE 中应用谓词:

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

字面 action 列表使用 toLower(act.value) 进行大小写折叠,因为 IAM 编写者会混用大小写(iam:PassRole、iam:passrole);* 通配符不会转换为小写。

示例 - 资源 ARN 匹配

要查找资源可指向特定角色的语句,请先聚合资源值,然后针对列表测试目标一次(参见“避免笛卡尔积”)。不要在单独的 MATCH 中将目标集合与 HAS_RESOURCE 项配对;这会生成笛卡尔积。

三个谓词涵盖了资源的各种情况:完整通配符(*)、包含目标名称的模式(arn:aws:iam::*:role/admin*),以及作为实际 ARN 前缀或组成部分的模式。

列表属性目录

提供程序目录位于 api/src/backend/tasks/jobs/attack_paths/provider_config.py(AWS_NORMALIZED_LISTS)。除策略语句外,它还包括 KMS 算法、ECS 容器定义列表(entry_point、command、links、dns_servers 等)、CloudFront 别名、Inspector 调查结果 URL 和漏洞列表、RDS 事件订阅类别等。要查询目录中尚未包含的列表属性,请先在其中添加条目,以便同步层将其具体化。


常见 openCypher 模式

账户/主体匹配以及服务信任和 assume-role 目标形态都出现在上述模板和子模式中。其他可复用模式:

JSON 编码属性

对象类型的 Cartography 属性(最值得注意的是 AWSPolicyStatement 和 S3PolicyStatement 上的 condition)以 JSON 编码字符串形式存储,例如 '{"StringEquals":{"aws:SourceAccount":"123456789012"}}'。查询时没有 JSON 解析器,因此请使用 CONTAINS 进行子字符串检查:

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

如需结构化检查,请获取行并在 Python 中解析。Cypher 无法遍历 JSON 对象键。

通过路径连通性查询 Internet 节点
OPTIONAL MATCH (internet:Internet)-[can_access:CAN_ACCESS]->(resource)

resource 必须已由上面的账户锚定模式绑定。

多标签 OR(多个资源类型)
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)
包含 Prowler 调查结果

在进行类型化调查结果探测前先对节点去重,避免对同一节点的每条路径出现都执行一次 OPTIONAL MATCH:

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

对于网络暴露查询,请将 Internet 节点及其边与路径一同聚合:

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 专属标签和关系

这些由同步任务添加,不属于 Cartography 架构的一部分。其他内容请查阅固定版本的 Cartography 架构(参见“创建步骤”)。

标签 / 关系说明
ProwlerFinding调查结果节点(status、severity、check_id)
InternetInternet 哨兵节点
CAN_ACCESS(Internet)-[:CAN_ACCESS]->(resource) 暴露边
HAS_FINDING(resource)-[:HAS_FINDING]->(:ProwlerFinding) 调查结果链接
TRUSTS_AWS_PRINCIPAL角色信任关系
STS_ASSUMEROLE_ALLOW可以 assume-role

参数

对于接受用户输入的查询:

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 兼容性

查询必须同时适用于 Neo4j 和 Amazon Neptune。请避免使用以下结构:

功能替代方案
APOC 过程(apoc.*)图中的实际节点和关系
Neptune 扩展标准 openCypher
reduce()UNWIND + collect()
FOREACHWITH + UNWIND + SET
正则表达式 =~toLower() + 精确匹配,或 STARTS WITH / CONTAINS
CALL () { UNION }在 WHERE 中使用多标签 OR(参见上方模式)
保留值与聚合表达式先投影聚合结果:WITH principal_paths, collect(...) AS target_paths,然后在下一个 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 }独立使用 MATCH (pattern) + WHERE pred;在下游 collect(path...) 前添加 WITH DISTINCT <path-vars>,以去重连接结果

对于目录中列表类型的属性(如 action、resource 等),请按照“将列表类型属性作为子节点”中展示的多条 MATCH 结构,通过 HAS_* 边遍历到子项节点。父节点不会将列表作为单个字段保存,因此 split(...) 和逗号分隔字符串谓词不适用。


最佳实践

  1. 每个 MATCH 都从账户锚点开始链式匹配。 未锚定的 MATCH (role:AWSRole) 会返回图中所有提供商的角色;MATCH (aws)--(role:AWSRole) 则限定了范围。第二个权限 MATCH,例如 MATCH (principal)--(policy2:AWSPolicy)--(stmt2:AWSPolicyStatement),是安全的,因为 principal 已绑定到账户的子图。
  2. 为发现项探测指定类型。 始终使用 OPTIONAL MATCH (n)-[pfr:HAS_FINDING]-(pf:{PROWLER_FINDING_LABEL} {{status: 'FAIL'}})。类型可让 Neptune 应用内联边过滤器;不指定类型的探测会扫描高连接度节点的每条关联边。
  3. 为每个 MATCH 添加注释。 每个子句用一行内联 // ... 说明其作用。
  4. 绝不使用内部标签。 _ProviderResource、_AWSResource、_Tenant_*、_Provider_* 是系统隔离标签,不得出现在查询文本中(无论是预定义查询还是自定义查询)。
  5. 通过路径连通性抵达 Internet 节点,使用 (internet:Internet)-[:CAN_ACCESS]->(resource),绝不要将其作为独立匹配项。
  6. 保留 RETURN 约定。 标准形式使用 paths, dpf, dpfr;网络暴露查询则添加 internet, can_access。序列化器和可视化器依赖这些名称。

命名约定

  • ID:kebab-case {provider}-{category}-{description},例如 aws-ec2-privesc-passrole-iam。
  • 常量:SHOUTING*SNAKE_CASE {PROVIDER}*{CATEGORY}\_{DESCRIPTION},例如 AWS_EC2_PRIVESC_PASSROLE_IAM。

创建步骤

  1. 先阅读 queries 模块,以遵循现有风格:

    api/src/backend/api/attack_paths/queries/
    ├── __init__.py
    ├── types.py         # dataclass definitions
    ├── registry.py
    └── {provider}.py
    
  2. 获取固定版本对应的 Cartography 架构。 不要猜测标签、属性或关系。读取依赖项固定版本:

    grep cartography api/pyproject.toml
    

    然后获取该确切标签对应的架构:

    # 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. 构建查询,使用规范的预定义模板以及适当的子模式(权限提升或网络暴露)。对于列表类型的属性(action/resource 等),通过展开的子节点进行遍历,例如 [:HAS_ACTION]->(:AWSPolicyStatementActionItem)(参见“作为子节点的列表类型属性”和 AWS_NORMALIZED_LISTS 目录)。

  4. 注册该常量,将其添加到提供商文件底部的 {PROVIDER}_QUERIES 列表中。


参考资料

评论

支持 Markdown