Skip to content

feat(attack-paths): grouped template-graph view with attack outcomes#11357

Draft
HugoPBrito wants to merge 4 commits into
masterfrom
feat/attack-paths-template-graph-view
Draft

feat(attack-paths): grouped template-graph view with attack outcomes#11357
HugoPBrito wants to merge 4 commits into
masterfrom
feat/attack-paths-template-graph-view

Conversation

@HugoPBrito
Copy link
Copy Markdown
Member

Context

The Attack Paths graph renders every concrete resource individually, anchored to a hub AWSAccount node that fans out to everything. This adds noise (the account is implicit), makes the graph hard to read at first sight, has no arrowheads (direction is only implied by layout), and does not represent the outcome of the attack chain (e.g. "code execution as a privileged role").

This PR redesigns the visualization around the structure of the attack instead of the concrete inventory.

Description

API (api/src/backend/api/attack_paths/)

  • _serialize_graph drops account/root-labeled nodes (AWSAccount, AzureSubscription, AzureTenant, GCPProject, KubernetesCluster, GitHubAccount) and their relationships. The account is still the Cypher MATCH anchor, so tenant/provider isolation is unaffected.
  • New AttackPathsQueryOutcome dataclass (label / description / severity). Outcomes are assigned by query id pattern in aws.py: privesc-passrole and code-exec paths → "Code execution", IAM/STS privesc → "Privilege escalation", internet-exposed chain → "Data exfiltration". Inventory queries keep outcome=None.
  • execute_query returns outcome alongside nodes / relationships.
  • New AttackPathsQueryOutcomeSerializer; outcome (allow_null) added to AttackPathsQuerySerializer and AttackPathsQueryResultSerializer.
  • attack_paths_queries action filters the catalog to queries with outcome is not None (78 → 67 surfaced; 11 inventory hidden).

UI (ui/app/(prowler)/attack-paths/(workflow)/query-builder/)

  • New _lib/template-graph.ts (buildTemplateGraph(data, expandedTypes, outcome)): groups concrete nodes by resource type into one synthetic node each, dedupes inter-group edges, drops intra-group self-loops, and appends an Outcome node wired from the sink representatives. Account and finding nodes are filtered out of this structural view.
  • _lib/layout.ts: new node types attackGroup (renamed off the reserved React Flow group type that paints a default gray container) and outcome; markerEnd: ArrowClosed on every edge.
  • New GroupNode (stacked-card visual + count badge + "click to expand") and OutcomeNode (terminal node colored by severity).
  • useGraphStore gains templateSource, outcome, expandedTypes and toggleExpandedType; the default rendered graph is the collapsed template, derived via buildTemplateGraph on every state change.
  • attack-paths-page.tsx: clicking a group expands its type; clicking an expanded concrete resource collapses its type back; outcome nodes are inert. A key based on expandedTypes forces React Flow to refit on expansion. Banner copy updated to reflect the new UX.
  • graph-legend.tsx skips the outcome marker label so the legend does not list it as a resource type.

Known limitations / follow-ups

  • Findings are intentionally excluded from the structural view. If we want them back, the natural place is on concrete resources once their type is expanded (reuse the existing Tier-1 expansion).
  • attack-paths-page.browser.test.tsx is quarantined with describe = describeBase.skip because it asserts the pre-redesign default view (finding nodes rendered eagerly, click-to-filter from findings). Rewriting it for the new UX is a follow-up.

Steps to review

  1. Run an AWS scan so the attack-paths ingestion populates Neo4j.
  2. Open Attack Paths. The query selector now only lists real attack paths (no aws-rds-instances, aws-iam-statements-*, aws-*-internet-exposed, aws-public-ip-resource-lookup).
  3. Run e.g. an EC2 PassRole privesc query. Expected: no AWSAccount node, the graph reads left to right with arrowheads, one node per resource type with a count badge, and a terminal Outcome node ("Code execution"). Inventory queries (if you call them directly via API) still render but with outcome: null.
  4. Click an AWS Role (or any other) group to expand it into the individual roles; click any of those concrete nodes to collapse the group back.
  5. Backend: pytest api/tests/test_attack_paths.py (43 tests). UI: pnpm vitest run _lib/template-graph.test.ts _lib/layout.test.ts (18 tests).

Checklist

  • Tests added for buildTemplateGraph, markerEnd, account stripping, and outcome passthrough.
  • CHANGELOG entries added (api + ui).
  • Screenshots — to attach in the PR thread.
  • Rewrite the browser E2E suite for the new default view.

