| name | threat-register |
| description | Use when the user asks for a fleet-wide or portfolio-wide view of threats (not findings) — "how many unmitigated critical threats do we have", "threat register", "which products carry the most open threat exposure", "what class-closing mitigations would pay off fleet-wide" — or asks to (re)build the threat register. Deterministically aggregates every *-threat-model.md (and legacy THREAT_MODEL.md) under analysis-results into threat-register/threat-register.{json,md,html}, keyed by the stable compound key <model-slug>:<Tn>. |
| argument-hint | [--root <analysis-results>] [--out <dir>] |
| user-invocable | true |
| allowed-tools | ["Read","Glob","Bash(python3 *build_threat_register.py:*)","Bash(python3 *lint_threat_model.py:*)","Bash(ls:*)","Bash(wc:*)"] |
threat-register
The fleet-wide roll-up of threats — the durable, survives-a-patch
abstractions from /threat-model artifacts — complementing the dashboards
that roll up findings. Findings answer "what defects exist at this
commit"; the threat register answers "what standing exposure does the
portfolio carry, what state is it in, and where would one control buy the
most".
Identity model
Threat IDs are document-scoped (T1, T2, …) by design — a threat must
not be SHA-pinned, and the disposition ledger tracks findings, not
threats. The register derives a stable compound key
<product>/<model-slug>:<Tn> for every threat; it is durable because
schema.md forbids renumbering or reusing threat IDs, and the builder
requalifies any colliding slug with its full path, so keys are globally
unique. Threats cite canonical finding IDs in their evidence column —
that is the join to the ledger, and it runs in that direction only.
Run
python3 <skill-dir>/build_threat_register.py --root <analysis-results>
One deterministic pass (~5s for 7,500 models). The builder reuses
scripts/lint_threat_model.py's parser, so it reads exactly what the
contract gate enforces; models that do not conform are skipped and
counted (models_skipped_nonconforming) — if that count is nonzero,
run the linter to find them, fix, and rebuild. It never edits a model.
Output
<root>/threat-register/threat-register.{json,md,html}:
- json — full register (every threat with key, product, model,
actors, surface, asset, impact, likelihood, status, controls, evidence,
rank score) plus totals, per-product roll-up, and quick wins.
- md — leadership summary: status/impact totals, top-25 open threats,
top quick wins, products by open-threat exposure.
- html — self-contained dashboard, no external assets.
Rank score is a fixed ordinal product (impact weight × likelihood
weight, documented in meta.scoring); it orders rows and nothing else —
it is not CVSS and never feeds the portfolio risk index, which remains
the ledger's job (docs/findings-lifecycle.md). Statuses are reported
exactly as the models state them; the register draws no conclusions.
Quick wins = section 8 mitigations with closes_class: yes and
effort XS/S that cover at least one unmitigated high/critical/
existential threat — the highest-leverage engineering asks in the fleet.
Keeping it current
The register is a projection: regenerate after any batch of
/threat-model runs, reviews, or updates (it is cheap enough to rebuild
on every consumption). Downstream: /generate-team-report and the
executive summary may cite per-product rows; /threat-model review
verdicts change status fields, which the next rebuild picks up.