---
name: dnr-respond
description: Incident response workup for a lead in hand — an alert, an IOC,
  or a /dnr-hunt finding. Scopes the lead across the logs, verdicts whether
  the attack succeeded, quantifies blast radius, confirms root cause in
  source with a local PoC, and writes a proposed (never executed)
  containment/remediation/recovery plan. Use when asked to "respond to this
  alert", "work this incident", "how bad is this", or "run dnr-respond".
  /dnr-hunt is the no-alert entry to the same track.
argument-hint: "<lead> [--logs dir] [--repo path] [--fresh]"
allowed-tools:
  - Read
  - Glob
  - Grep
  - Write
  - Bash(rg:*)
  - Bash(grep:*)
  - Bash(awk:*)
  - Bash(sed:*)
  - Bash(sort:*)
  - Bash(uniq:*)
  - Bash(cut:*)
  - Bash(head:*)
  - Bash(tail:*)
  - Bash(wc:*)
  - Bash(ls:*)
  - Bash(du:*)
  - Bash(file:*)
  - Bash(jq:*)
  - Bash(python3:*)
  - Bash(curl:*)
  - Bash(kill:*)
---

# dnr-respond

Lead-in-hand incident response: take one concrete starting point and answer
the four questions an incident commander needs, in order — **what happened,
did it succeed, how far did it go, and what do we do about it.** The output
is a verdict, a quantified blast radius, a root cause proven in source, and
a response plan that is *proposed, never executed*.

Invoke with `/dnr-respond <lead> [--logs dir] [--repo path] [--fresh]`.

**The lead (`$1`) can be:**
- an incident id from the hunt's `INCIDENTS.json` (e.g. `INC-2`) — the
  `/dnr-hunt` handoff
- an alert pasted from an alert queue (JSON line or prose)
- an IOC: an IP, an account name, a route, a session identifier
- free text ("a customer says their address was quoted in a phishing email")

**Other arguments:**
- `--logs` = logs directory or dnr target directory (same resolution as
  `/dnr-hunt`; defaults to `targets/dnrcanary` if it exists, else cwd).
