Lesson 08 / 11
Threat Classification Methods
Systematic threat generation's two numbers: applying six categories to 43 graph elements produces 156 raw threats, 108 of which (69.2%) are valid for this system and 48 of which turn out invalid, elimination spends 72 minutes for no return, the total session load comes to 666 minutes, and generation limited to exposed elements alone finds 92.6% of the valid threats 63.5 minutes cheaper.
Published Updated
In this lesson
- Runs systematic generation, which applies a fixed category list per element type, over the graph
- Separates the number of raw threats generated from the number valid for this system
- Calculates elimination's per-minute cost and compares three separate generation sets by minutes per unit of coverage
Contents
The previous lesson derived 58 items from 23 boundary crossings. Those 58 items were not a threat list: each one stated a control type — “a source identity check is required here,” “authorization is required here.” It did not say what could go wrong.
The step that closes this gap is looking at a graph element and asking “what could happen here.” This question depends on the person and on their attention that day; two people produce two different lists. What makes it independent of the person is applying a fixed category list to every element type. This lesson runs that systematic generation and separates two numbers: how many threats the method produces, and how many of those are actually valid for this system.
A Fixed Question List per Element Type
The widely used method is a classification method that generates threats systematically for every data flow diagram element. It has six categories, and the categories are known by what they do: identity spoofing, tampering, repudiation, information disclosure, denial of service, and privilege escalation. The method has a single rule: every element is asked all of the categories applicable to its type — none is skipped, none is added on top.
The inventory below is a model; it is the same structure as the graph from the previous lesson and carries no attack step. Element properties are not written by hand — they are derived from the graph.
TM9: every element’s five properties are calculated from the graph — exposure, carrying a sensitive class, being in the work flow, having multiple roles, and producing a transaction. TM10: generation is the product of an element’s type and the categories applicable to that type. TM11: a generated threat is invalid for this system if the element’s property does not satisfy the relevant condition. TM12: every raw threat spends a fixed elimination time, and every threat found valid spends an additional writing time.
// threat/element.mjs — inventory of graph elements (model): the same structure as the graph in // 02, this time as an element list. There is no real system; no attack step is carried. const ZONE = { internet: 0, field: 1, edge: 2, internal: 3, management: 4, data: 5 }; const NODE = Object.fromEntries(` subscriber D internet | oversight-body D internet | meter-device D field | field-team D field subscriber-portal S edge | collection-endpoint S edge | field-app S field validator S internal | billing S internal | work-order S internal | report-generator S internal | identity-service S internal management-console S management | measurement-store P data | billing-store P data identity-store P data | audit-log P data` .trim().split(/\s*[|\n]\s*/).map((r) => { const [name, type, zone] = r.trim().split(/\s+/); return [name, { name, type, zone, level: ZONE[zone] }]; })); const FLOW = ` subscriber subscriber-portal identity | subscriber-portal identity-service identity identity-service identity-store identity | subscriber-portal billing billing billing billing-store billing | billing subscriber-portal billing meter-device collection-endpoint measurement | collection-endpoint validator measurement validator measurement-store measurement | validator billing measurement field-team field-app identity | field-app work-order order work-order field-app order | work-order measurement-store measurement field-app collection-endpoint measurement | management-console validator setting management-console collection-endpoint setting | management-console identity-service setting report-generator measurement-store measurement | report-generator billing-store billing oversight-body report-generator identity | report-generator oversight-body billing billing audit-log record | identity-service audit-log record work-order audit-log record | billing work-order order` .trim().split(/\s*[|\n]\s*/).map((r) => { const [source, target, cls] = r.trim().split(/\s+/); return { source, target, class: cls, name: `${source}->${target}` }; }); // TM9: every element's property is derived from the graph; not written by hand. const incoming = (d) => FLOW.filter((a) => a.target === d); const outgoing = (d) => FLOW.filter((a) => a.source === d); const neighbors = (d) => [...incoming(d), ...outgoing(d)]; const isSensitiveClass = (s) => s === "identity" || s === "billing"; const isCriticalClass = (s) => s === "measurement" || s === "billing" || s === "order"; export const ELEMENT = [ ...Object.values(NODE).map((d) => ({ name: d.name, type: d.type, exposed: d.type === "D" ? outgoing(d.name).length > 0 : incoming(d.name).some((a) => NODE[a.source].level < d.level), sensitive: neighbors(d.name).some((a) => isSensitiveClass(a.class)), critical: neighbors(d.name).some((a) => isCriticalClass(a.class)), multiRole: new Set(incoming(d.name).map((a) => NODE[a.source].zone)).size >= 2, transacts: d.type === "P" || (d.type === "D" ? outgoing(d.name).length > 0 : outgoing(d.name).some((a) => NODE[a.target].type === "P")), })), ...FLOW.map((a) => ({ name: a.name, type: "A", exposed: NODE[a.source].zone !== NODE[a.target].zone, sensitive: isSensitiveClass(a.class), critical: isCriticalClass(a.class), multiRole: false, transacts: false, })), ]; // TM10: systematic generation applies to every element type the categories applicable to that type. export const CATEGORY = ["identity-spoofing", "tampering", "repudiation", "information-disclosure", "denial-of-service", "privilege-escalation"]; export const APPLIES = { D: ["identity-spoofing", "repudiation"], S: CATEGORY, P: ["tampering", "repudiation", "information-disclosure", "denial-of-service"], A: ["tampering", "information-disclosure", "denial-of-service"], }; // TM11: a generated threat is invalid if the element's property does not satisfy the condition below. export const VALID_IF = { "identity-spoofing": (o) => o.exposed, "tampering": (o) => o.type !== "A" || o.exposed, "repudiation": (o) => o.transacts, "information-disclosure": (o) => o.sensitive, "denial-of-service": (o) => o.critical, "privilege-escalation": (o) => o.multiRole, }; // TM12: every generated threat spends a fixed elimination time, and every threat found valid // spends an additional writing time (fictional minutes). export const ELIMINATION_MIN = 1.5, WRITE_MIN = 4;
// threat/generation.mjs — how many threats systematic generation gives, how many are invalid // for this system, how many minutes elimination takes. import { ELEMENT, CATEGORY, APPLIES, VALID_IF, ELIMINATION_MIN, WRITE_MIN } from "./element.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 generate = (els) => els.flatMap((o) => APPLIES[o.type].map((k) => ({ element: o, category: k, valid: VALID_IF[k](o) }))); const RAW = generate(ELEMENT), VALID = RAW.filter((t) => t.valid); const TYPE = { D: "external entity", S: "process", P: "data store", A: "data flow" }; console.log(`${ELEMENT.length} elements, ${CATEGORY.length} categories; raw generation ${RAW.length} threats, ` + `valid ${VALID.length} (${((VALID.length / RAW.length) * 100).toFixed(1)}%), ` + `invalid ${RAW.length - VALID.length}`); const A = [-17, 10, 12, 5, 7, 9, 9]; console.log("\n1. generation per element type"); write(A, "element type", "elements", "categories", "raw", "valid", "invalid", "valid %"); for (const t of ["D", "S", "P", "A"]) { const o = ELEMENT.filter((x) => x.type === t), h = generate(o), g = h.filter((x) => x.valid); write(A, TYPE[t], o.length, APPLIES[t].length, h.length, g.length, h.length - g.length, ((g.length / h.length) * 100).toFixed(1)); } const B = [-23, 5, 7, 9, 9, 2, -32]; console.log("\n2. generation and elimination per category"); write(B, "category", "raw", "valid", "invalid", "valid %", "", "validity condition"); const CONDITION = { "identity-spoofing": "exposed", "tampering": "if a flow, crosses a boundary", "repudiation": "transacts", "information-disclosure": "carries a sensitive class", "denial-of-service": "in the work flow", "privilege-escalation": "multi-role process" }; for (const k of CATEGORY) { const h = RAW.filter((t) => t.category === k), g = h.filter((t) => t.valid); write(B, k, h.length, g.length, h.length - g.length, ((g.length / h.length) * 100).toFixed(1), "", CONDITION[k]); } const C = [-23, 10, 5, 7, 11, 11]; console.log("\n3. three generation sets: all, exposed only, processes only"); write(C, "set", "elements", "raw", "valid", "total min", "valid/min"); const SET = { "all elements": ELEMENT, "exposed elements only": ELEMENT.filter((o) => o.exposed), "processes only": ELEMENT.filter((o) => o.type === "S") }; for (const [name, els] of Object.entries(SET)) { const h = generate(els), g = h.filter((x) => x.valid); const min = h.length * ELIMINATION_MIN + g.length * WRITE_MIN; write(C, name, els.length, h.length, g.length, min.toFixed(1), (g.length / min).toFixed(3)); } const exposed = generate(ELEMENT.filter((o) => o.exposed)).filter((x) => x.valid); console.log(`generation limited to exposed elements finds ` + `${((exposed.length / VALID.length) * 100).toFixed(1)}% of the valid threats; the ` + `${VALID.length - exposed.length} threats it misses arise only in internal elements`); const CODE = Object.fromEntries(CATEGORY.map((k) => [k, k.slice(0, 2).toUpperCase()])); const D = [-30, -11, 5, 7, 2, -19]; console.log("\n4. the three elements producing the most and fewest valid threats (" + CATEGORY.map((k) => `${CODE[k]}=${k}`).join(", ") + ")"); write(D, "element", "type", "raw", "valid", "", "valid categories"); const ranked = ELEMENT.map((o) => ({ o, h: APPLIES[o.type], g: APPLIES[o.type].filter((k) => VALID_IF[k](o)) })).sort((x, y) => y.g.length - x.g.length); for (const s of [...ranked.slice(0, 3), ...ranked.slice(-3)]) write(D, s.o.name, TYPE[s.o.type], s.h.length, s.g.length, "", s.g.map((k) => CODE[k]).join(" ") || "-"); for (const t of ["S", "A"]) { const g = VALID.filter((x) => x.element.type === t).length; console.log(`${TYPE[t]} elements: ` + `${((ELEMENT.filter((o) => o.type === t).length / ELEMENT.length) * 100).toFixed(1)}% of the elements, ` + `${((g / VALID.length) * 100).toFixed(1)}% of the valid threats`); } console.log(`eliminating the ${RAW.length - VALID.length} invalid threats spends ` + `${((RAW.length - VALID.length) * ELIMINATION_MIN).toFixed(1)} minutes and returns nothing`);
43 elements, 6 categories; raw generation 156 threats, valid 108 (69.2%), invalid 48 1. generation per element type element type elements categories raw valid invalid valid % external entity 4 2 8 8 0 100.0 process 9 6 54 40 14 74.1 data store 4 4 16 12 4 75.0 data flow 26 3 78 48 30 61.5 2. generation and elimination per category category raw valid invalid valid % validity condition identity-spoofing 13 11 2 84.6 exposed tampering 39 36 3 92.3 if a flow, crosses a boundary repudiation 17 13 4 76.5 transacts information-disclosure 39 17 22 43.6 carries a sensitive class denial-of-service 39 24 15 61.5 in the work flow privilege-escalation 9 7 2 77.8 multi-role process 3. three generation sets: all, exposed only, processes only set elements raw valid total min valid/min all elements 43 156 108 666.0 0.162 exposed elements only 38 135 100 602.5 0.166 processes only 9 54 40 241.0 0.166 generation limited to exposed elements finds 92.6% of the valid threats; the 8 threats it misses arise only in internal elements 4. the three elements producing the most and fewest valid threats (ID=identity-spoofing, TA=tampering, RE=repudiation, IN=information-disclosure, DE=denial-of-service, PR=privilege-escalation) element type raw valid valid categories billing process 6 6 ID TA RE IN DE PR subscriber-portal process 6 5 ID TA IN DE PR validator process 6 5 ID TA RE DE PR identity-service->audit-log data flow 3 1 TA work-order->audit-log data flow 3 1 TA billing->work-order data flow 3 1 DE process elements: 20.9% of the elements, 37.0% of the valid threats data flow elements: 60.5% of the elements, 44.4% of the valid threats eliminating the 48 invalid threats spends 72.0 minutes and returns nothing
The numbers are in the measurement class; their inputs are TM9–TM12.
One Hundred Fifty-Six Raw Threats
When the applicable ones of the six categories are asked of forty-three elements, 156 raw threats come out. Generation is cheap and automatic: nobody thinks about “what could happen here” — the table just gets filled in. This is where the method’s entire value comes from — no element is skipped, and the list does not change from person to person.
The first table shows where the generation comes from. The twenty-six data flow elements each take three categories, producing 78 threats — half of the raw generation. The nine process elements each take six categories and produce 54 threats. The four external entity elements take only two categories: an external entity is not asked about tampering or privilege escalation, because the element sits outside the system and holds no authority inside it.
The second table breaks the raw count down category by category. Tampering, information disclosure, and denial of service are the busiest three with 39 threats each; because all three are applicable to processes, stores, and flows alike, they produce a count close to the total element count. Privilege escalation produces only nine threats, because it is asked only of processes.
One Hundred Eight Valid
The second number comes out after elimination. Of the 156 raw threats, 108 are valid for this system (69.2%), and 48 are invalid. Invalidity is not arbitrary: every category has a validity condition, and the condition looks at a property of the element that is derived from the graph.
The split varies widely from category to category. 92.3% of tampering is valid; its condition only asks flows to cross a boundary, and 23 of the 26 flows do. Information disclosure, on the other hand, is only 43.6% valid: the condition is that the element carries a sensitive class — identity or billing — and the category falls flat on the 22 elements that carry a measurement, setting, or record class. Denial of service sits in the middle (61.5%): interrupting an element that is not in the work flow does not stop the work.
The fourth table shows the same split at the element level. billing produces a valid threat in
all six of the six categories — it is exposed, carries a sensitive class, sits in the work flow,
has multiple roles, and writes to a store. At the other end, flows like work-order->audit-log are
valid in only one of three categories. The concentration is striking: the nine process elements,
only 20.9% of the elements, carry 37.0% of the valid threats; the 26 data flow elements, 60.5%
of the elements, carry 44.4% of the threats.
Elimination’s Minute
Generation’s cheapness is what makes elimination expensive. Every raw threat has to be assessed — the invalid ones too. In the model, eliminating a threat takes 1.5 fictional minutes, and writing up a threat found valid takes an additional 4 minutes.
The total is 666 minutes — 11.1 hours. Of this, 234 minutes go to elimination and 432 minutes to writing. 72 minutes of elimination are spent on the 48 invalid threats and return nothing — reading and discarding rows that will never turn into a countermeasure. A threat modeling session’s duration should therefore be read together with the number of threats eliminated, not the number generated.
The third table compares three separate generation sets. All elements: 156 raw, 108 valid, 666 minutes, 0.162 valid threats per minute. Exposed elements only: 135 raw, 100 valid, 602.5 minutes, 0.166 per minute. Processes only: 54 raw, 40 valid, 241 minutes, 0.166 again.
The three sets’ per-minute efficiency is nearly identical, and this runs against expectation. Prioritizing elements that cross a boundary saves 63.5 minutes and loses 8 threats — a saving of less than a tenth of the total, because 88% of the elements are already exposed. Looking only at processes, on the other hand, saves 425 minutes but leaves out 63% of the valid threats.
The conclusion that follows is this: in this method, a shortcut does not work by raising efficiency — it works by shrinking coverage. For a team that wants to cut the time down, the only honest path is to write down which set of elements is being left out.
Summary
- Systematic generation asks 43 graph elements the categories applicable to their type and produces 156 raw threats; generation is cheap, does not vary by person, and skips no element.
- 108 of the raw threats (69.2%) are valid for this system, and 48 are invalid; the validity condition looks at a property of the element derived from the graph.
- Validity ranges from 43.6% to 92.3% depending on category; information disclosure falls flat on 22 elements, while tampering almost never does.
- The session’s duration is 666 minutes; 72 minutes go to eliminating invalid threats and return nothing. Duration is read together with the threats eliminated, not the threats generated.
- The three generation sets’ per-minute efficiency is nearly identical (0.162–0.166); a shortcut saves from coverage, not efficiency — looking only at processes misses 63% of the valid threats.
Next Step
Systematic generation starts from the element: the same questions are asked of every box, every arrow, and every store. This approach’s strength and its limit sit in the same place — it treats the element on its own. Every one of the 108 valid threats is the intersection of one element and one category; none of them arises from two elements being used together.
But real damage usually does not arise from a single element — it arises from several elements being used one after another: information obtained in one place becomes valid somewhere else. The way to find items of this kind is to start not from the element but from the goal — what the attacker wants to obtain and by what path they get there. The next lesson runs scenario-based modeling over the same graph and measures it on the coverage axis: how many threats the scenarios find, how many of the systematic method’s finds they miss, and what the intersection of the two is.
To keep your progress and take notes, Log in
My notes
Log in to take notes.