mcpField
R
review-attack-path

review-attack-path

github.com/subimagesecSecurity052 installsUpdated 2d ago
Repository

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

  1. 01Walk through a known attack path
  2. 02Find paths involving an asset
  3. 03Probe for unmodeled next steps
  4. 04Run preview-only scenario simulations
  5. 05Label conclusions by evidence tier
  6. 06Create tickets or send notifications

Use cases

  1. 01Assess the impact at an attack path's terminal asset
  2. 02Check which attack paths involve an asset
  3. 03Look for lateral movement beyond a path endpoint
  4. 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-path

After installing, type/review-attack-pathin Claude Code to confirm it loaded.

Details

From the project README

Review an attack path

What this does

Three modes against the SubImage attack-path engine and the underlying Neo4j graph:

  1. Walk a known path in plain English, surface the most sensitive terminal asset, and propose the fastest fix.
  2. Pivot from an asset to find which paths it sits on and review the worst.
  3. 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 modeRequired valueIf 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 hunta 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:

ValueIf 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.

  1. 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.
  2. Never invent labels, relationships, transitions, or security semantics. If the schema does not show a relationship, the path does not exist.
  3. Adjacency in the graph does not imply exploitability. A relationship between two nodes is necessary but not sufficient.
  4. Prefer missing a weak idea over reporting a bad one. Bad transition reports are worse than silence.
  5. If evidence is weak or ambiguous, say so explicitly. Do not paper over uncertainty.

Workflow

Mode A: walk a known attack path
  1. Read the path's ordered steps. An attack path is an :AttackPath:Signal and 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 position
    

    Use s.description, which is the rendered step text. s.templated_description still holds unrendered placeholders and is not for display. This result is bounded by construction (one path's steps), so it takes no LIMIT.

    FROM|TO is 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.

  2. 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.
  3. Translate each transition into a plain-language sentence ("from X the attacker pivots to Y because <reason from the transition>"). Do not relist the JSON.

  4. Rank impact: production / customer data / privileged identity > internal / dev > sandbox.

Mode B: pivot from an asset
  1. Find the paths that touch the asset. An asset participates through an AttackerCapacity, so the join runs REASON|ON to the capacity and GRANTS back 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 20
    

    Do not rewrite this as (:AttackPathStep)-[:FROM|TO]->(n). FROM and TO are derived edges holding the endpoints a step displays: TO comes from the step's own capacity, FROM from the previous step's. An asset can be the REASON a 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.

    coalesce on the status is not defensive padding: a path Signal can carry no status property, and that counts as active.

    context_type is 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.

  2. 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.

  3. 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.

  1. Pick the terminal node of the path you are extending (or use <ASSET_ID> directly if the user named it).
  2. Inspect what the schema says lives next to that node:
    subimageListModuleSchemaNodes(module="<module-of-the-node>")
    subimageGetNodesSchema(node_names=["<TerminalLabel>"])
    
    Note the relationship types and target labels.
  3. 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.
  4. Apply the 5-test checklist below before reporting anything.
  5. 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").

  1. subimageGetScenarioCapabilities(node_label="<NODE_LABEL>") to discover valid capabilities.
  2. Tell the user which capabilities are available and ask which to grant.
  3. subimageCreateAttackPathScenario(..., preview_only=true). Always pass preview_only=true: the user can inspect the result without it being persisted as a real path.
  4. 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:

  • subimageRunCypher takes 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 LIMIT you write, and reports the true total_count alongside. That is well above anything here, but quote total_count rather 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]->, and REQUIRED to 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 subimageRunCypher returns 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:

  1. The source and target labels are supported by real rows in this tenant's graph (not just the schema).
  2. The relationship between them exists in the graph (a Cypher probe found at least one row).
  3. The attacker's current position plausibly allows the step (capability, network reach, identity).
  4. The step provides meaningful new impact, privilege, or reach (not just "another node of the same kind").
  5. 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:

  1. subimageListLinearTeams() to get team ids.
  2. 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=true on 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

Comments

Markdown supported