How to
How to threat model with an AI agent
A threat model is a data-flow diagram plus a register: trust boundaries, elements, and the threats against each one with a mitigation and a status. Give the agent the flow; it writes the register as one typed block, and chiltepin check confirms every threat points at a real element.
1. Give the agent the request
Install the skill once — npx skills add jdiejim/chiltepin -g — then say what the reader needs, in plain words. Do not name a block type; picking it is the skill's job.
Threat model the login flow: browser, CDN, auth API, session store, users database. STRIDE, with what we already mitigate and what we accept. Write docs/security/login-threats.md and run chiltepin check.
List the parts so the agent models your flow, not a generic one. Saying “what we already mitigate and what we accept” makes it fill status honestly instead of marking everything mitigated.
2. What the agent picks, and why
The skill reads the request as the reader's question and answers it with two to five blocks, each a different lens. For this request it picks:
sequence— the flow first — the login messages in order, so the reader knows what is being attacked before reading what attacks itthreatmodel— the model — boundaries, nodes and edges with a channel each, and the STRIDE register with category, mitigation, severity and status per threattable— the accepted risks pulled out on their own, with an owner and a review date — the part a security review actually signs
Before writing each one it runs chiltepin block <type>, which prints the fields, enums and terse forms from the schema. It writes YAML; it never places a shape.
The block it wrote
The threatmodel block the agent wrote for the login flow in the gallery, unedited: three trust boundaries, five elements, four channels and eleven STRIDE threats with a status each.
```threatmodel
id: login-stride
title: Login — STRIDE
boundaries:
- { id: inet, col: 1, row: 1, cols: 1, rows: 2, label: Internet }
- { id: edge, col: 2, row: 1, cols: 1, rows: 2, label: Edge }
- { id: core, col: 3, row: 1, cols: 2, rows: 2, label: Private network }
nodes:
- { id: browser, col: 1, row: 1, name: Browser, kind: external }
- { id: edge, col: 2, row: 1, name: CDN + WAF }
- { id: auth, col: 3, row: 1, name: Auth API }
- { id: sessions, col: 4, row: 1, name: Session store (Redis), kind: store }
- { id: users, col: 4, row: 2, name: Users DB (Postgres), kind: store }
edges:
- { from: browser, to: edge, label: "POST /login (email, password)", channel: tls }
- { from: edge, to: auth, label: "forward + signed client IP", channel: tls }
- { from: auth, to: users, label: "SELECT hash by email; UPDATE failed_attempts", channel: internal }
- { from: auth, to: sessions, label: "SET sid -> user_id, TTL 12 h", channel: internal }
threats:
- { id: T1, target: browser, category: S, threat: "Credential stuffing with leaked email + password lists", mitigation: "Edge rate limit 10/min per IP and 5/h per email; breached-password check on login", severity: high, status: mitigated }
- { id: T2, target: browser, category: S, threat: "Phishing page relays password and TOTP code in real time", mitigation: "None today; passkeys planned for Q1", severity: high, status: accepted }
- { id: T3, target: auth, category: S, threat: "Brute force of the 6-digit TOTP code", mitigation: "5 code attempts per pending login, then the login is discarded", severity: high, status: mitigated }
- { id: T4, target: edge, category: T, threat: "Spoofed X-Forwarded-For dodges the per-IP limit", mitigation: "Auth API reads only the CDN-signed client IP header and drops the rest", severity: medium, status: mitigated }
- { id: T5, target: sessions, category: T, threat: "Another service on the network rewrites the sid -> user_id map", mitigation: "Redis ACL user scoped to the Auth API; network policy allows only Auth", severity: medium, status: open }
- { id: T6, target: auth, category: R, threat: "User denies a login from a device they did not use", mitigation: "Login audit row (time, IP, UA); email on first login from a new device", severity: medium, status: mitigated }
- { id: T7, target: auth, category: I, threat: "Response time reveals whether the email exists", mitigation: "Run Argon2id against a fixed dummy hash when no row is found", severity: medium, status: open }
- { id: T8, target: users, category: I, threat: "Hash column read through SQL injection or a leaked backup", mitigation: "Parameterised queries; Argon2id m=64 MiB t=3; backups encrypted at rest", severity: high, status: mitigated }
- { id: T9, target: edge, category: D, threat: "Login flood exhausts Argon2id CPU on the Auth API", mitigation: "WAF challenge above 500 req/s on /login; hashing worker pool capped at 32", severity: high, status: mitigated }
- { id: T10, target: sessions, category: E, threat: "Session fixation: attacker plants a sid before the victim logs in", mitigation: "New sid on every successful login; the pre-login sid is deleted", severity: high, status: mitigated }
- { id: T11, target: browser, category: E, threat: "Session cookie stolen through XSS", mitigation: "HttpOnly + Secure + SameSite=Lax cookie; CSP with no inline scripts", severity: high, status: mitigated }
```3. Check it
The agent runs npx -y chiltepin check docs/<file>.md --json. Every diagnostic has a stable code, the file and line, and a hint. One it might see on a first draft:
E_SCHEMA docs/security/login-threats.md:40 threatmodel: threats.0.category: invalid value "Spoofing"
hint: Use one of: S | T | R | I | D | E.It fixes what the hint says and re-runs the check. A change is not done until the check passes — and in CI the same command fails the build, so a document cannot drift silently.
4. Render it
npx chiltepin html docs/security/login-threats.md -p renders the diagram and the register together. chiltepin build publishes it beside the architecture docs with doc#id links resolved, so the model can point at the C4 view it covers; chiltepin pdf produces the copy an auditor wants.
See it done
The agent-written document for this request is in the gallery: Login path — threat model, shown exactly as generated from “Threat-model the login path — where can it be attacked and what stops each attack?”. Related: threat model diagrams, the threat model template, and the blocks threatmodel, sequence, dfd.
Frequently asked questions
- What does a threat model document contain?
- A data-flow diagram with trust boundaries, and a register of threats against each element: what the threat is, its STRIDE category, the mitigation, a severity and a status (mitigated, accepted, open). Chiltepin’s threatmodel block holds both in one structure, so the diagram and the register cannot disagree about which nodes exist.
- Can an AI agent be trusted to threat model?
- It is good at coverage and bad at knowing your controls. Ask it for STRIDE per element and it will enumerate the standard threats systematically; tell it what you already mitigate and it records that. The review is yours; the value is that nothing obvious is missing and the result is a checked, reviewable diff rather than a whiteboard photo.
- What does chiltepin check verify in a threat model?
- That every threat targets a node that exists, every edge joins real nodes, categories are STRIDE letters, severity and status use the documented enums, and every boundary is well-formed. It also warns when a block is past its density budget, which for a threat register means split it by flow.
- How do I keep the model current?
- It lives in the repository next to the code it models. When a service is added, the agent updates the block in the same pull request; chiltepin check in CI fails if a threat now targets a node that was removed. Status changes from accepted to mitigated are a one-word diff.