Skip to content
academia.sh

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.

Start typing to search.

↑↓ ↵ Esc navigate · open · close