- `--repo` = application source (defaults to the target's `app/`).
- `--fresh` = ignore any checkpoint in the run dir's `.dnr-respond-state/`
  and mint a new run dir.

## Hard rules (shared with /dnr-hunt — the track's contract)

1. **Log evidence alone never confirms a vulnerability.**
   `confirmed_exploited` requires log evidence + the flaw in source + a PoC
   fired locally this session. Otherwise the ceiling is `suspected`.
2. **Query, don't read.** Never `Read` or `cat` a log file; `wc`/`head`/
   `tail` to profile, `grep`/`awk`/`sort`/`uniq`/`python3` to interrogate.
   Small files (alert queues, error logs under ~200 lines) may be read whole.
3. **Propose, never execute.** No blocking, disabling, rotating, or config
   changes — in any environment, including the demo app. The plan names
   actions; humans execute them.
4. **Local only.** PoCs fire at `127.0.0.1` against an instance you
   started. Never touch a remote host.
5. **Every number is a query result.** "How many records" and "how many
   customers" get answered by pipelines run this session, never estimated.

## Run directory and checkpointing

**First action — establish the run directory**, the same way `/dnr-hunt`
does, but as a *consumer*. Let `TARGET` be the basename of the resolved
`--logs`/target path (default `targets/dnrcanary`) and `SCOPE` that path.

- **Lead is `INC-n`** (the `/dnr-hunt` handoff, or a second lead from a hunt
  you already started responding to): work the response into the investigation
  that incident lives in. `{RUN}` is the newest `results/TARGET/<timestamp>/`
  directory that contains an `INCIDENTS.json` — find it with Bash, e.g.
  `ls -1d results/TARGET/*/ | sort -r | while read d; do [ -f "$d/INCIDENTS.json" ] && echo "$d" && break; done`.
  Do **not** call `rundir` for an `INC-n` lead — `rundir` mints a fresh empty
  dir once a prior `/dnr-respond` in that dir has completed, which would strand
  the incident's `INCIDENTS.json` in the old dir. Only if no run dir holds an
  `INCIDENTS.json` (you were handed `INC-n` but never ran the hunt) fall through
  to the `rundir` line below.
- **Any other lead** (alert, IOC, free text): Bash
  `python3 .claude/skills/_lib/checkpoint.py rundir results/TARGET --state .dnr-respond-state --scope "SCOPE"`
  (append `--fresh` if it's in `$ARGUMENTS`). This adopts the newest run dir for
  the target if one exists, else mints a fresh `results/TARGET/<timestamp>/`.

That path is `{RUN}`; substitute the literal path everywhere below, and
double-quote it (and `SCOPE`) in Bash.

Checkpoint mechanics are identical to `/dnr-hunt`, state dir
`{RUN}/.dnr-respond-state/`, phases 0–4. When `{RUN}` already holds a
**completed** `.dnr-respond-state` from an earlier lead, `load` reports
`complete` and the rules below reset it cleanly — a second lead enriches the
same `INCIDENTS.json` and writes its own `RESPONSE-<lead>.md`. On start:
`python3 .claude/skills/_lib/checkpoint.py load {RUN}/.dnr-respond-state` —
absent/complete/`--fresh` → `reset` and start at Phase 0; running with
`phase_done == N` → read `phase0.json`…`phaseN.json`, resume at N+1. After
each phase: Write payload to `{RUN}/.dnr-respond-state/_chunk.tmp`, then
`checkpoint.py save {RUN}/.dnr-respond-state <N> <name> --from {RUN}/.dnr-respond-state/_chunk.tmp`.
Never write `progress.json` directly; never pass payload via heredoc/stdin.
After outputs: `checkpoint.py done {RUN}/.dnr-respond-state 4`.

## Phase 0 — Parse the lead, locate inputs

1. Resolve `--logs` (dnr target config or bare directory) and inventory log
   files (`du`, `wc -l`, `head -2` each — identify formats and field
   positions). The corpus is expected to exist already — the target README
   covers generating it.
2. Parse the lead into starting entities:
   - `INC-n` → read that entry from `{RUN}/INCIDENTS.json` (the run dir
     adopted above); its IPs, accounts, routes, and timeline rows are the
     seed set.
   - alert line → source IP, rule, path, timestamp.
   - IOC → the entity itself.
   - free text → extract the observables (who reported, what data, when);
     the seed set may start as just a route or a table name.
3. State the working question in one sentence (e.g. "did 203.0.113.66's
   activity against /login succeed, and if not, what did?"). Everything in
   Phase 1 serves that question.

Checkpoint `phase0.json`:
`{"lead": ..., "lead_type": ..., "entities": {"ips": [], "accounts": [], "routes": [], "windows": []}, "question": ..., "logs_dir": ..., "repo": ..., "files": [...]}`

## Phase 1 — Scope: pull everything the entities touched

For each seed entity, extract its complete activity, then expand one ring:

- **Per IP:** every request — time-ordered, with route, status, size, UA,
  and authenticated user. Summarize phases of behavior, don't paste
  hundreds of lines.
- **Per account:** every login (from which IPs), every authenticated
  request. Compare against that account's historical pattern in the same
  corpus — new IP, new UA, new volume, new hours are all signal. (A new
  IP *alone* is baseline noise — users roam; combine it with volume,
  cadence, or what the session did next before treating it as a lead.)
- **Per route:** who else hit it the same way in the window? One attacker
  found via an alert may not be the only one.
- **Ring expansion (once):** entities discovered above join the set —
  the account an IP logged into, the second IP that touched the same
  account, the route a session pivoted to. Expand once, deliberately;
  /dnr-hunt's open-ended loop is the tool when scope keeps growing.
- **Correlate the error log and alert queue** rows for all entities into
  one timeline.

Checkpoint `phase1.json`:
`{"activity": [{"entity", "summary", "first_seen", "last_seen", "requests", "key_lines": [...]}], "expanded_entities": [...], "timeline": [...]}`

## Phase 2 — Verdict and blast radius

Answer the working question with evidence on both sides:

- **Did it succeed?** Status codes alone don't answer this — a 401 wall
  says no; 200s with anomalous sizes, error-then-success progressions, and
  data-shaped responses say yes. State the verdict and the discriminating
  evidence.
- **Blast radius, quantified.** What data was reached: which
  routes/tables, how many records, over what time span, belonging to how
  many distinct users/customers. Each figure comes from a pipeline (count
  the exfil-page sizes, count the distinct ids walked, join walked ids to
  owners via the app's own data if available locally — running the
  target's deterministic `seed_command` early, before Phase 3, is fine).
- **Containment status check (read-only).** Is the activity ongoing at the
  end of the corpus window, or did it stop? When was the last attacker
  action?
- If the verdict is "did not succeed", the workup doesn't end — say what
  the attacker was *trying* and why it failed; that feeds the plan's
  hardening section and the `ruled_out` record.

Checkpoint `phase2.json`:
`{"verdict": ..., "succeeded": bool, "discriminators": [...], "blast_radius": {"data": ..., "records": N, "distinct_victims": N, "window": ...}, "ongoing": bool, "last_activity": ...}`

## Phase 3 — Root cause and PoC

Same bar as `/dnr-hunt` Phase 3–4:

1. Read the implicated handler(s) in `--repo`; name the flaw (or the
   defense) at file/function precision; check sibling routes for the same
   flaw class.
2. Seed and start the app locally per target config: `seed_command` /
   `app_command` are target-relative — run them with the target
   directory as cwd; launch in the background via a single command that
   prints the PID (the same `python3 -c "...subprocess.Popen..."` pattern
   /dnr-hunt Phase 4 shows, with the port override env var if the default
   port is busy). Fire the **minimal** PoC, record command + response
   excerpt, fire the negative PoC against defended routes, then
   `kill <pid>` (never by port or pattern).
3. No runnable app → verdict ceiling is `suspected`, stated explicitly.

Checkpoint `phase3.json`:
`{"root_cause": {"file", "function", "mechanism"}, "poc": {"command", "status", "response_excerpt"}, "negative_poc": {...}, "siblings_checked": [...]}`

## Phase 4 — The response plan (PROPOSED, NEVER EXECUTED) and outputs

Write the plan to `{RUN}/RESPONSE.md` — or `{RUN}/RESPONSE-<lead>.md` (e.g.
`RESPONSE-INC-2.md`) if a `RESPONSE.md` from a different lead already
exists in the run dir; never silently overwrite another incident's plan.
Sections, every action carrying **what / why / risk-if-wrong**:

1. **Verdict & summary** — three sentences: what happened, did it succeed,
   how far it went.
2. **Timeline** — the merged entity timeline from Phase 1.
3. **Containment (proposed)** — block/deny candidates (IPs, sessions,
   tokens), accounts to suspend, routes to rate-limit or disable. For each:
   the evidence line that justifies it and what breaks if the call is wrong
   (e.g. blocking a NAT IP locks out legitimate users behind it).
4. **Eradication & remediation (proposed)** — the code fix, described at
   file/function level, handed off as a `/triage`-compatible vuln entry;
   interim mitigations if the fix will take time.
5. **Recovery (proposed)** — exactly which credentials rotate (the queried
   list, not "all users"), which customers get notified (the distinct-victim
   list from Phase 2), what monitoring confirms the attacker is out.
6. **Detection engineering (proposed)** — the rules that would have caught
   this earlier, derived from what you actually used to find it: the
   discriminating queries from Phase 2 turned into alert logic (e.g.
   error-rate per source on a route; sequential-id access cadence;
   account-from-new-IP at volume). Note which existing alerts fired,
   which were noise, and what that says about the queue.
7. **Open questions** — what this corpus cannot answer (earlier history,
   other log sources, whether stolen data was used elsewhere).

Then update `{RUN}/INCIDENTS.json` (same schema as `/dnr-hunt`; create it if
absent): add or enrich this incident — verdict, quantified impact, root
cause, PoC, timeline. Extra keys are fine — graders match on content, not
structure. Negative outcomes go to `ruled_out`.

Mark complete (`checkpoint.py done {RUN}/.dnr-respond-state 4`) and point the
user onward (substitute the literal `{RUN}` path):
- for the fix, from inside `{RUN}`: `/triage INCIDENTS.json --repo <repo>`
  then `/patch TRIAGE.json --repo <repo>` — both write to the directory
  they're invoked from, so running them there lands the verdicts and
  patches beside the incidents
- on a dnr target: `python <target>/grade.py {RUN}/INCIDENTS.json` to
  self-score (the user runs this)
- a human reviews and executes (or rejects) each proposed action; nothing
  in RESPONSE.md has been done

## Customizing beyond the demo

- Feed it your real alert queue one alert at a time — the lead parser
  takes any JSON or prose alert; the method doesn't change.
- Auth logs, CDN/WAF exports, and EDR threat events slot into Phase 1 as
  additional per-entity activity sources; keep the single merged timeline.
- The Phase 4 plan template is the part most worth adapting to your org:
  map containment to your actual controls (WAF rules, IdP session
  revocation, feature flags) — still as proposals routed to the owning
  team, never as actions the agent takes.
