Skip to content
academia.sh

Lesson 06 / 11

The Purpose of Threat Modeling

A threat modeling session's own measure: in an 18-item inventory, the session takes 40 developer hours, finds 10 items, saves 866 hours against 4.0 hours per item, produces a cost difference ranging from 4x to 30x when the same item is caught late, brings total fix hours down from 1,872 to 1,006, and leaves the eight items the session never touches carrying 48.4% of the total hours.

Published Updated

In this lesson

  • Measures a threat modeling session in duration, items found, and hours per item
  • Compares the same item's fix cost at design versus a late stage and calculates the factor
  • Separates the class of items that cannot be seen at design and states the session's coverage limit
Contents

The previous topic distributed security work across stages, wrote requirements in testable form, and asked how to divide limited development capacity. The common input to all of those calculations was a ready-made item list: which gap existed was already known, and only the order was under discussion. At the design stage, though, no such list appears on its own.

Threat modeling is the method for producing that list at the design stage. This lesson does not measure the method’s steps but its purpose and price: how many hours a session takes, how many items it finds, how many hours it spends per item, and how many times more expensive the items it finds would have been if caught late. The topic works on the same fictional regional measurement network’s software throughout.

Where the Session Stops

The inventory below is a model: there is no real product, no real team measurement, and no real record. Items are described by only three columns — where they originate, where they can be seen, and how many developer hours their fix takes. No exploit step is written.

TM1: fix work is measured in developer hours; the base hour count is the item’s cost at the design stage, and it grows by a multiplier in later stages. TM2: the stage an item originates in and the stage it becomes visible in are separate things; an item born in code may only become visible at runtime. TM3: the session runs at the design stage and finds only items whose visible stage is no later than design. TM4: the session’s cost is the product of participant count and its duration.

// threat/item.mjs — stage set and item inventory (model): derived from the development records
// of a fictional regional measurement network's software; not a real measurement. No exploit
// step is present; an item is described only by where it originates, where it becomes visible,
// and how many developer hours its fix takes.

export const STAGE = ["requirement", "design", "code", "build", "deploy", "runtime"];

// TM1: fix work is measured in developer hours. `base` is the hour count for fixing the item at
// the design stage; it grows by the multiplier below in later stages.
export const MULTIPLIER = { requirement: 1, design: 1, code: 4, build: 6, deploy: 12,
  "runtime": 30 };

// TM2: rows are written as "name origin visible default base". `origin` is the stage the item is
// produced in, `visible` is the earliest stage it can be seen in, `default` is the stage it is
// actually caught in when there is no threat modeling session.
export const ITEM = `
  trust-boundary-not-drawn           design       design         deploy    6
  meter-identity-not-verified        design       design         runtime   8
  billing-record-modifiable          design       design         deploy   10
  subscriber-data-flows-unprotected  design       design         deploy    4
  authorization-decision-on-client   design       design         code      7
  audit-log-missing                  requirement  design         runtime   5
  field-device-carries-secret        design       design         deploy    9
  reporting-endpoint-unauthenticated design       design         code      3
  measurement-backup-unprotected     design       design         runtime   6
  session-duration-undefined         requirement  design         code      2
  input-length-unbounded             code         code           build     2
  query-concatenated-manually        code         code           build     4
  timing-difference-leaks            code         code           runtime   5
  concurrent-request-race-condition  code         runtime        runtime  12
  dependency-version-unpinned        build        build          deploy    3
  leftover-tool-in-image             build        build          deploy    2
  default-credential-exposed         deploy       deploy         runtime   4
  credential-in-log-entry            code         runtime        runtime   6`
  .trim().split("\n").map((r) => {
    const [name, origin, visible, def, base] = r.trim().split(/\s+/);
    return { name, origin, visible, default: def, base: +base };
  });

export const order = (a) => STAGE.indexOf(a);
export const cost = (k, stage) => k.base * MULTIPLIER[stage];

// TM3: the threat modeling session runs at the design stage and finds only the items whose
// `visible` stage is no later than design; it never touches the rest.
export const isFound = (k) => order(k.visible) <= order("design");

