About review-attack-path
review-attack-path is a skill for security analysts using SubImage to inspect known attack paths, find paths involving an asset, or identify plausible unmodeled next steps. It separates confirmed evidence, inference, and hypotheses, and can optionally overlay a non-persisted what-if simulation.
Key features
- 01Walk through a known attack path
- 02Find paths involving an asset
- 03Probe for unmodeled next steps
- 04Run preview-only scenario simulations
- 05Label conclusions by evidence tier
- 06Create tickets or send notifications
Use cases
- 01Assess the impact at an attack path's terminal asset
- 02Check which attack paths involve an asset
- 03Look for lateral movement beyond a path endpoint
- 04Simulate paths after a hypothetical compromise
MCP support
Uses SubImage tools such as subimageRunCypher to inspect graph data and schema, and tools for scenario simulation, tickets, and notifications. The skill is designed around SubImage's MCP tool workflow.
Install
Copy the command and run it in a terminal.
npx skills add https:/ / github.com/ subimagesec/ skills --skill review-attack-pathAfter installing, type/review-attack-pathin Claude Code to confirm it loaded.
Details
From the project READMEReview an attack path
What this does
Three modes against the SubImage attack-path engine and the underlying Neo4j graph:
- Walk a known path in plain English, surface the most sensitive terminal asset, and propose the fastest fix.
- Pivot from an asset to find which paths it sits on and review the worst.
- Hunt for n+1 extensions: probe the graph past a path's terminal node to find credible next steps the engine has not yet modeled, and surface them inline as candidate transitions.
Optional what-if simulation overlays any of the above.
When to use
✅ User has an attack path id from the SubImage UI or a list response.
✅ User has an asset and wants to know if it is on any path.
✅ User wants to extend an existing path: "find lateral movement from here", "assess blast radius of ", "what could come next".
✅ User wants a what-if: "if attacker compromises X, what happens?".
✅ User just finished subimage-mcp:investigate-cve and confirmed the pivot.
❌ User wants the full path catalog: just list (:AttackPath:Signal) and summarize without going step by step.
❌ User wants the static rule set behind the engine: that is subimage-mcp:triage-new-findings for findings, or the doc.
Prerequisites
Paths, their steps, and the n+1 probes all read the graph through subimageRunCypher, backed by subimageListModuleSchemaNodes and subimageGetNodesSchema for schema exploration. What-if simulation uses subimageGetScenarioCapabilities then subimageCreateAttackPathScenario, which run the engine rather than read the graph. Ticket and notification follow-ups use subimageListLinearTeams, subimageCreateTicket, subimageSendNotification.
Required inputs
Pick one entry mode and collect the matching values. If anything is missing, ask the user explicitly.
| Entry mode | Required value | If missing, ask |
|---|---|---|
| Walk a known path | <ATTACK_PATH_ID> | "What is the attack path id? You can find it in the URL when viewing a path in SubImage, or list the active paths with Cypher and pick one (see Mode A)." |
| Pivot from an asset | <ASSET_ID> (resource ARN or fully-qualified id) | "What is the asset id? Resource ARN for AWS, fully-qualified id for GCP/Azure, or the SubImage detail-page URL works." |
| n+1 extension hunt | a starting <ATTACK_PATH_ID> or an <ASSET_ID> already known to be a terminal node of interest | "Where should I start the n+1 hunt: from a specific path's terminal node, or from an asset I should treat as compromised?" |
For a what-if scenario, also collect:
| Value | If missing, ask |
|---|---|
<NODE_LABEL> | "Which node label is the starting point? (e.g. AWSEC2Instance, AWSS3Bucket, User)." |
| Capabilities to grant | "Which attacker capabilities should I simulate? Run subimageGetScenarioCapabilities first to see what is valid for this node label." |
Posture (read this before calling any tool)
The graph is the source of truth. Be conservative.
- Distinguish three confidence tiers in every output. Tag each statement implicitly via the section it sits in:
- Confirmed: directly returned by the attack-path engine or by a Cypher query that hit specific labeled rows.
- Inferred: combining two confirmed facts via a documented schema relationship, with a Cypher probe that backs the join.
- Hypothetical: result of a what-if scenario, or a graph-shape observation without a concrete row backing it. Always say so.
- Never invent labels, relationships, transitions, or security semantics. If the schema does not show a relationship, the path does not exist.
- Adjacency in the graph does not imply exploitability. A relationship between two nodes is necessary but not sufficient.
- Prefer missing a weak idea over reporting a bad one. Bad transition reports are worse than silence.
- If evidence is weak or ambiguous, say so explicitly. Do not paper over uncertainty.
Workflow
Mode A: walk a known attack path
-
Read the path's ordered steps. An attack path is an
:AttackPath:Signaland its steps hang off it by position:MATCH (a:AttackPath:Signal {id: '<ATTACK_PATH_ID>'})-[h:HAS_STEP]->(s:AttackPathStep) OPTIONAL MATCH (s)-[:FROM]->(src) OPTIONAL MATCH (s)-[:TO]->(dst) RETURN a.title AS title, a.criticality_score AS criticality, h.position AS position, s.transition_id AS transition, s.capability AS capability, s.description AS description, collect(DISTINCT src.id) AS from_ids, collect(DISTINCT dst.id) AS to_ids ORDER BY positionUse
s.description, which is the rendered step text.s.templated_descriptionstill holds unrendered placeholders and is not for display. This result is bounded by construction (one path's steps), so it takes noLIMIT.FROM|TOis the correct join here, because these are the endpoints the step displays and the path id is already known. It is not the join for finding which paths touch an asset; see Mode B. -
Decompose the path into four parts:
- Initial compromise: the entry node and what gets the attacker in.
- Key pivots: the 1 to 3 transitions that materially change the attacker's position (privilege gain, network reach, data access).
- Terminal node: the last node in the chain.
- Terminal impact: what the attacker now owns or can do.
-
Translate each transition into a plain-language sentence ("from
Xthe attacker pivots toYbecause<reason from the transition>"). Do not relist the JSON. -
Rank impact: production / customer data / privileged identity > internal / dev > sandbox.
Mode B: pivot from an asset
-
Find the paths that touch the asset. An asset participates through an
AttackerCapacity, so the join runsREASON|ONto the capacity andGRANTSback to the step:MATCH (n {id: '<ASSET_ID>'})-[:REASON|ON]-(c:AttackerCapacity) <-[:GRANTS]-(:AttackPathStep)<-[:HAS_STEP]-(a:AttackPath:Signal) WHERE coalesce(a.status, 'active') IN ['active', 'accepted'] RETURN DISTINCT a.id AS id, a.title AS title, a.criticality_score AS criticality, a.context_type AS context, coalesce(a.status, 'active') AS status ORDER BY criticality DESC, id LIMIT 20Do not rewrite this as
(:AttackPathStep)-[:FROM|TO]->(n).FROMandTOare derived edges holding the endpoints a step displays:TOcomes from the step's own capacity,FROMfrom the previous step's. An asset can be theREASONa capacity holds without ever being an endpoint, and matching endpoints reports "no path" for exactly those assets. The endpoint join is the right one for rendering a path you already have, which is what Mode A does; it is the wrong one for discovering paths.coalesceon the status is not defensive padding: a path Signal can carry nostatusproperty, and that counts as active.context_typeis returned rather than filtered, matching the product's own lookup. Narrow to'default'when the user asked for the environment-wide picture, or when the same path is coming back once per context. -
If empty: "No known attack path involves
<ASSET_ID>right now. The asset may still be at risk via paths the engine has not modeled. Want me to run the n+1 extension hunt (Mode C) starting from this asset?" This reassurance is only sound with the capacity join above. -
If non-empty: pick the top 1 to 3, run Mode A on each.
Mode C: hunt for n+1 extensions
This is what the analyst mode adds: inspect the terminal node of a path and probe the graph to determine whether a credible next step exists that the engine does not yet model.
- Pick the terminal node of the path you are extending (or use
<ASSET_ID>directly if the user named it). - Inspect what the schema says lives next to that node:
Note the relationship types and target labels.subimageListModuleSchemaNodes(module="<module-of-the-node>") subimageGetNodesSchema(node_names=["<TerminalLabel>"]) - For each plausible relationship, run a small Cypher probe to confirm that real rows exist (see Cypher rules below). Examples of credible n+1 categories:
- Lateral movement: from a compromised compute instance, reachable peers via security groups / VPC peering / shared service accounts.
- Privilege escalation: from a non-admin identity, edges to roles or policies that effectively grant admin (
iam:PassRole,AssumeRole, group memberships, custom roles with star permissions). - Data exfiltration: from any compromised compute, reachable storage with sensitive data labels or public read.
- Cross-account / cross-tenant: trust relationships, cross-account role assumes, federated identity bridges.
- Apply the 5-test checklist below before reporting anything.
- Surface each credible extension in the output (format below). Increment a counter; mention it in the final output.
Mode D: what-if scenario (overlay)
Only when the user asks ("what if X is compromised", "simulate granting Y").
subimageGetScenarioCapabilities(node_label="<NODE_LABEL>")to discover valid capabilities.- Tell the user which capabilities are available and ask which to grant.
subimageCreateAttackPathScenario(..., preview_only=true). Always passpreview_only=true: the user can inspect the result without it being persisted as a real path.- Walk the resulting path(s) using Mode A.
Cypher rules for graph probes
These apply to the Mode A and Mode B queries above as well as to Mode C probes:
subimageRunCyphertakes no query parameters, so<ATTACK_PATH_ID>and<ASSET_ID>are inlined as literals. Escape backslashes and single quotes before substituting; ARNs and fully-qualified GCP/Azure ids can carry either, and an unescaped apostrophe breaks the statement.- The tool returns at most 100 rows whatever
LIMITyou write, and reports the truetotal_countalongside. That is well above anything here, but quotetotal_countrather than the page size if a probe ever hits it. - The attack-path relationship inventory is closed. Steps have
<-[:HAS_STEP]-,-[:GRANTS]->,-[:FROM]->,-[:TO]->; capacities have<-[:GRANTS]-,-[:ON]->,-[:REASON]->, andREQUIREDto other capacities. There is no(:AttackPathStep)-[:ON]->or(:AttackPathStep)-[:REASON]->; those two hang off the capacity. - Always cap exploratory queries:
LIMIT 5. Wider scans burn tokens and add no signal. - Start broad, then refine. First query confirms the shape exists; second query gets the specific rows.
- For text matching, prefer
toLower(prop) CONTAINS "term"over regex. - Never use unbounded variable-length paths (
-[*]->). Bound them:-[*1..3]->at most. - Verify labels and relationships exist on real rows before reasoning about them. If
MATCH (a:Foo)-[:BAR]->(b)returns 0 rows, the relationship is conceptual; do not present it as exploitable. - If
subimageRunCypherreturns 0 rows, that is itself a confirmed fact: the relationship is not present in this tenant.
When to surface a candidate extension
Include a candidate in the output only when all five are true:
- The source and target labels are supported by real rows in this tenant's graph (not just the schema).
- The relationship between them exists in the graph (a Cypher probe found at least one row).
- The attacker's current position plausibly allows the step (capability, network reach, identity).
- The step provides meaningful new impact, privilege, or reach (not just "another node of the same kind").
- The step is not obviously already modeled by an existing transition (check by walking a few existing paths between similar node types).
If any one fails, do not surface it. State internally why and move on.
Candidate extension format
Render each candidate as a sub-block with these fields exactly:
- context:
Attack path analysis: <user request, one sentence> - problem:
Potential missing transition: <SourceLabel> can reach <TargetLabel> via <relationship>, enabling <impact>. Transition type: <lateral-movement | privilege-escalation | data-exfiltration | cross-account>. Evidence: <N> matches. Cypher pattern: <one-line MATCH>. - workaround:
partial - workaround_detail:
The current path stops at <terminal node>, but graph evidence suggests the attacker may continue to <target> via <mechanism>. - suggested_improvement:
Add a transition from <SourceLabel> to <TargetLabel> under conditions: <list of validated conditions>.
Output
For each path reviewed (Modes A and B):
# Attack path: <short title>
**Criticality**: <score / category> ; **Steps**: <N>
## In plain English
The attacker starts at [[entity:<Label>:<id>|<short>]] (<one-line context>).
1. <transition 1 in plain language>
2. <transition 2 in plain language>
3. ...
N. The chain ends at [[entity:<Label>:<id>|<short>]] which gives them <impact>.
## What this means
- **Initial compromise**: <one line>
- **Key pivot(s)**: <one line, naming the transitions that mattered>
- **Terminal impact**: <one line, naming what the attacker now controls>
- **Most sensitive impacted asset**: [[entity:<Label>:<id>|<short>]] (<why it matters>)
- **Why this path is exploitable today**: <rooted in the actual transitions, no speculation>
## Fastest remediation
<one or two concrete actions in priority order. Name the policy, the SG rule, the secret, the role.>
## What to do next
- Mitigate: <action>; owner: <team or `unknown`>
- Re-run after the change: SubImage refreshes paths on the next sync.
After Mode C, append:
## n+1 extension hunt
- probes run: <count>
- credible extensions found: <count>
- candidate extensions surfaced: <count>
<for each surfaced candidate, render the five-field sub-block from "Candidate extension format" above>
If Mode C found nothing credible, say so plainly: "No credible n+1 extension supported by graph evidence. The terminal node <id> does not currently sit next to anything that survives the 5-test checklist."
Always tag entities with [[entity:<Label>:<node.id>|<display>]] so the chat UI links them.
Optional follow-ups
If the user wants a ticket created:
subimageListLinearTeams()to get team ids.subimageCreateTicket(team_id=..., title="Mitigate: <path title>", description="<the narrative + remediation>").
Confirm the team before creating. Same rule for subimageSendNotification: confirm the channel.
Anti-patterns
- Returning the raw transitions list as a markdown table. Forbidden by the chat system prompt for tool data, and useless when the goal is to make the path readable.
- Hand-waving on remediation ("review IAM policies"). Name the policy, the rule, the resource.
- Speculating about transitions the engine did not produce ("the attacker could also..."). If the user asks, run Mode C with Cypher probes; never claim an extension without evidence.
- Reporting a transition idea after one weak probe. Run the 5-test checklist; if any fails, do not report.
- Walking 5+ paths in one response. The user only acts on the top 1 or 2; deeper than that becomes wallpaper.
- Forgetting
preview_only=trueon what-if scenarios. The unflagged form persists the scenario; that is rarely what the user wants on first look. - Treating schema relationships as confirmed exploitability. Probe first, then report.
References
- Tool guide (always loaded by
subimageReadMe): Domain 4 "Attack Path Analysis", Domain 1 "Knowledge Graph Exploration" for Cypher. - Companion skill for vulnerability triage that often pivots here:
subimage-mcp:investigate-cve. - Architecture: https://app.subimage.io/docs/architecture/attack_path