Skip to content

Build your first threat model

This is the shortest path to something real: one command that reads a repository’s annotations and produces its threat model, a dashboard, and a SARIF export. It sends no traffic, needs no target, and needs no coding agent.

By the end you can read the three numbers that matter and, more usefully, see which threats a probe would never have been able to test.

  1. From the repository root:

    Terminal window
    bravos model .
    run 20260910-004305-demo · branch bravos/run-20260910-004305-demo
    11 annotations · 3 exposures · 4 threats tracked
    report /Users/you/.bravos/runs/20260910-004305-demo/rounds/1/model-report/threat-model.md
    report (json) /Users/you/.bravos/runs/20260910-004305-demo/rounds/1/model-report/threat-model.json
    dashboard /Users/you/.bravos/runs/20260910-004305-demo/rounds/1/model-report/threat-dashboard.html
    sarif /Users/you/.bravos/runs/20260910-004305-demo/rounds/1/findings.sarif
    MODEL READY — open /Users/you/.bravos/runs/20260910-004305-demo/rounds/1/model-report/threat-dashboard.html
    → see it in the Bravos dashboard: bravos dashboard

    It takes about a second. The run id is the timestamp plus the directory name.

  2. Check what it did to your repository.

    Terminal window
    git status --short
    git branch --show-current
    bravos/run-20260910-004305-demo

    Your working tree is untouched: git status prints exactly what it printed before. What changed is the current branch. Bravos checked out bravos/run-<run-id> so that anything a later phase writes lands somewhere revertible. bravos model . --no-checkpoint skips the branch, and either way the model tier commits nothing.

  3. Read the three numbers.

    11 annotations · 3 exposures · 4 threats tracked
    Number Means
    annotations every guardlink annotation in the repository: assets, threats, flows, mitigations, and exposures together
    exposures the @exposes subset that survives into the export: a threat hypothesised against an asset at a line
    threats tracked rows in this run’s ledger

    Threats tracked exceeds exposures here because the ledger also carries the exposures guardlink’s export drops. The next step is where those show up.

bravos model tells you what is there. bravos inspect tells you where the view is lossy, which is the part worth your attention.

Terminal window
bravos inspect .
/Users/you/demo
guardlink 2.0.0 · cxg pins 6 files
sarif: /Users/you/demo/whitebox/findings.sarif
SUMMARY
exposures 3 (3 critical)
confirmed 0
unique pairs 2 ← what `guardlink diff` can distinguish
annotations 11 assets 2 threats 3 flows 0
EXPOSURES (3)
critical #orders-dao #sqli app/data/orders-dao.js:4
critical #orders-dao #sqli app/data/orders-dao.js:8
critical #orders #idor app/routes/orders.js:7
PAIR COLLISIONS (1 groups, 1 findings maskable)
Distinct exposures sharing one asset::threat pair. `guardlink diff` keys on that pair
alone, so discovering another member of a group reports 0 added — a false dry round.
orders-dao::sqli ×2
critical app/data/orders-dao.js:4
critical app/data/orders-dao.js:8
SUPPRESSED FROM EXPORT (1)
A @mitigates or @accepts anywhere in the repo removes every exposure sharing that
pair from the SARIF export. cxg never sees these and cannot test them.
high orders::xss ×1 via @mitigates
REACHABILITY
file-anchored 0 usable for goal synthesis
asset-guessed 0 route inferred, often wrong
no route 3 needs agent enrichment
CXG CORRELATION
hypotheses 3
correlated 3/3 ok

Four things to take from it.

Unique pairs, against exposures. guardlink identifies an exposure by its asset and threat alone, so three findings collapse into two identities. A second, different weakness in a pair that is already known cannot be distinguished from the first, and guardlink diff reports nothing added.

Pair collisions. The block names the group it happened in. Bravos keys its own ledger on asset, threat, file and a hash of the message instead, which is what lets a round say what it actually added.

Suppressed from export. One @mitigates anywhere removes every exposure sharing that asset and threat pair. A control that is 90% correct is more dangerous than none, because it ends the investigation silently. The mitigation-audit lens exists to attack exactly these.

Reachability and correlation. No annotation here carries a route, so every goal will need an agent to work out what to send, which is the slow part of a verify run. Correlation short of 3/3 means findings would arrive that cannot be traced back to an exposure, and an unattributable finding is never written back.

Every threat is in it already, all of them unverified:

Terminal window
bravos ledger
4 threats tracked · 0 settled · 3 outstanding
UNVERIFIED (3)
critical #orders-dao #sqli app/data/orders-dao.js:8
critical #orders-dao #sqli app/data/orders-dao.js:4
critical #orders #idor app/routes/orders.js:7
SUPPRESSED (1)
high orders xss app/routes/orders.js:13

The suppressed row is the point. It is counted, not absent: something a probe cannot reach is a gap in coverage, and a tool that dropped it would report the run as more complete than it was.

Terminal window
bravos dashboard

A local page over every run on this machine, on http://127.0.0.1:8787 by default. It is read-only: it displays runs and hands you commands, and nothing in it starts a run. --port moves it and --no-open stops it launching a browser. The command does not return; stop it with Ctrl-C.

  • A threat model report and dashboard, under $BRAVOS_HOME/runs/<run-id>/.
  • A SARIF export, the file cxg consumes.
  • A ledger of every tracked threat, all of it still unverified.

Every threat in that ledger is a hypothesis. Nothing has been tested. Making them true or false is the verify tier’s job, and it sends real exploit traffic: