River
Y CombinatorBacked by Y Combinator
FREE TEMPLATE

STRIDE Threat Model Template

STRIDE per element fixes how many questions your design makes applicable before anybody asks one. This pack counts them, then reports coverage against that total.

Free download  ·  No account needed

Every STRIDE template hands you the same thing: six rows, one per letter, fill each one in. The template ranking first for this search says it outright, that there should be at least one issue identified for each of S, T, R, I, D and E. So a system's coverage target is six. That is a floor being read as a finish line, and it is why threat models get signed off with every arrow on the architecture diagram untouched.

STRIDE per element is not six categories per box, and the matrix is not folklore. OWASP Threat Dragon encodes it in one file: an external entity carries two of the six, a data flow three, a data store four, a process all six. Count your design and the arithmetic falls out. Two external entities, five processes, three stores and ten flows is 76 applicable cells, and that number exists before anybody has a security thought.

Seventy-six is the denominator. This pack computes it from the census, opens every cell as a row whether or not anyone has answered it, and reports coverage as a fraction. Every empty cell says which kind of empty it is: ruled out with the reason recorded, never opened, or waiting on a property of the system nobody wrote down. Those three read identically in a normal threat table. The one that ships is never opened, and no threat modelling tool has a word for it.

The cells nobody asked about, counted

Every cell figure recomputes from the element type. Nothing in the Cells column was typed in by a person.

Element Inventory

Ledger, the metering and invoicing service at a fictional company, Cardinal Systems. Cells follow the normalised type: external entity 2, data flow 3, data store 4, process 6.

IDElementTypeAs drawnCellsAnsweredNever openedBlocked
EE-02StripeExternal entityCircle2200
PR-01Portal web appProcessCircle6600
PR-02Metering ingestProcessSquare6240
PR-05Internal admin consoleProcessNot drawn6060
DS-02Invoice PDF bucketData storeS3 stencil4202
DS-03Usage event streamData storeQueue stencil4130
DF-03Ingest to usage streamData flowUnlabelled arrow3030
DF-07Presigned invoice fetchData flowDashed arrow3300
DF-10Admin console to databaseData flowNot drawn3030
OUT-01Stripe card vaultOut of scopeOutside boundary0n/an/an/a

Full census: 2 external entities, 5 processes, 3 data stores, 10 data flows. 2×2 + 5×6 + 3×4 + 10×3 = 76 applicable cells, of which 43 answered, 26 never opened, 7 blocked. PR-02 is drawn as a square because another team owns it; renormalising it to a process reclaimed 4 cells that were never in anyone’s denominator. PR-05 and DF-10 exist in one sentence of the design review notes and brought 9 more.

Threat Register

One row per cell, not per threat. An empty cell is a row that says which kind of empty it is.

CellElementCategoryEmptinessResponseThreat or reasonOwner
C-041Presigned fetchInfo disclosureAnsweredMitigateURL valid 7 days is a bearer token for one tenant’s invoice, and it lands in a shared inbox. Cut to 120s.R. Achebe
C-015Admin to portalTamperingAnsweredEliminateInvoice amount was posted from the client. Field removed; server recomputes from metered usage.R. Achebe
C-024StripeRepudiationAnsweredTransferStripe retains the event log by event id and reconciliation reads from it, so we hold no second copy.D. Okafor
C-072Usage streamDenial of serviceAnsweredAcceptOne tenant can fill 7 days of retention and stall every tenant’s invoicing. Accepted as RR-02.S. Iyer
C-032Billing databaseRepudiationRuled out, no reasonMarked not applicable with an empty reason, which is indistinguishable from nobody thinking about it.Unassigned
C-055Invoice generatorElevation of privBlockedWaiting on: does the scheduled task assume its own role or inherit the cluster instance role?S. Iyer
C-061Admin consoleElevation of privNever openedElement was never drawn, so none of its 6 cells were ever in the denominator.Unassigned
C-052Metering ingestElevation of privNever openedOne of the 4 cells that appeared only after this element was retyped from external entity to process.Unassigned
C-043Presigned fetchElevation of privNot applicable by matrixA data flow carries three categories. This one is outside the denominator, recorded so it is not read as a gap.

Four states of empty, and only one of them is a finding you can act on today. No threat modelling tool has a word for never opened, which is exactly why it is the state that ships.

Trust Boundary Register

Keyed on the boundary, because the answered cells concentrate at the crossings rather than on the boxes.

IDBoundaryFar side controlled byCrossingsAuthenticatedCellsAnswered
TB-01Internet edgeNobody, it is the open internet33 of 399
TB-02Payment processorStripe11 of 133
TB-03App tier to data tierCardinal platform team84 of 8248
TB-04Corporate networkNobody could name an owner10 of 130
TB-05Observability vendorDatadog0 drawnn/a0n/a

Coverage never fails evenly. It collapses at one boundary, and TB-03 is the ordinary place: eight crossings between the tiers, half of them unauthenticated, 8 of 24 cells answered. That is one design property rather than eight findings. TB-04 is not drawn anywhere and nobody could name who owns its far side. TB-05 has nothing crossing it, so it contributes no cells and cannot be wrong, which is what makes it easy to leave.