// TM4: the session's cost is participant count x session count x session duration.
export const SESSION = { participants: 5, count: 4, hours: 2 };
export const SESSION_HOURS = SESSION.participants * SESSION.count * SESSION.hours;
// threat/session.mjs — how many hours the session takes, how many items it finds, hours per
// item, and how many times more expensive the same item is when caught late.
import { STAGE, MULTIPLIER, ITEM, SESSION, SESSION_HOURS, order, cost, isFound } from "./item.mjs";

const write = (g, ...s) => console.log(s.map((v, i) =>
  (g[i] < 0 ? String(v).padEnd(-g[i]) : String(v).padStart(g[i]))).join(""));
const sum = (ks, f) => ks.reduce((s, k) => s + f(k), 0);
const found = ITEM.filter(isFound), missed = ITEM.filter((k) => !isFound(k));

console.log(`${ITEM.length} items, ${STAGE.length} stages; multipliers ` +
  `${STAGE.map((a) => `${a} x${MULTIPLIER[a]}`).join(", ")}`);

const A = [-36, -13, -9, -9, 7, 11, 12, 8];
console.log("\n1. where the item originates, where it can be seen, where it is caught");
write(A, "item", "origin", "visible", "default", "delay", "at design", "at default", "factor");
for (const k of ITEM) {
  const t = cost(k, "design"), v = cost(k, k.default);
  write(A, k.name, k.origin, k.visible, k.default,
    order(k.default) - order(k.origin), t, v, (v / t).toFixed(1));
}

const B = [-15, 7, 8, 15, 8, 9];
console.log("\n2. item class by visible stage");
write(B, "visible stage", "items", "item %", "default hours", "hour %", "session");
for (const a of STAGE) {
  const g = ITEM.filter((k) => k.visible === a);
  if (!g.length) continue;
  const s = sum(g, (k) => cost(k, k.default));
  write(B, a, g.length, ((g.length / ITEM.length) * 100).toFixed(1), s,
    ((s / sum(ITEM, (k) => cost(k, k.default))) * 100).toFixed(1),
    order(a) <= order("design") ? "finds" : "misses");
}

const WITHOUT_SESSION = sum(ITEM, (k) => cost(k, k.default));
const WITH_SESSION = sum(found, (k) => cost(k, "design")) +
  sum(missed, (k) => cost(k, k.default)) + SESSION_HOURS;
const C = [-40, 14, 22];
console.log("\n3. the session's own measure");
for (const [name, x, y] of [
  ["session count x participants x hours", `${SESSION.count}x${SESSION.participants}x${SESSION.hours}`,
    SESSION_HOURS + " hours"],
  ["items found", found.length, (SESSION_HOURS / found.length).toFixed(1) + " hours/item"],
  ["items untouched", missed.length, ((missed.length / ITEM.length) * 100).toFixed(1) + "%"],
  ["total fix without session", WITHOUT_SESSION + " hours", "-"],
  ["total fix with session", WITH_SESSION + " hours", (WITHOUT_SESSION / WITH_SESSION).toFixed(2) + "x"],
  ["hours the session saves", WITHOUT_SESSION - WITH_SESSION + " hours",
    ((WITHOUT_SESSION - WITH_SESSION) / SESSION_HOURS).toFixed(1) + "x session hours"],
]) write(C, name, x, y);

const D = [-19, 7, 19, 18, 8];
console.log("\n4. items the session touches and does not, separately");
write(D, "group", "items", "hours w/o session", "hours w/ session", "factor");
for (const [name, ks, hours] of [
  ["found by session", found, sum(found, (k) => cost(k, "design"))],
  ["missed by session", missed, sum(missed, (k) => cost(k, k.default))],
]) {
  const o = sum(ks, (k) => cost(k, k.default));
  write(D, name, ks.length, o, hours, (o / hours).toFixed(1));
}
const worst = [...ITEM].sort((x, y) =>
  cost(y, y.default) - cost(x, x.default))[0];
console.log(`the single most expensive item is ${worst.name}: ${cost(worst, worst.default)} hours, ` +
  `the session ${isFound(worst) ? "finds" : "misses"} it; the ${missed.length} items the session ` +
  `misses carry ${((sum(missed, (k) => cost(k, k.default)) / WITHOUT_SESSION) * 100).toFixed(1)}% ` +
  `of the total fix hours`);
