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)
- 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.
- 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.
- Propose, never execute. No blocking, disabling, rotating, or config
changes — in any environment, including the demo app. The plan names
actions; humans execute them.
- Local only. PoCs fire at
127.0.0.1 against an instance you
started. Never touch a remote host.
- 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
- 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.
- 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.
- 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:
- 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.
- 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).
- 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:
- Verdict & summary — three sentences: what happened, did it succeed,
how far it went.
- Timeline — the merged entity timeline from Phase 1.
- 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).
- 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.
- 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.
- 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.
- 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.
1---2name: dnr-respond3description: 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.4---5
6# dnr-respond
7
8Lead-in-hand incident response: take one concrete starting point and answer
9the four questions an incident commander needs, in order — **what happened,
10did it succeed, how far did it go, and what do we do about it.** The output
11is a verdict, a quantified blast radius, a root cause proven in source, and
12a response plan that is *proposed, never executed*.
13
14Invoke with `/dnr-respond <lead> [--logs dir] [--repo path] [--fresh]`.
15
16**The lead (`$1`) can be:**
17- an incident id from the hunt's `INCIDENTS.json` (e.g. `INC-2`) — the
18 `/dnr-hunt` handoff
19- an alert pasted from an alert queue (JSON line or prose)
20- an IOC: an IP, an account name, a route, a session identifier
21- free text ("a customer says their address was quoted in a phishing email")
22
23**Other arguments:**
24- `--logs` = logs directory or dnr target directory (same resolution as
25 `/dnr-hunt`; defaults to `targets/dnrcanary` if it exists, else cwd).
26- `--repo` = application source (defaults to the target's `app/`).
27- `--fresh` = ignore any checkpoint in the run dir's `.dnr-respond-state/`
28 and mint a new run dir.
29
30## Hard rules (shared with /dnr-hunt — the track's contract)
31
321. **Log evidence alone never confirms a vulnerability.**
33 `confirmed_exploited` requires log evidence + the flaw in source + a PoC
34 fired locally this session. Otherwise the ceiling is `suspected`.
352. **Query, don't read.** Never `Read` or `cat` a log file; `wc`/`head`/
36 `tail` to profile, `grep`/`awk`/`sort`/`uniq`/`python3` to interrogate.
37 Small files (alert queues, error logs under ~200 lines) may be read whole.
383. **Propose, never execute.** No blocking, disabling, rotating, or config
39 changes — in any environment, including the demo app. The plan names
40 actions; humans execute them.
414. **Local only.** PoCs fire at `127.0.0.1` against an instance you
42 started. Never touch a remote host.
435. **Every number is a query result.** "How many records" and "how many
44 customers" get answered by pipelines run this session, never estimated.
45
46## Run directory and checkpointing
47
48**First action — establish the run directory**, the same way `/dnr-hunt`
49does, but as a *consumer*. Let `TARGET` be the basename of the resolved
50`--logs`/target path (default `targets/dnrcanary`) and `SCOPE` that path.
51
52- **Lead is `INC-n`** (the `/dnr-hunt` handoff, or a second lead from a hunt
53 you already started responding to): work the response into the investigation
54 that incident lives in. `{RUN}` is the newest `results/TARGET/<timestamp>/`
55 directory that contains an `INCIDENTS.json` — find it with Bash, e.g.
56 `ls -1d results/TARGET/*/ | sort -r | while read d; do [ -f "$d/INCIDENTS.json" ] && echo "$d" && break; done`.
57 Do **not** call `rundir` for an `INC-n` lead — `rundir` mints a fresh empty
58 dir once a prior `/dnr-respond` in that dir has completed, which would strand
59 the incident's `INCIDENTS.json` in the old dir. Only if no run dir holds an
60 `INCIDENTS.json` (you were handed `INC-n` but never ran the hunt) fall through
61 to the `rundir` line below.
62- **Any other lead** (alert, IOC, free text): Bash
63 `python3 .claude/skills/_lib/checkpoint.py rundir results/TARGET --state .dnr-respond-state --scope "SCOPE"`
64 (append `--fresh` if it's in `$ARGUMENTS`). This adopts the newest run dir for
65 the target if one exists, else mints a fresh `results/TARGET/<timestamp>/`.
66
67That path is `{RUN}`; substitute the literal path everywhere below, and
68double-quote it (and `SCOPE`) in Bash.
69
70Checkpoint mechanics are identical to `/dnr-hunt`, state dir
71`{RUN}/.dnr-respond-state/`, phases 0–4. When `{RUN}` already holds a
72**completed** `.dnr-respond-state` from an earlier lead, `load` reports
73`complete` and the rules below reset it cleanly — a second lead enriches the
74same `INCIDENTS.json` and writes its own `RESPONSE-<lead>.md`. On start:
75`python3 .claude/skills/_lib/checkpoint.py load {RUN}/.dnr-respond-state` —
76absent/complete/`--fresh` → `reset` and start at Phase 0; running with
77`phase_done == N` → read `phase0.json`…`phaseN.json`, resume at N+1. After
78each phase: Write payload to `{RUN}/.dnr-respond-state/_chunk.tmp`, then
79`checkpoint.py save {RUN}/.dnr-respond-state <N> <name> --from {RUN}/.dnr-respond-state/_chunk.tmp`.
80Never write `progress.json` directly; never pass payload via heredoc/stdin.
81After outputs: `checkpoint.py done {RUN}/.dnr-respond-state 4`.
82
83## Phase 0 — Parse the lead, locate inputs
84
851. Resolve `--logs` (dnr target config or bare directory) and inventory log
86 files (`du`, `wc -l`, `head -2` each — identify formats and field
87 positions). The corpus is expected to exist already — the target README
88 covers generating it.
892. Parse the lead into starting entities:
90 - `INC-n` → read that entry from `{RUN}/INCIDENTS.json` (the run dir
91 adopted above); its IPs, accounts, routes, and timeline rows are the
92 seed set.
93 - alert line → source IP, rule, path, timestamp.
94 - IOC → the entity itself.
95 - free text → extract the observables (who reported, what data, when);
96 the seed set may start as just a route or a table name.
973. State the working question in one sentence (e.g. "did 203.0.113.66's
98 activity against /login succeed, and if not, what did?"). Everything in
99 Phase 1 serves that question.
100
101Checkpoint `phase0.json`:
102`{"lead": ..., "lead_type": ..., "entities": {"ips": [], "accounts": [], "routes": [], "windows": []}, "question": ..., "logs_dir": ..., "repo": ..., "files": [...]}`
103
104## Phase 1 — Scope: pull everything the entities touched
105
106For each seed entity, extract its complete activity, then expand one ring:
107
108- **Per IP:** every request — time-ordered, with route, status, size, UA,
109 and authenticated user. Summarize phases of behavior, don't paste
110 hundreds of lines.
111- **Per account:** every login (from which IPs), every authenticated
112 request. Compare against that account's historical pattern in the same
113 corpus — new IP, new UA, new volume, new hours are all signal. (A new
114 IP *alone* is baseline noise — users roam; combine it with volume,
115 cadence, or what the session did next before treating it as a lead.)
116- **Per route:** who else hit it the same way in the window? One attacker
117 found via an alert may not be the only one.
118- **Ring expansion (once):** entities discovered above join the set —
119 the account an IP logged into, the second IP that touched the same
120 account, the route a session pivoted to. Expand once, deliberately;
121 /dnr-hunt's open-ended loop is the tool when scope keeps growing.
122- **Correlate the error log and alert queue** rows for all entities into
123 one timeline.
124
125Checkpoint `phase1.json`:
126`{"activity": [{"entity", "summary", "first_seen", "last_seen", "requests", "key_lines": [...]}], "expanded_entities": [...], "timeline": [...]}`
127
128## Phase 2 — Verdict and blast radius
129
130Answer the working question with evidence on both sides:
131
132- **Did it succeed?** Status codes alone don't answer this — a 401 wall
133 says no; 200s with anomalous sizes, error-then-success progressions, and
134 data-shaped responses say yes. State the verdict and the discriminating
135 evidence.
136- **Blast radius, quantified.** What data was reached: which
137 routes/tables, how many records, over what time span, belonging to how
138 many distinct users/customers. Each figure comes from a pipeline (count
139 the exfil-page sizes, count the distinct ids walked, join walked ids to
140 owners via the app's own data if available locally — running the
141 target's deterministic `seed_command` early, before Phase 3, is fine).
142- **Containment status check (read-only).** Is the activity ongoing at the
143 end of the corpus window, or did it stop? When was the last attacker
144 action?
145- If the verdict is "did not succeed", the workup doesn't end — say what
146 the attacker was *trying* and why it failed; that feeds the plan's
147 hardening section and the `ruled_out` record.
148
149Checkpoint `phase2.json`:
150`{"verdict": ..., "succeeded": bool, "discriminators": [...], "blast_radius": {"data": ..., "records": N, "distinct_victims": N, "window": ...}, "ongoing": bool, "last_activity": ...}`
151
152## Phase 3 — Root cause and PoC
153
154Same bar as `/dnr-hunt` Phase 3–4:
155
1561. Read the implicated handler(s) in `--repo`; name the flaw (or the
157 defense) at file/function precision; check sibling routes for the same
158 flaw class.
1592. Seed and start the app locally per target config: `seed_command` /
160 `app_command` are target-relative — run them with the target
161 directory as cwd; launch in the background via a single command that
162 prints the PID (the same `python3 -c "...subprocess.Popen..."` pattern
163 /dnr-hunt Phase 4 shows, with the port override env var if the default
164 port is busy). Fire the **minimal** PoC, record command + response
165 excerpt, fire the negative PoC against defended routes, then
166 `kill <pid>` (never by port or pattern).
1673. No runnable app → verdict ceiling is `suspected`, stated explicitly.
168
169Checkpoint `phase3.json`:
170`{"root_cause": {"file", "function", "mechanism"}, "poc": {"command", "status", "response_excerpt"}, "negative_poc": {...}, "siblings_checked": [...]}`
171
172## Phase 4 — The response plan (PROPOSED, NEVER EXECUTED) and outputs
173
174Write the plan to `{RUN}/RESPONSE.md` — or `{RUN}/RESPONSE-<lead>.md` (e.g.
175`RESPONSE-INC-2.md`) if a `RESPONSE.md` from a different lead already
176exists in the run dir; never silently overwrite another incident's plan.
177Sections, every action carrying **what / why / risk-if-wrong**:
178
1791. **Verdict & summary** — three sentences: what happened, did it succeed,
180 how far it went.
1812. **Timeline** — the merged entity timeline from Phase 1.
1823. **Containment (proposed)** — block/deny candidates (IPs, sessions,
183 tokens), accounts to suspend, routes to rate-limit or disable. For each:
184 the evidence line that justifies it and what breaks if the call is wrong
185 (e.g. blocking a NAT IP locks out legitimate users behind it).
1864. **Eradication & remediation (proposed)** — the code fix, described at
187 file/function level, handed off as a `/triage`-compatible vuln entry;
188 interim mitigations if the fix will take time.
1895. **Recovery (proposed)** — exactly which credentials rotate (the queried
190 list, not "all users"), which customers get notified (the distinct-victim
191 list from Phase 2), what monitoring confirms the attacker is out.
1926. **Detection engineering (proposed)** — the rules that would have caught
193 this earlier, derived from what you actually used to find it: the
194 discriminating queries from Phase 2 turned into alert logic (e.g.
195 error-rate per source on a route; sequential-id access cadence;
196 account-from-new-IP at volume). Note which existing alerts fired,
197 which were noise, and what that says about the queue.
1987. **Open questions** — what this corpus cannot answer (earlier history,
199 other log sources, whether stolen data was used elsewhere).
200
201Then update `{RUN}/INCIDENTS.json` (same schema as `/dnr-hunt`; create it if
202absent): add or enrich this incident — verdict, quantified impact, root
203cause, PoC, timeline. Extra keys are fine — graders match on content, not
204structure. Negative outcomes go to `ruled_out`.
205
206Mark complete (`checkpoint.py done {RUN}/.dnr-respond-state 4`) and point the
207user onward (substitute the literal `{RUN}` path):
208- for the fix, from inside `{RUN}`: `/triage INCIDENTS.json --repo <repo>`
209 then `/patch TRIAGE.json --repo <repo>` — both write to the directory
210 they're invoked from, so running them there lands the verdicts and
211 patches beside the incidents
212- on a dnr target: `python <target>/grade.py {RUN}/INCIDENTS.json` to
213 self-score (the user runs this)
214- a human reviews and executes (or rejects) each proposed action; nothing
215 in RESPONSE.md has been done
216
217## Customizing beyond the demo
218
219- Feed it your real alert queue one alert at a time — the lead parser
220 takes any JSON or prose alert; the method doesn't change.
221- Auth logs, CDN/WAF exports, and EDR threat events slot into Phase 1 as
222 additional per-entity activity sources; keep the single merged timeline.
223- The Phase 4 plan template is the part most worth adapting to your org:
224 map containment to your actual controls (WAF rules, IdP session
225 revocation, feature flags) — still as proposals routed to the owning
226 team, never as actions the agent takes.