Threat modeling with STRIDE drew
the picture by hand: a process gets all six categories, a store gets four, and a flow gets
three plus Spoofing only when it crosses a trust boundary that is actually declared. This
exercise is threats(dfd) — the function that reads a DFD as JSON and applies
that same table mechanically, over 7 checks, including the exact 3-node diagram the lesson
draws.
threats(dfd: dict) -> list[dict] — one dict
{"element": id, "kind": "process"|"store"|"flow", "category": letter} per
threat. dfd has four keys: "processes" and "stores"
are lists of {"id": str, "zone": str}; "flows" is a list of
{"id": str, "from": ref, "to": ref} where ref is either a
process/store id string or an inline {"zone": str} for an
external endpoint like a browser; "boundaries" is a list of
[zoneA, zoneB] pairs — a trust boundary that has actually been drawn between
those two zones.process gets all six categories
"STRIDE". A store gets "TRID" — Tampering,
Repudiation, Information disclosure, Denial of service — never Spoofing or Elevation. A
flow always gets Tampering, Information disclosure, Denial of service
("TID"), plus Spoofing only when its two endpoints resolve to
different zones and that exact zone pair appears in boundaries — a zone
difference with no matching boundary entry is not a crossing.(element, category) so the same input always comes
back in the same order. A flow endpoint that is a string not found among any process or
store id, and is not an inline {"zone": ...}, is an unknown
element — raise ValueError.The checks are ordinary Python and ship with the page like everything
else on a static site — open devtools and you can read every one. Check 4 is the trap that
catches "any zone difference is a crossing": it hands you two zones that differ with an
empty boundaries list and expects no Spoofing, because nothing on that
DFD ever drew a boundary between them.
id to
its zone, before you touch flows. A flow's from/to
needs that lookup to resolve to a zone unless it is already an inline
{"zone": ...}."STRIDE" for every
process, "TRID" for every store, full stop. Nothing about a specific process
or store changes its table.{from_zone, to_zone} against every entry in boundaries as
an unordered pair (a boundary ["internet", "app"] matches a flow going either
direction). Zones merely differing, with boundaries empty or naming a different
pair, must not add Spoofing.from or to is a plain
string that is not any process or store's id, and it is not a
{"zone": ...} dict, raise ValueError rather than silently
treating it as its own zone.sorted(out, key=lambda t: (t["element"], t["category"])). Sorting a
per-element loop's output as you go is easy to get subtly wrong once a flow's categories
aren't emitted in the same letter order every time.