18 items, 6 stages; multipliers requirement x1, design x1, code x4, build x6, deploy x12, runtime x30

1. where the item originates, where it can be seen, where it is caught
item                                origin       visible  default    delay  at design  at default  factor
trust-boundary-not-drawn            design       design   deploy         3          6          72    12.0
meter-identity-not-verified         design       design   runtime        4          8         240    30.0
billing-record-modifiable           design       design   deploy         3         10         120    12.0
subscriber-data-flows-unprotected   design       design   deploy         3          4          48    12.0
authorization-decision-on-client    design       design   code           1          7          28     4.0
audit-log-missing                   requirement  design   runtime        5          5         150    30.0
field-device-carries-secret         design       design   deploy         3          9         108    12.0
reporting-endpoint-unauthenticated  design       design   code           1          3          12     4.0
measurement-backup-unprotected      design       design   runtime        4          6         180    30.0
session-duration-undefined          requirement  design   code           2          2           8     4.0
input-length-unbounded              code         code     build          1          2          12     6.0
query-concatenated-manually         code         code     build          1          4          24     6.0
timing-difference-leaks             code         code     runtime        3          5         150    30.0
concurrent-request-race-condition   code         runtime  runtime        3         12         360    30.0
dependency-version-unpinned         build        build    deploy         1          3          36    12.0
leftover-tool-in-image              build        build    deploy         1          2          24    12.0
default-credential-exposed          deploy       deploy   runtime        1          4         120    30.0
credential-in-log-entry             code         runtime  runtime        3          6         180    30.0

2. item class by visible stage
visible stage    items  item %  default hours  hour %  session
design              10    55.6            966    51.6    finds
code                 3    16.7            186     9.9   misses
build                2    11.1             60     3.2   misses
deploy               1     5.6            120     6.4   misses
runtime              2    11.1            540    28.8   misses

3. the session's own measure
session count x participants x hours             4x5x2              40 hours
items found                                         10        4.0 hours/item
items untouched                                      8                 44.4%
total fix without session                   1872 hours                     -
total fix with session                      1006 hours                 1.86x
hours the session saves                      866 hours   21.6x session hours

4. items the session touches and does not, separately
group                items  hours w/o session  hours w/ session  factor
found by session        10                966                60    16.1
missed by session        8                906               906     1.0
the single most expensive item is concurrent-request-race-condition: 360 hours, the session misses it; the 8 items the session misses carry 48.4% of the total fix hours

The numbers are in the measurement class; their inputs are TM1–TM4.

The Forty-Hour Session

The third table answers this lesson’s main question. Four sessions, five participants, two hours per session: a total of 40 developer hours. The session finds ten items, meaning 4.0 hours per item. This number describes threat modeling not as a method but as a work item; for a team that needs approval, this is the only form that can be argued over.

The payoff sits in the same row. Without the session, the total fix work for the eighteen items would be 1,872 hours; with the session it drops to 1,006 hours. The difference is 866 hours, which is 21.6 times the 40 hours spent. The size of this ratio comes from a single fact: the items the session finds were items that would otherwise have been caught late, and the late-stage multiplier was high.

Looked at over the total, though, the gain is far more modest: 1,872 hours drops to 1,006 hours, which is 1.86 times. The same measurement gives two different magnitudes, and both are correct. For the items the session touches, the ratio is 16.1; for the whole inventory, it is 1.86. Which number gets written down depends on whether what the session covers is stated explicitly.

The Same Item, Three Stages Later

The last column of the first table answers a single question for every item: when the same item is caught at its default stage, how many times its design-stage cost does it end up costing. The values spread between 4.0 and 30.0, and this range is the lesson’s most important finding — the sentence “catching it late is expensive” does not carry one single factor; it carries a distribution that depends on how far the item can travel unseen.

It is enough to compare the two extremes. authorization-decision-on-client is caught in code: 28 hours instead of 7, a factor of 4.0. meter-identity-not-verified travels all the way to runtime: 240 hours instead of 8, a factor of 30.0. The two items’ base hours are close to each other; what produces the thirtyfold difference between them is not the item’s own size but how many stages it advances through unseen.