License

By submitting this pull request, I confirm that my contribution is made under the terms of the Apache 2.0 license.

- Drop account/root-labeled nodes (AWSAccount, AzureSubscription, AzureTenant, GCPProject, KubernetesCluster, GitHubAccount) and their relationships from the serialized attack-paths graph. The account stays the Cypher MATCH anchor; tenant/provider isolation is unaffected.
- Add AttackPathsQueryOutcome (label, description, severity) and an outcome field on AttackPathsQueryDefinition. Assign outcomes to every real attack-path query by id pattern in aws.py (privesc-passrole and code-exec to 'Code execution', IAM/STS privesc to 'Privilege escalation', internet-exposed chain to 'Data exfiltration'). The 11 inventory queries keep outcome=None.
- execute_query attaches outcome to the run-query response; expose it via new AttackPathsQueryOutcomeSerializer in AttackPathsQuerySerializer and AttackPathsQueryResultSerializer (allow_null).
- attack_paths_queries action filters the catalog to queries with an outcome (78 -> 67 surfaced; 11 inventory hidden).
- Existing tests that used AWSAccount as a generic node updated to AWSRole; new tests cover account stripping and outcome passthrough.
Redesigns the Attack Paths graph around the structure of the attack:

- New _lib/template-graph.ts groups concrete nodes by resource type into one synthetic node per type (with a count badge), dedupes inter-group edges, drops intra-group self-loops, and appends a terminal Outcome node wired from the sink representatives. Account and finding nodes are filtered out of this structural view.
- _lib/layout.ts adds 'attackGroup' and 'outcome' node types and a MarkerType.ArrowClosed markerEnd on every edge so attack direction is explicit. 'attackGroup' is intentionally named off the reserved React Flow 'group' type, which otherwise paints a default gray container behind the node.
- New GroupNode (stacked-card visual, type icon, count badge, click-to-expand) and OutcomeNode (severity-colored terminal with Crosshair) components, registered in NODE_TYPES.
- useGraphStore gains templateSource, outcome, expandedTypes and toggleExpandedType; the rendered graph is the collapsed template, recomputed via buildTemplateGraph on every state change.
- attack-paths-page handleNodeClick: grouped type -> expand; expanded concrete resource -> collapse its type; outcome node is inert. A key based on expandedTypes forces React Flow to refit on expansion. Banner copy updated.
- graph-legend.tsx skips the outcome marker label so the legend does not list it as a resource type.
- Unit tests for buildTemplateGraph (grouping, edge dedup, expand, sink->outcome wiring, finding/account drop, empty input) and for the new edge markerEnd in layoutWithDagre.
The browser E2E suite asserts the pre-redesign default view (finding nodes rendered eagerly in the default graph, click-to-filter from findings). The new template-graph view replaces that default with a grouped structural view that excludes findings, so the existing flows do not apply. Quarantined with describe.skip until the suite is rewritten for the new UX.
@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented May 26, 2026

Conflict Markers Resolved

All conflict markers have been successfully resolved in this pull request.

@github-actions
Copy link
Copy Markdown
Contributor

⚠️ Changes detected in the following folders without a corresponding update to the CHANGELOG.md:

  • api
  • ui

Please add an entry to the corresponding CHANGELOG.md file to maintain a clear history of changes.

Document the API (account stripping + outcome metadata + filtered catalog) and UI (grouped template view with expand-on-click, outcome node, arrowheads) changes under the current API/UI version sections, referencing PR #11357.
@github-actions
Copy link
Copy Markdown
Contributor

🔒 Container Security Scan

Image: prowler-api:b394985
Last scan: 2026-05-26 07:15:59 UTC

📊 Vulnerability Summary

Severity Count
🔴 Critical 13
Total 13

11 package(s) affected

⚠️ Action Required

Critical severity vulnerabilities detected. These should be addressed before merging:

  • Review the detailed scan results
  • Update affected packages to patched versions
  • Consider using a different base image if updates are unavailable

📋 Resources:

@github-actions
Copy link
Copy Markdown
Contributor

🔒 Container Security Scan

Image: prowler-ui:b394985
Last scan: 2026-05-26 07:16:13 UTC

📊 Vulnerability Summary

Severity Count
🔴 Critical 2
Total 2

2 package(s) affected

⚠️ Action Required

Critical severity vulnerabilities detected. These should be addressed before merging:

  • Review the detailed scan results
  • Update affected packages to patched versions
  • Consider using a different base image if updates are unavailable

📋 Resources:

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant