name: attack-path-stitcher description: Stitches confirmed single-asset findings into multi-hop attack paths across the organization. Builds a graph where nodes are assets and edges are confirmed exploit hops citing the findings that enable them.
Attack Path Stitcher
The Validation Run task (#3) produces confirmed findings per asset. Real attacker risk lives in chains: a finding on asset A leaks credentials that enable a finding on asset B that pivots into asset C. This skill builds that graph.
Mounted onto cloud-agent task #6.
Trigger
Cron daily (default 03:00 UTC). May also re-run after a Validation Run task batch completes.
Workflow
- Load inputs.
validated/*.json— every confirmed finding across all engagements.artifacts/org-surface.json— the org-wide surface graph (assets, services, network zones, trust relationships).findings/finding-NNN/evidence/raw-source.txt— for credential / token extraction during stitching.
- Build asset nodes. One node per asset in
org-surface.json, attributed with:tier,services,network_zone,trust_relationships. - Build edges — one edge per detected pivot. See
reference/edge-detectors.mdfor the seven detectors:- Credential reuse (creds leaked on A reused as auth on B)
- Shared secret / API key (same secret appears in two assets' evidence)
- Trust-zone transitive access (A in zone X has implicit reach to B in zone X)
- AD path hops (kerberoast / DC sync / RBCD chains)
- Cloud IAM role chains (assume-role from compromised asset)
- SSRF → internal asset reach (A's SSRF reaches B's internal endpoint)
- Supply-chain (A is a dependency of B per
source-code-scanningSBOM)
- Compute reachability closure. For each tier-
crown_jewelnode, BFS backwards through edges to find every external-facing node that can reach it. Mark these as "entry points". - Write graph to
artifacts/attack-paths.jsonplus a human DOT fileartifacts/attack-paths.dot(renderable with Graphviz).
Implementation runs through tools/chain-merger.py which handles the graph construction. The skill provides the rules the tool consults; the tool does the iteration.
Output
{OUTPUT_DIR}/
artifacts/
attack-paths.json # nodes, edges, entry_points, crown_jewel_paths
attack-paths.dot # Graphviz source
attack-paths.md # ranked list of distinct paths (human read)
attack-paths.json schema:
{
"generated_at": "2026-05-13T03:00:00Z",
"nodes": [
{"id": "asset42", "tier": "revenue", "services": ["http/443"], "zone": "dmz",
"external": true, "findings": ["finding-012", "finding-018"], "max_cvss": 9.8}
],
"edges": [
{"src": "asset42", "dst": "asset77", "detector": "credential-reuse",
"via_findings": ["finding-012", "finding-019"],
"evidence": "credential (userpass) present in evidence of asset42 and asset77",
"feasibility": 1.0}
],
"entry_points": ["asset42", "asset05"],
"confirmed_paths": [
{"jewel": "asset99", "paths": [
{"hops": ["asset42", "asset77", "asset99"],
"edges": [{"src":"asset42","dst":"asset77","detector":"credential-reuse","feasibility":1.0,"via_findings":["finding-012"]},
{"src":"asset77","dst":"asset99","detector":"ssrf-reach","feasibility":1.0,"via_findings":["finding-024"]}],
"feasibility": 1.0, "max_cvss": 9.8, "path_class": "confirmed"}
]}
],
"inferred_paths": [
{"jewel": "asset99", "paths": [
{"hops": ["asset05", "asset99"], "edges": [...],
"feasibility": 0.5, "max_cvss": 7.5, "path_class": "inferred"}
]}
],
"truncation": {
"edge_cap_hit": false, "depth_truncated_count": 0,
"topn_dropped_count": 0, "max_depth": 8, "edge_cap": 50000
}
}
Crucial for RFP §3.3 compliance: confirmed_paths contains ONLY paths where every edge has feasibility 1.0 AND every edge cites at least one validated finding. These are the "confirmed attack paths" the RFP requires. inferred_paths carries topology / supply-chain hops with no PoC evidence — surfaced for analyst review but excluded from remediation SLA buckets by risk-prioritiser.
Rules
- Edges require evidence. An edge is only written if at least one finding's evidence corroborates the pivot. No speculative edges.
- Bi-directional ≠ assumed. If A reaches B, do not infer B reaches A. Each direction needs its own evidence.
- Deduplicate by
(src, dst, detector). Multiple findings that enable the same hop merge into one edge withvia_findingslisting all of them. - Feasibility ∈ {1.0, 0.5, 0.25}. Reliable PoC re-run = 1.0; conditional (race, timing, specific user) = 0.5; theoretical (logically follows but never demonstrated) = 0.25.
- Limit path enumeration. For each crown-jewel, return top-10 paths per class (confirmed + inferred separately) sorted by
feasibility × max_cvss / hop_count. Full graph is inattack-paths.jsonfor downstream prioritisation. - Read-only. Stitcher never re-fires PoCs and never touches
findings/. It only reads. - Bound graph size. Stop edge construction at 50,000 edges; cap path-search depth at
--max-depth(default 8 hops). Emittruncation.edge_cap_hit,truncation.depth_truncated_count, andtruncation.topn_dropped_countin the JSON so downstream consumers can detect silent path loss. - Confirmed vs inferred is non-negotiable. A path appears in
confirmed_pathsonly if every edge has feasibility 1.0 AND every edge has a non-emptyvia_findings. Trust-zone-only, shared-secret-only, and supply-chain-only chains land ininferred_paths. This split is the contract that lets the RFP-§3.3 claim "confirmed attack paths" stand. - Schema enforcement on input.
tools/chain-merger.pydropsvalidated/{id}.jsonrows missingfinding_idorasset, or whoseverdict != "VALID", with stderr WARNs. Upstream validator must comply with the schema inprojects/rfp-3.2/task-03-validation-run.md.
References
reference/edge-detectors.md— the 7 detector rules with concrete signal patterns.projects/rfp-3.3/task-06-attack-path-stitcher.md— cloud-agent runtime contract.