The delay column counts this directly. audit-log-missing originates at the requirement stage and is caught at runtime: five stages late. The same item’s base is only 5 hours; a five-stage delay raises it to 150 hours. By contrast, leftover-tool-in-image originates at the build stage and is caught at deploy — one stage late, 24 hours instead of 2. concurrent-request-race-condition, the most expensive item in the inventory, reaches 360 hours with a three-stage delay and alone carries roughly a fifth of the total without the session.

The Coverage Ceiling

Read on its own, the first table’s origin column gives the session’s upper bound. In the inventory, 2 items originate at the requirement stage, 8 at design, 5 at code, 2 at build, 1 at deploy. When the design stage ends, the number of items not yet born is eight.

An item cannot be seen before it is born; the visible column is never earlier than the origin column in any row. This puts a structural ceiling on the session’s coverage: no item born after design can be found in a design-stage session — that item does not yet exist while the session is running. The eight items being left out comes from sequence, not from any weakness in the session.

Below the ceiling, the session’s own success is measured. Of the ten items born at the requirement or design stage, all ten show design in the visible column; in this inventory, the session reaches the entire ceiling. In a real session this ratio is lower, and the two numbers must be written separately: how many items could structurally be found, and how many of those were actually found. When the two are conflated, a team reporting 100% coverage has never measured 44.4% of the inventory at all.

Two items show this distinction directly. concurrent-request-race-condition and credential-in-log-entry originate at the code stage but only become visible at runtime; both have a three-stage delay. These fall within the scope of neither the design session nor a code-reading review — they are not visible even at the stage where they are born.

The Eight Items That Cannot Be Seen at Design

The fourth table states this lesson’s limit. The ten items the session touches drop from 966 hours to 60 hours — a factor of 16.1. The eight items the session misses drop from 906 hours to 906 hours — a factor of 1.0. No matter how well the session is run, it does nothing for these eight items.

The second table separates out where those eight stand. Three items can only be seen in code, two in build, one in deploy, two only at runtime. As a share of item count that is 44.4% of the inventory; as a share of fix hours it is 48.4%. So even the most thorough session run at the design stage leaves behind a share approaching half of the total work.

This class is not random. A race condition, a timing difference, and a credential landing in a log entry do not appear in the design as a box or an arrow; they only surface in the behavior of code that has been written or a system that is running. The design document does not carry these items, so no session that reads the design can find them. The two items visible only at runtime alone carry 540 hours — roughly two-thirds of the 866 hours the session saves.

The conclusion that follows is plain: a threat modeling session does not replace the reviews that come after it. The claim “we catch everything at design,” made without stating the session’s coverage limit, is a claim that, once measured, leaves out half of the inventory.

Summary

  • A threat modeling session is a measurable work item: four sessions, five participants, two hours each — 40 developer hours, 10 items, 4.0 hours per item.
  • The session saves 866 hours, which is 21.6 times the 40 hours spent. The same measurement gives 1.86 times over the whole inventory and 16.1 times over just the items it touches.
  • The same item’s late-catch factor spreads between 4.0 and 30.0; what determines the factor is not the item’s size but how many stages it advances through unseen.
  • A 5-hour item delayed by five stages rises to 150 hours; a 2-hour item delayed by one stage rises to 24 hours — the delay count matters more than the base hours.
  • Eight items in the inventory cannot be seen at design, and the session never touches them — 44.4% of the item count, 48.4% of the fix hours. For these items, the session’s factor is 1.0.

Next Step

This lesson stated the session’s price and its coverage limit, but never opened up what happens inside the session. How the ten items were found remained a black box: “the design was read and items came out.”

Reading the design is not a method; a design document contains hundreds of sentences, and which sentence gives rise to a security item is not obvious. The method’s first step is to turn the design into a structure that can be counted over: which component takes which data from whom and gives it to whom, and which of those exchanges cross from one trust level to another. The next lesson builds that structure and takes the topic’s core measurement: the number of flows that cross a trust boundary in a data flow diagram, the items each boundary-crossing flow carries, and how many flows stay invisible when no boundary is drawn.

To keep your progress and take notes, Log in

My notes

Log in to take notes.

Start typing to search.

↑↓ ↵ Esc navigate · open · close