What comes in the pack

01

Element Inventory, with the type as drawn beside the type that counts

One row per element and per data flow. The verbatim column holds what your source material shows: a circle, two parallel lines, a vendor stencil, an unlabelled arrow, or nothing at all when the element was named only in prose. The normalised column holds the type the matrix runs on, and the cell count follows it. When the two disagree that is usually the most useful finding on the row, and it is always the deflating direction that hurts.

02

Threat Register keyed on the cell, not on the threat

Every applicable cell gets a row whether or not anybody has answered it, so an unasked question is something you can read rather than an absence you cannot. Five states: answered, ruled out with the reason, ruled out with no reason recorded, never opened, and blocked naming the property nobody supplied. A fifth row type records the cells the matrix does not make applicable, so they stop reading as gaps.

03

Trust Boundary Register that attributes every crossing

One row per boundary, with who controls the far side and every flow that crosses it, including the flows that cross two boundaries in a single hop. It surfaces the two boundaries a diagram cannot: the one that exists in a routing table and was never drawn, and the one drawn with nothing crossing it, which contributes no cells and therefore cannot be wrong.

04

Three documents a security reviewer actually reads

A narrative built on the four questions, with coverage stated in the opening block. A mitigation plan sequenced by what blocks the release rather than by threat. A residual risk note where every accept carries a named person and a review date. All three ship filled with a worked model rather than a skeleton, including the reasoning behind choosing eliminate over mitigate.

05

Six prompts that work the boundary, not the backlog

Take the census, mark the trust boundaries, work every cell on one boundary's crossings in a single pass, answer one cell properly, write the narrative, close out for sign-off. Working a boundary is the unit that pays: its crossings usually share both their exposure and their fix, so you get one design decision instead of eight findings that turn out to be the same finding.

06

A standard your team fills in once

The four rulings that move the denominator. Whether a service another team owns is a process or an external entity. Whether a stream with a retention window is a store. Where the model's edge sits, and what has to be true to ship. Every prompt reads it, so the registers inherit one vocabulary instead of drifting between reviewers.

How it works

  1. 1

    Send the design

    An architecture document, diagrams in any form, the Terraform config, an API spec, or design review notes. River reads the prose as carefully as the pictures, because the elements nobody models are drawn nowhere.

  2. 2

    The census computes the denominator

    Every element and flow lands in the inventory, its type transcribed as drawn then normalised. The cell count follows the normalised type, and the column totals. That total is what a review wants.

  3. 3

    Boundaries get marked and every cell opens

    River finds the boundaries from the design rather than the drawing, then opens every applicable cell as a row. A first pass full of unasked questions is the correct output, not an incomplete one.

  4. 4

    Work a boundary, then close out

    Answer a boundary's crossings in one pass and name the design change that closes the most cells. Close-out writes the coverage total on the day of signing, so a later reader knows what shipped.

Frequently asked questions

Why 76 cells and not six?

Because the six categories do not all apply to every element. Microsoft's STRIDE description names six threats; the applicability matrix says which apply where, and Threat Dragon's source encodes it as two for an external entity, three for a flow, four for a store, six for a process. Count your design and the total falls out.

Is the per-element matrix a River invention?

No. It is written down in the source of the reference tools. Threat Dragon's stride.json lists the applicable categories per element type, and the same file's neighbouring module repeats the matrix as a comment table. This pack reads the matrix straight off it rather than paraphrasing a blog post about it.

Do I still need a data flow diagram?

You need the census, and the diagram is how most teams get to it. River takes the census from whatever you have, including prose, and ships the flow and its boundaries as readable text beside the sheets. If the design was never written down, reconstruct the architecture first. A picture nobody can count is how a model ends up with six rows.

Will automated threat generation do this for me?

It will fill in the parts it covers, which are lopsided. OWASP pytm's rule library holds 114 threats whose target classes run server 54, process 44, data flow 16, data store 5, external entity 2. Your stores come back nearly clean because of the library rather than your design.

Why does an empty cell need a reason?

Because four different things look identical without one. Ruled out on purpose, ruled out and never explained, never asked, and blocked on a fact nobody recorded all render as silence. The reference tool's status list offers not started, needs investigation, not applicable and mitigated, and none of them means nobody looked.

Can I ship below full coverage?

Yes, and most teams do. Coverage below the total is a normal release state when every gap is enumerated, owned and dated. What does not ship is a gap nobody knows the size of, which is the whole reason the pack reports a fraction rather than a list.

How does this fit with the rest of our documents?

It sits upstream of findings. Use it on a design before it ships; use vulnerability triage for scanner output on a system that already exists, and keep the resulting design decisions in the decision record pack so a later reader knows what was accepted.

Find out what your threat model never asked

Send the design. River takes the census, computes the applicable total, and opens every cell you have not answered as a row you can read.

Install the threat model pack