diff --git a/api/CHANGELOG.md b/api/CHANGELOG.md index 4b404586a1..d99d1ea6f8 100644 --- a/api/CHANGELOG.md +++ b/api/CHANGELOG.md @@ -2,6 +2,14 @@ All notable changes to the **Prowler API** are documented in this file. +## [1.20.0] (Prowler UNRELEASED) + +### 🔄 Changed + +- Attack Paths: Queries definition now has short description and attribution [(#9983)](https://github.com/prowler-cloud/prowler/pull/9983) + +--- + ## [1.19.0] (Prowler v5.18.0) ### 🚀 Added diff --git a/api/src/backend/api/attack_paths/queries/aws.py b/api/src/backend/api/attack_paths/queries/aws.py index 60208b7e51..610816a57b 100644 --- a/api/src/backend/api/attack_paths/queries/aws.py +++ b/api/src/backend/api/attack_paths/queries/aws.py @@ -1,17 +1,18 @@ from api.attack_paths.queries.types import ( + AttackPathsQueryAttribution, AttackPathsQueryDefinition, AttackPathsQueryParameterDefinition, ) from tasks.jobs.attack_paths.config import PROWLER_FINDING_LABEL -# Privilege Escalation Queries (based on pathfinding.cloud research) -# https://github.com/DataDog/pathfinding.cloud -# ------------------------------------------------------------------- +# Custom Attack Path Queries +# -------------------------- AWS_INTERNET_EXPOSED_EC2_SENSITIVE_S3_ACCESS = AttackPathsQueryDefinition( id="aws-internet-exposed-ec2-sensitive-s3-access", - name="Identify internet-exposed EC2 instances with sensitive S3 access", + name="Internet-Exposed EC2 with Sensitive S3 Access", + short_description="Find SSH-exposed EC2 instances that can assume roles to read tagged sensitive S3 buckets.", description="Detect EC2 instances with SSH exposed to the internet that can assume higher-privileged roles to read tagged sensitive S3 buckets despite bucket-level public access blocks.", provider="aws", cypher=f""" @@ -35,8 +36,7 @@ AWS_INTERNET_EXPOSED_EC2_SENSITIVE_S3_ACCESS = AttackPathsQueryDefinition( YIELD rel AS can_access UNWIND nodes(path_s3) + nodes(path_ec2) + nodes(path_role) + nodes(path_assume_role) as n - OPTIONAL MATCH (n)-[pfr]-(pf:{PROWLER_FINDING_LABEL}) - WHERE pf.status = 'FAIL' + OPTIONAL MATCH (n)-[pfr]-(pf:{PROWLER_FINDING_LABEL} {{status: 'FAIL', provider_uid: $provider_uid}}) RETURN path_s3, path_ec2, path_role, path_assume_role, collect(DISTINCT pf) as dpf, collect(DISTINCT pfr) as dpfr, internet, can_access """, @@ -62,15 +62,15 @@ AWS_INTERNET_EXPOSED_EC2_SENSITIVE_S3_ACCESS = AttackPathsQueryDefinition( AWS_RDS_INSTANCES = AttackPathsQueryDefinition( id="aws-rds-instances", - name="Identify provisioned RDS instances", + name="RDS Instances Inventory", + short_description="List all provisioned RDS database instances in the account.", description="List the selected AWS account alongside the RDS instances it owns.", provider="aws", cypher=f""" MATCH path = (aws:AWSAccount {{id: $provider_uid}})--(rds:RDSInstance) UNWIND nodes(path) as n - OPTIONAL MATCH (n)-[pfr]-(pf:{PROWLER_FINDING_LABEL}) - WHERE pf.status = 'FAIL' + OPTIONAL MATCH (n)-[pfr]-(pf:{PROWLER_FINDING_LABEL} {{status: 'FAIL', provider_uid: $provider_uid}}) RETURN path, collect(DISTINCT pf) as dpf, collect(DISTINCT pfr) as dpfr """, @@ -79,7 +79,8 @@ AWS_RDS_INSTANCES = AttackPathsQueryDefinition( AWS_RDS_UNENCRYPTED_STORAGE = AttackPathsQueryDefinition( id="aws-rds-unencrypted-storage", - name="Identify RDS instances without storage encryption", + name="Unencrypted RDS Instances", + short_description="Find RDS instances with storage encryption disabled.", description="Find RDS instances with storage encryption disabled within the selected account.", provider="aws", cypher=f""" @@ -87,8 +88,7 @@ AWS_RDS_UNENCRYPTED_STORAGE = AttackPathsQueryDefinition( WHERE rds.storage_encrypted = false UNWIND nodes(path) as n - OPTIONAL MATCH (n)-[pfr]-(pf:{PROWLER_FINDING_LABEL}) - WHERE pf.status = 'FAIL' + OPTIONAL MATCH (n)-[pfr]-(pf:{PROWLER_FINDING_LABEL} {{status: 'FAIL', provider_uid: $provider_uid}}) RETURN path, collect(DISTINCT pf) as dpf, collect(DISTINCT pfr) as dpfr """, @@ -97,7 +97,8 @@ AWS_RDS_UNENCRYPTED_STORAGE = AttackPathsQueryDefinition( AWS_S3_ANONYMOUS_ACCESS_BUCKETS = AttackPathsQueryDefinition( id="aws-s3-anonymous-access-buckets", - name="Identify S3 buckets with anonymous access", + name="S3 Buckets with Anonymous Access", + short_description="Find S3 buckets that allow anonymous access.", description="Find S3 buckets that allow anonymous access within the selected account.", provider="aws", cypher=f""" @@ -105,8 +106,7 @@ AWS_S3_ANONYMOUS_ACCESS_BUCKETS = AttackPathsQueryDefinition( WHERE s3.anonymous_access = true UNWIND nodes(path) as n - OPTIONAL MATCH (n)-[pfr]-(pf:{PROWLER_FINDING_LABEL}) - WHERE pf.status = 'FAIL' + OPTIONAL MATCH (n)-[pfr]-(pf:{PROWLER_FINDING_LABEL} {{status: 'FAIL', provider_uid: $provider_uid}}) RETURN path, collect(DISTINCT pf) as dpf, collect(DISTINCT pfr) as dpfr """, @@ -115,7 +115,8 @@ AWS_S3_ANONYMOUS_ACCESS_BUCKETS = AttackPathsQueryDefinition( AWS_IAM_STATEMENTS_ALLOW_ALL_ACTIONS = AttackPathsQueryDefinition( id="aws-iam-statements-allow-all-actions", - name="Identify IAM statements that allow all actions", + name="IAM Statements Allowing All Actions", + short_description="Find IAM policy statements that allow all actions via wildcard (*).", description="Find IAM policy statements that allow all actions via '*' within the selected account.", provider="aws", cypher=f""" @@ -124,8 +125,7 @@ AWS_IAM_STATEMENTS_ALLOW_ALL_ACTIONS = AttackPathsQueryDefinition( AND any(x IN stmt.action WHERE x = '*') UNWIND nodes(path) as n - OPTIONAL MATCH (n)-[pfr]-(pf:{PROWLER_FINDING_LABEL}) - WHERE pf.status = 'FAIL' + OPTIONAL MATCH (n)-[pfr]-(pf:{PROWLER_FINDING_LABEL} {{status: 'FAIL', provider_uid: $provider_uid}}) RETURN path, collect(DISTINCT pf) as dpf, collect(DISTINCT pfr) as dpfr """, @@ -134,7 +134,8 @@ AWS_IAM_STATEMENTS_ALLOW_ALL_ACTIONS = AttackPathsQueryDefinition( AWS_IAM_STATEMENTS_ALLOW_DELETE_POLICY = AttackPathsQueryDefinition( id="aws-iam-statements-allow-delete-policy", - name="Identify IAM statements that allow iam:DeletePolicy", + name="IAM Statements Allowing Policy Deletion", + short_description="Find IAM policy statements that allow iam:DeletePolicy.", description="Find IAM policy statements that allow the iam:DeletePolicy action within the selected account.", provider="aws", cypher=f""" @@ -143,8 +144,7 @@ AWS_IAM_STATEMENTS_ALLOW_DELETE_POLICY = AttackPathsQueryDefinition( AND any(x IN stmt.action WHERE x = "iam:DeletePolicy") UNWIND nodes(path) as n - OPTIONAL MATCH (n)-[pfr]-(pf:{PROWLER_FINDING_LABEL}) - WHERE pf.status = 'FAIL' + OPTIONAL MATCH (n)-[pfr]-(pf:{PROWLER_FINDING_LABEL} {{status: 'FAIL', provider_uid: $provider_uid}}) RETURN path, collect(DISTINCT pf) as dpf, collect(DISTINCT pfr) as dpfr """, @@ -153,7 +153,8 @@ AWS_IAM_STATEMENTS_ALLOW_DELETE_POLICY = AttackPathsQueryDefinition( AWS_IAM_STATEMENTS_ALLOW_CREATE_ACTIONS = AttackPathsQueryDefinition( id="aws-iam-statements-allow-create-actions", - name="Identify IAM statements that allow create actions", + name="IAM Statements Allowing Create Actions", + short_description="Find IAM policy statements that allow any create action.", description="Find IAM policy statements that allow actions containing 'create' within the selected account.", provider="aws", cypher=f""" @@ -162,8 +163,7 @@ AWS_IAM_STATEMENTS_ALLOW_CREATE_ACTIONS = AttackPathsQueryDefinition( AND any(x IN stmt.action WHERE toLower(x) CONTAINS "create") UNWIND nodes(path) as n - OPTIONAL MATCH (n)-[pfr]-(pf:{PROWLER_FINDING_LABEL}) - WHERE pf.status = 'FAIL' + OPTIONAL MATCH (n)-[pfr]-(pf:{PROWLER_FINDING_LABEL} {{status: 'FAIL', provider_uid: $provider_uid}}) RETURN path, collect(DISTINCT pf) as dpf, collect(DISTINCT pfr) as dpfr """, @@ -176,7 +176,8 @@ AWS_IAM_STATEMENTS_ALLOW_CREATE_ACTIONS = AttackPathsQueryDefinition( AWS_EC2_INSTANCES_INTERNET_EXPOSED = AttackPathsQueryDefinition( id="aws-ec2-instances-internet-exposed", - name="Identify internet-exposed EC2 instances", + name="Internet-Exposed EC2 Instances", + short_description="Find EC2 instances flagged as exposed to the internet.", description="Find EC2 instances flagged as exposed to the internet within the selected account.", provider="aws", cypher=f""" @@ -190,8 +191,7 @@ AWS_EC2_INSTANCES_INTERNET_EXPOSED = AttackPathsQueryDefinition( YIELD rel AS can_access UNWIND nodes(path) as n - OPTIONAL MATCH (n)-[pfr]-(pf:{PROWLER_FINDING_LABEL}) - WHERE pf.status = 'FAIL' + OPTIONAL MATCH (n)-[pfr]-(pf:{PROWLER_FINDING_LABEL} {{status: 'FAIL', provider_uid: $provider_uid}}) RETURN path, collect(DISTINCT pf) as dpf, collect(DISTINCT pfr) as dpfr, internet, can_access """, @@ -200,7 +200,8 @@ AWS_EC2_INSTANCES_INTERNET_EXPOSED = AttackPathsQueryDefinition( AWS_SECURITY_GROUPS_OPEN_INTERNET_FACING = AttackPathsQueryDefinition( id="aws-security-groups-open-internet-facing", - name="Identify internet-facing resources with open security groups", + name="Open Security Groups on Internet-Facing Resources", + short_description="Find internet-facing resources with security groups allowing inbound from 0.0.0.0/0.", description="Find internet-facing resources associated with security groups that allow inbound access from '0.0.0.0/0'.", provider="aws", cypher=f""" @@ -216,8 +217,7 @@ AWS_SECURITY_GROUPS_OPEN_INTERNET_FACING = AttackPathsQueryDefinition( YIELD rel AS can_access UNWIND nodes(path_ec2) as n - OPTIONAL MATCH (n)-[pfr]-(pf:{PROWLER_FINDING_LABEL}) - WHERE pf.status = 'FAIL' + OPTIONAL MATCH (n)-[pfr]-(pf:{PROWLER_FINDING_LABEL} {{status: 'FAIL', provider_uid: $provider_uid}}) RETURN path_ec2, collect(DISTINCT pf) as dpf, collect(DISTINCT pfr) as dpfr, internet, can_access """, @@ -226,7 +226,8 @@ AWS_SECURITY_GROUPS_OPEN_INTERNET_FACING = AttackPathsQueryDefinition( AWS_CLASSIC_ELB_INTERNET_EXPOSED = AttackPathsQueryDefinition( id="aws-classic-elb-internet-exposed", - name="Identify internet-exposed Classic Load Balancers", + name="Internet-Exposed Classic Load Balancers", + short_description="Find Classic Load Balancers exposed to the internet with their listeners.", description="Find Classic Load Balancers exposed to the internet along with their listeners.", provider="aws", cypher=f""" @@ -240,8 +241,7 @@ AWS_CLASSIC_ELB_INTERNET_EXPOSED = AttackPathsQueryDefinition( YIELD rel AS can_access UNWIND nodes(path) as n - OPTIONAL MATCH (n)-[pfr]-(pf:{PROWLER_FINDING_LABEL}) - WHERE pf.status = 'FAIL' + OPTIONAL MATCH (n)-[pfr]-(pf:{PROWLER_FINDING_LABEL} {{status: 'FAIL', provider_uid: $provider_uid}}) RETURN path, collect(DISTINCT pf) as dpf, collect(DISTINCT pfr) as dpfr, internet, can_access """, @@ -250,7 +250,8 @@ AWS_CLASSIC_ELB_INTERNET_EXPOSED = AttackPathsQueryDefinition( AWS_ELBV2_INTERNET_EXPOSED = AttackPathsQueryDefinition( id="aws-elbv2-internet-exposed", - name="Identify internet-exposed ELBv2 load balancers", + name="Internet-Exposed ALB/NLB Load Balancers", + short_description="Find ELBv2 (ALB/NLB) load balancers exposed to the internet with their listeners.", description="Find ELBv2 load balancers exposed to the internet along with their listeners.", provider="aws", cypher=f""" @@ -264,8 +265,7 @@ AWS_ELBV2_INTERNET_EXPOSED = AttackPathsQueryDefinition( YIELD rel AS can_access UNWIND nodes(path) as n - OPTIONAL MATCH (n)-[pfr]-(pf:{PROWLER_FINDING_LABEL}) - WHERE pf.status = 'FAIL' + OPTIONAL MATCH (n)-[pfr]-(pf:{PROWLER_FINDING_LABEL} {{status: 'FAIL', provider_uid: $provider_uid}}) RETURN path, collect(DISTINCT pf) as dpf, collect(DISTINCT pfr) as dpfr, internet, can_access """, @@ -274,7 +274,8 @@ AWS_ELBV2_INTERNET_EXPOSED = AttackPathsQueryDefinition( AWS_PUBLIC_IP_RESOURCE_LOOKUP = AttackPathsQueryDefinition( id="aws-public-ip-resource-lookup", - name="Identify resources by public IP address", + name="Resource Lookup by Public IP", + short_description="Find the AWS resource associated with a given public IP address.", description="Given a public IP address, find the related AWS resource and its adjacent node within the selected account.", provider="aws", cypher=f""" @@ -305,8 +306,7 @@ AWS_PUBLIC_IP_RESOURCE_LOOKUP = AttackPathsQueryDefinition( YIELD rel AS can_access UNWIND nodes(path) as n - OPTIONAL MATCH (n)-[pfr]-(pf:{PROWLER_FINDING_LABEL}) - WHERE pf.status = 'FAIL' + OPTIONAL MATCH (n)-[pfr]-(pf:{PROWLER_FINDING_LABEL} {{status: 'FAIL', provider_uid: $provider_uid}}) RETURN path, collect(DISTINCT pf) as dpf, collect(DISTINCT pfr) as dpfr, internet, can_access """, @@ -320,38 +320,24 @@ AWS_PUBLIC_IP_RESOURCE_LOOKUP = AttackPathsQueryDefinition( ], ) +# Privilege Escalation Queries (based on pathfinding.cloud research) +# https://github.com/DataDog/pathfinding.cloud +# ------------------------------------------------------------------- -AWS_IAM_PRIVESC_PASSROLE_EC2 = 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).", +# BEDROCK-001 +AWS_BEDROCK_PRIVESC_PASSROLE_CODE_INTERPRETER = AttackPathsQueryDefinition( + id="aws-bedrock-privesc-passrole-code-interpreter", + name="Bedrock Code Interpreter with Privileged Role (BEDROCK-001)", + short_description="Create a Bedrock AgentCore Code Interpreter with a privileged role attached.", + description="Detect principals who can pass IAM roles and create Bedrock AgentCore Code Interpreters. This allows creating a code interpreter with a privileged role attached, gaining that role's permissions.", + attribution=AttackPathsQueryAttribution( + text="pathfinding.cloud - BEDROCK-001 - iam:PassRole + bedrock-agentcore:CreateCodeInterpreter", + link="https://pathfinding.cloud/paths/bedrock-001", + ), provider="aws", cypher=f""" - // Create a single shared virtual EC2 instance node - CALL apoc.create.vNode(['EC2Instance'], {{ - id: 'potential-ec2-passrole', - name: 'New EC2 Instance', - description: 'Attacker-controlled EC2 with privileged role' - }}) - YIELD node AS ec2_node - - // Create a single shared virtual escalation outcome node (styled like a finding) - CALL apoc.create.vNode(['PrivilegeEscalation'], {{ - id: 'effective-administrator-passrole-ec2', - check_title: 'Privilege Escalation', - name: 'Effective Administrator', - status: 'FAIL', - severity: 'critical' - }}) - YIELD node AS escalation_outcome - - WITH ec2_node, escalation_outcome - - // 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) + // Find principals with iam:PassRole permission + MATCH path_principal = (aws:AWSAccount {{id: $provider_uid}})--(principal:AWSPrincipal)--(passrole_policy:AWSPolicy)--(stmt_passrole:AWSPolicyStatement) WHERE stmt_passrole.effect = 'Allow' AND any(action IN stmt_passrole.action WHERE toLower(action) = 'iam:passrole' @@ -359,8 +345,55 @@ AWS_IAM_PRIVESC_PASSROLE_EC2 = AttackPathsQueryDefinition( OR action = '*' ) - // Find statements granting ec2:RunInstances - MATCH path_ec2 = (principal)--(ec2_policy:AWSPolicy)--(stmt_ec2:AWSPolicyStatement) + // Find bedrock-agentcore:CreateCodeInterpreter permission + MATCH (principal)--(bedrock_policy:AWSPolicy)--(stmt_bedrock:AWSPolicyStatement) + WHERE stmt_bedrock.effect = 'Allow' + AND any(action IN stmt_bedrock.action WHERE + toLower(action) = 'bedrock-agentcore:createcodeinterpreter' + OR toLower(action) = 'bedrock-agentcore:*' + OR action = '*' + ) + + // Find roles that trust Bedrock service (can be passed to Bedrock) + MATCH path_target = (aws)--(target_role:AWSRole)-[:TRUSTS_AWS_PRINCIPAL]->(:AWSPrincipal {{arn: 'bedrock.amazonaws.com'}}) + WHERE any(resource IN stmt_passrole.resource WHERE + resource = '*' + OR target_role.arn CONTAINS resource + OR resource CONTAINS target_role.name + ) + + UNWIND nodes(path_principal) + nodes(path_target) as n + OPTIONAL MATCH (n)-[pfr]-(pf:{PROWLER_FINDING_LABEL} {{status: 'FAIL', provider_uid: $provider_uid}}) + + RETURN path_principal, path_target, + collect(DISTINCT pf) as dpf, collect(DISTINCT pfr) as dpfr + """, + parameters=[], +) + +# EC2-001 +AWS_EC2_PRIVESC_PASSROLE_IAM = AttackPathsQueryDefinition( + id="aws-ec2-privesc-passrole-iam", + name="EC2 Instance Launch with Privileged Role (EC2-001)", + short_description="Launch EC2 instances with privileged IAM roles to gain their permissions via IMDS.", + 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.", + attribution=AttackPathsQueryAttribution( + text="pathfinding.cloud - EC2-001 - iam:PassRole + ec2:RunInstances", + link="https://pathfinding.cloud/paths/ec2-001", + ), + provider="aws", + cypher=f""" + // Find principals with iam:PassRole permission + MATCH path_principal = (aws:AWSAccount {{id: $provider_uid}})--(principal:AWSPrincipal)--(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 ec2:RunInstances permission + MATCH (principal)--(ec2_policy:AWSPolicy)--(stmt_ec2:AWSPolicyStatement) WHERE stmt_ec2.effect = 'Allow' AND any(action IN stmt_ec2.action WHERE toLower(action) = 'ec2:runinstances' @@ -369,149 +402,465 @@ AWS_IAM_PRIVESC_PASSROLE_EC2 = AttackPathsQueryDefinition( ) // 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 - ) + MATCH path_target = (aws)--(target_role:AWSRole)-[:TRUSTS_AWS_PRINCIPAL]->(:AWSPrincipal {{arn: 'ec2.amazonaws.com'}}) + WHERE 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:*') - ) - - 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', {{ - reference: 'https://pathfinding.cloud/paths/ec2-001' - }}, 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:{PROWLER_FINDING_LABEL}) - 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=[], -) - -# TODO: Add ProwlerFinding nodes -AWS_GLUE_PRIVESC_PASSROLE_DEV_ENDPOINT = AttackPathsQueryDefinition( - id="aws-glue-privesc-passrole-dev-endpoint", - name="Privilege Escalation: Glue Dev Endpoint with PassRole", - description="Detect principals that can escalate privileges by passing a role to a Glue development endpoint. The attacker creates a dev endpoint with an arbitrary role attached, then accesses those credentials through the endpoint.", - provider="aws", - cypher=""" - CALL apoc.create.vNode(['PrivilegeEscalation'], { - id: 'effective-administrator-glue', - check_title: 'Privilege Escalation', - name: 'Effective Administrator (Glue)', - status: 'FAIL', - severity: 'critical' - }) - YIELD node AS escalation_outcome - - WITH escalation_outcome - - // Find principals in the account - MATCH path_principal = (aws:AWSAccount {id: $provider_uid})--(principal:AWSPrincipal) - - // Principal can assume roles (up to 2 hops) - OPTIONAL MATCH path_assume = (principal)-[:STS_ASSUMEROLE_ALLOW*0..2]->(acting_as:AWSRole) - WITH escalation_outcome, principal, path_principal, path_assume, - CASE WHEN path_assume IS NULL THEN principal ELSE acting_as END AS effective_principal - - // Find iam:PassRole permission - MATCH path_passrole = (effective_principal)--(passrole_policy:AWSPolicy)--(passrole_stmt:AWSPolicyStatement) - WHERE passrole_stmt.effect = 'Allow' - AND any(action IN passrole_stmt.action WHERE toLower(action) = 'iam:passrole' OR action = '*') - - // Find Glue CreateDevEndpoint permission - MATCH (effective_principal)--(glue_policy:AWSPolicy)--(glue_stmt:AWSPolicyStatement) - WHERE glue_stmt.effect = 'Allow' - AND any(action IN glue_stmt.action WHERE toLower(action) = 'glue:createdevendpoint' OR action = '*' OR toLower(action) = 'glue:*') - - // Find target role with elevated permissions - MATCH (aws)--(target_role:AWSRole)--(target_policy:AWSPolicy)--(target_stmt:AWSPolicyStatement) - WHERE target_stmt.effect = 'Allow' - AND ( - any(action IN target_stmt.action WHERE action = '*') - OR any(action IN target_stmt.action WHERE toLower(action) = 'iam:*') - ) - - // Deduplicate before creating virtual nodes - WITH DISTINCT escalation_outcome, aws, principal, effective_principal, target_role - - // Create virtual Glue endpoint node (one per unique principal->target pair) - CALL apoc.create.vNode(['GlueDevEndpoint'], { - name: 'New Dev Endpoint', - description: 'Glue endpoint with target role attached', - id: effective_principal.arn + '->' + target_role.arn - }) - YIELD node AS glue_endpoint - - CALL apoc.create.vRelationship(effective_principal, 'CREATES_ENDPOINT', { - permissions: ['iam:PassRole', 'glue:CreateDevEndpoint'], - technique: 'new-passrole' - }, glue_endpoint) - YIELD rel AS create_rel - - CALL apoc.create.vRelationship(glue_endpoint, 'RUNS_AS', {}, target_role) - YIELD rel AS runs_rel - - CALL apoc.create.vRelationship(target_role, 'GRANTS_ACCESS', { - reference: 'https://pathfinding.cloud/paths/glue-001' - }, escalation_outcome) - YIELD rel AS grants_rel - - // Re-match paths for visualization - MATCH path_principal = (aws)--(principal) - MATCH path_target = (aws)--(target_role) + UNWIND nodes(path_principal) + nodes(path_target) as n + OPTIONAL MATCH (n)-[pfr]-(pf:{PROWLER_FINDING_LABEL} {{status: 'FAIL', provider_uid: $provider_uid}}) RETURN path_principal, path_target, - glue_endpoint, escalation_outcome, create_rel, runs_rel, grants_rel + collect(DISTINCT pf) as dpf, collect(DISTINCT pfr) as dpfr """, parameters=[], ) -AWS_IAM_PRIVESC_ATTACH_ROLE_POLICY_ASSUME_ROLE = 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).", +# EC2-002 +AWS_EC2_PRIVESC_MODIFY_INSTANCE_ATTRIBUTE = AttackPathsQueryDefinition( + id="aws-ec2-privesc-modify-instance-attribute", + name="EC2 Role Hijacking via UserData Injection (EC2-002)", + short_description="Inject malicious scripts into EC2 instance userData to gain the attached role's permissions.", + description="Detect principals who can modify EC2 instance userData, stop, and start instances. This allows injecting malicious scripts that execute on instance restart, gaining the permissions of the instance's attached IAM role.", + attribution=AttackPathsQueryAttribution( + text="pathfinding.cloud - EC2-002 - ec2:ModifyInstanceAttribute + ec2:StopInstances + ec2:StartInstances", + link="https://pathfinding.cloud/paths/ec2-002", + ), provider="aws", cypher=f""" - // Create a virtual escalation outcome node (styled like a finding) - CALL apoc.create.vNode(['PrivilegeEscalation'], {{ - id: 'effective-administrator', - check_title: 'Privilege Escalation', - name: 'Effective Administrator', - status: 'FAIL', - severity: 'critical' - }}) - YIELD node AS admin_outcome + // Find principals with ec2:ModifyInstanceAttribute permission + MATCH path_principal = (aws:AWSAccount {{id: $provider_uid}})--(principal:AWSPrincipal)--(modify_policy:AWSPolicy)--(stmt_modify:AWSPolicyStatement) + WHERE stmt_modify.effect = 'Allow' + AND any(action IN stmt_modify.action WHERE + toLower(action) = 'ec2:modifyinstanceattribute' + OR toLower(action) = 'ec2:*' + OR action = '*' + ) - WITH admin_outcome + // Find ec2:StopInstances permission (can be same or different policy) + MATCH (principal)--(stop_policy:AWSPolicy)--(stmt_stop:AWSPolicyStatement) + WHERE stmt_stop.effect = 'Allow' + AND any(action IN stmt_stop.action WHERE + toLower(action) = 'ec2:stopinstances' + OR toLower(action) = 'ec2:*' + OR action = '*' + ) - // Find principals in the account - MATCH path_principal = (aws:AWSAccount {{id: $provider_uid}})--(principal:AWSPrincipal) + // Find ec2:StartInstances permission (can be same or different policy) + MATCH (principal)--(start_policy:AWSPolicy)--(stmt_start:AWSPolicyStatement) + WHERE stmt_start.effect = 'Allow' + AND any(action IN stmt_start.action WHERE + toLower(action) = 'ec2:startinstances' + OR toLower(action) = 'ec2:*' + OR action = '*' + ) - // Find statements granting iam:AttachRolePolicy - MATCH path_attach = (principal)--(attach_policy:AWSPolicy)--(stmt_attach:AWSPolicyStatement) + // Find EC2 instances with instance profiles (potential targets) + MATCH path_target = (aws)--(ec2:EC2Instance)-[:STS_ASSUMEROLE_ALLOW]->(target_role:AWSRole) + + UNWIND nodes(path_principal) + nodes(path_target) as n + OPTIONAL MATCH (n)-[pfr]-(pf:{PROWLER_FINDING_LABEL} {{status: 'FAIL', provider_uid: $provider_uid}}) + + RETURN path_principal, path_target, + collect(DISTINCT pf) as dpf, collect(DISTINCT pfr) as dpfr + """, + parameters=[], +) + +# EC2-003 +AWS_EC2_PRIVESC_PASSROLE_SPOT_INSTANCES = AttackPathsQueryDefinition( + id="aws-ec2-privesc-passrole-spot-instances", + name="Spot Instance Launch with Privileged Role (EC2-003)", + short_description="Launch EC2 Spot Instances with privileged IAM roles to gain their permissions via IMDS.", + description="Detect principals who can pass IAM roles and request EC2 Spot Instances. This allows launching a spot instance with a privileged role attached, gaining that role's permissions via the instance metadata service.", + attribution=AttackPathsQueryAttribution( + text="pathfinding.cloud - EC2-003 - iam:PassRole + ec2:RequestSpotInstances", + link="https://pathfinding.cloud/paths/ec2-003", + ), + provider="aws", + cypher=f""" + // Find principals with iam:PassRole permission + MATCH path_principal = (aws:AWSAccount {{id: $provider_uid}})--(principal:AWSPrincipal)--(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 ec2:RequestSpotInstances permission + MATCH (principal)--(spot_policy:AWSPolicy)--(stmt_spot:AWSPolicyStatement) + WHERE stmt_spot.effect = 'Allow' + AND any(action IN stmt_spot.action WHERE + toLower(action) = 'ec2:requestspotinstances' + OR toLower(action) = 'ec2:*' + OR action = '*' + ) + + // Find roles that trust EC2 service (can be passed to EC2 spot instances) + MATCH path_target = (aws)--(target_role:AWSRole)-[:TRUSTS_AWS_PRINCIPAL]->(:AWSPrincipal {{arn: 'ec2.amazonaws.com'}}) + WHERE any(resource IN stmt_passrole.resource WHERE + resource = '*' + OR target_role.arn CONTAINS resource + OR resource CONTAINS target_role.name + ) + + UNWIND nodes(path_principal) + nodes(path_target) as n + OPTIONAL MATCH (n)-[pfr]-(pf:{PROWLER_FINDING_LABEL} {{status: 'FAIL', provider_uid: $provider_uid}}) + + RETURN path_principal, path_target, + collect(DISTINCT pf) as dpf, collect(DISTINCT pfr) as dpfr + """, + parameters=[], +) + +# EC2-004 +AWS_EC2_PRIVESC_LAUNCH_TEMPLATE = AttackPathsQueryDefinition( + id="aws-ec2-privesc-launch-template", + name="Launch Template Poisoning for Role Access (EC2-004)", + short_description="Inject malicious userData into launch templates that reference privileged roles, no PassRole needed.", + description="Detect principals who can create new launch template versions and modify launch templates. This allows injecting malicious user data into existing templates that already reference privileged IAM roles, without requiring iam:PassRole permissions.", + attribution=AttackPathsQueryAttribution( + text="pathfinding.cloud - EC2-004 - ec2:CreateLaunchTemplateVersion + ec2:ModifyLaunchTemplate", + link="https://pathfinding.cloud/paths/ec2-004", + ), + provider="aws", + cypher=f""" + // Find principals with ec2:CreateLaunchTemplateVersion permission + MATCH path_principal = (aws:AWSAccount {{id: $provider_uid}})--(principal:AWSPrincipal)--(create_policy:AWSPolicy)--(stmt_create:AWSPolicyStatement) + WHERE stmt_create.effect = 'Allow' + AND any(action IN stmt_create.action WHERE + toLower(action) = 'ec2:createlaunchtemplateversion' + OR toLower(action) = 'ec2:*' + OR action = '*' + ) + + // Find ec2:ModifyLaunchTemplate permission + MATCH (principal)--(modify_policy:AWSPolicy)--(stmt_modify:AWSPolicyStatement) + WHERE stmt_modify.effect = 'Allow' + AND any(action IN stmt_modify.action WHERE + toLower(action) = 'ec2:modifylaunchtemplate' + OR toLower(action) = 'ec2:*' + OR action = '*' + ) + + // Find launch templates in the account (potential targets) + MATCH path_target = (aws)--(template:LaunchTemplate) + + UNWIND nodes(path_principal) + nodes(path_target) as n + OPTIONAL MATCH (n)-[pfr]-(pf:{PROWLER_FINDING_LABEL} {{status: 'FAIL', provider_uid: $provider_uid}}) + + RETURN path_principal, path_target, + collect(DISTINCT pf) as dpf, collect(DISTINCT pfr) as dpfr + """, + parameters=[], +) + +# ECS-001 +AWS_ECS_PRIVESC_PASSROLE_CREATE_SERVICE = AttackPathsQueryDefinition( + id="aws-ecs-privesc-passrole-create-service", + name="ECS Service Creation with Privileged Role (ECS-001 - New Cluster)", + short_description="Create an ECS cluster and service with a privileged Fargate task role to execute arbitrary code.", + description="Detect principals who can pass IAM roles, create ECS clusters, register task definitions, and create services. This allows creating a Fargate task with a privileged role attached, gaining that role's permissions to execute arbitrary code via the container.", + provider="aws", + attribution=AttackPathsQueryAttribution( + text="pathfinding.cloud - ECS-001 - iam:PassRole + ecs:CreateCluster + ecs:RegisterTaskDefinition + ecs:CreateService", + link="https://pathfinding.cloud/paths/ecs-001", + ), + cypher=f""" + // Find principals with iam:PassRole permission + MATCH path_principal = (aws:AWSAccount {{id: $provider_uid}})--(principal:AWSPrincipal)--(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 ecs:CreateCluster permission + MATCH (principal)--(cluster_policy:AWSPolicy)--(stmt_cluster:AWSPolicyStatement) + WHERE stmt_cluster.effect = 'Allow' + AND any(action IN stmt_cluster.action WHERE + toLower(action) = 'ecs:createcluster' + OR toLower(action) = 'ecs:*' + OR action = '*' + ) + + // Find ecs:RegisterTaskDefinition permission + MATCH (principal)--(taskdef_policy:AWSPolicy)--(stmt_taskdef:AWSPolicyStatement) + WHERE stmt_taskdef.effect = 'Allow' + AND any(action IN stmt_taskdef.action WHERE + toLower(action) = 'ecs:registertaskdefinition' + OR toLower(action) = 'ecs:*' + OR action = '*' + ) + + // Find ecs:CreateService permission + MATCH (principal)--(service_policy:AWSPolicy)--(stmt_service:AWSPolicyStatement) + WHERE stmt_service.effect = 'Allow' + AND any(action IN stmt_service.action WHERE + toLower(action) = 'ecs:createservice' + OR toLower(action) = 'ecs:*' + OR action = '*' + ) + + // Find roles that trust ECS tasks service (can be passed to ECS tasks) + MATCH path_target = (aws)--(target_role:AWSRole)-[:TRUSTS_AWS_PRINCIPAL]->(:AWSPrincipal {{arn: 'ecs-tasks.amazonaws.com'}}) + WHERE any(resource IN stmt_passrole.resource WHERE + resource = '*' + OR target_role.arn CONTAINS resource + OR resource CONTAINS target_role.name + ) + + UNWIND nodes(path_principal) + nodes(path_target) as n + OPTIONAL MATCH (n)-[pfr]-(pf:{PROWLER_FINDING_LABEL} {{status: 'FAIL', provider_uid: $provider_uid}}) + + RETURN path_principal, path_target, + collect(DISTINCT pf) as dpf, collect(DISTINCT pfr) as dpfr + """, + parameters=[], +) + +# ECS-002 +AWS_ECS_PRIVESC_PASSROLE_RUN_TASK = AttackPathsQueryDefinition( + id="aws-ecs-privesc-passrole-run-task", + name="ECS Task Execution with Privileged Role (ECS-002 - New Cluster)", + short_description="Create an ECS cluster and run a one-off Fargate task with a privileged role to execute arbitrary code.", + description="Detect principals who can pass IAM roles, create ECS clusters, register task definitions, and run tasks. This allows creating a Fargate task with a privileged role attached, gaining that role's permissions to execute arbitrary code via the container. Unlike ecs:CreateService, ecs:RunTask executes the task once without creating a persistent service.", + attribution=AttackPathsQueryAttribution( + text="pathfinding.cloud - ECS-002 - iam:PassRole + ecs:CreateCluster + ecs:RegisterTaskDefinition + ecs:RunTask", + link="https://pathfinding.cloud/paths/ecs-002", + ), + provider="aws", + cypher=f""" + // Find principals with iam:PassRole permission + MATCH path_principal = (aws:AWSAccount {{id: $provider_uid}})--(principal:AWSPrincipal)--(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 ecs:CreateCluster permission + MATCH (principal)--(cluster_policy:AWSPolicy)--(stmt_cluster:AWSPolicyStatement) + WHERE stmt_cluster.effect = 'Allow' + AND any(action IN stmt_cluster.action WHERE + toLower(action) = 'ecs:createcluster' + OR toLower(action) = 'ecs:*' + OR action = '*' + ) + + // Find ecs:RegisterTaskDefinition permission + MATCH (principal)--(taskdef_policy:AWSPolicy)--(stmt_taskdef:AWSPolicyStatement) + WHERE stmt_taskdef.effect = 'Allow' + AND any(action IN stmt_taskdef.action WHERE + toLower(action) = 'ecs:registertaskdefinition' + OR toLower(action) = 'ecs:*' + OR action = '*' + ) + + // Find ecs:RunTask permission + MATCH (principal)--(runtask_policy:AWSPolicy)--(stmt_runtask:AWSPolicyStatement) + WHERE stmt_runtask.effect = 'Allow' + AND any(action IN stmt_runtask.action WHERE + toLower(action) = 'ecs:runtask' + OR toLower(action) = 'ecs:*' + OR action = '*' + ) + + // Find roles that trust ECS tasks service (can be passed to ECS tasks) + MATCH path_target = (aws)--(target_role:AWSRole)-[:TRUSTS_AWS_PRINCIPAL]->(:AWSPrincipal {{arn: 'ecs-tasks.amazonaws.com'}}) + WHERE any(resource IN stmt_passrole.resource WHERE + resource = '*' + OR target_role.arn CONTAINS resource + OR resource CONTAINS target_role.name + ) + + UNWIND nodes(path_principal) + nodes(path_target) as n + OPTIONAL MATCH (n)-[pfr]-(pf:{PROWLER_FINDING_LABEL} {{status: 'FAIL', provider_uid: $provider_uid}}) + + RETURN path_principal, path_target, + collect(DISTINCT pf) as dpf, collect(DISTINCT pfr) as dpfr + """, + parameters=[], +) + +# ECS-003 +AWS_ECS_PRIVESC_PASSROLE_CREATE_SERVICE_EXISTING_CLUSTER = AttackPathsQueryDefinition( + id="aws-ecs-privesc-passrole-create-service-existing-cluster", + name="ECS Service Creation with Privileged Role (ECS-003 - Existing Cluster)", + short_description="Deploy a Fargate service with a privileged role on an existing ECS cluster.", + description="Detect principals who can pass IAM roles, register ECS task definitions, and create services on existing clusters. Unlike ECS-001, this does not require ecs:CreateCluster since it targets clusters that already exist. The attacker registers a task definition with a privileged role and launches it as a Fargate service, gaining that role's permissions.", + attribution=AttackPathsQueryAttribution( + text="pathfinding.cloud - ECS-003 - iam:PassRole + ecs:RegisterTaskDefinition + ecs:CreateService", + link="https://pathfinding.cloud/paths/ecs-003", + ), + provider="aws", + cypher=f""" + // Find principals with iam:PassRole permission + MATCH path_principal = (aws:AWSAccount {{id: $provider_uid}})--(principal:AWSPrincipal)--(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 ecs:RegisterTaskDefinition permission + MATCH (principal)--(taskdef_policy:AWSPolicy)--(stmt_taskdef:AWSPolicyStatement) + WHERE stmt_taskdef.effect = 'Allow' + AND any(action IN stmt_taskdef.action WHERE + toLower(action) = 'ecs:registertaskdefinition' + OR toLower(action) = 'ecs:*' + OR action = '*' + ) + + // Find ecs:CreateService permission + MATCH (principal)--(service_policy:AWSPolicy)--(stmt_service:AWSPolicyStatement) + WHERE stmt_service.effect = 'Allow' + AND any(action IN stmt_service.action WHERE + toLower(action) = 'ecs:createservice' + OR toLower(action) = 'ecs:*' + OR action = '*' + ) + + // Find roles that trust ECS tasks service (can be passed to ECS tasks) + MATCH path_target = (aws)--(target_role:AWSRole)-[:TRUSTS_AWS_PRINCIPAL]->(:AWSPrincipal {{arn: 'ecs-tasks.amazonaws.com'}}) + WHERE any(resource IN stmt_passrole.resource WHERE + resource = '*' + OR target_role.arn CONTAINS resource + OR resource CONTAINS target_role.name + ) + + UNWIND nodes(path_principal) + nodes(path_target) as n + OPTIONAL MATCH (n)-[pfr]-(pf:{PROWLER_FINDING_LABEL} {{status: 'FAIL', provider_uid: $provider_uid}}) + + RETURN path_principal, path_target, + collect(DISTINCT pf) as dpf, collect(DISTINCT pfr) as dpfr + """, + parameters=[], +) + +# ECS-004 +AWS_ECS_PRIVESC_PASSROLE_RUN_TASK_EXISTING_CLUSTER = AttackPathsQueryDefinition( + id="aws-ecs-privesc-passrole-run-task-existing-cluster", + name="ECS Task Execution with Privileged Role (ECS-004 - Existing Cluster)", + short_description="Run a one-off Fargate task with a privileged role on an existing ECS cluster.", + description="Detect principals who can pass IAM roles, register ECS task definitions, and run tasks on existing clusters. Unlike ECS-002, this does not require ecs:CreateCluster since it targets clusters that already exist. The attacker registers a task definition with a privileged role and runs it as a one-off Fargate task, gaining that role's permissions.", + attribution=AttackPathsQueryAttribution( + text="pathfinding.cloud - ECS-004 - iam:PassRole + ecs:RegisterTaskDefinition + ecs:RunTask", + link="https://pathfinding.cloud/paths/ecs-004", + ), + provider="aws", + cypher=f""" + // Find principals with iam:PassRole permission + MATCH path_principal = (aws:AWSAccount {{id: $provider_uid}})--(principal:AWSPrincipal)--(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 ecs:RegisterTaskDefinition permission + MATCH (principal)--(taskdef_policy:AWSPolicy)--(stmt_taskdef:AWSPolicyStatement) + WHERE stmt_taskdef.effect = 'Allow' + AND any(action IN stmt_taskdef.action WHERE + toLower(action) = 'ecs:registertaskdefinition' + OR toLower(action) = 'ecs:*' + OR action = '*' + ) + + // Find ecs:RunTask permission + MATCH (principal)--(runtask_policy:AWSPolicy)--(stmt_runtask:AWSPolicyStatement) + WHERE stmt_runtask.effect = 'Allow' + AND any(action IN stmt_runtask.action WHERE + toLower(action) = 'ecs:runtask' + OR toLower(action) = 'ecs:*' + OR action = '*' + ) + + // Find roles that trust ECS tasks service (can be passed to ECS tasks) + MATCH path_target = (aws)--(target_role:AWSRole)-[:TRUSTS_AWS_PRINCIPAL]->(:AWSPrincipal {{arn: 'ecs-tasks.amazonaws.com'}}) + WHERE any(resource IN stmt_passrole.resource WHERE + resource = '*' + OR target_role.arn CONTAINS resource + OR resource CONTAINS target_role.name + ) + + UNWIND nodes(path_principal) + nodes(path_target) as n + OPTIONAL MATCH (n)-[pfr]-(pf:{PROWLER_FINDING_LABEL} {{status: 'FAIL', provider_uid: $provider_uid}}) + + RETURN path_principal, path_target, + collect(DISTINCT pf) as dpf, collect(DISTINCT pfr) as dpfr + """, + parameters=[], +) + +# GLUE-001 +AWS_GLUE_PRIVESC_PASSROLE_DEV_ENDPOINT = AttackPathsQueryDefinition( + id="aws-glue-privesc-passrole-dev-endpoint", + name="Glue Dev Endpoint with Privileged Role (GLUE-001)", + short_description="Create a Glue development endpoint with a privileged role attached to gain its permissions.", + description="Detect principals who can pass IAM roles and create Glue development endpoints. This allows creating a dev endpoint with a privileged role attached, gaining that role's permissions.", + attribution=AttackPathsQueryAttribution( + text="pathfinding.cloud - GLUE-001 - iam:PassRole + glue:CreateDevEndpoint", + link="https://pathfinding.cloud/paths/glue-001", + ), + provider="aws", + cypher=f""" + // Find principals with iam:PassRole permission + MATCH path_principal = (aws:AWSAccount {{id: $provider_uid}})--(principal:AWSPrincipal)--(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 glue:CreateDevEndpoint permission + MATCH (principal)--(glue_policy:AWSPolicy)--(stmt_glue:AWSPolicyStatement) + WHERE stmt_glue.effect = 'Allow' + AND any(action IN stmt_glue.action WHERE + toLower(action) = 'glue:createdevendpoint' + OR toLower(action) = 'glue:*' + OR action = '*' + ) + + // Find roles that trust Glue service (can be passed to Glue) + MATCH path_target = (aws)--(target_role:AWSRole)-[:TRUSTS_AWS_PRINCIPAL]->(:AWSPrincipal {{arn: 'glue.amazonaws.com'}}) + WHERE any(resource IN stmt_passrole.resource WHERE + resource = '*' + OR target_role.arn CONTAINS resource + OR resource CONTAINS target_role.name + ) + + UNWIND nodes(path_principal) + nodes(path_target) as n + OPTIONAL MATCH (n)-[pfr]-(pf:{PROWLER_FINDING_LABEL} {{status: 'FAIL', provider_uid: $provider_uid}}) + + RETURN path_principal, path_target, + collect(DISTINCT pf) as dpf, collect(DISTINCT pfr) as dpfr + """, + parameters=[], +) + +# IAM-014 +AWS_IAM_PRIVESC_ATTACH_ROLE_POLICY_ASSUME_ROLE = AttackPathsQueryDefinition( + id="aws-iam-privesc-attach-role-policy-assume-role", + name="Role Policy Attachment and Assumption (IAM-014)", + short_description="Attach policies to IAM roles and then assume them to gain elevated access.", + description="Detect principals who can both attach policies to roles AND assume those roles. This allows modifying a role's permissions then assuming it to gain elevated access.", + attribution=AttackPathsQueryAttribution( + text="pathfinding.cloud - IAM-014 - iam:AttachRolePolicy + sts:AssumeRole", + link="https://pathfinding.cloud/paths/iam-014", + ), + provider="aws", + cypher=f""" + // Find principals with iam:AttachRolePolicy permission + MATCH path_principal = (aws:AWSAccount {{id: $provider_uid}})--(principal:AWSPrincipal)--(attach_policy:AWSPolicy)--(stmt_attach:AWSPolicyStatement) WHERE stmt_attach.effect = 'Allow' AND any(action IN stmt_attach.action WHERE toLower(action) = 'iam:attachrolepolicy' @@ -519,8 +868,8 @@ AWS_IAM_PRIVESC_ATTACH_ROLE_POLICY_ASSUME_ROLE = AttackPathsQueryDefinition( OR action = '*' ) - // Find statements granting sts:AssumeRole - MATCH path_assume = (principal)--(assume_policy:AWSPolicy)--(stmt_assume:AWSPolicyStatement) + // Find sts:AssumeRole permission + MATCH (principal)--(assume_policy:AWSPolicy)--(stmt_assume:AWSPolicyStatement) WHERE stmt_assume.effect = 'Allow' AND any(action IN stmt_assume.action WHERE toLower(action) = 'sts:assumerole' @@ -531,147 +880,26 @@ AWS_IAM_PRIVESC_ATTACH_ROLE_POLICY_ASSUME_ROLE = AttackPathsQueryDefinition( // 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 ) - // Deduplicate before creating virtual relationships - WITH DISTINCT admin_outcome, aws, principal, target_role - - // Create virtual relationships showing the attack path - CALL apoc.create.vRelationship(principal, 'CAN_MODIFY', {{ - via: 'iam:AttachRolePolicy' - }}, target_role) - YIELD rel AS modify_rel - - CALL apoc.create.vRelationship(target_role, 'LEADS_TO', {{ - technique: 'iam:AttachRolePolicy + sts:AssumeRole', - via: 'sts:AssumeRole', - reference: 'https://pathfinding.cloud/paths/iam-014' - }}, admin_outcome) - YIELD rel AS escalation_rel - - // Re-match paths for visualization - MATCH path_principal = (aws)--(principal) - MATCH path_target = (aws)--(target_role) - UNWIND nodes(path_principal) + nodes(path_target) as n - OPTIONAL MATCH (n)-[pfr]-(pf:{PROWLER_FINDING_LABEL}) - WHERE pf.status = 'FAIL' + OPTIONAL MATCH (n)-[pfr]-(pf:{PROWLER_FINDING_LABEL} {{status: 'FAIL', provider_uid: $provider_uid}}) RETURN path_principal, path_target, - admin_outcome, modify_rel, escalation_rel, - collect(DISTINCT pf) as dpf, collect(DISTINCT pfr) as dpfr + collect(DISTINCT pf) as dpf, collect(DISTINCT pfr) as dpfr """, parameters=[], ) -# TODO: Add ProwlerFinding nodes -AWS_BEDROCK_PRIVESC_PASSROLE_CODE_INTERPRETER = AttackPathsQueryDefinition( - id="aws-bedrock-privesc-passrole-code-interpreter", - name="Privilege Escalation: Bedrock Code Interpreter with PassRole", - description="Detect principals that can escalate privileges by passing a role to a Bedrock AgentCore Code Interpreter. The attacker creates a code interpreter with an arbitrary role, then invokes it to execute code with those credentials.", - provider="aws", - cypher=""" - CALL apoc.create.vNode(['PrivilegeEscalation'], { - id: 'effective-administrator-bedrock', - check_title: 'Privilege Escalation', - name: 'Effective Administrator (Bedrock)', - status: 'FAIL', - severity: 'critical' - }) - YIELD node AS escalation_outcome - - WITH escalation_outcome - - // Find principals in the account - MATCH path_principal = (aws:AWSAccount {id: $provider_uid})--(principal:AWSPrincipal) - - // Principal can assume roles (up to 2 hops) - OPTIONAL MATCH path_assume = (principal)-[:STS_ASSUMEROLE_ALLOW*0..2]->(acting_as:AWSRole) - WITH escalation_outcome, aws, principal, path_principal, path_assume, - CASE WHEN path_assume IS NULL THEN principal ELSE acting_as END AS effective_principal - - // Find iam:PassRole permission - MATCH path_passrole = (effective_principal)--(passrole_policy:AWSPolicy)--(passrole_stmt:AWSPolicyStatement) - WHERE passrole_stmt.effect = 'Allow' - AND any(action IN passrole_stmt.action WHERE toLower(action) = 'iam:passrole' OR action = '*') - - // Find Bedrock AgentCore permissions - MATCH (effective_principal)--(bedrock_policy:AWSPolicy)--(bedrock_stmt:AWSPolicyStatement) - WHERE bedrock_stmt.effect = 'Allow' - AND ( - any(action IN bedrock_stmt.action WHERE toLower(action) = 'bedrock-agentcore:createcodeinterpreter' OR action = '*' OR toLower(action) = 'bedrock-agentcore:*') - ) - AND ( - any(action IN bedrock_stmt.action WHERE toLower(action) = 'bedrock-agentcore:startsession' OR action = '*' OR toLower(action) = 'bedrock-agentcore:*') - ) - AND ( - any(action IN bedrock_stmt.action WHERE toLower(action) = 'bedrock-agentcore:invoke' OR action = '*' OR toLower(action) = 'bedrock-agentcore:*') - ) - - // Find target roles with elevated permissions that could be passed - MATCH (aws)--(target_role:AWSRole)--(target_policy:AWSPolicy)--(target_stmt:AWSPolicyStatement) - WHERE target_stmt.effect = 'Allow' - AND ( - any(action IN target_stmt.action WHERE action = '*') - OR any(action IN target_stmt.action WHERE toLower(action) = 'iam:*') - ) - - // Deduplicate per (principal, target_role) pair - WITH DISTINCT escalation_outcome, aws, principal, target_role - - // Group by principal, collect target_roles - WITH escalation_outcome, aws, principal, - collect(DISTINCT target_role) AS target_roles, - count(DISTINCT target_role) AS target_count - - // Create single virtual Bedrock node per principal - CALL apoc.create.vNode(['BedrockCodeInterpreter'], { - name: 'New Code Interpreter', - description: toString(target_count) + ' admin role(s) can be passed', - id: principal.arn, - target_role_count: target_count - }) - YIELD node AS bedrock_agent - - // Connect from principal (not effective_principal) to keep graph connected - CALL apoc.create.vRelationship(principal, 'CREATES_INTERPRETER', { - permissions: ['iam:PassRole', 'bedrock-agentcore:CreateCodeInterpreter', 'bedrock-agentcore:StartSession', 'bedrock-agentcore:Invoke'], - technique: 'new-passrole' - }, bedrock_agent) - YIELD rel AS create_rel - - // UNWIND target_roles to show which roles can be passed - UNWIND target_roles AS target_role - - CALL apoc.create.vRelationship(bedrock_agent, 'PASSES_ROLE', {}, target_role) - YIELD rel AS pass_rel - - CALL apoc.create.vRelationship(target_role, 'GRANTS_ACCESS', { - reference: 'https://pathfinding.cloud/paths/bedrock-001' - }, escalation_outcome) - YIELD rel AS grants_rel - - // Re-match path for visualization - MATCH path_principal = (aws)--(principal) - - RETURN path_principal, - bedrock_agent, target_role, escalation_outcome, create_rel, pass_rel, grants_rel, target_count - """, - parameters=[], -) - - # AWS Queries List # ---------------- @@ -688,8 +916,15 @@ AWS_QUERIES: list[AttackPathsQueryDefinition] = [ AWS_CLASSIC_ELB_INTERNET_EXPOSED, AWS_ELBV2_INTERNET_EXPOSED, AWS_PUBLIC_IP_RESOURCE_LOOKUP, - AWS_IAM_PRIVESC_PASSROLE_EC2, + AWS_BEDROCK_PRIVESC_PASSROLE_CODE_INTERPRETER, + AWS_EC2_PRIVESC_PASSROLE_IAM, + AWS_EC2_PRIVESC_MODIFY_INSTANCE_ATTRIBUTE, + AWS_EC2_PRIVESC_PASSROLE_SPOT_INSTANCES, + AWS_EC2_PRIVESC_LAUNCH_TEMPLATE, + AWS_ECS_PRIVESC_PASSROLE_CREATE_SERVICE, + AWS_ECS_PRIVESC_PASSROLE_RUN_TASK, + AWS_ECS_PRIVESC_PASSROLE_CREATE_SERVICE_EXISTING_CLUSTER, + AWS_ECS_PRIVESC_PASSROLE_RUN_TASK_EXISTING_CLUSTER, AWS_GLUE_PRIVESC_PASSROLE_DEV_ENDPOINT, AWS_IAM_PRIVESC_ATTACH_ROLE_POLICY_ASSUME_ROLE, - AWS_BEDROCK_PRIVESC_PASSROLE_CODE_INTERPRETER, ] diff --git a/api/src/backend/api/attack_paths/queries/types.py b/api/src/backend/api/attack_paths/queries/types.py index d798dbbcdb..3a70805cd7 100644 --- a/api/src/backend/api/attack_paths/queries/types.py +++ b/api/src/backend/api/attack_paths/queries/types.py @@ -1,6 +1,14 @@ from dataclasses import dataclass, field +@dataclass +class AttackPathsQueryAttribution: + """Source attribution for an Attack Path query.""" + + text: str + link: str + + @dataclass class AttackPathsQueryParameterDefinition: """ @@ -23,7 +31,9 @@ class AttackPathsQueryDefinition: id: str name: str + short_description: str description: str provider: str cypher: str + attribution: AttackPathsQueryAttribution | None = None parameters: list[AttackPathsQueryParameterDefinition] = field(default_factory=list) diff --git a/api/src/backend/api/specs/v1.yaml b/api/src/backend/api/specs/v1.yaml index 432528c562..ec5f9e6ec5 100644 --- a/api/src/backend/api/specs/v1.yaml +++ b/api/src/backend/api/specs/v1.yaml @@ -616,7 +616,7 @@ paths: operationId: attack_paths_scans_queries_retrieve description: Retrieve the catalog of Attack Paths queries available for this Attack Paths scan. - summary: List attack paths queries + summary: List Attack Paths queries parameters: - in: query name: fields[attack-paths-scans] @@ -714,7 +714,7 @@ paths: description: Bad request (e.g., Unknown Attack Paths query for the selected provider) '404': - description: No attack paths found for the given query and parameters + description: No Attack Paths found for the given query and parameters '500': description: Attack Paths query execution failed due to a database error /api/v1/compliance-overviews: @@ -12438,6 +12438,8 @@ components: type: string name: type: string + short_description: + type: string description: type: string provider: @@ -12446,12 +12448,42 @@ components: type: array items: $ref: '#/components/schemas/AttackPathsQueryParameter' + attribution: + allOf: + - $ref: '#/components/schemas/AttackPathsQueryAttribution' + nullable: true required: - id - name + - short_description - description - provider - parameters + AttackPathsQueryAttribution: + type: object + required: + - type + - id + additionalProperties: false + properties: + type: + type: string + description: The [type](https://jsonapi.org/format/#document-resource-object-identification) + member is used to describe resource objects that share common attributes + and relationships. + enum: + - attack-paths-query-attributions + id: {} + attributes: + type: object + properties: + text: + type: string + link: + type: string + required: + - text + - link AttackPathsQueryParameter: type: object required: diff --git a/api/src/backend/api/tests/test_attack_paths.py b/api/src/backend/api/tests/test_attack_paths.py index 2c4e1484f8..4208107710 100644 --- a/api/src/backend/api/tests/test_attack_paths.py +++ b/api/src/backend/api/tests/test_attack_paths.py @@ -83,6 +83,7 @@ def test_execute_attack_paths_query_serializes_graph( definition = attack_paths_query_definition_factory( id="aws-rds", name="RDS", + short_description="Short desc", description="", cypher="MATCH (n) RETURN n", parameters=[], @@ -143,6 +144,7 @@ def test_execute_attack_paths_query_wraps_graph_errors( definition = attack_paths_query_definition_factory( id="aws-rds", name="RDS", + short_description="Short desc", description="", cypher="MATCH (n) RETURN n", parameters=[], diff --git a/api/src/backend/api/tests/test_views.py b/api/src/backend/api/tests/test_views.py index 4bb7509bc7..fb2db1c1d2 100644 --- a/api/src/backend/api/tests/test_views.py +++ b/api/src/backend/api/tests/test_views.py @@ -3830,6 +3830,7 @@ class TestAttackPathsScanViewSet: AttackPathsQueryDefinition( id="aws-rds", name="RDS inventory", + short_description="List account RDS assets.", description="List account RDS assets", provider=provider.provider, cypher="MATCH (n) RETURN n", @@ -3892,6 +3893,7 @@ class TestAttackPathsScanViewSet: query_definition = AttackPathsQueryDefinition( id="aws-rds", name="RDS inventory", + short_description="List account RDS assets.", description="List account RDS assets", provider=provider.provider, cypher="MATCH (n) RETURN n", @@ -4049,6 +4051,7 @@ class TestAttackPathsScanViewSet: query_definition = AttackPathsQueryDefinition( id="aws-empty", name="empty", + short_description="", description="", provider=provider.provider, cypher="MATCH (n) RETURN n", diff --git a/api/src/backend/api/v1/serializers.py b/api/src/backend/api/v1/serializers.py index e2ae7fc8d2..6c42d83aa5 100644 --- a/api/src/backend/api/v1/serializers.py +++ b/api/src/backend/api/v1/serializers.py @@ -1176,6 +1176,14 @@ class AttackPathsScanSerializer(RLSSerializer): return provider.uid if provider else None +class AttackPathsQueryAttributionSerializer(BaseSerializerV1): + text = serializers.CharField() + link = serializers.CharField() + + class JSONAPIMeta: + resource_name = "attack-paths-query-attributions" + + class AttackPathsQueryParameterSerializer(BaseSerializerV1): name = serializers.CharField() label = serializers.CharField() @@ -1190,7 +1198,9 @@ class AttackPathsQueryParameterSerializer(BaseSerializerV1): class AttackPathsQuerySerializer(BaseSerializerV1): id = serializers.CharField() name = serializers.CharField() + short_description = serializers.CharField() description = serializers.CharField() + attribution = AttackPathsQueryAttributionSerializer(allow_null=True, required=False) provider = serializers.CharField() parameters = AttackPathsQueryParameterSerializer(many=True) diff --git a/api/src/backend/conftest.py b/api/src/backend/conftest.py index e50f92e1ab..00617017bb 100644 --- a/api/src/backend/conftest.py +++ b/api/src/backend/conftest.py @@ -1663,6 +1663,7 @@ def attack_paths_query_definition_factory(): definition_payload = { "id": "aws-test", "name": "Attack Paths Test Query", + "short_description": "Synthetic short description for tests.", "description": "Synthetic Attack Paths definition for tests.", "provider": "aws", "cypher": "RETURN 1", diff --git a/skills/prowler-attack-paths-query/SKILL.md b/skills/prowler-attack-paths-query/SKILL.md index 9a68c8a3ea..35fb9341d3 100644 --- a/skills/prowler-attack-paths-query/SKILL.md +++ b/skills/prowler-attack-paths-query/SKILL.md @@ -18,9 +18,7 @@ allowed-tools: Read, Edit, Write, Glob, Grep, Bash, WebFetch, Task ## Overview -Attack Paths queries are openCypher queries that analyze cloud infrastructure graphs -(ingested via Cartography) to detect security risks like privilege escalation paths, -network exposure, and misconfigurations. +Attack Paths queries are openCypher queries that analyze cloud infrastructure graphs (ingested via Cartography) to detect security risks like privilege escalation paths, network exposure, and misconfigurations. Queries are written in **openCypher Version 9** to ensure compatibility with both Neo4j and Amazon Neptune. @@ -34,10 +32,11 @@ Queries can be created from: - The JSON index contains: `id`, `name`, `description`, `services`, `permissions`, `exploitationSteps`, `prerequisites`, etc. - Reference: https://github.com/DataDog/pathfinding.cloud - **Fetching a single path by ID** — The aggregated `paths.json` is too large for WebFetch + **Fetching a single path by ID** - The aggregated `paths.json` is too large for WebFetch (content gets truncated). Use Bash with `curl` and a JSON parser instead: Prefer `jq` (concise), fall back to `python3` (guaranteed in this Python project): + ```bash # With jq curl -s https://raw.githubusercontent.com/DataDog/pathfinding.cloud/main/docs/paths.json \ @@ -50,6 +49,7 @@ Queries can be created from: 2. **Listing Available Attack Paths** - Use Bash to list available paths from the JSON index: + ```bash # List all path IDs and names (jq) curl -s https://raw.githubusercontent.com/DataDog/pathfinding.cloud/main/docs/paths.json \ @@ -84,6 +84,7 @@ Example: `api/src/backend/api/attack_paths/queries/aws.py` ```python from api.attack_paths.queries.types import ( + AttackPathsQueryAttribution, AttackPathsQueryDefinition, AttackPathsQueryParameterDefinition, ) @@ -92,8 +93,13 @@ from tasks.jobs.attack_paths.config import PROWLER_FINDING_LABEL # {REFERENCE_ID} (e.g., EC2-001, GLUE-001) AWS_{QUERY_NAME} = AttackPathsQueryDefinition( id="aws-{kebab-case-name}", - name="Privilege Escalation: {permission1} + {permission2}", - description="{Detailed description of the Attack Paths}.", + name="{Human-friendly label} ({REFERENCE_ID})", + short_description="{Brief explanation of the attack, no technical permissions.}", + description="{Detailed description of the attack vector and impact.}", + attribution=AttackPathsQueryAttribution( + text="pathfinding.cloud - {REFERENCE_ID} - {permission1} + {permission2}", + link="https://pathfinding.cloud/paths/{reference_id_lowercase}", + ), provider="aws", cypher=f""" // Find principals with {permission1} @@ -114,7 +120,7 @@ AWS_{QUERY_NAME} = AttackPathsQueryDefinition( OR action = '*' ) - // Find target resources + // Find target resources (MUST chain from `aws` for provider isolation) MATCH path_target = (aws)--(target_role:AWSRole)-[:TRUSTS_AWS_PRINCIPAL]->(:AWSPrincipal {{arn: '{service}.amazonaws.com'}}) WHERE any(resource IN stmt.resource WHERE resource = '*' @@ -231,11 +237,13 @@ This informs query design by showing what data is actually available to query. Use the standard pattern (see above) with: - **id**: Auto-generated as `{provider}-{kebab-case-description}` -- **name**: Human-readable, e.g., "Privilege Escalation: {perm1} + {perm2}" -- **description**: Explain the attack vector and impact +- **name**: Short, human-friendly label. No raw IAM permissions. For sourced queries (e.g., pathfinding.cloud), append the reference ID in parentheses: `"EC2 Instance Launch with Privileged Role (EC2-001)"`. If the name already has parentheses, prepend the ID inside them: `"ECS Service Creation with Privileged Role (ECS-003 - Existing Cluster)"`. +- **short_description**: Brief explanation of the attack, no technical permissions. E.g., "Launch EC2 instances with privileged IAM roles to gain their permissions via IMDS." +- **description**: Full technical explanation of the attack vector and impact. Plain text only, no HTML or technical permissions here. - **provider**: Provider identifier (aws, azure, gcp, kubernetes, github) - **cypher**: The openCypher query with proper escaping - **parameters**: Optional list of user-provided parameters (use `parameters=[]` if none needed) +- **attribution**: Optional `AttackPathsQueryAttribution(text, link)` for sourced queries. The `text` includes the source, reference ID, and technical permissions (e.g., `"pathfinding.cloud - EC2-001 - iam:PassRole + ec2:RunInstances"`). The `link` is the URL with a lowercase ID (e.g., `"https://pathfinding.cloud/paths/ec2-001"`). Omit (defaults to `None`) for non-sourced queries. ### 5. Add Query to Provider List @@ -397,6 +405,16 @@ parameters=[ 6. **Validate schema first**: Ensure all node labels and properties exist in Cartography schema +7. **Chain all MATCHes from the root account node**: Every `MATCH` clause must connect to the `aws` variable (or another variable already bound to the account's subgraph). The tenant database contains data from multiple providers — an unanchored `MATCH` would return nodes from all providers, breaking provider isolation. + + ```cypher + // WRONG: matches ALL AWSRoles across all providers in the tenant DB + MATCH (role:AWSRole) WHERE role.name = 'admin' + + // CORRECT: scoped to the specific account's subgraph + MATCH (aws)--(role:AWSRole) WHERE role.name = 'admin' + ``` + --- ## openCypher Compatibility diff --git a/ui/CHANGELOG.md b/ui/CHANGELOG.md index 7ac2c70086..e21830c872 100644 --- a/ui/CHANGELOG.md +++ b/ui/CHANGELOG.md @@ -2,6 +2,14 @@ All notable changes to the **Prowler UI** are documented in this file. +## [1.19.0] (Prowler UNRELEASED) + +### 🔄 Changed + +- Attack Paths: Query list now shows their name and short description, when one is selected it also shows a longer description and an attribution if it has it [(#9983)](https://github.com/prowler-cloud/prowler/pull/9983) + +--- + ## [1.18.1] (Prowler UNRELEASED) ### 🐞 Fixed diff --git a/ui/app/(prowler)/attack-paths/(workflow)/query-builder/_components/query-selector.tsx b/ui/app/(prowler)/attack-paths/(workflow)/query-builder/_components/query-selector.tsx index 65acb8a84a..f7d36c9a68 100644 --- a/ui/app/(prowler)/attack-paths/(workflow)/query-builder/_components/query-selector.tsx +++ b/ui/app/(prowler)/attack-paths/(workflow)/query-builder/_components/query-selector.tsx @@ -35,7 +35,7 @@ export const QuerySelector = ({
{query.attributes.name} - {query.attributes.description} + {query.attributes.short_description}
diff --git a/ui/app/(prowler)/attack-paths/(workflow)/query-builder/page.tsx b/ui/app/(prowler)/attack-paths/(workflow)/query-builder/page.tsx index a817b9a715..ff66a7e68f 100644 --- a/ui/app/(prowler)/attack-paths/(workflow)/query-builder/page.tsx +++ b/ui/app/(prowler)/attack-paths/(workflow)/query-builder/page.tsx @@ -340,6 +340,33 @@ export default function AttackPathAnalysisPage() { onQueryChange={handleQueryChange} /> + {queryBuilder.selectedQueryData && ( +
+

+ {queryBuilder.selectedQueryData.attributes.description} +

+ {queryBuilder.selectedQueryData.attributes.attribution && ( +

+ Source:{" "} + + { + queryBuilder.selectedQueryData.attributes + .attribution.text + } + +

+ )} +
+ )} + {queryBuilder.selectedQuery && (