Lesson 07 / 11
Data Flow Diagrams
The topic's core measurement: in a graph of 17 nodes, 26 flows, and six trust zones, 23 flows cross a trust boundary, and these crossings produce a total of 58 items at 2.52 per flow, with 58.6% of the items coming from flows that go from a lower to a higher level; a coarse outer/inner drawing sees only 7 of the 23 crossings and only 15 of the 58 items, and the 16 crossings that stay invisible carry 43 items.
Published Updated
In this lesson
- Models the data flow graph as nodes, flows, and trust zones, and counts boundary crossings
- Separates the items each boundary-crossing flow produces by type and calculates items per flow
- Measures how many flows and items stay invisible when boundary resolution drops
Contents
The previous lesson stated the session’s price and its coverage ceiling, but left what happens inside the session as a black box. Reading the design text is not a method: which of hundreds of sentences gives rise to a security item is not obvious, and two people extract two different lists from the same text.
The method’s first step is to turn the design into a structure that can be counted over. That structure is the data flow diagram: which component takes which data from whom and gives it to whom. This lesson builds the graph and takes the topic’s core measurement — the number of flows that cross a trust boundary, the items each crossing produces, and how many flows stay invisible when no boundary is drawn.
A Graph Is Not a Diagram
A diagram is a drawing; what gets counted is not the drawing but the graph behind it. In the terms drawn from the Architectural Decisions and Documentation course, a diagram is a view, and the graph is the structure the view is derived from. The same graph can be written as a box-and-arrow drawing or, as below, row by row; the measurement gives the same number either way.
The graph below is a model: there is no real system, no real hostname, no real service endpoint. It works with the fictional regional measurement network’s software.
TM5: every node sits in a trust zone, and the zone has a numeric trust level. TM6: a node is one of three types — external entity, process, data store. TM7: every flow carries a data class. TM8: a flow whose two ends sit in different zones crosses the trust boundary; every boundary-crossing flow produces items under the four rules below, and a flow that does not cross produces no items.
// threat/graph.mjs — the fictional regional measurement network's software's data flow graph // (model): there is no real system, no real hostname, no real service endpoint. The graph only // says "who takes what from whom"; it carries no attack step. // TM5: every node sits in a trust zone, and the zone has a trust level. export const ZONE = { internet: 0, field: 1, edge: 2, internal: 3, management: 4, data: 5 }; // TM6: a node is written as "name type zone"; type is external entity (D), process (S), or store (P). export 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] }]; })); // TM7: a flow is written as "source target class"; class is the data class carried. export 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}` }; }); // TM8: a flow crosses the trust boundary if its two ends' zones differ. Every boundary-crossing // flow produces items under the four rules below; a flow that does not cross produces no items. export function items(a, zoneOf = (n) => NODE[n].zone, levelOf = (n) => NODE[n].level) { if (zoneOf(a.source) === zoneOf(a.target)) return []; const k = []; if (levelOf(a.source) < levelOf(a.target)) k.push("input-validation", "source-identity"); if (levelOf(a.source) > levelOf(a.target)) k.push("output-filtering"); if (a.class === "identity" || a.class === "billing") k.push("transport-protection"); if (NODE[a.target].type === "P") k.push("authorization"); return k; }
// threat/boundary.mjs — how many flows cross the trust boundary, the items each crossing // carries, and how many flows stay invisible when boundary resolution drops. import { ZONE, NODE, FLOW, items } from "./graph.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 crossing = FLOW.filter((a) => NODE[a.source].zone !== NODE[a.target].zone); console.log(`${Object.keys(NODE).length} nodes, ${FLOW.length} flows, ` + `${Object.keys(ZONE).length} trust zones; boundary-crossing flows ${crossing.length}, ` + `non-crossing ${FLOW.length - crossing.length}`); const A = [-12, 7, 8, 9, 7, 11, 10]; console.log("\n1. zones and their contents"); write(A, "zone", "level", "entity", "process", "store", "out flows", "crossing"); for (const [z, l] of Object.entries(ZONE)) { const ns = Object.values(NODE).filter((x) => x.zone === z); const out = FLOW.filter((a) => NODE[a.source].zone === z); write(A, z, l, ns.filter((x) => x.type === "D").length, ns.filter((x) => x.type === "S").length, ns.filter((x) => x.type === "P").length, out.length, out.filter((a) => NODE[a.target].zone !== z).length); } const B = [-42, -14, -8, -67]; console.log("\n2. every boundary-crossing flow and the items it produces"); write(B, "flow", "class", "level", "items"); for (const a of crossing) write(B, a.name, a.class, `${NODE[a.source].level}->${NODE[a.target].level}`, items(a).join(" ")); const ALL = crossing.flatMap((a) => items(a)); console.log(`the ${crossing.length} boundary-crossing flows produce a total of ${ALL.length} ` + `items, ${(ALL.length / crossing.length).toFixed(2)} per flow`); const C = [-22, 7, 8, 8]; console.log("\n3. items by type"); write(C, "item type", "count", "item %", "flow %"); for (const t of [...new Set(ALL)].sort()) { const n = ALL.filter((x) => x === t).length; write(C, t, n, ((n / ALL.length) * 100).toFixed(1), ((n / FLOW.length) * 100).toFixed(1)); } // Resolution: the same graph is measured with three separate boundary drawings. The coarse // drawing recognizes only the outer/inner split; the boundary-less drawing counts every node // in a single zone. const COARSE = { internet: "outer", field: "outer", edge: "inner", internal: "inner", management: "inner", data: "inner" }; const drawing = { "no boundary drawn": { z: () => "single", l: () => 0 }, "coarse: outer/inner": { z: (x) => COARSE[NODE[x].zone], l: (x) => (COARSE[NODE[x].zone] === "outer" ? 0 : 1) }, "detailed: 6 zones": { z: (x) => NODE[x].zone, l: (x) => NODE[x].level }, }; const D = [-21, 15, 11, 7, 8]; console.log("\n4. what stays invisible when boundary resolution drops"); write(D, "drawing", "visible flows", "visible %", "items", "item %"); const full = { flows: crossing.length, items: ALL.length }; for (const [name, c] of Object.entries(drawing)) { const v = FLOW.filter((a) => c.z(a.source) !== c.z(a.target)); const k = v.flatMap((a) => items(a, c.z, c.l)); write(D, name, v.length, ((v.length / full.flows) * 100).toFixed(1), k.length, ((k.length / full.items) * 100).toFixed(1)); } const coarse = drawing["coarse: outer/inner"]; const coarseV = FLOW.filter((a) => coarse.z(a.source) !== coarse.z(a.target)); const missed = crossing.filter((a) => !coarseV.includes(a)); console.log(`the coarse drawing misses ${missed.length} boundary crossings carrying ` + `${missed.flatMap((a) => items(a)).length} items; ${missed.filter((a) => NODE[a.target].type === "P").length} of these enter a data store`);
17 nodes, 26 flows, 6 trust zones; boundary-crossing flows 23, non-crossing 3 1. zones and their contents zone level entity process store out flows crossing internet 0 2 0 0 2 2 field 1 2 1 0 4 3 edge 2 0 2 0 3 3 internal 3 0 5 0 14 12 management 4 0 1 0 3 3 data 5 0 0 4 0 0 2. every boundary-crossing flow and the items it produces flow class level items subscriber->subscriber-portal identity 0->2 input-validation source-identity transport-protection subscriber-portal->identity-service identity 2->3 input-validation source-identity transport-protection identity-service->identity-store identity 3->5 input-validation source-identity transport-protection authorization subscriber-portal->billing billing 2->3 input-validation source-identity transport-protection billing->billing-store billing 3->5 input-validation source-identity transport-protection authorization billing->subscriber-portal billing 3->2 output-filtering transport-protection meter-device->collection-endpoint measurement 1->2 input-validation source-identity collection-endpoint->validator measurement 2->3 input-validation source-identity validator->measurement-store measurement 3->5 input-validation source-identity authorization field-app->work-order order 1->3 input-validation source-identity work-order->field-app order 3->1 output-filtering work-order->measurement-store measurement 3->5 input-validation source-identity authorization field-app->collection-endpoint measurement 1->2 input-validation source-identity management-console->validator setting 4->3 output-filtering management-console->collection-endpoint setting 4->2 output-filtering management-console->identity-service setting 4->3 output-filtering report-generator->measurement-store measurement 3->5 input-validation source-identity authorization report-generator->billing-store billing 3->5 input-validation source-identity transport-protection authorization oversight-body->report-generator identity 0->3 input-validation source-identity transport-protection report-generator->oversight-body billing 3->0 output-filtering transport-protection billing->audit-log record 3->5 input-validation source-identity authorization identity-service->audit-log record 3->5 input-validation source-identity authorization work-order->audit-log record 3->5 input-validation source-identity authorization the 23 boundary-crossing flows produce a total of 58 items, 2.52 per flow 3. items by type item type count item % flow % authorization 9 15.5 34.6 input-validation 17 29.3 65.4 output-filtering 6 10.3 23.1 source-identity 17 29.3 65.4 transport-protection 9 15.5 34.6 4. what stays invisible when boundary resolution drops drawing visible flows visible % items item % no boundary drawn 0 0.0 0 0.0 coarse: outer/inner 7 30.4 15 25.9 detailed: 6 zones 23 100.0 58 100.0 the coarse drawing misses 16 boundary crossings carrying 43 items; 9 of these enter a data store
The numbers are in the measurement class; their inputs are TM5–TM8.
Twenty-Three Crossings
Seventeen nodes, twenty-six flows, six trust zones. None of these three numbers is threat modeling’s input; the number that is an input is 23 — the number of flows that cross a trust boundary. Only three of the twenty-six flows stay within the same zone: the field team talking to its own app, the validator giving billing a measurement, and billing producing a work order.
This ratio comes from the graph’s shape. Zones exist to increase isolation, but the work of the
system has to travel across them: a subscriber’s request starts in the internet zone and ends in
the data zone, meaning a single transaction crosses three separate boundaries. The first table’s
last column states this zone by zone. Of the fourteen flows originating in the internal zone, twelve
leave it; no flow leaves the data zone, because stores are only written-to and read-from
endpoints.
This is the number that is meaningful on its own for security. The Attack Surface lesson measured the surface as a path count; at the design stage there are no paths yet, only flows. At this stage the surface’s countable equivalent is the number of flows that cross a trust boundary, and that number depends on where the boundaries are drawn.
The Item a Crossing Produces
The second table turns every boundary crossing into an item list. Twenty-three crossings produce 58 items; 2.52 per crossing. The rule set has four entries, and none of them relies on intuition: a flow whose trust level goes from lower to higher requires input validation and source identity, a flow going the opposite direction requires output filtering, a flow carrying identity- or billing-class data requires transport protection, a flow entering a data store requires authorization.
The third table gives the distribution. input-validation and source-identity lead with
seventeen items each; together they carry 58.6% of the 58 items. Both have a single source:
seventeen of the twenty-three crossings go from a lower trust level to a higher one.
output-filtering is the smallest group with six items; transport-protection and authorization
sit in the middle with nine each.
The busiest rows produce four items at once: identity-service->identity-store,
billing->billing-store, and report-generator->billing-store. All three go from lower to higher,
carry a data class that needs protection, and write to a store. The plainest row is
work-order->field-app: a single item, output-filtering. The item count is not determined by how
important the flow is but by how many rules it triggers at once; this distinction is what makes
the method systematic.
The same systematicity also produces the rule’s blind spot. The management console sits at the
highest level short of the fifth trust level, and all three of its outgoing flows go downward; so
all three produce only output-filtering, and none produces input-validation. This is consistent
from the rule set’s point of view — data coming from a more trusted source is not validated — but
what the three flows carry is setting-class data, and changing the validator’s setting affects
the entire measurement chain. Trust level is a ranking, not a certificate of immunity. The item
set that systematic generation produces cannot go beyond the accuracy of the trust-level
assignments; if an assignment is wrong, the method stays silent, and that silence never shows up as
a zero in any table.
When No Boundary Is Drawn
The fourth table is this lesson’s most striking result. The same twenty-six flows are measured at three separate drawing resolutions, and the result gives three separate numbers.
If no boundary is drawn at all, every node is counted in a single zone: zero crossings, zero items. The system has not changed; only its measure has been erased. This is the most common reason a threat modeling session ends with zero items — the graph was drawn, but no boundary was placed on it.
The coarse drawing — only the outer/inner split — sees 7 crossings, meaning 30.4% of the real crossings, and produces 15 items, meaning 25.9% of them. The detailed drawing gives 23 crossings and 58 items. The difference is 16 crossings and 43 items; nine of these items come from flows entering a data store.
Where the loss happens is clear. The coarse drawing counts the internal network as a single
trusted box, so flows like validator->measurement-store, billing->billing-store, or
management-console->identity-service never cross a boundary at all. These are the system’s
highest-value flows: the places where identity and billing data get written to stores. The items
that disappear as boundary resolution drops are not chosen at random; what stays inside the
internal zone is what disappears, and what sits inside the internal zone is the store itself.
The rule that follows is plain: a threat modeling session’s first output is not the threat list — it is the zone list. The number of zones and the number of items that can be found move in the same direction, and when zones are drawn wrong, every step that follows runs on an incomplete set.
Summary
- At the design stage, the surface’s countable equivalent is the number of flows that cross a trust boundary: in a graph of 17 nodes and 26 flows, 23 flows cross a boundary, and only 3 stay within the same zone.
- The 23 boundary-crossing flows produce 58 items under four rules; 2.52 per crossing. The item count depends not on how important the flow is but on how many rules it triggers at once.
- 58.6% of the items come from the 17 flows that go from a lower trust level to a higher one; output filtering is the smallest group with only 6 items.
- When no boundary is drawn at all, the measurement gives zero crossings, zero items; the system has not changed, only its measure has been erased.
- A coarse outer/inner drawing sees 7 of the 23 crossings and 15 of the 58 items; the 16 crossings that stay invisible carry 43 items, and 9 of these are flows entering a data store.
Next Step
This lesson produced a countable surface: 23 crossings and 58 items. But 58 items are not a threat list — the items are control types; they say “an identity check is required here,” not “this specific thing could happen here.” The difference between the two is the method’s second step.
Looking at a graph element and asking “what could go wrong here” depends on the person and on their attention that day. What makes this independent of the person is applying a fixed category list per element type: asking the same questions of every flow, every process, and every store. The next lesson runs this systematic generation and separates two numbers: how many threats the method produces, and how many of those are actually valid for this system. Generation is cheap; elimination is expensive, and it is measured in minutes.
To keep your progress and take notes, Log in
My notes
Log in to take notes.