From c93ea035fcc640677ef699b9b56ad12c454fecff Mon Sep 17 00:00:00 2001 From: Josema Camacho Date: Fri, 10 Jul 2026 11:19:08 +0200 Subject: [PATCH] fix(api): make AWS Attack Paths query aggregation Neo4j compatible (#11931) --- ...ws-attack-paths-neo4j-aggregation.fixed.md | 1 + .../backend/api/attack_paths/queries/aws.py | 39 ++++++++++++------- skills/prowler-attack-paths-query/SKILL.md | 4 +- 3 files changed, 30 insertions(+), 14 deletions(-) create mode 100644 api/changelog.d/aws-attack-paths-neo4j-aggregation.fixed.md diff --git a/api/changelog.d/aws-attack-paths-neo4j-aggregation.fixed.md b/api/changelog.d/aws-attack-paths-neo4j-aggregation.fixed.md new file mode 100644 index 0000000000..17d53096da --- /dev/null +++ b/api/changelog.d/aws-attack-paths-neo4j-aggregation.fixed.md @@ -0,0 +1 @@ +AWS Attack Paths privilege escalation queries no longer fail on Neo4j with `Aggregation column contains implicit grouping expressions` diff --git a/api/src/backend/api/attack_paths/queries/aws.py b/api/src/backend/api/attack_paths/queries/aws.py index fa42854156..60660a1632 100644 --- a/api/src/backend/api/attack_paths/queries/aws.py +++ b/api/src/backend/api/attack_paths/queries/aws.py @@ -418,7 +418,8 @@ AWS_APPRUNNER_PRIVESC_UPDATE_SERVICE = AttackPathsQueryDefinition( // Find existing App Runner services with roles attached (potential targets) MATCH path_target = (aws)--(target_role:AWSRole)-[:TRUSTS_AWS_PRINCIPAL]->(:AWSPrincipal {{arn: 'tasks.apprunner.amazonaws.com'}}) - WITH principal_paths + collect(DISTINCT path_target) AS paths + WITH principal_paths, collect(DISTINCT path_target) AS target_paths + WITH principal_paths + target_paths AS paths UNWIND paths AS p UNWIND nodes(p) AS n @@ -523,7 +524,8 @@ AWS_BEDROCK_PRIVESC_INVOKE_CODE_INTERPRETER = AttackPathsQueryDefinition( // Find roles that trust the Bedrock AgentCore service (already attached to existing code interpreters) MATCH path_target = (aws)--(target_role:AWSRole)-[:TRUSTS_AWS_PRINCIPAL]->(:AWSPrincipal {{arn: 'bedrock-agentcore.amazonaws.com'}}) - WITH principal_paths + collect(DISTINCT path_target) AS paths + WITH principal_paths, collect(DISTINCT path_target) AS target_paths + WITH principal_paths + target_paths AS paths UNWIND paths AS p UNWIND nodes(p) AS n @@ -607,7 +609,8 @@ AWS_CLOUDFORMATION_PRIVESC_UPDATE_STACK = AttackPathsQueryDefinition( // Find roles that trust CloudFormation service (already attached to existing stacks) MATCH path_target = (aws)--(target_role:AWSRole)-[:TRUSTS_AWS_PRINCIPAL]->(:AWSPrincipal {{arn: 'cloudformation.amazonaws.com'}}) - WITH principal_paths + collect(DISTINCT path_target) AS paths + WITH principal_paths, collect(DISTINCT path_target) AS target_paths + WITH principal_paths + target_paths AS paths UNWIND paths AS p UNWIND nodes(p) AS n @@ -753,7 +756,8 @@ AWS_CLOUDFORMATION_PRIVESC_CHANGESET = AttackPathsQueryDefinition( // Find roles that trust CloudFormation service (already attached to existing stacks) MATCH path_target = (aws)--(target_role:AWSRole)-[:TRUSTS_AWS_PRINCIPAL]->(:AWSPrincipal {{arn: 'cloudformation.amazonaws.com'}}) - WITH principal_paths + collect(DISTINCT path_target) AS paths + WITH principal_paths, collect(DISTINCT path_target) AS target_paths + WITH principal_paths + target_paths AS paths UNWIND paths AS p UNWIND nodes(p) AS n @@ -844,7 +848,8 @@ AWS_CODEBUILD_PRIVESC_START_BUILD = AttackPathsQueryDefinition( // Find roles that trust CodeBuild service (already attached to existing projects) MATCH path_target = (aws)--(target_role:AWSRole)-[:TRUSTS_AWS_PRINCIPAL]->(:AWSPrincipal {{arn: 'codebuild.amazonaws.com'}}) - WITH principal_paths + collect(DISTINCT path_target) AS paths + WITH principal_paths, collect(DISTINCT path_target) AS target_paths + WITH principal_paths + target_paths AS paths UNWIND paths AS p UNWIND nodes(p) AS n @@ -880,7 +885,8 @@ AWS_CODEBUILD_PRIVESC_START_BUILD_BATCH = AttackPathsQueryDefinition( // Find roles that trust CodeBuild service (already attached to existing projects) MATCH path_target = (aws)--(target_role:AWSRole)-[:TRUSTS_AWS_PRINCIPAL]->(:AWSPrincipal {{arn: 'codebuild.amazonaws.com'}}) - WITH principal_paths + collect(DISTINCT path_target) AS paths + WITH principal_paths, collect(DISTINCT path_target) AS target_paths + WITH principal_paths + target_paths AS paths UNWIND paths AS p UNWIND nodes(p) AS n @@ -1096,7 +1102,8 @@ AWS_EC2_PRIVESC_MODIFY_INSTANCE_ATTRIBUTE = AttackPathsQueryDefinition( // Find EC2 instances with instance profiles (potential targets) MATCH path_target = (aws)--(ec2:EC2Instance)-[:STS_ASSUMEROLE_ALLOW]->(target_role:AWSRole) - WITH principal_paths + collect(DISTINCT path_target) AS paths + WITH principal_paths, collect(DISTINCT path_target) AS target_paths + WITH principal_paths + target_paths AS paths UNWIND paths AS p UNWIND nodes(p) AS n @@ -1187,7 +1194,8 @@ AWS_EC2_PRIVESC_LAUNCH_TEMPLATE = AttackPathsQueryDefinition( // Find launch templates in the account (potential targets) MATCH path_target = (aws)--(template:LaunchTemplate) - WITH principal_paths + collect(DISTINCT path_target) AS paths + WITH principal_paths, collect(DISTINCT path_target) AS target_paths + WITH principal_paths + target_paths AS paths UNWIND paths AS p UNWIND nodes(p) AS n @@ -1223,7 +1231,8 @@ AWS_EC2INSTANCECONNECT_PRIVESC_SEND_SSH_PUBLIC_KEY = AttackPathsQueryDefinition( // Find EC2 instances with attached roles (targets for credential theft via IMDS) MATCH path_target = (aws)--(ec2:EC2Instance)-[:STS_ASSUMEROLE_ALLOW]->(target_role:AWSRole) - WITH principal_paths + collect(DISTINCT path_target) AS paths + WITH principal_paths, collect(DISTINCT path_target) AS target_paths + WITH principal_paths + target_paths AS paths UNWIND paths AS p UNWIND nodes(p) AS n @@ -1539,7 +1548,8 @@ AWS_ECS_PRIVESC_EXECUTE_COMMAND = AttackPathsQueryDefinition( // Target: roles already attached to running tasks (trust ECS tasks service) MATCH path_target = (aws)--(target_role:AWSRole)-[:TRUSTS_AWS_PRINCIPAL]->(:AWSPrincipal {{arn: 'ecs-tasks.amazonaws.com'}}) - WITH principal_paths + collect(DISTINCT path_target) AS paths + WITH principal_paths, collect(DISTINCT path_target) AS target_paths + WITH principal_paths + target_paths AS paths UNWIND paths AS p UNWIND nodes(p) AS n @@ -1622,7 +1632,8 @@ AWS_GLUE_PRIVESC_UPDATE_DEV_ENDPOINT = AttackPathsQueryDefinition( // Find roles that trust Glue service (already attached to existing dev endpoints) MATCH path_target = (aws)--(target_role:AWSRole)-[:TRUSTS_AWS_PRINCIPAL]->(:AWSPrincipal {{arn: 'glue.amazonaws.com'}}) - WITH principal_paths + collect(DISTINCT path_target) AS paths + WITH principal_paths, collect(DISTINCT path_target) AS target_paths + WITH principal_paths + target_paths AS paths UNWIND paths AS p UNWIND nodes(p) AS n @@ -3337,7 +3348,8 @@ AWS_SSM_PRIVESC_START_SESSION = AttackPathsQueryDefinition( // Find EC2 instances with attached roles (targets for credential theft via IMDS) MATCH path_target = (aws)--(ec2:EC2Instance)-[:STS_ASSUMEROLE_ALLOW]->(target_role:AWSRole) - WITH principal_paths + collect(DISTINCT path_target) AS paths + WITH principal_paths, collect(DISTINCT path_target) AS target_paths + WITH principal_paths + target_paths AS paths UNWIND paths AS p UNWIND nodes(p) AS n @@ -3373,7 +3385,8 @@ AWS_SSM_PRIVESC_SEND_COMMAND = AttackPathsQueryDefinition( // Find EC2 instances with attached roles (targets for credential theft via IMDS) MATCH path_target = (aws)--(ec2:EC2Instance)-[:STS_ASSUMEROLE_ALLOW]->(target_role:AWSRole) - WITH principal_paths + collect(DISTINCT path_target) AS paths + WITH principal_paths, collect(DISTINCT path_target) AS target_paths + WITH principal_paths + target_paths AS paths UNWIND paths AS p UNWIND nodes(p) AS n diff --git a/skills/prowler-attack-paths-query/SKILL.md b/skills/prowler-attack-paths-query/SKILL.md index 9fedff4472..a70734aa6f 100644 --- a/skills/prowler-attack-paths-query/SKILL.md +++ b/skills/prowler-attack-paths-query/SKILL.md @@ -195,7 +195,8 @@ When all matching principals can target the same independent resource set, colle ```cypher WITH aws, collect(DISTINCT path_principal) AS principal_paths MATCH path_target = (aws)--(target) -WITH principal_paths + collect(DISTINCT path_target) AS paths +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`. @@ -419,6 +420,7 @@ Queries must run on both Neo4j and Amazon Neptune. Avoid these constructs: | `FOREACH` | `WITH` + `UNWIND` + `SET` | | Regex `=~` | `toLower()` + exact match, or `STARTS WITH` / `CONTAINS` | | `CALL () { UNION }` | Multi-label `OR` in `WHERE` (see pattern above) | +| Carried value plus aggregate expression | Project 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` |