From d9e978af29290d284d790ddb9337901215ef3370 Mon Sep 17 00:00:00 2001 From: "Andoni A." <14891798+andoniaf@users.noreply.github.com> Date: Wed, 7 Jan 2026 10:50:29 +0100 Subject: [PATCH] initial version --- .../api/attack_paths/query_definitions.py | 364 ++++++++++++++++++ docker-compose-dev.yml | 6 +- ui/dependency-log.json | 6 +- ui/package-lock.json | 55 ++- ui/package.json | 1 + 5 files changed, 419 insertions(+), 13 deletions(-) diff --git a/api/src/backend/api/attack_paths/query_definitions.py b/api/src/backend/api/attack_paths/query_definitions.py index 8a65440d6c..680186e981 100644 --- a/api/src/backend/api/attack_paths/query_definitions.py +++ b/api/src/backend/api/attack_paths/query_definitions.py @@ -336,6 +336,370 @@ _QUERY_DEFINITIONS: dict[str, list[AttackPathsQueryDefinition]] = { ), ], ), + # ===================================================================== + # Privilege Escalation Queries (based on pathfinding.cloud research) + # Reference: https://github.com/DataDog/pathfinding.cloud + # ===================================================================== + AttackPathsQueryDefinition( + id="aws-iam-privesc-create-policy-version", + name="Privilege Escalation: iam:CreatePolicyVersion", + description="Detect principals with iam:CreatePolicyVersion permission who can modify policies attached to themselves or others, enabling privilege escalation by creating a new policy version with elevated permissions. This is a self-escalation path (pathfinding.cloud: iam-001).", + provider="aws", + cypher=""" + // Find principals with iam:CreatePolicyVersion permission + MATCH path_principal = (aws:AWSAccount {id: $provider_uid})--(principal:AWSPrincipal) + + // Find policies attached to the principal + MATCH path_attached = (principal)--(attached_policy:AWSPolicy) + WHERE attached_policy.type = 'Customer Managed' + + // Find policy statements that grant iam:CreatePolicyVersion + MATCH path_perms = (principal)--(perms_policy:AWSPolicy)--(stmt:AWSPolicyStatement) + WHERE stmt.effect = 'Allow' + AND ( + any(action IN stmt.action WHERE + toLower(action) = 'iam:createpolicyversion' + OR toLower(action) = 'iam:*' + OR action = '*' + ) + ) + // Check resource constraints - can they modify the attached policy? + AND ( + any(resource IN stmt.resource WHERE + resource = '*' + OR attached_policy.arn CONTAINS resource + OR resource CONTAINS attached_policy.name + ) + ) + + // Create a virtual "Escalation" node to visualize the attack outcome + CALL apoc.create.vNode(['PrivilegeEscalation'], { + id: 'privesc-' + principal.arn, + name: 'Effective Administrator', + technique: 'iam:CreatePolicyVersion', + severity: 'CRITICAL', + reference: 'https://pathfinding.cloud/paths/iam-001' + }) + YIELD node AS escalation_outcome + + CALL apoc.create.vRelationship(principal, 'CAN_ESCALATE_TO', { + via: 'iam:CreatePolicyVersion', + target_policy: attached_policy.arn + }, escalation_outcome) + YIELD rel AS escalation_rel + + UNWIND nodes(path_principal) + nodes(path_attached) + nodes(path_perms) as n + OPTIONAL MATCH (n)-[pfr]-(pf:ProwlerFinding) + WHERE pf.status = 'FAIL' + + RETURN path_principal, path_attached, path_perms, + escalation_outcome, escalation_rel, + collect(DISTINCT pf) as dpf, collect(DISTINCT pfr) as dpfr + """, + parameters=[], + ), + AttackPathsQueryDefinition( + id="aws-iam-privesc-attach-role-policy-assume-role", + name="Privilege Escalation: iam:AttachRolePolicy + sts:AssumeRole", + description="Detect principals who can both attach policies to roles AND assume those roles. This two-step attack allows modifying a role's permissions then assuming it to gain elevated access. This is a principal-access escalation path (pathfinding.cloud: iam-014).", + provider="aws", + cypher=""" + // Find principals in the account + MATCH path_principal = (aws:AWSAccount {id: $provider_uid})--(principal:AWSPrincipal) + + // Find statements granting iam:AttachRolePolicy + MATCH path_attach = (principal)--(attach_policy:AWSPolicy)--(stmt_attach:AWSPolicyStatement) + WHERE stmt_attach.effect = 'Allow' + AND any(action IN stmt_attach.action WHERE + toLower(action) = 'iam:attachrolepolicy' + OR toLower(action) = 'iam:*' + OR action = '*' + ) + + // Find statements granting sts:AssumeRole + MATCH path_assume = (principal)--(assume_policy:AWSPolicy)--(stmt_assume:AWSPolicyStatement) + WHERE stmt_assume.effect = 'Allow' + AND any(action IN stmt_assume.action WHERE + toLower(action) = 'sts:assumerole' + OR toLower(action) = 'sts:*' + OR action = '*' + ) + + // Find target roles that the principal can both modify AND assume + MATCH path_target = (aws)--(target_role:AWSRole) + WHERE target_role.arn CONTAINS $provider_uid + // Can attach policy to this role + AND any(resource IN stmt_attach.resource WHERE + resource = '*' + OR target_role.arn CONTAINS resource + OR resource CONTAINS target_role.name + ) + // Can assume this role + AND any(resource IN stmt_assume.resource WHERE + resource = '*' + OR target_role.arn CONTAINS resource + OR resource CONTAINS target_role.name + ) + + // Create visualization of the escalation path + CALL apoc.create.vNode(['PrivilegeEscalation'], { + id: 'privesc-' + principal.arn + '-via-' + target_role.name, + name: 'Effective Administrator', + technique: 'iam:AttachRolePolicy + sts:AssumeRole', + severity: 'CRITICAL', + reference: 'https://pathfinding.cloud/paths/iam-014' + }) + YIELD node AS escalation_outcome + + CALL apoc.create.vRelationship(principal, 'CAN_MODIFY', { + via: 'iam:AttachRolePolicy' + }, target_role) + YIELD rel AS modify_rel + + CALL apoc.create.vRelationship(target_role, 'CAN_BE_ASSUMED_BY', { + via: 'sts:AssumeRole' + }, escalation_outcome) + YIELD rel AS assume_rel + + UNWIND nodes(path_principal) + nodes(path_attach) + nodes(path_assume) + nodes(path_target) as n + OPTIONAL MATCH (n)-[pfr]-(pf:ProwlerFinding) + WHERE pf.status = 'FAIL' + + RETURN path_principal, path_attach, path_assume, path_target, + escalation_outcome, modify_rel, assume_rel, + collect(DISTINCT pf) as dpf, collect(DISTINCT pfr) as dpfr + """, + parameters=[], + ), + AttackPathsQueryDefinition( + id="aws-iam-privesc-passrole-ec2", + name="Privilege Escalation: iam:PassRole + ec2:RunInstances", + description="Detect principals who can launch EC2 instances with privileged IAM roles attached. This allows gaining the permissions of the passed role by accessing the EC2 instance metadata service. This is a new-passrole escalation path (pathfinding.cloud: ec2-001).", + provider="aws", + cypher=""" + // Find principals in the account + MATCH path_principal = (aws:AWSAccount {id: $provider_uid})--(principal:AWSPrincipal) + + // Find statements granting iam:PassRole + MATCH path_passrole = (principal)--(passrole_policy:AWSPolicy)--(stmt_passrole:AWSPolicyStatement) + WHERE stmt_passrole.effect = 'Allow' + AND any(action IN stmt_passrole.action WHERE + toLower(action) = 'iam:passrole' + OR toLower(action) = 'iam:*' + OR action = '*' + ) + + // Find statements granting ec2:RunInstances + MATCH path_ec2 = (principal)--(ec2_policy:AWSPolicy)--(stmt_ec2:AWSPolicyStatement) + WHERE stmt_ec2.effect = 'Allow' + AND any(action IN stmt_ec2.action WHERE + toLower(action) = 'ec2:runinstances' + OR toLower(action) = 'ec2:*' + OR action = '*' + ) + + // Find roles that trust EC2 service (can be passed to EC2) + MATCH path_target = (aws)--(target_role:AWSRole) + WHERE target_role.arn CONTAINS $provider_uid + // Check if principal can pass this role + AND any(resource IN stmt_passrole.resource WHERE + resource = '*' + OR target_role.arn CONTAINS resource + OR resource CONTAINS target_role.name + ) + + // Check if target role has elevated permissions (optional, for severity assessment) + OPTIONAL MATCH (target_role)--(role_policy:AWSPolicy)--(role_stmt:AWSPolicyStatement) + WHERE role_stmt.effect = 'Allow' + AND ( + any(action IN role_stmt.action WHERE action = '*') + OR any(action IN role_stmt.action WHERE toLower(action) = 'iam:*') + ) + + // Create visualization + CALL apoc.create.vNode(['EC2Instance'], { + id: 'potential-ec2-' + principal.arn, + name: 'New EC2 Instance', + description: 'Attacker-controlled EC2 with privileged role' + }) + YIELD node AS ec2_node + + CALL apoc.create.vNode(['PrivilegeEscalation'], { + id: 'privesc-ec2-' + principal.arn + '-' + target_role.name, + name: CASE WHEN role_stmt IS NOT NULL THEN 'Effective Administrator' ELSE 'Elevated Access' END, + technique: 'iam:PassRole + ec2:RunInstances', + severity: CASE WHEN role_stmt IS NOT NULL THEN 'CRITICAL' ELSE 'HIGH' END, + reference: 'https://pathfinding.cloud/paths/ec2-001' + }) + YIELD node AS escalation_outcome + + CALL apoc.create.vRelationship(principal, 'CAN_LAUNCH', { + via: 'ec2:RunInstances + iam:PassRole' + }, ec2_node) + YIELD rel AS launch_rel + + CALL apoc.create.vRelationship(ec2_node, 'ASSUMES_ROLE', {}, target_role) + YIELD rel AS assumes_rel + + CALL apoc.create.vRelationship(target_role, 'GRANTS_ACCESS', {}, escalation_outcome) + YIELD rel AS grants_rel + + UNWIND nodes(path_principal) + nodes(path_passrole) + nodes(path_ec2) + nodes(path_target) as n + OPTIONAL MATCH (n)-[pfr]-(pf:ProwlerFinding) + WHERE pf.status = 'FAIL' + + RETURN path_principal, path_passrole, path_ec2, path_target, + ec2_node, escalation_outcome, launch_rel, assumes_rel, grants_rel, + collect(DISTINCT pf) as dpf, collect(DISTINCT pfr) as dpfr + """, + parameters=[], + ), + AttackPathsQueryDefinition( + id="aws-iam-privesc-passrole-lambda", + name="Privilege Escalation: iam:PassRole + lambda:CreateFunction + lambda:InvokeFunction", + description="Detect principals who can create Lambda functions with privileged IAM roles and invoke them. This allows executing code with the permissions of the passed role. This is a new-passrole escalation path (pathfinding.cloud: lambda-001).", + provider="aws", + cypher=""" + // Find principals in the account + MATCH path_principal = (aws:AWSAccount {id: $provider_uid})--(principal:AWSPrincipal) + + // Find statements granting iam:PassRole + MATCH path_passrole = (principal)--(passrole_policy:AWSPolicy)--(stmt_passrole:AWSPolicyStatement) + WHERE stmt_passrole.effect = 'Allow' + AND any(action IN stmt_passrole.action WHERE + toLower(action) = 'iam:passrole' + OR toLower(action) = 'iam:*' + OR action = '*' + ) + + // Find statements granting lambda:CreateFunction + MATCH path_create = (principal)--(create_policy:AWSPolicy)--(stmt_create:AWSPolicyStatement) + WHERE stmt_create.effect = 'Allow' + AND any(action IN stmt_create.action WHERE + toLower(action) = 'lambda:createfunction' + OR toLower(action) = 'lambda:*' + OR action = '*' + ) + + // Find statements granting lambda:InvokeFunction + MATCH path_invoke = (principal)--(invoke_policy:AWSPolicy)--(stmt_invoke:AWSPolicyStatement) + WHERE stmt_invoke.effect = 'Allow' + AND any(action IN stmt_invoke.action WHERE + toLower(action) = 'lambda:invokefunction' + OR toLower(action) = 'lambda:*' + OR action = '*' + ) + + // Find roles that can be passed (ideally those trusting Lambda service) + MATCH path_target = (aws)--(target_role:AWSRole) + WHERE target_role.arn CONTAINS $provider_uid + AND any(resource IN stmt_passrole.resource WHERE + resource = '*' + OR target_role.arn CONTAINS resource + OR resource CONTAINS target_role.name + ) + + // Check if target role has elevated permissions + OPTIONAL MATCH (target_role)--(role_policy:AWSPolicy)--(role_stmt:AWSPolicyStatement) + WHERE role_stmt.effect = 'Allow' + AND ( + any(action IN role_stmt.action WHERE action = '*') + OR any(action IN role_stmt.action WHERE toLower(action) = 'iam:*') + ) + + // Create visualization + CALL apoc.create.vNode(['LambdaFunction'], { + id: 'potential-lambda-' + principal.arn, + name: 'New Lambda Function', + description: 'Attacker-controlled Lambda with privileged role' + }) + YIELD node AS lambda_node + + CALL apoc.create.vNode(['PrivilegeEscalation'], { + id: 'privesc-lambda-' + principal.arn + '-' + target_role.name, + name: CASE WHEN role_stmt IS NOT NULL THEN 'Effective Administrator' ELSE 'Elevated Access' END, + technique: 'iam:PassRole + lambda:CreateFunction + lambda:InvokeFunction', + severity: CASE WHEN role_stmt IS NOT NULL THEN 'CRITICAL' ELSE 'HIGH' END, + reference: 'https://pathfinding.cloud/paths/lambda-001' + }) + YIELD node AS escalation_outcome + + CALL apoc.create.vRelationship(principal, 'CAN_CREATE_AND_INVOKE', { + via: 'lambda:CreateFunction + lambda:InvokeFunction + iam:PassRole' + }, lambda_node) + YIELD rel AS create_rel + + CALL apoc.create.vRelationship(lambda_node, 'EXECUTES_AS', {}, target_role) + YIELD rel AS executes_rel + + CALL apoc.create.vRelationship(target_role, 'GRANTS_ACCESS', {}, escalation_outcome) + YIELD rel AS grants_rel + + UNWIND nodes(path_principal) + nodes(path_passrole) + nodes(path_create) + nodes(path_invoke) + nodes(path_target) as n + OPTIONAL MATCH (n)-[pfr]-(pf:ProwlerFinding) + WHERE pf.status = 'FAIL' + + RETURN path_principal, path_passrole, path_create, path_invoke, path_target, + lambda_node, escalation_outcome, create_rel, executes_rel, grants_rel, + collect(DISTINCT pf) as dpf, collect(DISTINCT pfr) as dpfr + """, + parameters=[], + ), + AttackPathsQueryDefinition( + id="aws-iam-privesc-role-chain", + name="Privilege Escalation: Role Assumption Chains to Admin", + description="Detect multi-hop role assumption chains where a principal can reach an administrative role through one or more intermediate role assumptions. This traces STS_ASSUMEROLE_ALLOW relationships to find paths to privileged roles.", + provider="aws", + cypher=""" + // Find principals in the account + MATCH path_principal = (aws:AWSAccount {id: $provider_uid})--(principal:AWSPrincipal) + + // Find role assumption chains (1-5 hops) to roles with elevated permissions + MATCH path_chain = (principal)-[:STS_ASSUMEROLE_ALLOW*1..5]->(target_role:AWSRole) + + // Target role must have administrative permissions + MATCH path_admin = (target_role)--(admin_policy:AWSPolicy)--(admin_stmt:AWSPolicyStatement) + WHERE admin_stmt.effect = 'Allow' + AND ( + any(action IN admin_stmt.action WHERE action = '*') + OR any(action IN admin_stmt.action WHERE toLower(action) = 'iam:*') + OR any(action IN admin_stmt.action WHERE toLower(action) CONTAINS 'admin') + ) + + // Calculate chain length for visualization + WITH principal, target_role, path_principal, path_chain, path_admin, + length(path_chain) as chain_length, + [node in nodes(path_chain) | node.name] as chain_nodes + + // Create escalation outcome visualization + CALL apoc.create.vNode(['PrivilegeEscalation'], { + id: 'privesc-chain-' + principal.arn + '-' + target_role.name, + name: 'Effective Administrator', + technique: 'sts:AssumeRole chain (' + toString(chain_length) + ' hops)', + severity: CASE WHEN chain_length = 1 THEN 'CRITICAL' ELSE 'HIGH' END, + chain_length: chain_length, + chain_path: chain_nodes, + reference: 'https://pathfinding.cloud/paths/sts-001' + }) + YIELD node AS escalation_outcome + + CALL apoc.create.vRelationship(target_role, 'GRANTS_ADMIN', { + hops: chain_length + }, escalation_outcome) + YIELD rel AS admin_rel + + UNWIND nodes(path_principal) + nodes(path_chain) + nodes(path_admin) as n + OPTIONAL MATCH (n)-[pfr]-(pf:ProwlerFinding) + WHERE pf.status = 'FAIL' + + RETURN path_principal, path_chain, path_admin, + escalation_outcome, admin_rel, + chain_length, chain_nodes, + collect(DISTINCT pf) as dpf, collect(DISTINCT pfr) as dpfr + ORDER BY chain_length ASC + """, + parameters=[], + ), ], } diff --git a/docker-compose-dev.yml b/docker-compose-dev.yml index bc16d52eb1..410ad154ed 100644 --- a/docker-compose-dev.yml +++ b/docker-compose-dev.yml @@ -1,7 +1,7 @@ services: api-dev: hostname: "prowler-api" - image: prowler-api-dev + # image: prowler-api-dev build: context: ./api dockerfile: Dockerfile @@ -114,7 +114,7 @@ services: retries: 10 worker-dev: - image: prowler-api-dev + # image: prowler-api-dev build: context: ./api dockerfile: Dockerfile @@ -141,7 +141,7 @@ services: - "worker" worker-beat: - image: prowler-api-dev + # image: prowler-api-dev build: context: ./api dockerfile: Dockerfile diff --git a/ui/dependency-log.json b/ui/dependency-log.json index f7616cce83..7bee4f9cc3 100644 --- a/ui/dependency-log.json +++ b/ui/dependency-log.json @@ -42,10 +42,10 @@ { "section": "dependencies", "name": "@langchain/core", - "from": "0.3.77", - "to": "0.3.78", + "from": "0.3.78", + "to": "0.3.77", "strategy": "installed", - "generatedAt": "2025-11-03T07:43:34.628Z" + "generatedAt": "2026-01-07T08:46:39.109Z" }, { "section": "dependencies", diff --git a/ui/package-lock.json b/ui/package-lock.json index 704017dcf8..5ccdbcc08e 100644 --- a/ui/package-lock.json +++ b/ui/package-lock.json @@ -61,6 +61,7 @@ "react-hook-form": "7.62.0", "react-markdown": "10.1.0", "recharts": "2.15.4", + "require-in-the-middle": "8.0.1", "rss-parser": "3.13.0", "server-only": "0.0.1", "sharp": "0.33.5", @@ -5273,7 +5274,6 @@ "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation/-/instrumentation-0.203.0.tgz", "integrity": "sha512-ke1qyM+3AK2zPuBPb6Hk/GCsc5ewbLvPNkEuELx/JmANeEp6ZjnZ+wypPAJSucTw0wvCGrUaibDSdcrGFoWxKQ==", "license": "Apache-2.0", - "peer": true, "dependencies": { "@opentelemetry/api-logs": "0.203.0", "import-in-the-middle": "^1.8.1", @@ -5495,6 +5495,20 @@ "@opentelemetry/api": "^1.3.0" } }, + "node_modules/@opentelemetry/instrumentation-ioredis/node_modules/require-in-the-middle": { + "version": "7.5.2", + "resolved": "https://registry.npmjs.org/require-in-the-middle/-/require-in-the-middle-7.5.2.tgz", + "integrity": "sha512-gAZ+kLqBdHarXB64XpAe2VCjB7rIRv+mU8tfRWziHRJ5umKsIHN2tLLv6EtMw7WCdP19S0ERVMldNvxYCHnhSQ==", + "license": "MIT", + "dependencies": { + "debug": "^4.3.5", + "module-details-from-path": "^1.0.3", + "resolve": "^1.22.8" + }, + "engines": { + "node": ">=8.6.0" + } + }, "node_modules/@opentelemetry/instrumentation-kafkajs": { "version": "0.13.0", "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-kafkajs/-/instrumentation-kafkajs-0.13.0.tgz", @@ -5696,6 +5710,20 @@ "@opentelemetry/api": "^1.7.0" } }, + "node_modules/@opentelemetry/instrumentation/node_modules/require-in-the-middle": { + "version": "7.5.2", + "resolved": "https://registry.npmjs.org/require-in-the-middle/-/require-in-the-middle-7.5.2.tgz", + "integrity": "sha512-gAZ+kLqBdHarXB64XpAe2VCjB7rIRv+mU8tfRWziHRJ5umKsIHN2tLLv6EtMw7WCdP19S0ERVMldNvxYCHnhSQ==", + "license": "MIT", + "dependencies": { + "debug": "^4.3.5", + "module-details-from-path": "^1.0.3", + "resolve": "^1.22.8" + }, + "engines": { + "node": ">=8.6.0" + } + }, "node_modules/@opentelemetry/redis-common": { "version": "0.38.2", "resolved": "https://registry.npmjs.org/@opentelemetry/redis-common/-/redis-common-0.38.2.tgz", @@ -5848,6 +5876,20 @@ "@opentelemetry/api": "^1.3.0" } }, + "node_modules/@prisma/instrumentation/node_modules/require-in-the-middle": { + "version": "7.5.2", + "resolved": "https://registry.npmjs.org/require-in-the-middle/-/require-in-the-middle-7.5.2.tgz", + "integrity": "sha512-gAZ+kLqBdHarXB64XpAe2VCjB7rIRv+mU8tfRWziHRJ5umKsIHN2tLLv6EtMw7WCdP19S0ERVMldNvxYCHnhSQ==", + "license": "MIT", + "dependencies": { + "debug": "^4.3.5", + "module-details-from-path": "^1.0.3", + "resolve": "^1.22.8" + }, + "engines": { + "node": ">=8.6.0" + } + }, "node_modules/@prisma/instrumentation/node_modules/semver": { "version": "7.7.3", "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz", @@ -22881,17 +22923,16 @@ } }, "node_modules/require-in-the-middle": { - "version": "7.5.2", - "resolved": "https://registry.npmjs.org/require-in-the-middle/-/require-in-the-middle-7.5.2.tgz", - "integrity": "sha512-gAZ+kLqBdHarXB64XpAe2VCjB7rIRv+mU8tfRWziHRJ5umKsIHN2tLLv6EtMw7WCdP19S0ERVMldNvxYCHnhSQ==", + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/require-in-the-middle/-/require-in-the-middle-8.0.1.tgz", + "integrity": "sha512-QT7FVMXfWOYFbeRBF6nu+I6tr2Tf3u0q8RIEjNob/heKY/nh7drD/k7eeMFmSQgnTtCzLDcCu/XEnpW2wk4xCQ==", "license": "MIT", "dependencies": { "debug": "^4.3.5", - "module-details-from-path": "^1.0.3", - "resolve": "^1.22.8" + "module-details-from-path": "^1.0.3" }, "engines": { - "node": ">=8.6.0" + "node": ">=9.3.0 || >=8.10.0 <9.0.0" } }, "node_modules/resolve": { diff --git a/ui/package.json b/ui/package.json index 9531cb43af..60442e6505 100644 --- a/ui/package.json +++ b/ui/package.json @@ -75,6 +75,7 @@ "react-hook-form": "7.62.0", "react-markdown": "10.1.0", "recharts": "2.15.4", + "require-in-the-middle": "8.0.1", "rss-parser": "3.13.0", "server-only": "0.0.1", "sharp": "0.33.5",