Threat modeling
You write a code-derived DFD and STRIDE threats with citations;
cite.mjs check <st>/threat-model.json re-reads every citation against the
bytes at head and validates the disposition grammar — everything else is
your judgement. Three rules hold everywhere:
- An element or a mitigation exists only because it is cited. Same
citation shape as a finding's (
references/dfd-elements.md), same
FAILED <locus>.<i> <why> vocabulary.
- A mitigation is a claim, not a fact (
references/mitigations-as-claims.md)
— you never grade your own claim; a fresh security-reviewer's
mitigation-review does, and even that never rewrites the model.
- You assign every element's, threat's and mitigation's own id
(
E-nnn/T-nnn/M-nnn) yourself; never write state, oid,
snippet_redacted or check_stamp — check stamps those, and
refuses a file carrying them from your hand (D10), the same rule as
findings. id anywhere but element/threat/mitigation position is
refused there too.
The shape
{ head?, elements: [{id, name, kind, citations}], threats: [{id, element_id, stride, title, mitigations, disposition}] }.
{
"elements": [
{ "id": "E-001", "name": "HTTP handler", "kind": "process",
"citations": [{ "path": "src/app.js", "lines": [1, 3], "snippet": "…" }] },
{ "id": "E-002", "name": "Orders table", "kind": "datastore",
"citations": [{ "path": "src/orders.js", "lines": [40, 45], "snippet": "…" }] }
],
"threats": [
{ "id": "T-001", "element_id": "E-002", "stride": "T",
"title": "Order total tampered before it is charged",
"mitigations": [
{ "id": "M-001", "claim": "createOrder recomputes the total from line items (lines 40-52)",
"citations": [{ "path": "src/orders.js", "lines": [40, 52], "snippet": "…" }] }
],
"disposition": "mitigated(M-001)" }
]
}
kind ∈ external | boundary | process | datastore | flow
(references/dfd-elements.md — which letters apply to which kind is in
references/stride.md).
stride ∈ S | T | R | I | D | E, one letter per threat.
disposition ∈ open | accepted(R-nnnn) | mitigated(M-nnn) | planned(TC-nnn) | out-of-scope(<reason>) — the full grammar, what each
ref must name and the one-run sequence are in references/dispositions.md.
ids (E-nnn, T-nnn, M-nnn) are unique across the whole model,
not just within their array.
Procedure
- Read
engagement.md's scope_paths — what you may cite.
- Enumerate entry points — routers, handlers, consumers, schedulers.
- Elements, one citation each —
external/process at entry points,
boundary at trust changes, datastore/flow at sinks and moves
(references/dfd-elements.md § Where to start).
- STRIDE per element — only the letters
references/stride.md's
table marks for that kind; one threat per effect.
- Mitigations as cited claims — a sentence a reviewer can confirm or
refute from the citation alone (
references/mitigations-as-claims.md).
- Dispositions — the one you can justify now;
mitigated(M-nnn) only
when the mitigation is already in the model under that threat.
cite.mjs check <st>/threat-model.json.
- Fix
TM-INVALID — the locus and the missing relationship name what
to correct; edit threat-model.json and check again.
- Candidate passive cases — when a threat is worth an admitted test,
draft it (never
admit it yourself — that is the lead's cases.mjs admit) at .agents/security-testing/cases/TC-NNN_<slug>.md, id: TC-NNN, a # heading as its title. A planned(TC-nnn) disposition
only validates once the lead has admitted it.
Result lines you will see
FAILED <locus>.<i> <why> — why ∈ bad-shape, path-not-in-scope,
range-over-40, not-in-tree, snippet-not-found.
TM-INVALID <locus>: <why> — a structural or disposition defect, one per
line, in document order (references/dispositions.md).
MODEL elements=<n> threats=<n> open=<n> then
CHECK verified=<n> failed=<n> — printed on every run; exit 4 on any
FAILED/TM-INVALID, else 0.
--md after those: the elements/threats/mitigations tables, then
TABLES sha256=<hex>.
Your return line
cite.mjs check exits 0, no TM-INVALID lines ⇒
MODEL_WRITTEN elements=<n> threats=<n> open=<n> (the counts from the
MODEL line). Anything else ⇒ the TM-INVALID lines verbatim — fix what
they name and check again before you reply if you can.
Commands
node scripts/cite.mjs --help (from secure-code-review/scripts/, the
only script this skill uses):
usage: cite <command> [options]
commands:
init
show
check
redact
show <path> [start end] [--at <oid>] for reading; check <threat-model.json> [--md [--no-snippets]] for validating; see
secure-code-review/SKILL.md for init and redact. Every string printed
or written is redacted (D9); the file is rewritten with check_stamp and,
per citation, state, oid, snippet_redacted — never edit a stamped
file by hand (REFUSED agent-written key <key>); fix the unstamped source
and let the lead re-run check.
References
references/dfd-elements.md (the five kinds, the citation), references/stride.md
(the six letters, which apply to which kind), references/mitigations-as-claims.md
(the claim shape, the mitigation-review contract), references/dispositions.md
(the five-value grammar, the failure format, the one-run sequence).
1---2name: threat-modeling3description: Use when deriving a data-flow diagram and STRIDE threats from code, with a citation per element and mitigations recorded as claims a fresh reviewer can confirm or refute; provides the threat-model.json shape cite.mjs check validates.4license: MIT5---67# Threat modeling89You write a **code-derived DFD and STRIDE threats with citations**;10`cite.mjs check <st>/threat-model.json` re-reads every citation against the11bytes at `head` and validates the disposition grammar — everything else is12your judgement. Three rules hold everywhere:13141. **An element or a mitigation exists only because it is cited.** Same15 citation shape as a finding's (`references/dfd-elements.md`), same16 `FAILED <locus>.<i> <why>` vocabulary.172. **A mitigation is a claim, not a fact** (`references/mitigations-as-claims.md`)18 — you never grade your own claim; a fresh `security-reviewer`'s19 `mitigation-review` does, and even that never rewrites the model.203. **You assign every element's, threat's and mitigation's own id21 (`E-nnn`/`T-nnn`/`M-nnn`) yourself; never write `state`, `oid`,22 `snippet_redacted` or `check_stamp`** — `check` stamps those, and23 refuses a file carrying them from your hand (D10), the same rule as24 findings. `id` anywhere but element/threat/mitigation position is25 refused there too.2627## The shape2829`{ head?, elements: [{id, name, kind, citations}], threats: [{id,30element_id, stride, title, mitigations, disposition}] }`.3132```json33{34 "elements": [35 { "id": "E-001", "name": "HTTP handler", "kind": "process",36 "citations": [{ "path": "src/app.js", "lines": [1, 3], "snippet": "…" }] },37 { "id": "E-002", "name": "Orders table", "kind": "datastore",38 "citations": [{ "path": "src/orders.js", "lines": [40, 45], "snippet": "…" }] }39 ],40 "threats": [41 { "id": "T-001", "element_id": "E-002", "stride": "T",42 "title": "Order total tampered before it is charged",43 "mitigations": [44 { "id": "M-001", "claim": "createOrder recomputes the total from line items (lines 40-52)",45 "citations": [{ "path": "src/orders.js", "lines": [40, 52], "snippet": "…" }] }46 ],47 "disposition": "mitigated(M-001)" }48 ]49}50```5152- `kind` ∈ `external | boundary | process | datastore | flow`53 (`references/dfd-elements.md` — which letters apply to which kind is in54 `references/stride.md`).55- `stride` ∈ `S | T | R | I | D | E`, one letter per threat.56- `disposition` ∈ `open | accepted(R-nnnn) | mitigated(M-nnn) |57 planned(TC-nnn) | out-of-scope(<reason>)` — the full grammar, what each58 ref must name and the one-run sequence are in `references/dispositions.md`.59- `id`s (`E-nnn`, `T-nnn`, `M-nnn`) are unique across the **whole** model,60 not just within their array.6162## Procedure63641. Read `engagement.md`'s `scope_paths` — what you may cite.652. **Enumerate entry points** — routers, handlers, consumers, schedulers.663. **Elements, one citation each** — `external`/`process` at entry points,67 `boundary` at trust changes, `datastore`/`flow` at sinks and moves68 (`references/dfd-elements.md` § Where to start).694. **STRIDE per element** — only the letters `references/stride.md`'s70 table marks for that `kind`; one threat per effect.715. **Mitigations as cited claims** — a sentence a reviewer can confirm or72 refute from the citation alone (`references/mitigations-as-claims.md`).736. **Dispositions** — the one you can justify now; `mitigated(M-nnn)` only74 when the mitigation is already in the model under that threat.757. **`cite.mjs check <st>/threat-model.json`.**768. **Fix `TM-INVALID`** — the locus and the missing relationship name what77 to correct; edit `threat-model.json` and `check` again.789. **Candidate passive cases** — when a threat is worth an admitted test,79 draft it (never `admit` it yourself — that is the lead's `cases.mjs80 admit`) at `.agents/security-testing/cases/TC-NNN_<slug>.md`, `id:81 TC-NNN`, a `# ` heading as its title. A `planned(TC-nnn)` disposition82 only validates once the lead has admitted it.8384## Result lines you will see8586- `FAILED <locus>.<i> <why>` — `why` ∈ `bad-shape`, `path-not-in-scope`,87 `range-over-40`, `not-in-tree`, `snippet-not-found`.88- `TM-INVALID <locus>: <why>` — a structural or disposition defect, one per89 line, in document order (`references/dispositions.md`).90- `MODEL elements=<n> threats=<n> open=<n>` then91 `CHECK verified=<n> failed=<n>` — printed on every run; exit `4` on any92 `FAILED`/`TM-INVALID`, else `0`.93- `--md` after those: the elements/threats/mitigations tables, then94 `TABLES sha256=<hex>`.9596## Your return line9798`cite.mjs check` exits `0`, no `TM-INVALID` lines ⇒99`MODEL_WRITTEN elements=<n> threats=<n> open=<n>` (the counts from the100`MODEL` line). Anything else ⇒ the `TM-INVALID` lines verbatim — fix what101they name and `check` again before you reply if you can.102103## Commands104105`node scripts/cite.mjs --help` (from `secure-code-review/scripts/`, the106only script this skill uses):107108```109usage: cite <command> [options]110111commands:112 init113 show114 check115 redact116```117118`show <path> [start end] [--at <oid>]` for reading; `check119<threat-model.json> [--md [--no-snippets]]` for validating; see120`secure-code-review/SKILL.md` for `init` and `redact`. Every string printed121or written is redacted (D9); the file is rewritten with `check_stamp` and,122per citation, `state`, `oid`, `snippet_redacted` — never edit a stamped123file by hand (`REFUSED agent-written key <key>`); fix the unstamped source124and let the lead re-run `check`.125126## References127128`references/dfd-elements.md` (the five kinds, the citation), `references/stride.md`129(the six letters, which apply to which kind), `references/mitigations-as-claims.md`130(the claim shape, the `mitigation-review` contract), `references/dispositions.md`131(the five-value grammar, the failure format, the one-run sequence).