---
title: 'Introduction to Cybersecurity'
source: 'https://academia.sh/en/courses/introduction-to-cybersecurity'
language: en
updated: '2026-09-13T05:08:02+00:00'
license: 'CC BY-SA 4.0'
lessons: 15
outcomes:
    - 'Separates asset, threat, vulnerability, exploit, and risk by tying each to a number'
    - 'Measures the attack surface as a countable unit and computes the cost of reducing it'
    - 'Compares how many paths each defense principle closes against how many legitimate jobs it stops'
---

# Introduction to Cybersecurity

Setting up the units security is measured in: how confidentiality, integrity, and availability collide on the same asset, separating the terms asset, threat, vulnerability, and exploit, computing risk as likelihood combined with impact, stepping through the chain from vulnerability to impact, turning the attack surface into a countable unit, and how many paths each of defense in depth, least privilege, isolation, zero trust, the split between authentication and authorization, and secure defaults closes, along with the function of blue, red, and purple teams, security roles, authorization and the legal boundary, and paths to professional competency.

## Security Concepts

Establishing security's units of measure: the confidentiality, integrity, and availability axes overlapping on the same asset, the asset, threat, and vulnerability terms separated into distinct lists in the same inventory, risk calculated as the composition of probability and impact and turned into a ranking, the chain from vulnerability to impact broken into steps, and attack surface reduced to a countable number of paths in an access graph.

### Confidentiality, Integrity and Availability

Source: https://academia.sh/en/courses/introduction-to-cybersecurity/confidentiality-integrity-and-availability

The three axes overlapping on the same asset: in a model with eight assets and six decisions, 27 (item, axis) pairs rise while 22 fall; the confidentiality total climbs from 34 to 41, integrity from 44 to 63, and availability from 47 to 51; a decision with zero net effect raises availability by nine points while lowering confidentiality by six; on key loss the availability total drops from 51 to 24 and four of eight assets are zeroed; and through all of it the average score falls only from 6.5 to 5.3.

The previous curriculum measured the same thing across five courses: when a layer was leased,
which decision got handed off and which number improved in return. Leasing and buying alike
carried the same silent assumption — that only the authorized reach that layer. This lesson
removes that assumption and puts three separate questions in its place: who can see the data, who
can change it, who can reach it when they need to.

These three questions are security's three axes: **confidentiality**, **integrity**, and
**availability**. Taken one at a time, all three look beyond dispute. Set on the same asset
together, they push against each other: a decision that raises one axis usually lowers another.
What this lesson measures is that push — how many items gain on which axis, and how many lose.

#### The Three Axes Sit on the Same Asset

The example is the fictional **regional measurement network** that runs through the whole
curriculum: a service that collects water-meter readings, a service that validates them, billing
and work-order services, field crews, and branch endpoints. Both the network and the assets are
fictional; the scores below are a **model**, not a measurement taken on a real system.

**SC1:** the asset inventory is eight items. **SC2:** every asset carries a base score between 0
and 10 on each of three axes; the score shows how well that axis is provided for that item.
**SC3:** every security decision touches specific items and writes a separate delta on each of
the three axes; the score never drops below 0 or rises above 10. **SC4:** two of the decisions
carry a **collapse condition** — if the condition fires, availability for the items that decision
touches is zeroed. **SC5:** decisions are applied in list order; the clamp acts at every step.

```js
// security/axis.mjs — fictional regional measurement network's asset inventory and three-axis model
// (model): there is no real network, real asset record, or real security measurement. No randomness.

export const AXIS = ["con", "int", "avl"];
export const CLAMP = (x) => Math.max(0, Math.min(10, x));

// SC2: every asset carries a 0..10 base score on three axes; scores are fictional.
export const ASSET = `
  reading-data         4 5 6
  billing-record       5 6 5
  work-order-queue     3 5 7
  identity-store       6 6 5
  configuration        4 6 6
  audit-log            5 4 5
  meter-firmware       3 7 6
  field-app            4 5 7`
  .trim().split("\n").map((r) => {
    const [name, ...p] = r.trim().split(/\s+/);
    return { name, base: Object.fromEntries(AXIS.map((a, i) => [a, +p[i]])) };
  });

// SC3: every decision touches a subset of items and writes a delta on three axes.
// SC4: if a collapse condition fires, availability for those items is zeroed.
export const DECISION = [
  { name: "encryption-at-rest", d: { con: 4, int: 0, avl: -1 }, collapse: "key-loss",
    item: ["reading-data", "billing-record", "identity-store", "audit-log"] },
  { name: "nightly-backup", d: { con: -2, int: 1, avl: 3 }, collapse: null,
    item: ["reading-data", "billing-record", "work-order-queue", "configuration", "audit-log"] },
  { name: "append-only-log", d: { con: 0, int: 4, avl: -1 }, collapse: null,
    item: ["audit-log", "billing-record"] },
  { name: "multi-factor-login", d: { con: 3, int: 1, avl: -2 }, collapse: "agent-loss",
    item: ["identity-store", "configuration", "field-app"] },
  { name: "signed-firmware", d: { con: 0, int: 4, avl: -2 }, collapse: null,
    item: ["meter-firmware", "field-app"] },
  { name: "second-copy", d: { con: -2, int: -1, avl: 3 }, collapse: null,
    item: ["reading-data", "billing-record", "work-order-queue"] },
];

export function apply(collapsed = []) {
  const s = Object.fromEntries(ASSET.map((v) => [v.name, { ...v.base }]));
  for (const k of DECISION)
    for (const name of k.item)
      for (const a of AXIS) s[name][a] = CLAMP(s[name][a] + k.d[a]);
  for (const k of DECISION)
    if (k.collapse && collapsed.includes(k.collapse))
      for (const name of k.item) s[name].avl = 0;
  return s;
}

// How many (item, axis) pairs a single decision raises and lowers on its own.
export function impact(k) {
  const up = k.item.length * AXIS.filter((a) => k.d[a] > 0).length;
  const down = k.item.length * AXIS.filter((a) => k.d[a] < 0).length;
  return { up, down, net: k.item.length * AXIS.reduce((a, ax) => a + k.d[ax], 0),
    worst: Math.min(...AXIS.map((a) => k.d[a])) };
}

export const total = (s, a) => ASSET.reduce((acc, v) => acc + s[v.name][a], 0);
```

```js
// security/measure.mjs — three axes on the same asset: who rises, who falls, what collapses.
import { AXIS, ASSET, DECISION, apply, impact, total } from "./axis.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 base = Object.fromEntries(ASSET.map((v) => [v.name, { ...v.base }]));
const after = apply();

console.log(`${ASSET.length} assets x ${AXIS.length} axes, ${DECISION.length} decisions; ` +
  `score range 0..10 (con confidentiality, int integrity, avl availability)`);

const A = [-21, 6, 6, 6, 4, 7, 6, 6];
console.log("\n1. base score and score after all six decisions");
write(A, "asset", "con", "int", "avl", "", "con'", "int'", "avl'");
for (const v of ASSET)
  write(A, v.name, ...AXIS.map((a) => base[v.name][a]), "", ...AXIS.map((a) => after[v.name][a]));
write(A, "total", ...AXIS.map((a) => total(base, a)), "", ...AXIS.map((a) => total(after, a)));

const B = [-22, 7, 10, 8, 7, 15];
console.log("\n2. how many (item, axis) pairs a decision raises and lowers");
write(B, "decision", "items", "up", "down", "net", "worst axis");
for (const k of DECISION) {
  const e = impact(k);
  write(B, k.name, k.item.length, e.up, e.down, (e.net > 0 ? "+" : "") + e.net,
    AXIS.find((x) => k.d[x] === e.worst) + " " + e.worst);
}
const T = DECISION.map(impact);
write(B, "TOTAL", DECISION.reduce((a, k) => a + k.item.length, 0),
  T.reduce((a, e) => a + e.up, 0), T.reduce((a, e) => a + e.down, 0),
  "+" + T.reduce((a, e) => a + e.net, 0), "-");

const C = [-16, 12, 12, 12, 16];
console.log("\n3. when a collapse condition fires (availability is zeroed)");
write(C, "condition", "con total", "int total", "avl total", "avl = 0 items");
for (const [name, cs] of [["no collapse", []], ["key-loss", ["key-loss"]],
  ["agent-loss", ["agent-loss"]], ["both", ["key-loss", "agent-loss"]]]) {
  const s = apply(cs);
  write(C, name, ...AXIS.map((a) => total(s, a)),
    ASSET.filter((v) => s[v.name].avl === 0).length + "/" + ASSET.length);
}

const D = [-21, 10, 11, 8];
console.log("\n4. lowest axis per asset (no collapse, then key loss)");
write(D, "asset", "lowest", "collapsed", "axis");
const broken = apply(["key-loss"]);
for (const v of ASSET) {
  const a = Math.min(...AXIS.map((x) => after[v.name][x]));
  const b = Math.min(...AXIS.map((x) => broken[v.name][x]));
  write(D, v.name, a, b, AXIS.find((x) => broken[v.name][x] === b));
}
const avg = (s) => (AXIS.reduce((a, x) => a + total(s, x), 0) / (ASSET.length * 3)).toFixed(1);
write(D, "average score", avg(after), avg(broken), "-");
```

```
8 assets x 3 axes, 6 decisions; score range 0..10 (con confidentiality, int integrity, avl availability)

1. base score and score after all six decisions
asset                   con   int   avl       con'  int'  avl'
reading-data              4     5     6          4     5    10
billing-record            5     6     5          5     9     9
work-order-queue          3     5     7          0     5    10
identity-store            6     6     5         10     7     2
configuration             4     6     6          5     8     7
audit-log                 5     4     5          7     9     6
meter-firmware            3     7     6          3    10     4
field-app                 4     5     7          7    10     3
total                    34    44    47         41    63    51

2. how many (item, axis) pairs a decision raises and lowers
decision                items        up    down    net     worst axis
encryption-at-rest          4         4       4    +12         avl -1
nightly-backup              5        10       5    +10         con -2
append-only-log             2         2       2     +6         avl -1
multi-factor-login          3         6       3     +6         avl -2
signed-firmware             2         2       2     +4         avl -2
second-copy                 3         3       6      0         con -2
TOTAL                      19        27      22    +38              -

3. when a collapse condition fires (availability is zeroed)
condition          con total   int total   avl total   avl = 0 items
no collapse               41          63          51             0/8
key-loss                  41          63          24             4/8
agent-loss                41          63          39             3/8
both                      41          63          14             6/8

4. lowest axis per asset (no collapse, then key loss)
asset                    lowest  collapsed    axis
reading-data                  4          0     avl
billing-record                5          0     avl
work-order-queue              0          0     con
identity-store                2          0     avl
configuration                 5          5     con
audit-log                     6          0     avl
meter-firmware                3          3     con
field-app                     3          3     avl
average score               6.5        5.3       -
```

Numbers belong to the **measurement** class; their inputs are SC1–SC5.

#### A Decision Works in Two Directions at Once

The second table reduces the six decisions to a single measure: how many (item, axis) pairs each
decision raises, and how many it lowers. In total, **27 pairs rise, 22 fall.** None of the six
decisions raises only — even encryption-at-rest, which has the highest net value, lowers
availability by one point on each of the four items it touches.

The `reading-data` row in the first table is the clearest form of that push. Its confidentiality
score starts at 4, encryption raises it to 8, the nightly backup drops it to 6, the second copy
drops it to 4. **The item is encrypted and its confidentiality score is exactly where it
started.** The reason is plain: encryption protects the data in one place, while the backup and
the copy multiply that same data in two more places. The same row's availability climbs from 6 to
10 — the gain is real, but it is paid for on the confidentiality axis.

The `work-order-queue` row is where the clamp does its work: confidentiality starts at 3, two
decisions each take two points, and the score stops at 0. Once an axis has hit the floor, there
is nothing left to pay on that axis; the next decision starts drawing the same cost from a
different item. The opposite end is `identity-store`: confidentiality climbs from 6 to 10, the
ceiling, while availability in return falls from 5 to 2. An axis pinned to the ceiling means that
taking any further confidentiality decision on that item produces **only an availability cost.**

#### A Decision with Zero Net Effect

The `second-copy` row is the most instructive item in the second table: its net value is **exactly
zero**. It touches three items, raises three pairs, lowers six. It gains three points of
availability per item (+9 total), while costing two points of confidentiality (−6) and one point
of integrity (−3) per item. A reading that reduces the total to a single number counts this
decision as "no effect" and drops it from the agenda.

But the decision's meaning is in its distribution, not its total. The same row carries three
separate questions: the copy keeps readings reachable during an outage; that same copy makes the
data visible in one more place; and temporary inconsistency between the copies weakens integrity.
These three do not cancel each other out, because they are **paid on different events**. A net of
zero does not mean three separate risks canceled one another; it means three separate ledgers were
summed into one row.

The same measure works for comparison too. `nightly-backup` raises ten pairs and lowers five, for
a net of +10; `encryption-at-rest` raises four and lowers four, for a net of +12. Counting by
pairs, the backup is broader; counting by net value, encryption is deeper. The two are not
comparable; no defense can be called "better" without saying **on which axis it was measured.**

#### Every Asset's Dominant Axis Is Its Own

The last columns of the first table give a third reading: once the same decisions have been
applied, the eight assets do not land in the same place on all three axes. `meter-firmware` sits
at 10 on integrity and 4 on availability; `work-order-queue` sits at 10 on availability and 0 on
confidentiality. Both items belong to the same network and passed through the same set of
decisions, yet one accumulated toward integrity and the other toward availability.

The difference is not chance; it comes from what the item does. Meter firmware runs on a field
device: running the wrong version corrupts every reading, while going unpatched for a few hours
corrupts nothing. The work-order queue runs the opposite way: who sees a record inside it is a low
cost, while the queue going down leaves field crews waiting. The same three axes fall into two
different rankings across two items.

The practical consequence is that a single security goal cannot hold across the whole inventory.
The rule "confidentiality comes first" has already hit the floor for `work-order-queue`, leaving
nothing left to apply there; the same rule has pinned `identity-store` to the ceiling and started
producing nothing but an availability cost. **Axis priority is set per asset, not per
organization** — and the scores counted through this lesson are the one record of where that
priority lands on each item.

#### A Collapse Zeroes One Axis

The third table runs the decisions' collapse conditions. When the encryption key is lost, the
confidentiality total stays at 41 — arguably still protected, since no one can read the data. At
the same time, the availability total drops from **51 to 24** and **four of eight** assets are
zeroed. When the multi-factor login's authenticator device is lost, availability drops to 39 and
three items are zeroed. When both conditions fire together, the total is 14 and six of the eight
items are unreachable.

What these rows share is that the confidentiality and integrity columns never move. The defense
that collapses does not break on its own axis; it keeps its promise more than fully. The whole
cost is written to the third axis. A reading that does not count availability as a security axis
sees no problem at all in this table — because the two columns it is watching stay fixed.

#### What the Average Conceals

The fourth table reads the same situation per asset and shows how a single number misleads. On
key loss, four of the eight assets drop to zero on their lowest axis. At the same time, **the
average score falls only from 6.5 to 5.3.** Four of twenty-four cells have been zeroed, and a
dashboard watching a single number records that as a one-point decline.

The reason is in the distribution: the other two axes of the zeroed items stay high, which keeps
the average up. `reading-data` sits at 4 on confidentiality, 5 on integrity, and 0 on
availability; its average is 3 and looks "moderate." But if that item cannot be reached, its
confidentiality score has no practical payoff. **The three axes do not add up; the lowest axis
determines the item's real condition.**

Two rows in the table also show this limit: `configuration` and `meter-firmware` are not affected
by key loss at all, because they are not among the items the encryption decision touches. The
scope of a collapse is the set of items the collapsing defense touches — nothing wider. Everything
counted through this lesson stands on those sets, and the lessons that follow will give the same
sets other names.

#### Summary

- The three axes overlap on the same asset: in a model with eight assets and six decisions, 27
  (item, axis) pairs rise while 22 fall; none of the six decisions runs in only one direction.
- The encrypted `reading-data` item's confidentiality score rises from 4 and returns to 4, because
  the backup and the second copy multiply the same data; its availability, meanwhile, climbs from
  6 to 10.
- The zero-net `second-copy` decision writes +9 on availability, −6 on confidentiality, and −3 on
  integrity; when the total is reduced to one row, the three separate ledgers disappear.
- On key loss the confidentiality and integrity totals never move, availability drops from 51 to
  24, and four of eight assets are zeroed; the two collapse conditions together write 14 and six
  items.
- The same zeroing shows up in the average as only a drop from 6.5 to 5.3; an item's condition is
  set by its lowest axis, not the average.
- The scope of a collapse is the set of items the collapsing decision touches; two assets are
  entirely unaffected by key loss.

#### Next Step

This lesson scored eight items and counted which direction six decisions moved those scores. But
the whole computation ran on a single list, and that list's parts were left unnamed: what exactly
was the thing called "asset," what was the name of the thing decisions counter, what was the thing
that lowers a score actually called. The next lesson splits the same inventory into three separate
lists — **asset**, **threat**, and **vulnerability** — and shows why multiplying the three does
not give the number of reachable paths.

### Asset, Threat and Vulnerability

Source: https://academia.sh/en/courses/introduction-to-cybersecurity/asset-threat-and-vulnerability

Separating three terms into the same inventory: the cartesian product of 8 assets, 6 threats, and 12 vulnerabilities is 576, while the valid-triple count drops to 19, or 3.3%; two assets each carrying two vulnerabilities produce 1 and 4 triples; a vulnerability no threat can reach carries zero triples; two threats carrying the same five triples touch 4 and 5 separate assets; closing five of twelve vulnerabilities closes 12 of the 19 triples; and narrowing an asset's zone closes 3, 2, 1, or no triples at all.

The previous lesson scored eight items and counted which direction six decisions moved those
scores. The whole computation ran on a single list, and the names of that list's parts were left
unclear: the thing being protected was called "asset," but what was the thing that would lower a
score, what was the thing that let it drop. This lesson splits the same inventory into three
separate lists and counts how the three combine.

**Asset** is the thing being protected: data, a service, a device, a record. **Threat** is an
agent capable of causing harm; the party capable of doing so is called a **threat actor**.
**Vulnerability** is the weakness in the asset that lets that harm happen. The three are separate
lists and are counted separately. In everyday speech they get used interchangeably; once counted,
it becomes clear they are not interchangeable.

#### Three Lists Answer Three Separate Questions

The example is again the fictional **regional measurement network**, and the asset list is the same
eight items as the previous lesson. The inventory below is a **model**: there is no real network,
no real threat record, no real vulnerability list.

**SC6:** every asset is named by the widest zone it can be reached from — outer, field, inner.
**SC7:** every vulnerability lives in exactly one asset and carries a type. **SC8:** every threat
is defined by the set of zones it can reach and the vulnerability types it can use. **SC9:** an
`(asset, vulnerability, threat)` triple is valid only if all three conditions hold together — the
vulnerability lives in that asset, the threat reaches that zone, the threat can use that
vulnerability type.

```js
// security/inventory.mjs — fictional measurement network's asset, threat, and vulnerability
// inventory (model): there is no real network, threat record, or vulnerability list. No randomness.

// SC6: every asset is named by the widest zone it can be reached from (outer, field, inner).
export const ASSET = `
  reading-data         inner
  billing-record       outer
  work-order-queue     field
  identity-store       inner
  configuration        inner
  audit-log            inner
  meter-firmware       field
  field-app            outer`
  .trim().split("\n").map((r) => { const [name, zone] = r.trim().split(/\s+/); return { name, zone }; });

// SC7: every vulnerability lives in exactly one asset and carries a type.
export const VULN = `
  outdated-library-version    billing-record       version
  default-password             configuration        credential
  broad-read-permission        reading-data          permission
  unvalidated-input            field-app             input
  open-management-endpoint     configuration         configuration
  unsigned-update               meter-firmware        integrity
  shared-account                identity-store        credential
  unencrypted-transfer          work-order-queue      transfer
  unretained-log                audit-log             visibility
  outdated-agent-version        field-app             version
  physical-access-point         meter-firmware         physical
  unvalidated-field             audit-log             input`
  .trim().split("\n").map((r) => {
    const [name, asset, type] = r.trim().split(/\s+/);
    return { name, asset, type, zone: ASSET.find((v) => v.name === asset).zone };
  });

// SC8: every threat is defined by the zones it can reach and the vulnerability types it can use.
export const THREAT = [
  { name: "external-opportunist", zone: ["outer"], type: ["version", "credential", "input", "configuration"] },
  { name: "targeted-external", zone: ["outer", "field"],
    type: ["version", "credential", "input", "configuration", "transfer", "integrity"] },
  { name: "field-proximate", zone: ["field"], type: ["physical", "integrity", "transfer"] },
  { name: "internal-user-error", zone: ["inner", "field"], type: ["configuration", "permission"] },
  { name: "malicious-insider", zone: ["inner", "field", "outer"],
    type: ["permission", "credential", "visibility", "transfer"] },
  { name: "supply-chain", zone: ["field", "inner"], type: ["integrity", "version"] },
];

// SC9: a triple is valid only if the vulnerability reaches that zone and the threat can use
// that vulnerability type.
export const valid = (v, t) => t.zone.includes(v.zone) && t.type.includes(v.type);
export const TRIPLE = VULN.flatMap((v) => THREAT.filter((t) => valid(v, t))
  .map((t) => ({ asset: v.asset, vuln: v.name, threat: t.name })));

export const vulnCount = (a) => VULN.filter((v) => v.asset === a).length;
export const tripleCount = (field, name) => TRIPLE.filter((u) => u[field] === name).length;

// SC10: an asset's zone can be narrowed by one step (outer -> field -> inner); narrowing
// changes which vulnerabilities in that asset are reachable, it does not close them.
export const NARROW = { outer: "field", field: "inner", inner: "inner" };
export function narrowedTriple(asset) {
  return VULN.reduce((n, v) => n + THREAT.filter((t) =>
    t.zone.includes(v.asset === asset ? NARROW[v.zone] : v.zone) && t.type.includes(v.type)).length, 0);
}
```

```js
// security/count.mjs — three lists in one inventory: how many assets, threats, vulns, and valid triples.
import { ASSET, VULN, THREAT, TRIPLE, NARROW, vulnCount, tripleCount, narrowedTriple } from "./inventory.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 cartesian = ASSET.length * THREAT.length * VULN.length;

console.log(`${ASSET.length} assets, ${THREAT.length} threats, ${VULN.length} vulns; ` +
  `cartesian product ${cartesian}, since each vuln is already tied to one asset the ` +
  `candidates are ${VULN.length * THREAT.length}, valid triples ${TRIPLE.length}`);

const A = [-21, 8, 8, 9, 14];
console.log("\n1. vulns and valid triples per asset");
write(A, "asset", "zone", "vulns", "triples", "triples/vuln");
for (const v of ASSET)
  write(A, v.name, v.zone, vulnCount(v.name), tripleCount("asset", v.name),
    vulnCount(v.name) ? (tripleCount("asset", v.name) / vulnCount(v.name)).toFixed(1) : "-");
write(A, "total", "-", VULN.length, TRIPLE.length, (TRIPLE.length / VULN.length).toFixed(1));

const B = [-26, 15, 8, 9];
console.log("\n2. how many threats can use each vuln");
write(B, "vuln", "type", "zone", "triples");
for (const v of [...VULN].sort((x, y) => tripleCount("vuln", y.name) - tripleCount("vuln", x.name)))
  write(B, v.name, v.type, v.zone, tripleCount("vuln", v.name));

const C = [-23, 8, 7, 9, 10];
console.log("\n3. reach and use per threat");
write(C, "threat", "zones", "types", "triples", "assets");
for (const t of THREAT)
  write(C, t.name, t.zone.length, t.type.length, tripleCount("threat", t.name),
    new Set(TRIPLE.filter((u) => u.threat === t.name).map((u) => u.asset)).size);

const D = [-48, 12, 9];
console.log("\n4. where the product narrows");
write(D, "step", "candidates", "left %");
const pct = (n) => ((n / cartesian) * 100).toFixed(1);
for (const [name, n] of [["asset x threat x vuln", cartesian],
  ["vuln already tied to one asset (vuln x threat)", VULN.length * THREAT.length],
  ["threat can reach that zone", VULN.reduce((a, x) =>
    a + THREAT.filter((t) => t.zone.includes(x.zone)).length, 0)],
  ["threat can use that vuln type", TRIPLE.length]]) write(D, name, n, pct(n));

const sorted = [...VULN].map((v) => tripleCount("vuln", v.name)).sort((x, y) => y - x);
const running = sorted.map(((s) => (n) => (s += n))(0));
console.log(`\nvuln order by triples carried: ${sorted.join(" ")}`);
console.log(`closing sum: ${running.join(" ")} (5 of 12 vulns close ${running[4]}/${TRIPLE.length}` +
  ` triples); vuln carrying zero triples: ` +
  VULN.filter((v) => tripleCount("vuln", v.name) === 0).map((v) => v.name).join(", "));

const E = [-21, 8, 9, 9, 9];
console.log("\n5. narrowing an asset's zone by one step (vuln count unchanged)");
write(E, "asset", "zone", "new", "triples", "closed");
for (const v of ASSET)
  write(E, v.name, v.zone, NARROW[v.zone], narrowedTriple(v.name), TRIPLE.length - narrowedTriple(v.name));
```

```
8 assets, 6 threats, 12 vulns; cartesian product 576, since each vuln is already tied to one asset the candidates are 72, valid triples 19

1. vulns and valid triples per asset
asset                    zone   vulns  triples  triples/vuln
reading-data            inner       1        2           2.0
billing-record          outer       1        2           2.0
work-order-queue        field       1        3           3.0
identity-store          inner       1        1           1.0
configuration           inner       2        2           1.0
audit-log               inner       2        1           0.5
meter-firmware          field       2        4           2.0
field-app               outer       2        4           2.0
total                       -      12       19           1.6

2. how many threats can use each vuln
vuln                                 type    zone  triples
unsigned-update                 integrity   field        3
unencrypted-transfer             transfer   field        3
outdated-library-version          version   outer        2
broad-read-permission          permission   inner        2
unvalidated-input                   input   outer        2
outdated-agent-version            version   outer        2
default-password               credential   inner        1
open-management-endpoint    configuration   inner        1
shared-account                 credential   inner        1
unretained-log                 visibility   inner        1
physical-access-point            physical   field        1
unvalidated-field                   input   inner        0

3. reach and use per threat
threat                    zones  types  triples    assets
external-opportunist          1      4        3         2
targeted-external             2      6        5         4
field-proximate               1      3        3         2
internal-user-error           2      2        2         2
malicious-insider             3      4        5         5
supply-chain                  2      2        1         1

4. where the product narrows
step                                              candidates   left %
asset x threat x vuln                                    576    100.0
vuln already tied to one asset (vuln x threat)            72     12.5
threat can reach that zone                                42      7.3
threat can use that vuln type                             19      3.3

vuln order by triples carried: 3 3 2 2 2 2 1 1 1 1 1 0
closing sum: 3 6 8 10 12 14 15 16 17 18 19 19 (5 of 12 vulns close 12/19 triples); vuln carrying zero triples: unvalidated-field

5. narrowing an asset's zone by one step (vuln count unchanged)
asset                    zone      new  triples   closed
reading-data            inner    inner       19        0
billing-record          outer    field       19        0
work-order-queue        field    inner       17        2
identity-store          inner    inner       19        0
configuration           inner    inner       19        0
audit-log               inner    inner       19        0
meter-firmware          field    inner       16        3
field-app               outer    field       18        1
```
Numbers belong to the **measurement** class; their inputs are SC6–SC9.

#### The Product Is Not a Path Count

The fourth table is this lesson's central finding. Multiplying the lengths of the three lists
gives 576. That number measures nothing, because the lists are not independent. The first
narrowing is definitional: every vulnerability already lives in exactly one asset, so the asset
dimension is not a separate factor — 72 candidates remain, **12.5%** of the product. The second
narrowing comes from reach: the threat has to reach the zone the vulnerability lives in, and the
candidates drop to 42. The third comes from capability: the threat has to be able to use that
vulnerability's type, leaving **19 triples** — **3.3%** of the starting product.

The gap is not a difference of scale but of kind. The sentence "we have eight assets, six threat
types, and twelve vulnerabilities" is an inventory; it says nothing about what could happen from
where. Multiplying the three produces a scare number. What is actually countable is where the
three **intersect**, and that intersection narrows under two conditions: reach and capability.

Still, the number 19 is not yet a path count. Every triple assumes the threat can touch the asset
directly; the zone is given as a label, not counted as a gate to pass through. Reaching an asset
in the inner zone actually requires passing over other assets first, and each of those crossings
is a separate step. What this lesson leaves behind is an **intersection count**; the path count
will be done in this topic's last lesson.

#### Same Number of Vulnerabilities, a Fourfold Difference

The first table shows why vulnerability count alone is not an indicator. `audit-log` and
`field-app` both carry **two vulnerabilities each**. The first produces 1 triple, the second 4.
The ratio per vulnerability is 0.5 against 2.0 — a **fourfold difference** at the same
vulnerability count.

The source of the difference shows up in the second table. `field-app` is in the outer zone and
its vulnerabilities are of type input and version; two of the three threats that reach the outer
zone can use those types. `audit-log` is in the inner zone and its vulnerabilities are of type
visibility and input; of the three threats that reach the inner zone, only one uses visibility,
and none use the input type.

The last row is this lesson's sharpest item: the `unvalidated-field` vulnerability carries **zero
triples**. The vulnerability is real, it sits in the list, it gets flagged in an audit — but no
threat can reach it and use it. That does not mean the vulnerability should be left unpatched; if
the zone boundary changes, the triple count stops being zero. What it means is this: **vulnerability
count is not an exposure measure.** One of twelve vulnerabilities is exposed to nothing, and
another is exposed to three at once.

#### A Threat's Two Separate Widths

The third table measures threats on two axes: how many zones they reach and how many
vulnerability types they can use. `targeted-external` reaches two zones but uses six types;
`malicious-insider` reaches three zones and uses four types. Both produce **five triples** — the
same number, by two different routes.

The difference is in the last column: `targeted-external` touches four separate assets,
`malicious-insider` touches five. Reach width and capability width give the same triple total, but
their spread differs. Capability width makes multiple vulnerabilities on the same asset usable;
reach width touches more assets. A defense decision cannot be written without knowing **which of
the two it is narrowing**.

`supply-chain` sits at the opposite end: it reaches two zones, uses two types, produces a single
triple, and touches a single asset. This row records that the threat list's length is not an
indicator either — one of six threats carries **one nineteenth** of the inventory.

The last two lines rank the return on closing. When the twelve vulnerabilities are ordered by
triple count, the sequence `3 3 2 2 2 2 1 1 1 1 1 0` comes out. Closing the five vulnerabilities
that carry the most closes **12 of the 19 triples**; the remaining seven triples need seven more
separate vulnerabilities closed, and the twelfth adds nothing. Closing order is set not by
vulnerability count but by **triple count carried**.

#### Which of the Three Lists Is Yours to Move

The fifth table tries a third lever. Closing a vulnerability shortens one list; the threat list
cannot be shortened, because who is capable of what is not the organization's decision to make.
What remains is reach: narrowing an asset's zone by one step changes the set of threats that can
reach its vulnerabilities **without closing any of them**.

The results show four separate behaviors. When `meter-firmware` is pulled from field to inner,
**3 triples close** — equal to the highest return of closing a single vulnerability, and both of
its vulnerabilities stay exactly where they are. `work-order-queue` closes 2, `field-app` closes
1. The four assets already in the inner zone have no further step left to narrow, and the triples
they close are zero.

The second row is the table's most instructive item: when `billing-record` is pulled from outer to
field, the triples closed are **zero**. The item has moved to a narrower zone, but its
version-type vulnerability can be used both by a threat that reaches the outer zone and by another
that reaches the field zone; a new triple opens exactly where one closed. **The return on
narrowing does not depend on how hidden the item now is, but on which set of threats is left
outside.**

The three levers are therefore not the same thing: closing a vulnerability deletes a row from the
list, narrowing a zone leaves the same row in place and changes the set of threats that reach it,
and the threat list is not yours to move at all. This is the last thing this lesson can count; the
second lever's real measure cannot be given without knowing how many steps sit between zones.

#### Summary

- Asset is the thing protected, threat is the agent capable of causing harm, and vulnerability is
  the weakness in the asset; the three are separate lists and are counted separately.
- The product of eight assets, six threats, and twelve vulnerabilities is 576; the valid-triple
  count is 19, or 3.3% of the product. The narrowing comes from two conditions: does the threat
  reach that zone, can it use that vulnerability type.
- Nineteen triples are still not a path count; every triple assumes the threat touches the asset
  directly and does not count zone-to-zone crossings as steps.
- Two assets carrying two vulnerabilities each produce 1 and 4 triples; one vulnerability carries
  zero triples, another carries three. Vulnerability count alone is not an exposure measure.
- Of two threats that produce five triples each, one touches four assets and the other five; reach
  width and capability width give the same total with a different spread.
- Narrowing an asset's zone by one step, without closing any vulnerability, closes 3, 2, 1, or 0
  triples; when other threats already reach the narrower zone, the return stays zero.

#### Next Step

This lesson counted nineteen triples and wrote every one of them at equal weight. But the single
triple carried through `physical-access-point` does not occur at the same frequency as the three
carried through `unencrypted-transfer`, nor does it cost the same when it does occur. In this
lesson, closing order was set purely by triple count carried; yet an item that carries few triples
can still be far heavier. The next lesson combines these two dimensions — **probability** and
**impact** — over a set of items, and shows how two different scoring schemes rank the same list
differently.

### Risk Definition

Source: https://academia.sh/en/courses/introduction-to-cybersecurity/risk-definition

Calculating risk as the composition of probability and impact: in a 12-item list the total expected loss comes to 176.05 fictional units; banded scoring changes the rank of 9 items in the same list, carries the largest shift to 6 positions, and leaves only 2 of the top five shared; for threshold scoring those numbers are 7, 4, and 4; a four-item budget closes 77.00, 67.60, and 71.40 units across the three scoring schemes; and two items carrying the same 18.00 expected loss produce 4.0 and 0.3 events over ten years.

The previous lesson counted nineteen triples and wrote every one of them at equal weight. Closing
order was built purely from triple count carried; but how often an item actually occurs, and what
it costs when it does, was not inside that number. This lesson adds those two dimensions and puts
the list in order.

**Risk** is the composition of an event's **probability** and its **impact**. In its plainest
form, risk is written as probability × impact, and the resulting number is called **expected
loss**. Vulnerability is a list, risk is a number; when the two are conflated, a zero-probability
vulnerability and a frequently occurring item end up on the same row. What this lesson shows is
that composition does not have a single form: the same two inputs produce three separate
rankings, and the choice of ranking decides where the budget goes.

#### Risk Is a Product

The items are the previous lesson's vulnerability list. The probability and impact values below
are a **model**: there is no real event record, no real probability estimate, no real currency.
Impact is denominated in **fictional units**.

**SC11:** every item carries a probability and an impact; probability is the likelihood of
occurring within a one-year window, impact is the loss in fictional units. **SC12:** expected loss
is the product of probability and impact. **SC13:** banded scoring reduces probability and impact
each to a band between 1 and 5 and multiplies the two bands. **SC14:** threshold scoring puts
items with impact of 100 fictional units or above first, then ranks them among themselves by
expected loss. **SC15:** the budget covers four items in one round. Ties do not disturb list
order.

```js
// security/risk.mjs — fictional measurement network's risk items (model): no real event record,
// no real probability estimate, no real currency. Impact is denominated in "fictional units".

// SC11: items are the previous lesson's vulnerability list; probability is the likelihood of
// occurring within a one-year window, impact is the loss in fictional units.
export const ITEM = `
  outdated-library-version    0.40    45
  default-password             0.60    28
  broad-read-permission        0.18   110
  unvalidated-input            0.50    40
  open-management-endpoint     0.12   160
  unsigned-update               0.03   600
  shared-account                0.45    30
  unencrypted-transfer          0.25    55
  unretained-log                0.70    14
  outdated-agent-version        0.55    20
  physical-access-point         0.06   240
  unvalidated-field             0.02    90`
  .trim().split("\n").map((r, i) => {
    const [name, p, e] = r.trim().split(/\s+/);
    return { i, name, p: +p, e: +e, expected: +(+p * +e).toFixed(2) };
  });

// SC13: banded scoring reduces probability and impact to a 1..5 band and multiplies the bands.
const band = (x, limit) => limit.findIndex((s) => x < s) + 1 || limit.length + 1;
export const pBand = (k) => band(k.p, [0.05, 0.15, 0.30, 0.50]);
export const iBand = (k) => band(k.e, [20, 50, 100, 200]);
export const bandScore = (k) => pBand(k) * iBand(k);

// SC14: threshold scoring puts items with impact 100 fictional units or above first.
export const THRESHOLD = 100;

// Rankings: ties do not disturb list order (stable sort).
export const RANK = {
  A: [...ITEM].sort((x, y) => y.expected - x.expected || x.i - y.i),
  B: [...ITEM].sort((x, y) => bandScore(y) - bandScore(x) || x.i - y.i),
  C: [...ITEM].sort((x, y) => (y.e >= THRESHOLD) - (x.e >= THRESHOLD)
    || y.expected - x.expected || x.i - y.i),
};
export const rank = (name, k) => RANK[name].findIndex((x) => x.name === k.name) + 1;
export const TOTAL = +ITEM.reduce((a, k) => a + k.expected, 0).toFixed(2);
```

```js
// security/rank.mjs — same items, three scoring schemes: how the rank shifts, what it does to budget.
import { ITEM, RANK, THRESHOLD, TOTAL, pBand, iBand, bandScore, rank } from "./risk.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 BUDGET = 4; // SC15: budget covers four items in one round

console.log(`${ITEM.length} items; risk = probability x impact (fictional units). ` +
  `total expected loss ${TOTAL} units; A expected loss, B banded product (1..5 x 1..5), ` +
  `C threshold that puts impact ${THRESHOLD} and above first`);

const A = [-27, 7, 8, 10, 5, 8, 7, 5, 5, 5];
console.log("\n1. same items, three scoring schemes");
write(A, "item", "prob", "impact", "expected", "p-b", "i-b", "band", "A", "B", "C");
for (const k of ITEM)
  write(A, k.name, k.p.toFixed(2), k.e, k.expected.toFixed(2), pBand(k), iBand(k), bandScore(k),
    rank("A", k), rank("B", k), rank("C", k));

const B = [-30, 9, 9];
console.log("\n2. compared to A");
write(B, "measure", "B", "C");
const shift = (name) => ITEM.map((k) => Math.abs(rank("A", k) - rank(name, k)));
const topFive = (name) => RANK[name].slice(0, 5).filter((k) => RANK.A.slice(0, 5).includes(k)).length;
for (const [name, f] of [["items with changed rank", (s) => shift(s).filter((x) => x > 0).length],
  ["largest rank shift", (s) => Math.max(...shift(s))],
  ["total absolute shift", (s) => shift(s).reduce((a, x) => a + x, 0)],
  ["top five shared", (s) => topFive(s) + "/5"]]) write(B, name, f("B"), f("C"));

const C = [-16, 26, 10, 9, 10];
console.log(`\n3. budget ${BUDGET} items: which scheme picks what`);
write(C, "scheme", "A-rank of picks", "closed", "share %", "tail");
const biggest = ITEM.reduce((a, k) => (k.e > a.e ? k : a));
for (const name of ["A", "B", "C"]) {
  const pick = RANK[name].slice(0, BUDGET);
  const closed = +pick.reduce((a, k) => a + k.expected, 0).toFixed(2);
  write(C, name, pick.map((k) => rank("A", k)).join(" "), closed.toFixed(2),
    ((closed / TOTAL) * 100).toFixed(1), pick.includes(biggest) ? "inside" : "outside");
}
console.log(`tail = single largest impact (${biggest.name}, ${biggest.e} units)`);

const D = [-27, 9, 8, 11, 8];
console.log("\n4. same expected loss, separate decision");
write(D, "item", "prob", "impact", "expected", "band");
const tied = ITEM.filter((k) => ITEM.some((x) => x !== k && x.expected === k.expected));
for (const k of tied) write(D, k.name, k.p.toFixed(2), k.e, k.expected.toFixed(2), bandScore(k));
console.log(`over ten years the expected total is equal; a single event loses ` +
  `${tied.map((k) => k.e).join(" and ")} units, and the expected event count over ten years is ` +
  `${tied.map((k) => (k.p * 10).toFixed(1)).join(" and ")}`);

const running = RANK.A.map(((s) => (k) => (((s += k.expected) / TOTAL) * 100).toFixed(1))(0));
console.log(`\ncumulative coverage in A order (%): ${running.join(" ")}`);
```

```
12 items; risk = probability x impact (fictional units). total expected loss 176.05 units; A expected loss, B banded product (1..5 x 1..5), C threshold that puts impact 100 and above first

1. same items, three scoring schemes
item                          prob  impact  expected  p-b     i-b   band    A    B    C
outdated-library-version      0.40      45     18.00    4       2      8    4    7    6
default-password              0.60      28     16.80    5       2     10    6    2    7
broad-read-permission         0.18     110     19.80    3       4     12    2    1    1
unvalidated-input             0.50      40     20.00    5       2     10    1    3    5
open-management-endpoint      0.12     160     19.20    2       4      8    3    8    2
unsigned-update               0.03     600     18.00    1       5      5    5   10    3
shared-account                0.45      30     13.50    4       2      8    9    9    9
unencrypted-transfer          0.25      55     13.75    3       3      9    8    6    8
unretained-log                0.70      14      9.80    5       1      5   11   11   11
outdated-agent-version        0.55      20     11.00    5       2     10   10    4   10
physical-access-point         0.06     240     14.40    2       5     10    7    5    4
unvalidated-field             0.02      90      1.80    1       3      3   12   12   12

2. compared to A
measure                               B        C
items with changed rank               9        7
largest rank shift                    6        4
total absolute shift                 30       14
top five shared                     2/5      4/5

3. budget 4 items: which scheme picks what
scheme                     A-rank of picks    closed  share %      tail
A                                  1 2 3 4     77.00     43.7   outside
B                                 2 6 1 10     67.60     38.4   outside
C                                  2 3 5 7     71.40     40.6    inside
tail = single largest impact (unsigned-update, 600 units)

4. same expected loss, separate decision
item                            prob  impact   expected    band
outdated-library-version        0.40      45      18.00       8
unsigned-update                 0.03     600      18.00       5
over ten years the expected total is equal; a single event loses 45 and 600 units, and the expected event count over ten years is 4.0 and 0.3

cumulative coverage in A order (%): 11.4 22.6 33.5 43.7 54.0 63.5 71.7 79.5 87.2 93.4 99.0 100.0
```
Numbers belong to the **measurement** class; their inputs are SC11–SC15.

#### Same List, Three Separate Rankings

The second table is this lesson's central finding. The input has not changed — the same twelve
items, the same probability, the same impact. Only the way they are composed has changed. Banded
scoring **changes the rank of nine items**, the largest shift is **six positions**, and the total
absolute shift is 30. Only **two** of the top five are shared. Threshold scoring is milder: seven
items change place, the largest shift is four, and four of the top five are shared.

The first table shows item by item where the shift comes from. `outdated-agent-version` sits
**tenth** in the expected-loss ranking (11.00 units), and **fourth** in the banded ranking. The
reason is where the bands are drawn: a probability of 0.55 falls into the top band (5), an impact
of 20 units falls into the second band (2), and the product is 10. The same score of 10 goes to
`physical-access-point`, whose impact is 240 units and whose expected loss is 14.40 units.
**Banding pulls the two ends toward each other.**

In the opposite direction sits `open-management-endpoint`: it has an expected loss of 19.20 units
and ranks third, but drops to eighth in the banded ranking. Its probability is 0.12 and falls into
the second band; yet there is no real gap between 0.12 and 0.18, only the 0.15 boundary.
`broad-read-permission` at 0.18 enters the third band and comes out first in the banded ranking.
**What sets the order between the two items is not the gap between them, but where the boundary
was placed.**

The band itself is not a flaw; estimating probability to two decimal places of precision is not
possible for most items, and the band removes that false precision. What is measured here is
this: in place of the false precision the band removes, a second false precision arrives, and this
second one is strong enough to change the ranking of nine items.

#### Budget Turns Ranking into a Decision

A rank difference on its own is just a debate; the third table turns it into a decision. If a
round can cover four items, the expected-loss ranking picks the top four and closes **77.00
units**, or **43.7%**, of the 176.05-unit total. The banded ranking, on the same budget, picks the
items ranked 2nd, 6th, 1st, and 10th in the expected-loss order and closes **67.60 units**
(38.4%). The gap of 9.40 units is **the cost of the scoring form.**

Threshold scoring sits in the middle: it closes 71.40 units (40.6%). But it is the only one of the
three scoring schemes that pulls the single largest impact — the 600-fictional-unit
`unsigned-update` item — inside the budget. The expected-loss ranking puts it fifth, and the
four-item budget leaves it outside; the banded ranking puts it tenth.

This row shows that expected loss has a blind spot of its own. Expected loss measures an
**average**; an average is the right number for events that repeat over a long stretch of time.
For an event that loses 600 units in a single occurrence, the average does not say whether the
organization can carry that loss. Threshold scoring corrects for this with a blunt rule, and gives
up 5.60 units of expected-loss coverage in return.

#### There Is No Tail in This List

The last row tests one of ranking's assumptions. Priority lists are usually built on the
expectation that a handful of items carry most of the total, and fixing that handful finishes most
of the job. The cumulative-coverage sequence in this list records that the expectation is not met.
The first item carries **11.4%** of the total, the first three carry **33.5%**, the first six
carry **63.5%**. Passing 90% coverage takes ten items.

When the distribution is this flat, the meaning of a priority list changes. In a list with a heavy
tail, the scoring form is not up for debate, because whichever form is used, the same two or three
items stay on top. In a flat list, the scoring form itself is the decision — which is exactly what
this lesson's second table records when it shows nine items changing place, and the third table
when it shows the gap between them at 9.40 units.

A second conclusion follows from the same sequence: the items at the bottom of the list are not
negligible. The last three items carry **12.8%** of the total; even the lowest,
`unvalidated-field`, writes 1.80 units — and this is exactly the item that carried zero triples in
the previous lesson. **Zero access paths does not mean zero risk**, because probability does not
come only from today's access situation. In a flat distribution, wherever the cutoff is placed,
the items left outside carry a share of the total that is not small, and that share counts as
**accepted** — and unless it is counted and written down, no one even notices it was accepted.

#### Same Product, Separate Decision

The fourth table sets two items side by side: `outdated-library-version` and `unsigned-update`.
Their expected losses are **exactly equal** — 18.00 fictional units. Their banded scores are 8
against 5. Over a ten-year window, the total expected loss from each is equal too.

The only thing that is not equal is the distribution. The first occurs an average of **4.0 times**
over ten years and writes 45 units each time; the second occurs **0.3 times** and, when it does,
writes 600 units. The first item is an operating expense: predictable, budgetable, carried a bit
at a time. The second is a disruption: in most ten-year windows it does not happen at all, and in
the window where it does, that single item writes more than three times the entire annual expected
loss.

Two items reduced to the same number therefore call for two separate decisions. For the first, the
right question is "how many times will it happen, and what will the total come to"; for the
second, "can we stay standing when it happens". The two questions look at two separate forms of
defense: for the frequent, small item, what should be measured is the per-unit reduction — cutting
each event's cost from 45 to 30 is a real gain across 4.0 events in ten years. For the rare, large
item, the same reduction means almost nothing; what should be measured there is whether the event
happens **at all**, or whether the 600 units can be spread out when it does. **Expected loss is a
ranking tool, not a threshold tool.** A risk number is therefore never sufficient on its own;
unless the composition that produced it and the distribution it conceals are written down
alongside it, the same 18.00 hides two different decisions.

#### Summary

- Risk is the composition of probability and impact; its plainest form is expected loss, and
  across the twelve-item list it totals 176.05 fictional units.
- Three rankings come from the same input: banded scoring changes the rank of nine items, the
  largest shift is six positions, and only two of the top five are shared; for threshold scoring
  those numbers are 7, 4, and 4.
- Banding pulls the two ends together: an item worth 11.00 units and one worth 14.40 units get the
  same score of 10; what sets the order is not the gap but where the boundary is placed.
- A four-item budget closes 77.00 (43.7%), 67.60 (38.4%), and 71.40 (40.6%) units across the three
  scoring schemes; the gap of 9.40 units is the cost of the scoring form.
- Only threshold scoring pulls the single largest impact into the budget, giving up 5.60 units of
  expected-loss coverage in return.
- Two items carrying the same 18.00 expected loss produce 4.0 and 0.3 events over ten years; one
  is an operating expense, the other is a question of whether the organization can carry the loss.

#### Next Step

This lesson counted every item as a single event: probability given, impact given, multiplied.
But the 600-unit impact of the `unsigned-update` item is not born in a single moment. First
something is reached, then a weakness is triggered, then something is executed, then privilege
expands, and impact is only written once all of these have completed. The number we call
probability is actually the probability of this **chain completing from start to end**. The next
lesson breaks the chain into its steps and counts how many paths breaking a given step closes.

### Exploit and Payload

Source: https://academia.sh/en/courses/introduction-to-cybersecurity/exploit-and-payload

Breaking the chain from vulnerability to impact into steps: 5 exploits and 4 payloads produce 12 valid chains; chain length ranges from 2 to 7 and totals 53 steps; breaking a single step cuts at most 8 chains; blocking a payload cuts at most 5 chains and blocking an exploit at most 4; the same break leaves 0 completed steps at access and 41 at impact; even when all six middle steps are broken, 1 of 12 chains still stands; and that single chain carries 25.7% of the total probability mass.

The previous lesson counted every item as a single event: probability given, impact given,
multiplied. But a 600-fictional-unit impact is not born in a single moment. First something is
reached, then a weakness is triggered, then something is executed, privilege expands, and impact
is only written once all of these have completed. The number we call probability is actually the
probability of this **chain completing from start to end**.

**Exploit** is the step sequence that actually uses a vulnerability. **Payload** is the effect
written after the exploit completes. The two are separate things, and that separation sets what
this lesson measures: the same exploit can carry more than one payload, and the same payload can
arrive with more than one exploit. This lesson builds the chain as a model and ties it to a single
question — **which step to break cuts how many chains**.

#### A Chain Is a Sequence of Steps

The structure below is a **model**: there is no working exploit, no tool, no payload here — only a
data structure made of step names. The vulnerability names come from the previous two lessons'
list. **Trying these steps on a real system without written authorization and a defined scope is
both illegal and unprofessional.**

**SC16:** a chain follows the eight-step canonical order, and every chain is a subsequence of that
order. **SC17:** an exploit is the step sequence that uses a vulnerability; a payload is the
effect written after the exploit completes, and it brings its own extra steps. **SC18:** a payload
matches an exploit only if the step it requires is present in that exploit. **SC19:** if a step is
broken, every chain that includes it is cut; at the moment it is cut, the chain's steps before it
have already completed.

```js
// security/chain.mjs — the step model of the chain from vulnerability to impact (model): there
// is no working exploit here, only a data structure made of step names. No randomness.

// SC16: a chain follows the eight-step canonical order; every chain is a subsequence of that order.
export const STEP = ["access", "discovery", "triggering", "execution",
  "privilege-escalation", "lateral-movement", "persistence", "impact"];
export const LETTER = { access: "A", discovery: "D", triggering: "T", execution: "E",
  "privilege-escalation": "P", "lateral-movement": "M", persistence: "S", impact: "I" };

// SC17: an exploit is the step sequence that uses a vulnerability; a payload is the effect
// written after the exploit completes, and it brings its own extra steps.
export const EXPLOIT = [
  { name: "credential-reuse", vuln: "default-password", step: ["access", "discovery"] },
  { name: "input-triggering", vuln: "unvalidated-input",
    step: ["access", "triggering", "execution"] },
  { name: "unsigned-package", vuln: "unsigned-update",
    step: ["access", "discovery", "triggering", "execution"] },
  { name: "account-sharing", vuln: "shared-account", step: ["access", "discovery"] },
  { name: "transfer-interception", vuln: "unencrypted-transfer", step: ["access"] },
];

// SC18: a payload matches an exploit only if the step it requires is present in that exploit.
export const PAYLOAD = [
  { name: "data-exfiltration", need: [], extra: ["impact"] },
  { name: "record-tampering", need: ["discovery"], extra: ["privilege-escalation", "impact"] },
  { name: "service-disruption", need: ["execution"], extra: ["impact"] },
  { name: "persistent-access", need: ["execution"], extra: ["lateral-movement", "persistence", "impact"] },
];

export const CHAIN = EXPLOIT.flatMap((e) => PAYLOAD
  .filter((p) => p.need.every((n) => e.step.includes(n)))
  .map((p) => ({ exploit: e.name, vuln: e.vuln, payload: p.name,
    step: STEP.filter((s) => e.step.includes(s) || p.extra.includes(s)) })));

export const mask = (c) => STEP.map((s) => (c.step.includes(s) ? LETTER[s] : ".")).join("");
export const passing = (s) => CHAIN.filter((c) => c.step.includes(s));
// SC19: if a step is broken, every chain that includes it is cut; at the moment it is cut,
// that chain's steps before it have already completed.
export const priorTotal = (s) => passing(s).reduce((n, c) => n + c.step.indexOf(s), 0);
export const closed = (set) =>
  CHAIN.filter((c) => c.step.some((s) => set.includes(s))).length;

// SC20: every step has a completion probability and steps are treated as independent (model);
// a chain's completion probability is the product of its step probabilities.
export const STEP_P = { access: 0.5, discovery: 0.7, triggering: 0.4, execution: 0.6,
  "privilege-escalation": 0.35, "lateral-movement": 0.5, persistence: 0.6, impact: 0.9 };
export const probability = (c) => c.step.reduce((p, s) => p * STEP_P[s], 1);
export const MASS = CHAIN.reduce((s, c) => s + probability(c), 0);
export const closedMass = (set) => CHAIN
  .filter((c) => c.step.some((s) => set.includes(s)))
  .reduce((s, c) => s + probability(c), 0);
```

```js
// security/break.mjs — which step to break closes how many chains, how much had already happened.
import { STEP, LETTER, EXPLOIT, PAYLOAD, CHAIN, MASS, mask, passing, priorTotal, closed,
  probability, closedMass }
  from "./chain.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 length = CHAIN.map((c) => c.step.length);

console.log(`${EXPLOIT.length} exploits x ${PAYLOAD.length} payloads -> ${CHAIN.length} valid chains; ` +
  `step order ${STEP.map((s) => LETTER[s]).join("")} = ${STEP.join(", ")}; ` +
  `chain length ${Math.min(...length)}..${Math.max(...length)}, ` +
  `total steps ${length.reduce((a, x) => a + x, 0)}`);

const A = [-26, 20, 10, 7];
console.log("\n1. valid chains");
write(A, "exploit", "payload", "mask", "steps");
for (const c of CHAIN) write(A, c.exploit, c.payload, mask(c), c.step.length);

const B = [-24, 11, 10, 8, 12];
console.log("\n2. if a step is broken");
write(B, "step", "in chains", "closed", "left", "steps done");
for (const s of STEP)
  write(B, s, passing(s).length, closed([s]), CHAIN.length - closed([s]), priorTotal(s));

const C = [-26, 11, 10, 8];
console.log("\n3. if a payload is blocked (exploit stays in place)");
write(C, "payload", "in chains", "closed", "left");
for (const p of PAYLOAD) {
  const n = CHAIN.filter((c) => c.payload === p.name).length;
  write(C, p.name, n, n, CHAIN.length - n);
}
for (const e of EXPLOIT) {
  const n = CHAIN.filter((c) => c.exploit === e.name).length;
  write(C, "exploit " + e.name.slice(0, 15), n, n, CHAIN.length - n);
}

const MID = STEP.slice(1, -1); // access and impact are the legitimate ends, cannot be removed outright
const D = [-30, 10, 8];
console.log("\n4. best single and pair of middle steps (access and impact excluded)");
write(D, "broken step set", "closed", "left");
const single = MID.map((s) => [s]).sort((x, y) => closed(y) - closed(x))[0];
const pair = MID.flatMap((s, i) => MID.slice(i + 1).map((t) => [s, t]))
  .sort((x, y) => closed(y) - closed(x))[0];
for (const set of [single, pair, MID])
  write(D, set.map((s) => LETTER[s]).join("+"), closed(set), CHAIN.length - closed(set));
const open = CHAIN.filter((c) => !c.step.some((s) => MID.includes(s)));
console.log(`chains still standing once every middle step is broken: ` +
  open.map((c) => `${c.exploit} + ${c.payload} (${mask(c)})`).join(", "));

const E = [-24, 11, 11, 12, 15];
console.log("\n5. chains closed vs probability mass closed");
write(E, "step", "closed", "chains %", "mass %", "mass/chain");
for (const s of STEP) {
  const c = (closed([s]) / CHAIN.length) * 100, m = (closedMass([s]) / MASS) * 100;
  write(E, s, closed([s]), c.toFixed(1), m.toFixed(1), (m / c).toFixed(2));
}
const mostLikely = [...CHAIN].sort((x, y) => probability(y) - probability(x));
console.log(`total mass ${MASS.toFixed(4)}; most likely chain ${mostLikely[0].exploit} + ` +
  `${mostLikely[0].payload} (${probability(mostLikely[0]).toFixed(4)}, ` +
  `${((probability(mostLikely[0]) / MASS) * 100).toFixed(1)}% of mass), least likely ` +
  `${mostLikely.at(-1).exploit} + ${mostLikely.at(-1).payload} (${probability(mostLikely.at(-1)).toFixed(4)})`);
```

```
5 exploits x 4 payloads -> 12 valid chains; step order ADTEPMSI = access, discovery, triggering, execution, privilege-escalation, lateral-movement, persistence, impact; chain length 2..7, total steps 53

1. valid chains
exploit                                payload      mask  steps
credential-reuse             data-exfiltration  AD.....I      3
credential-reuse              record-tampering  AD..P..I      4
input-triggering             data-exfiltration  A.TE...I      4
input-triggering            service-disruption  A.TE...I      4
input-triggering             persistent-access  A.TE.MSI      6
unsigned-package             data-exfiltration  ADTE...I      5
unsigned-package              record-tampering  ADTEP..I      6
unsigned-package            service-disruption  ADTE...I      5
unsigned-package             persistent-access  ADTE.MSI      7
account-sharing              data-exfiltration  AD.....I      3
account-sharing               record-tampering  AD..P..I      4
transfer-interception        data-exfiltration  A......I      2

2. if a step is broken
step                      in chains    closed    left  steps done
access                           12        12       0           0
discovery                         8         8       4           8
triggering                        7         7       5          11
execution                         7         7       5          18
privilege-escalation              3         3       9           8
lateral-movement                  2         2      10           7
persistence                       2         2      10           9
impact                           12        12       0          41

3. if a payload is blocked (exploit stays in place)
payload                     in chains    closed    left
data-exfiltration                   5         5       7
record-tampering                    3         3       9
service-disruption                  2         2      10
persistent-access                   2         2      10
exploit credential-reus             2         2      10
exploit input-triggerin             3         3       9
exploit unsigned-packag             4         4       8
exploit account-sharing             2         2      10
exploit transfer-interc             1         1      11

4. best single and pair of middle steps (access and impact excluded)
broken step set                   closed    left
D                                      8       4
D+T                                   11       1
D+T+E+P+M+S                           11       1
chains still standing once every middle step is broken: transfer-interception + data-exfiltration (A......I)

5. chains closed vs probability mass closed
step                         closed   chains %      mass %     mass/chain
access                           12      100.0       100.0           1.00
discovery                         8       66.7        60.1           0.90
triggering                        7       58.3        25.7           0.44
execution                         7       58.3        25.7           0.44
privilege-escalation              3       25.0        14.1           0.56
lateral-movement                  2       16.7         3.1           0.19
persistence                       2       16.7         3.1           0.19
impact                           12      100.0       100.0           1.00
total mass 1.7492; most likely chain transfer-interception + data-exfiltration (0.4500, 25.7% of mass), least likely unsigned-package + persistent-access (0.0227)
```
Numbers belong to the **measurement** class; their inputs are SC16–SC19.

#### Five Exploits, Twelve Chains

The first table shows once more how a product narrows. Five exploits and four payloads make
twenty combinations; the valid ones are **twelve**, because not every payload matches every
exploit. `service-disruption` and `persistent-access` require the execution step;
`transfer-interception` consists only of the access step, so it carries a single payload and
produces the shortest chain, at two steps.

Chain lengths range from **2 to 7** and total **53 steps**. Length comes from the payload: the
`unsigned-package` exploit is four steps, the `persistent-access` payload adds three more on top,
and the chain climbs to seven. The same exploit ends at five steps with the `data-exfiltration`
payload. **The exploit is fixed; the payload sets the chain's length.**

This is why the separation matters. The `input-triggering` mask appears in three chains as
`A.TE...I`, `A.TE...I`, and `A.TE.MSI` — the first two are **exactly the same step sequence** and
differ only in the payload's name. Looked at from the defense side, these two chains pass through
the same places, produce the same records, and get cut by the same control; the only difference is
the effect written once they occur.

#### Which Step to Break Cuts How Many Chains

The second table is this lesson's central measurement. For each of the eight steps there are two
numbers: how many chains it cuts, and how many steps have already completed when it is cut.

The order of cutting power is access and impact (12), discovery (8), triggering and execution (7),
privilege-escalation (3), lateral-movement and persistence (2). The order decreases from the front
of the chain to the back, because chains are shared early on and branch later. **An earlier step
closes more paths.**

The last column reverses the same ranking. When `access` is broken, **no step has completed**;
when `impact` is broken, **41 steps** have already completed across the twelve chains. `execution`
cuts 7 chains and leaves 18 completed steps behind it; `discovery` cuts 8 chains and leaves only
8. Two steps with close to the same cutting power leave behind very different amounts of work
already done.

The 12 that appears in the table for both end steps is misleading. `access` is in every chain, but
breaking it outright means shutting down the service itself — legitimate requests pass through the
same step. `impact` is also in every chain, but breaking it there means limiting the damage after
the event, not closing a path. **The usable lever sits in the six middle steps.**

#### Blocking a Payload Does Not Cut the Chain

The third table sets three levers side by side. Blocking a payload closes at most **5 chains**
(`data-exfiltration`); blocking an exploit closes at most **4** (`unsigned-package`); breaking a
step closes **8**. All three columns run over the same twelve chains, and the difference comes
purely from where the lever is placed.

The payload is the weakest lever, and the reason is visible in the table: the payload sits at
**the end of the chain** and is interchangeable. The `unsigned-package` exploit carries four
payloads; blocking one of them leaves the remaining three chains in place. Cutting the same
exploit closes all four at once. Breaking a step is wider still, because a single step is shared
across multiple exploits: the `discovery` step appears in three separate exploits and cuts eight
chains at once.

The practical payoff of this ordering is where defense gets written. A control that recognizes and
stops a specific effect closes a single column; putting the same effort into a step the chain
shares closes four times as much. **A defense placed at the end of the chain has to be repeated
once for every branch the chain has.**

#### The Chain the Middle Steps Cannot Reach

The fourth table shows the lever's limit. The best single middle step is `discovery`, closing 8
chains and leaving 4. The best pair is `discovery` and `triggering`: **11 chains close, 1
remains.** When all six middle steps are broken, the result does not change — still 11 close, 1
remains.

The chain still standing is the `transfer-interception` exploit with the `data-exfiltration`
payload, and its mask is `A......I`. This chain has no step in its middle: it goes straight from
access to impact. Every defense written into the six middle steps never touches it at all, because
it does not pass through any of them.

This row records a limit of the chain model itself. Breaking the chain into steps makes it
possible to choose where a defense goes; but a chain whose middle-step set is empty falls outside
that choice. The only way to close it is to touch the first step — access — and access is exactly
the step the table sets aside as "cannot be removed outright." The next question is unavoidable
because of this: access was written as a single step, yet how many separate forms does reaching an
asset actually take.

#### Chain Count and Probability Mass Are Not the Same Thing

The fifth table gives every chain a completion probability — the product of its step
probabilities — and counts not how many chains a broken step closes, but **how much probability
mass** it closes. The total mass of the twelve chains is 1.7492.

The two columns do not move together. `discovery` closes **66.7%** of the chains and **60.1%** of
the mass; the ratio, 0.90, is balanced. `triggering` and `execution` close **58.3%** of the chains
but only **25.7%** of the mass — a ratio of 0.44. Two controls that do almost the same job by
count differ by two and a half times in terms of realized probability. The reason is length:
chains that pass through execution are longer, and every additional step multiplies the
probability down once more. `lateral-movement` and `persistence` are the extreme case — they close
16.7% of the chains and 3.1% of the mass.

The last line ties back to the previous lesson's number. The most likely chain is the
`transfer-interception` and `data-exfiltration` pair: two steps, a probability of 0.4500, and on
its own it carries **25.7%** of the total mass. The least likely is the seven-step
`unsigned-package` with `persistent-access`: 0.0227. The third lesson wrote a probability of 0.03
for the `unsigned-update` item and 0.25 for `unencrypted-transfer`; this is where those two
numbers come from — **as the chain lengthens, probability falls and impact grows.**

This sharpens the fourth table's finding even further. The single chain still standing once every
middle step is broken is **also the chain carrying a quarter of the mass**. Counted by number, it
is a remainder of 1 in 12; weighed by probability, it is the single largest share the defense
leaves outside.

#### Summary

- Exploit is the step sequence that uses a vulnerability, and payload is the effect written after
  the exploit completes; five exploits with four payloads produce 12 valid chains, lengths 2 to 7,
  totaling 53 steps.
- Breaking a step closes more paths the closer it sits to the start of the chain: 12, 8, 7, 7, 3,
  2, 2. Two chains of the same exploit can follow the identical step sequence and differ only in
  the payload's name.
- The same break leaves a different amount of completed work depending on where it sits in the
  chain: 0 steps at access, 8 at discovery, 18 at execution, 41 at impact.
- The three levers are not equal: blocking a payload closes at most 5, blocking an exploit at most
  4, and breaking a step 8 chains; the payload is the weakest lever because it sits at the end of
  the chain.
- When all six middle steps are broken, 1 of 12 chains still stands — the one with no step in its
  middle, going straight from access to impact.
- Chain count and probability mass are separate measures: triggering closes 58.3% of the chains
  but 25.7% of the mass; the one chain still standing carries 25.7% of the mass.

#### Next Step

Every measurement in this lesson stood on a single assumption: that `access` is one step. All
twelve of the twelve chains started from that step, and that step was never opened up. But
reaching an asset is not passing through a single door — there are separate paths starting from a
branch endpoint, a field app, a shared interface, or a management endpoint, and each passes
through a different number of stops. The topic's last lesson models these paths on a graph,
**actually runs the path count**, and counts how many paths reducing the attack surface closes and
how many legitimate work items it stops.

### Attack Surface

Source: https://academia.sh/en/courses/introduction-to-cybersecurity/attack-surface

Converting surface into a countable unit: in a 13-node, 21-edge access graph, four entry points to two targets produce 35 separate paths; the entry point carrying only 1.4% of total legitimate work opens 37.1% of the paths; a single edge can close 14 paths; one edge stops 4 legitimate work items and closes no path at all; greedy narrowing by ratio zeroes the paths at a cost of 31 legitimate work items, and closing all entry points reaches the same result at a cost of 27.

Every measurement in the previous lesson stood on a single assumption: that access is one step.
All twelve of the twelve chains started from that step, and that step was never opened up. But
reaching an asset is not passing through a single door. This lesson opens that step up and
establishes the unit of measure the rest of the curriculum will stand on.

**Attack surface** is the whole of the entry points that can be used to reach an asset, plus the
crossings between them. "The surface is wide" carries no decision. The surface's countable
counterpart is the count of **attack paths**: how many separate sequences go from source to
target. From this lesson on, every defense will be measured by the same question — **how many
paths does it close, how many remain, how much legitimate work stops**.

#### Surface Is a Path Count

The graph below is a **model**: there is no real network, no real hostname, no real service
endpoint. **Running a count like this on a real network without written authorization and a
defined scope is both illegal and unprofessional.**

**SC21:** every edge is an access crossing and carries the count of fictional legitimate-work
items that pass through it per day. **SC22:** a path is a sequence from the source to a target
that visits no node twice; the path count below is actually run with the depth-first walk that
follows. **SC23:** when an edge is removed, every path that goes through it closes, and the
legitimate work that edge carried stops.

```js
// security/graph.mjs — fictional measurement network's access graph (model): there is no real
// network, real hostname, or real service endpoint. The graph is directed and acyclic. No randomness.

// SC21: an edge is written "source target legitimate-work"; legitimate work is the count of
// fictional work items passing through that edge per day.
export const EDGE = `
  outer-network         branch-endpoint       12
  outer-network         field-app              9
  outer-network         shared-interface       4
  outer-network         management-endpoint    2
  branch-endpoint       network-gateway       12
  field-app             network-gateway        9
  shared-interface      billing                4
  management-endpoint   network-gateway        2
  management-endpoint   validator              1
  network-gateway       collector             15
  network-gateway       billing                6
  network-gateway       work-order             5
  network-gateway       report-endpoint        4
  collector             validator             15
  collector             measurement-store     14
  validator             measurement-store     13
  validator             billing                7
  billing               measurement-store      5
  billing               identity-store         3
  work-order            validator              2
  work-order            identity-store         4`
  .trim().split("\n").map((r) => {
    const [a, b, work] = r.trim().split(/\s+/);
    return { from: a, to: b, work: +work, id: `${a}->${b}` };
  });

export const SOURCE = "outer-network";
export const TARGET = ["measurement-store", "identity-store"];
export const NODE = [...new Set(EDGE.flatMap((e) => [e.from, e.to]))];

// SC22: a path is a sequence from the source to a target that visits no node twice; the path
// count below is actually run with the depth-first walk that follows.
export function paths(edges = EDGE) {
  const adj = {};
  for (const e of edges) (adj[e.from] ||= []).push(e.to);
  const found = [], trail = [SOURCE], seen = new Set([SOURCE]);
  (function walk(d) {
    if (TARGET.includes(d)) return found.push([...trail]);
    for (const n of adj[d] || []) {
      if (seen.has(n)) continue;
      seen.add(n); trail.push(n);
      walk(n);
      trail.pop(); seen.delete(n);
    }
  })(SOURCE);
  return found;
}

export const PATH = paths();
export const remainingPaths = (removed) =>
  paths(EDGE.filter((e) => !removed.includes(e.id))).length;
export const stoppedWork = (removed) =>
  EDGE.filter((e) => removed.includes(e.id)).reduce((s, e) => s + e.work, 0);
export const TOTAL_WORK = EDGE.reduce((s, e) => s + e.work, 0);
```

```js
// security/surface.mjs — the surface is a path count: how many paths, what closes them, how much legitimate work stands.
import { EDGE, NODE, TARGET, PATH, SOURCE, TOTAL_WORK, remainingPaths, stoppedWork } 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 len = PATH.map((p) => p.length - 1);
const entry = [...new Set(EDGE.filter((e) => e.from === SOURCE).map((e) => e.to))];

console.log(`${NODE.length} nodes, ${EDGE.length} edges, ${entry.length} entry points, ` +
  `${TARGET.length} targets; path count ${PATH.length}, path length ` +
  `${Math.min(...len)}..${Math.max(...len)}, total legitimate work ${TOTAL_WORK}`);

const A = [-21, 9, 10, 11];
console.log("\n1. where paths enter, where they exit");
write(A, "point", "paths", "shortest", "longest");
for (const e of entry) {
  const p = PATH.filter((x) => x[1] === e);
  write(A, e, p.length, Math.min(...p.map((x) => x.length - 1)),
    Math.max(...p.map((x) => x.length - 1)));
}
for (const t of TARGET) {
  const p = PATH.filter((x) => x.at(-1) === t);
  write(A, t, p.length, Math.min(...p.map((x) => x.length - 1)),
    Math.max(...p.map((x) => x.length - 1)));
}

const B = [-38, 11, 9, 8, 11];
console.log("\n2. if an edge is removed (path/work ratio, high to low)");
write(B, "edge", "leg. work", "closed", "left", "path/work");
const measured = EDGE.map((e) => ({ ...e, closes: PATH.length - remainingPaths([e.id]) }));
for (const e of [...measured].sort((x, y) => y.closes / y.work - x.closes / x.work))
  write(B, e.id, e.work, e.closes, PATH.length - e.closes, (e.closes / e.work).toFixed(2));

const C = [-36, 11, 12, 14, 13];
console.log("\n3. greedy narrowing: each round removes the highest path/work edge");
write(C, "removed edge", "leg. work", "paths left", "work stopped", "work left %");
const removed = [];
while (remainingPaths(removed) > 0) {
  const pick = EDGE.filter((e) => !removed.includes(e.id))
    .map((e) => ({ e, closes: remainingPaths(removed) - remainingPaths([...removed, e.id]) }))
    .sort((x, y) => y.closes / y.e.work - x.closes / x.e.work)[0];
  removed.push(pick.e.id);
  write(C, pick.e.id, pick.e.work, remainingPaths(removed), stoppedWork(removed),
    (((TOTAL_WORK - stoppedWork(removed)) / TOTAL_WORK) * 100).toFixed(1));
}

const D = [-30, 12, 14, 13];
console.log("\n4. three ways to shrink the surface");
write(D, "approach", "paths left", "work stopped", "work left %");
const entryEdge = EDGE.filter((e) => e.from === SOURCE).map((e) => e.id);
const free = measured.filter((e) => e.closes === 0).map((e) => e.id);
for (const [name, set] of [["nothing", []], ["edges that close zero paths", free],
  ["all entry points", entryEdge], ["greedy pick", removed]])
  write(D, name, remainingPaths(set), stoppedWork(set),
    (((TOTAL_WORK - stoppedWork(set)) / TOTAL_WORK) * 100).toFixed(1));
console.log(`edge that closes zero paths: ${free.join(", ") || "none"}`);

const E = [-21, 16, 14, 14, 14];
console.log("\n5. what counts as the surface measure (if an entry point is removed)");
write(E, "entry point", "share of pts %", "paths closed", "path share %", "work share %");
for (const e of entry) {
  const edge = EDGE.find((x) => x.from === SOURCE && x.to === e);
  const closes = PATH.length - remainingPaths([edge.id]);
  write(E, e, (100 / entry.length).toFixed(1), closes, ((closes / PATH.length) * 100).toFixed(1),
    ((edge.work / TOTAL_WORK) * 100).toFixed(1));
}
```

```
13 nodes, 21 edges, 4 entry points, 2 targets; path count 35, path length 3..6, total legitimate work 148

1. where paths enter, where they exit
point                    paths  shortest    longest
branch-endpoint             10         4          6
field-app                   10         4          6
shared-interface             2         3          3
management-endpoint         13         3          6
measurement-store           21         3          6
identity-store              14         3          6

2. if an edge is removed (path/work ratio, high to low)
edge                                    leg. work   closed    left  path/work
outer-network->management-endpoint              2       13      22       6.50
management-endpoint->network-gateway            2       10      25       5.00
work-order->validator                           2        9      26       4.50
billing->identity-store                         3       11      24       3.67
management-endpoint->validator                  1        3      32       3.00
network-gateway->work-order                     5       12      23       2.40
billing->measurement-store                      5       11      24       2.20
validator->billing                              7       14      21       2.00
outer-network->field-app                        9       10      25       1.11
field-app->network-gateway                      9       10      25       1.11
network-gateway->billing                        6        6      29       1.00
outer-network->branch-endpoint                 12       10      25       0.83
branch-endpoint->network-gateway               12       10      25       0.83
network-gateway->collector                     15       12      23       0.80
work-order->identity-store                      4        3      32       0.75
collector->validator                           15        9      26       0.60
validator->measurement-store                   13        7      28       0.54
outer-network->shared-interface                 4        2      33       0.50
shared-interface->billing                       4        2      33       0.50
collector->measurement-store                   14        3      32       0.21
network-gateway->report-endpoint                4        0      35       0.00

3. greedy narrowing: each round removes the highest path/work edge
removed edge                          leg. work  paths left  work stopped  work left %
outer-network->management-endpoint            2          22             2         98.6
work-order->validator                         2          16             4         97.3
billing->identity-store                       3          11             7         95.3
billing->measurement-store                    5           6            12         91.9
work-order->identity-store                    4           4            16         89.2
network-gateway->collector                   15           0            31         79.1

4. three ways to shrink the surface
approach                        paths left  work stopped  work left %
nothing                                 35             0        100.0
edges that close zero paths             35             4         97.3
all entry points                         0            27         81.8
greedy pick                              0            31         79.1
edge that closes zero paths: network-gateway->report-endpoint

5. what counts as the surface measure (if an entry point is removed)
entry point            share of pts %  paths closed  path share %  work share %
branch-endpoint                  25.0            10          28.6           8.1
field-app                        25.0            10          28.6           6.1
shared-interface                 25.0             2           5.7           2.7
management-endpoint              25.0            13          37.1           1.4
```
Numbers belong to the **measurement** class; their inputs are SC21–SC23.

#### Thirty-Five Paths

Thirteen nodes and twenty-one edges produce **35 paths**. This is the number the surface will use
in this lesson and through the rest of the curriculum. Neither node count nor edge count can stand
in for it: if edges grow by 5%, the path count can double, because path count depends on **how
the edges combine**.

The first table separates where paths enter and where they exit. **21 paths** go to
`measurement-store`, **14** to `identity-store`. On the entry side, though, there is an unexpected
distribution: `branch-endpoint` and `field-app` each carry ten paths, `shared-interface` only two,
`management-endpoint` **thirteen** — the highest of the four. The management endpoint is the
network's smallest gate, and it opens the most paths, because it connects to two of the graph's
middle nodes at once.

The shortest path is three steps and leaves from two endpoints — `shared-interface` and
`management-endpoint`; the longest is six steps.
The length distribution changes where a defense should go: a three-step path has two points to
place an intermediate control, a six-step path has five.

#### An Edge's Two Numbers

The second table is this lesson's central measurement, and it names every edge with two numbers:
how many paths it closes, how many legitimate work items it stops. The ordering is by the ratio
between them.

The top row is the `outer-network->management-endpoint` edge: it **closes 13 paths and stops only
2 legitimate work items** — a ratio of 6.50. This is not the edge that closes the most paths; that
is `validator->billing`, which closes 14 paths but stops 7 legitimate work items (ratio 2.00).
Judged by "closes the most" alone, the second edge wins; once the cost is counted too, the first
is **three times as efficient**.

The bottom of the table shows the opposite direction. `collector->measurement-store` closes only 3
paths and stops 14 legitimate work items — a ratio of 0.21. This edge is the network's actual job:
readings get written to the store through it. Removing it narrows the surface almost not at all,
and stops a tenth of the work.

The last row is the item every surface-reduction list should look for.
`network-gateway->report-endpoint` **closes no path at all** — 35 paths stay 35 — and stops 4
legitimate work items. The reporting endpoint connects to no target, so it does not sit on any
path. In a simplification made without counting the surface, this edge could get removed as an
"open end"; once measured, what shows up is a decision that has a cost and a return of zero.

#### What Counts as the Surface Measure

The fifth table shows why no other measure can substitute for path count. Each of the four entry
points is 25% of the entry-point count; the share of paths they close is, respectively, **28.6%,
28.6%, 5.7%, and 37.1%**. Four gates of the same size carry shares of the surface that differ by
six and a half times.

A surface assessment made by counting entry points writes these four items as equal and produces
the sentence "we have four open ends." An assessment that counts edges falls into the same error:
paths coming through `shared-interface` use two edges, so do paths starting at
`management-endpoint`, but one opens two paths and the other thirteen. The one countable thing is
**combination**, and the count of combination is the path.

The last column reads the same table in the opposite direction. `management-endpoint` carries only
**1.4%** of total legitimate work and opens **37.1%** of the paths; `branch-endpoint` carries 8.1%
of the work and 28.6% of the paths. Reading the two ratios together gives the order for surface
reduction directly: the entry point carrying the least work while opening the most paths comes
first. The three columns gathered through this lesson — paths closed, paths remaining, cost in
legitimate work — turn, for the first time in this table, into a **ranking rule**.

#### The Cost of Reducing to Zero

The third table tries to reduce the paths to zero: each round removes the highest-ratio edge and
the paths are counted again. After six edges, the paths remaining are zero and the legitimate work
stopped is 31; **79.1%** of the network's work stays standing.

The order is instructive on its own. The first removal takes 35 paths down to 22, the second to
16, the third to 11. The first three edges close **68.6%** of the paths at a total cost of 7
legitimate work items. The cost paid for the remaining eleven paths is 24 legitimate work items.
**The return on surface reduction is concentrated in the first steps; closing the last paths gets
exponentially more expensive.** The last row is the clearest form of this: `network-gateway->collector`
alone stops 15 legitimate work items and closes the four paths that remain.

The fourth table compares the greedy choice against two alternatives. Removing the edges that
close zero paths spends 4 legitimate work items and **closes none of the 35 paths**. Cutting all
entry points off from the outer network zeroes the paths and stops 27 legitimate work items. The
greedy choice by ratio also zeroes the paths, but stops **31 legitimate work items**.

This last comparison is this lesson's most striking result: choosing the edge that looks most
efficient at every step gives a **more expensive total** in the end. The greedy choice spends the
cheap edges in the first rounds and ends up having to remove an expensive one. The cheapest
complete cut is not an edge ranking but a **cut selection** — which set of edges together
separates the source from the targets. When a defense list is ordered item by item on efficiency,
unless what the chosen items do **together** is counted, the total cost comes out four units
higher.

#### Summary

- Attack surface's countable counterpart is path count: in a 13-node, 21-edge graph, 35 paths run
  from four entry points to two targets, with lengths between 3 and 6 steps.
- Each of the four entry points is 25% of the entry-point count, but they open 28.6%, 28.6%,
  5.7%, and 37.1% of the paths; the entry point carrying the least legitimate work (1.4%) carries
  the most paths. Counting entry points or edges does not measure the surface.
- Every edge is named with two numbers: the edge that closes the most paths is 14 paths / 7 work
  (ratio 2.00), the most efficient is 13 paths / 2 work (ratio 6.50), and among the path-closing
  edges the least efficient is 3 paths / 14 work (ratio 0.21).
- One edge stops 4 legitimate work items and closes no path; because it connects to no target, it
  does not sit on any path.
- The first three removals close 68.6% of the paths at a cost of 7 legitimate work items; the cost
  paid for the remaining eleven paths is 24 legitimate work items.
- Zeroing the paths costs 31 legitimate work items by greedy selection and 27 by cutting all entry
  points; the most efficient choice item by item is not the cheapest choice overall.

#### Next Step

This topic established security's units of measure. The three axes were overlapped on the same
asset, asset, threat, and vulnerability were split into separate lists, risk was ranked as the
composition of probability and impact, the chain from vulnerability to impact was broken into
steps, and this lesson reduced surface to a single number: **35 paths**. Every question now has a
unit.

Yet through all of these measurements, one thing was never actually done. Paths were counted,
removing edges was tried, and its cost was written down — but removal is not a defense; it is
stopping work. A real defense leaves the edge in place and puts a control on top of it: adds a
layer, narrows privilege, splits into compartments, removes location-based trust, separates
authentication from authorization, turns defaults off. Each of these principles can be run over
the same 35 paths, and the same three numbers can be written for each: how many paths closed, how
many remain, how much legitimate work stopped. The next topic — **Defense Principles** — measures
six principles one at a time with these same three columns.

## Defense Principles

Six defense principles are measured on the same path set: the path defense in depth closes layer by layer, the path least privilege closes through narrowing and the legitimate work it stops, the item a compartment reaches when it falls under compartmentalization, the path count and extra step per request that change when location-based trust is removed, the path that opens when authentication and authorization are collapsed into the wrong layer, and the path secure defaults opens when one item is forgotten.

### Defense in Depth

Source: https://academia.sh/en/courses/introduction-to-cybersecurity/defense-in-depth

Five defense layers are added in sequence over the same access graph: the new path each layer closes, the path left over, the legitimate flow needing an exception, and the managed item count are all counted. A layer that closes no new path is found, and how order changes the distribution of closures is shown.

The previous topic turned the attack surface into a countable unit: how many separate paths
there are to reach an asset got written down. What was counted was the path left open, and no
defense was run against that number yet. This topic takes the same unit of measure and runs
each of six defense principles against it in turn. Every lesson gives three columns: how many
paths there are, how many the principle closes, and how much legitimate work stands in exchange.

The network model is extended at one point. The surface count counted paths to two targets; for
the defense principles to be measurable, the network's four protected assets and the vendor
maintenance connection also enter the model, because which asset a principle protects is only
visible once assets are counted separately. The extended model is the same fictional network and
is rebuilt below; all six lessons that follow run on this single path set.

The first principle is **defense in depth**: instead of relying on a single control, independent
layers are stacked on top of each other, so the next one still stands when one is bypassed. This
carries no decision — it answers neither how many layers, what order, nor what cost. This lesson
adds layers to the same graph in sequence and counts the **new path each one closes**. The count
here measures a defense's scope; running it on a real network without **written authorization
and a defined scope** cannot be done — doing so is both illegal and unprofessional.

- **DP1.** The network is fictional: the regional measurement network's four entry points, six
  intermediate nodes, and four assets are modeled with `node`. What is measured is this model;
  this extended graph replaces the narrowed graph from the surface count and does not change
  through the topic.
- **DP2.** A path is a step sequence from an entry point to an asset with no repeated node.
- **DP3.** Which step each layer stops was not measured, it was chosen. In a real network this
  mapping is worked out by testing; the lesson's point is not the mapping itself but the count
  run on top of it.
- **DP4.** A layer either stops a step or it does not. Partial blocking and the possibility of
  evasion are not modeled; these would lower the count of closed paths, not change its direction.
- **DP5.** Legitimate flow passes through the same graph's edges. If a layer stops a step of a
  legitimate flow, that flow requires a hand-written exception; the exception count and the daily
  requests it affects are the measure of the cost.
- **DP6.** The managed item count is a given model value for each layer: the total of rule,
  mapping, and exclusion lines that layer's maintenance requires.

#### Protected Network and Path Set

The model separates three node types: externally reached entry points, services in between, and
protected assets. Edges are the steps that can be crossed from one node to another.

```js
// network.mjs — fictional regional measurement network's access graph and legitimate flows (model)
export const ENTRY = ['field-endpoint', 'branch-network', 'public-endpoint', 'maintenance-endpoint'];
export const ASSET = ['measurement-store', 'invoice-db', 'subscriber-registry', 'configuration'];
export const EDGE = [
  ['field-endpoint', 'collector'], ['field-endpoint', 'work-order'], ['branch-network', 'collector'],
  ['branch-network', 'work-order'], ['branch-network', 'management'], ['public-endpoint', 'collector'],
  ['maintenance-endpoint', 'management'], ['maintenance-endpoint', 'queue'], ['collector', 'queue'],
  ['collector', 'measurement-store'], ['queue', 'validator'], ['queue', 'billing'],
  ['validator', 'measurement-store'], ['validator', 'invoice-db'], ['billing', 'invoice-db'],
  ['billing', 'subscriber-registry'], ['work-order', 'queue'], ['work-order', 'subscriber-registry'],
  ['management', 'collector'], ['management', 'validator'], ['management', 'work-order'],
  ['management', 'configuration'],
];
// Legitimate flow: work that runs daily; the step sequence passes through the same graph's edges.
export const FLOW = [
  ['field-reading', ['field-endpoint', 'collector', 'queue'], 1440],
  ['validation', ['queue', 'validator', 'measurement-store'], 1440],
  ['public-reading', ['public-endpoint', 'collector', 'measurement-store'], 300],
  ['branch-query', ['branch-network', 'collector', 'measurement-store'], 210],
  ['invoice-write', ['queue', 'billing', 'invoice-db'], 48],
  ['work-order-opening', ['branch-network', 'work-order', 'subscriber-registry'], 32],
  ['work-order-queuing', ['branch-network', 'work-order', 'queue'], 32],
  ['subscriber-update', ['billing', 'subscriber-registry'], 6],
  ['configuration-change', ['branch-network', 'management', 'configuration'], 2],
  ['maintenance-session', ['maintenance-endpoint', 'management', 'collector'], 1],
  ['queue-maintenance', ['maintenance-endpoint', 'queue'], 1],
  ['reconciliation', ['management', 'validator', 'invoice-db'], 1],
];
export const edgeName = (a, b) => `${a}>${b}`;
export const steps = (d) => d.slice(0, -1).map((x, i) => edgeName(x, d[i + 1]));
export function paths(closed = new Set()) {
  const neighbor = new Map();
  for (const [a, b] of EDGE) if (!closed.has(edgeName(a, b))) neighbor.set(a, [...(neighbor.get(a) || []), b]);
  const out = [];
  const walk = (d, y) => {
    if (ASSET.includes(d)) return void out.push(y);
    for (const k of neighbor.get(d) || []) if (!y.includes(k)) walk(k, [...y, k]);
  };
  for (const g of ENTRY) walk(g, [g]);
  return out;
}
export const blocked = (closed) => FLOW.filter(([, d]) => steps(d).some((k) => closed.has(k)));
```

```js
// run1.mjs — the path set is built: how many separate paths from an entry point to an asset
import { ENTRY, ASSET, EDGE, FLOW, paths } from './network.mjs';

const y = paths();
const intermediate = [...new Set(EDGE.flat())].filter((d) => !ENTRY.includes(d) && !ASSET.includes(d));
console.log(`entry ${ENTRY.length}, intermediate node ${intermediate.length}, asset ${ASSET.length}, edge ${EDGE.length}, path ${y.length}`);
const count = (f) => y.reduce((m, p) => m.set(f(p), (m.get(f(p)) || 0) + 1), new Map());
const print = (b, m) => { console.log(`\n${b}`); for (const [k, v] of m) console.log(`  ${String(k).padEnd(22)}${v}`); };
print('paths per entry point', count((p) => p[0]));
print('paths per asset', count((p) => p.at(-1)));
print('paths by step count', new Map([...count((p) => p.length - 1)].sort()));
print('paths through intermediate node', new Map(intermediate.map((d) => [d, y.filter((p) => p.includes(d)).length])
  .sort((a, b) => b[1] - a[1])));
console.log(`\nlegitimate flow ${FLOW.length}, daily legitimate request ${FLOW.reduce((s, [, , n]) => s + n, 0)}`);
```

```
entry 4, intermediate node 6, asset 4, edge 22, path 55

paths per entry point
  field-endpoint        10
  branch-network        23
  public-endpoint       5
  maintenance-endpoint  17

paths per asset
  measurement-store     17
  invoice-db            22
  subscriber-registry   14
  configuration         2

paths by step count
  2                     7
  3                     12
  4                     20
  5                     16

paths through intermediate node
  queue                 40
  management            26
  collector             25
  validator             24
  work-order            20
  billing               20

legitimate flow 12, daily legitimate request 3513
```

Fourteen nodes and twenty-two edges produce fifty-five paths. The path count is two and a half
times the edge count; every edge added to the graph is not a path on its own but a multiplier of
every path it lies on. The branch network alone is the start of twenty-three paths, because it
connects to three separate intermediate nodes. The maintenance endpoint has only two edges yet
opens seventeen paths: the management station it connects to is itself connected to four more
nodes. An entry point's danger is measured not by how many connections it has, but by how many
connections the node it connects to has.

The asset distribution shows a second thing. Twenty-two paths go to the invoice database, two to
the configuration store. Both sit on the same network, but one is reachable from eleven times as
many paths as the other. The step distribution, in turn, determines where a defense should be
placed: only seven of the paths are two steps long, thirty-six are four steps or more. Long
paths pass through most of the intermediate nodes, meaning a single control sitting in the
middle cuts all of them at once.

The last table states directly where a control belongs. The queue lies on forty of the
fifty-five paths; a control stopping every step into the queue touches seventy-three percent of
the paths alone. The management station carries twenty-six paths, the measurement collector
twenty-five. At the low end, the work-order and billing services carry the least load, twenty
paths each. These numbers rank placement, but with one warning: the bottleneck is also the
bottleneck for legitimate work. A control on the queue sits in the path of one thousand four
hundred forty daily field-reading uploads and as many daily validation requests.

#### Path Closed Layer by Layer

Five layers are added to the same graph in sequence. Each layer stops specific steps; a path
counts as closed once at least one of its steps has been stopped.

```js
// layer.mjs — five defense layers and the step each one stops (model decision)
export const LAYER = [
  ['network-filter', 22, ['field-endpoint>work-order', 'branch-network>management', 'maintenance-endpoint>management', 'maintenance-endpoint>queue']],
  ['authentication', 14, ['public-endpoint>collector', 'branch-network>management', 'maintenance-endpoint>management', 'management>configuration']],
  ['endpoint-control', 9, ['field-endpoint>work-order', 'branch-network>management', 'maintenance-endpoint>queue']],
  ['input-validation', 11, ['queue>billing', 'validator>invoice-db']],
  ['data-control', 7, ['validator>measurement-store', 'management>validator']],
];
```

```js
// run2.mjs — layers are added in sequence; the new path each one closes is counted
import { paths, blocked } from './network.mjs';
import { LAYER } from './layer.mjs';

const total = paths().length, closed = new Set();
let previous = total;
console.log('layer              item   alone       newly closed  left   exception  daily extra step');
for (const [k, item, edges] of LAYER) {
  const alone = total - paths(new Set(edges)).length;
  edges.forEach((e) => closed.add(e));
  const left = paths(closed).length, cut = blocked(closed);
  console.log(`${k.padEnd(19)}${String(item).padEnd(7)}${String(alone).padEnd(12)}${String(previous - left).padEnd(14)}` +
    `${String(left).padEnd(7)}${String(cut.length).padEnd(11)}${cut.reduce((s, [, , n]) => s + n, 0)}`);
  previous = left;
}
console.log('\npath remaining after five layers:');
for (const p of paths(closed)) console.log(`  ${p.join(' > ')}`);
```

```
layer              item   alone       newly closed  left   exception  daily extra step
network-filter     22     35          35            20     3          4
authentication     14     31          5             15     4          304
endpoint-control   9      22          0             15     4          304
input-validation   11     32          9             6      6          353
data-control       7      14          3             3      7          1793

path remaining after five layers:
  field-endpoint > collector > measurement-store
  branch-network > collector > measurement-store
  branch-network > work-order > subscriber-registry
```

The first layer closes thirty-five of the fifty-five paths and its cost is almost nothing: three
legitimate flows need an exception, and together they total four daily requests. This is why the
network filter looks cheap — the flows that run often already sit on edges that pass through the
filter.

In the following layers the ratio reverses. Authentication closes five new paths, but the daily
requests needing an exception jump from four to three hundred four, because the public-reading
endpoint falls under this layer. Data control closes only three paths; in exchange, one thousand
seven hundred ninety-three daily requests gain an extra step — the entire validation flow passes
through this layer. The cost paid per closed path is one-ninth of a request in the first layer,
five hundred ninety-seven requests in the last.

The exception column builds up a separate burden of its own. By the end of the five layers,
seven of the twelve legitimate flows depend on a hand-written exception — fifty-eight percent of
the flows have been carved out of some control. When an exception is removed or written wrong, a
path counted as closed reopens, but the table does not show this — the table assumes the
exceptions are correct.

The last three rows are this lesson's hard finding. After five layers, fifty-two of the
fifty-five paths are closed, three are still open. The three remaining paths go from the field
endpoint to the measurement store, from the branch network to the measurement store, and from
the branch network to the subscriber registry — that is, the legitimate channel itself. Adding a
layer does not close these three; closing them means stopping the work. Defense in depth does
not zero out the path count, it narrows it down to the width of the legitimate channel.

#### The Layer Assumed Free

The third row in the table reads zero: endpoint control closes no new path. The same row's left
side says it closes twenty-two paths on its own. The two numbers do not contradict each other;
they say the layer's value depends on order.

```js
// run3.mjs — how the closed path shifts when the order changes, and what happens when a layer is removed
import { paths } from './network.mjs';
import { LAYER } from './layer.mjs';

const total = paths().length, short = (a) => a.split('-')[0];
function run(list) {
  const closed = new Set(), m = new Map();
  let previous = total;
  for (const [a, , edges] of list) {
    edges.forEach((e) => closed.add(e));
    const left = paths(closed).length;
    m.set(a, previous - left); previous = left;
  }
  return [m, previous, list.reduce((s, [, n]) => s + n, 0)];
}
const ep = LAYER.find(([a]) => a.startsWith('endpoint-'));
console.log(`order                ${LAYER.map(([a]) => short(a).padEnd(16)).join('')}left  item`);
for (const [label, list] of [['as written', LAYER],
  ['endpoint first', [ep, ...LAYER.filter((k) => k !== ep)]],
  ['endpoint removed', LAYER.filter((k) => k !== ep)]]) {
  const [m, left, item] = run(list);
  console.log(`${label.padEnd(21)}${LAYER.map(([a]) => String(m.get(a) ?? '-').padEnd(16)).join('')}` +
    `${String(left).padEnd(7)}${item}`);
}
const alone = LAYER.reduce((s, [, , k]) => s + total - paths(new Set(k)).length, 0);
console.log(`\nsum of alone totals ${alone}, actually closed ${total - run(LAYER)[1]}, ` +
  `per item ${((total - run(LAYER)[1]) / run(LAYER)[2]).toFixed(2)} path`);
```

```
order                network         authentication  endpoint        input           data            left  item
as written           35              5               0               9               3               3      63
endpoint first       13              5               22              9               3               3      63
endpoint removed     35              5               -               9               3               3      54

sum of alone totals 134, actually closed 52, per item 0.83 path
```
All three rows arrive at the same result: the remaining path is three in every case. Changing
the order only changes the distribution of the closing. When endpoint control is moved to the
front, twenty-two paths are credited to it and thirteen to the network filter; left at the end,
it gets zero. How many paths a layer "closes" is not a number on its own — it depends on what
came before it.

The third row delivers the real verdict. When endpoint control is removed entirely, the
remaining path is still three, but the managed item count drops from sixty-three to fifty-four.
On this path set, the layer buys nothing in exchange for nine items of maintenance load. The
sentence "adding one more layer does no harm" is falsified here by the numbers: the cost of
adding it is not zero, it is nine items.

The decision is not this short, though. The measurement assumed the network filter stays
standing. If the filter is bypassed, endpoint control is the layer closing twenty-two paths on
its own, and the remaining path is then far more than three. The correct reading: this layer
does not buy coverage, it buys **redundancy** — and that redundancy costs nine items. The sum in
the row above is the numerical counterpart of the same fallacy: standalone measurements sum to
one hundred thirty-four, what actually closes is fifty-two. A report that counts the same path
twice shows coverage as two and a half times larger than it is.

The model's silent assumption also shows up here. Every layer was taken as independent: one
falling does not affect another. In reality most layers are configured through the same
management plane and administered by the same identities. If a subject who can change the
network filter's rule table can also change endpoint control's exclusion list, the two layers
count as one, and the table's fifty-two is an upper bound. This is the managed item count's
second meaning: sixty-three items is not only maintenance load, it is also **sixty-three places
that can be misconfigured**. Defense in depth is measured not by layer count but by how many
genuinely separate control surfaces the layers actually spread across.

#### Summary

- The fictional network carries twenty-two edges across four entry points, six intermediate
  nodes, and four assets; 55 separate paths run from entry point to asset. 22 paths go to the
  invoice database, 2 to the configuration store — reachability differs elevenfold on the same
  network.
- When five layers are added in sequence, 52 of the 55 paths close, 3 stay open. The remaining
  three paths are the legitimate channel itself; adding a layer does not close them.
- The cost per closed path grows as the layers progress: the network filter closes 35 paths with
  4 daily exception requests, data control closes 3 paths with 1,793 daily extra steps.
- Endpoint control closes 0 new paths in its actual position but closes 22 paths on its own; the
  layer's count depends not on itself but on what precedes it.
- When the layer is removed, the remaining path is still 3, and managed items drop from 63 to
  54. The layer buys redundancy, not coverage, and that redundancy costs 9 items.
- The sum of what the layers close on their own is 134, what actually closes is 52. Counting the
  overlap inflates the defense's apparent coverage by 2.5 times.

#### Next Step

This lesson placed layers on the network and counted how many paths each one closes. Every
closed path closed because a step **could not be crossed**: the filter did not let it through,
the identity was not recognized, the input was not accepted. But there is a second condition for
crossing a step in the graph — the **permission** of the subject taking that step to touch the
target item. None of the five layers looked at that permission; all of them checked the step
itself. The next lesson's question is the permission set: how many paths open when the
permission items granted to each subject are translated into a path set, how many paths close
when permissions are narrowed down to only what is actually used, and in exchange for that
narrowing, which legitimate flow stops, and at how many requests per day.

### Least Privilege

Source: https://academia.sh/en/courses/introduction-to-cybersecurity/least-privilege

Every step of the same access graph is tied to a permission item, and the path it opens is counted side by side with the daily legitimate request it carries. Removing items that carry no legitimate traffic is measured as cost-free, then the stopped-request-per-path threshold is swept to find where narrowing turns expensive.

The previous lesson added five defense layers to the same graph and closed fifty-two of the
fifty-five paths. Every closed path closed because a step could not be crossed: the filter did
not let it through, the identity was not recognized, the input was not accepted. None of the
layers asked a second question — does the subject taking the step have **permission** to touch
the target item? This lesson asks that question and reads the graph again as a permission set.

**Least privilege** is granting each subject the narrowest permission set sufficient to do its
work. The Cloud Governance, Security and Cost course measured this same principle through the
ratio of granted permission to used permission; here the unit of measure is path count. When a
permission item is removed, a step drops from the graph and every path passing through that step
closes. Because legitimate work also passes through the same step, every removal produces two
numbers: the path that closes and the daily request that stops. The principle only counts as
measured when both are written down together.

- **DP7.** A permission item is a `(subject, item, action)` triple and corresponds one to one
  with a step in the graph. Twenty-two edges means twenty-two permission items.
- **DP8.** The action mapping is a model decision: each step was assigned `read`, `write`, or
  `manage`. In reality a step can require more than one action; this would grow the item count,
  not change the direction of the measure.
- **DP9.** A permission item is either granted or not. Condition expressions, record-level
  narrowing, and time-boxed permission are not modeled.
- **DP10.** The daily legitimate request counts come from the previous lesson's flow model; they
  were not measured, they were chosen.
- **DP11.** A legitimate flow stops entirely once one of its steps closes. Continuing over a
  backup path is not modeled; this shows the stop count as higher than it might be.

#### The Step's Second Condition

The graph has not changed; a second reading sits on top of it. Every edge is now the name of a
permission item, and when that item is removed the edge drops with it.

```js
// network.mjs — fictional regional measurement network's access graph and legitimate flows (model)
export const ENTRY = ['field-endpoint', 'branch-network', 'public-endpoint', 'maintenance-endpoint'];
export const ASSET = ['measurement-store', 'invoice-db', 'subscriber-registry', 'configuration'];
export const EDGE = [
  ['field-endpoint', 'collector'], ['field-endpoint', 'work-order'], ['branch-network', 'collector'],
  ['branch-network', 'work-order'], ['branch-network', 'management'], ['public-endpoint', 'collector'],
  ['maintenance-endpoint', 'management'], ['maintenance-endpoint', 'queue'], ['collector', 'queue'],
  ['collector', 'measurement-store'], ['queue', 'validator'], ['queue', 'billing'],
  ['validator', 'measurement-store'], ['validator', 'invoice-db'], ['billing', 'invoice-db'],
  ['billing', 'subscriber-registry'], ['work-order', 'queue'], ['work-order', 'subscriber-registry'],
  ['management', 'collector'], ['management', 'validator'], ['management', 'work-order'],
  ['management', 'configuration'],
];
// Legitimate flow: work that runs daily; the step sequence passes through the same graph's edges.
export const FLOW = [
  ['field-reading', ['field-endpoint', 'collector', 'queue'], 1440],
  ['validation', ['queue', 'validator', 'measurement-store'], 1440],
  ['public-reading', ['public-endpoint', 'collector', 'measurement-store'], 300],
  ['branch-query', ['branch-network', 'collector', 'measurement-store'], 210],
  ['invoice-write', ['queue', 'billing', 'invoice-db'], 48],
  ['work-order-opening', ['branch-network', 'work-order', 'subscriber-registry'], 32],
  ['work-order-queuing', ['branch-network', 'work-order', 'queue'], 32],
  ['subscriber-update', ['billing', 'subscriber-registry'], 6],
  ['configuration-change', ['branch-network', 'management', 'configuration'], 2],
  ['maintenance-session', ['maintenance-endpoint', 'management', 'collector'], 1],
  ['queue-maintenance', ['maintenance-endpoint', 'queue'], 1],
  ['reconciliation', ['management', 'validator', 'invoice-db'], 1],
];
export const edgeName = (a, b) => `${a}>${b}`;
export const steps = (d) => d.slice(0, -1).map((x, i) => edgeName(x, d[i + 1]));
export function paths(closed = new Set()) {
  const neighbor = new Map();
  for (const [a, b] of EDGE) if (!closed.has(edgeName(a, b))) neighbor.set(a, [...(neighbor.get(a) || []), b]);
  const out = [];
  const walk = (d, y) => {
    if (ASSET.includes(d)) return void out.push(y);
    for (const k of neighbor.get(d) || []) if (!y.includes(k)) walk(k, [...y, k]);
  };
  for (const g of ENTRY) walk(g, [g]);
  return out;
}
export const blocked = (closed) => FLOW.filter(([, d]) => steps(d).some((k) => closed.has(k)));
```

```js
// permission.mjs — the permission item each step requires: (subject, item, action)
export const ACTION = {
  'field-endpoint>collector': 'write', 'field-endpoint>work-order': 'write', 'branch-network>collector': 'read',
  'branch-network>work-order': 'write', 'branch-network>management': 'manage', 'public-endpoint>collector': 'read',
  'maintenance-endpoint>management': 'manage', 'maintenance-endpoint>queue': 'manage', 'collector>queue': 'write',
  'collector>measurement-store': 'write', 'queue>validator': 'read', 'queue>billing': 'read',
  'validator>measurement-store': 'write', 'validator>invoice-db': 'write', 'billing>invoice-db': 'write',
  'billing>subscriber-registry': 'write', 'work-order>queue': 'write', 'work-order>subscriber-registry': 'write',
  'management>collector': 'manage', 'management>validator': 'manage', 'management>work-order': 'manage',
  'management>configuration': 'write',
};
export const itemName = (k) => `${k.split('>')[0]}.${k.split('>')[1]}.${ACTION[k]}`;
```

```js
// run1.mjs — permission matrix: how many paths each item opens, how many daily legitimate requests it carries
import { FLOW, EDGE, edgeName, steps, paths } from './network.mjs';
import { itemName } from './permission.mjs';

const y = paths(), item = EDGE.map(([a, b]) => edgeName(a, b));
const opens = (k) => y.filter((p) => steps(p).includes(k)).length;
const request = (k) => FLOW.filter(([, d]) => steps(d).includes(k)).reduce((s, [, , n]) => s + n, 0);
console.log(`permission item ${item.length}, path ${y.length}, daily legitimate request ${FLOW.reduce((s, [, , n]) => s + n, 0)}`);
console.log('\npermission item                         opens path  daily legitimate request');
for (const [k, a, i] of item.map((k) => [k, opens(k), request(k)]).sort((p, q) => q[1] - p[1]))
  console.log(`${itemName(k).padEnd(40)}${String(a).padEnd(12)}${i}`);
const idle = item.filter((k) => request(k) === 0);
console.log(`\nitem carrying no legitimate traffic ${idle.length}: ${idle.map(itemName).join(', ')}`);
```

```
permission item 22, path 55, daily legitimate request 3513

permission item                         opens path  daily legitimate request
collector.queue.write                   20          1440
queue.validator.read                    20          1440
queue.billing.read                      20          48
work-order.queue.write                  16          32
branch-network.management.manage        13          2
maintenance-endpoint.management.manage  13          1
validator.measurement-store.write       12          1440
validator.invoice-db.write              12          1
billing.invoice-db.write                10          48
billing.subscriber-registry.write       10          6
management.collector.manage             10          1
management.work-order.manage            10          0
field-endpoint.collector.write          5           1440
field-endpoint.work-order.write         5           0
branch-network.collector.read           5           210
branch-network.work-order.write         5           64
public-endpoint.collector.read          5           300
collector.measurement-store.write       5           510
maintenance-endpoint.queue.manage       4           1
work-order.subscriber-registry.write    4           32
management.validator.manage             4           1
management.configuration.write          2           2

item carrying no legitimate traffic 2: field-endpoint.work-order.write, management.work-order.manage
```

The table reads differently from how it looks, because it places two columns side by side. A
high left column does not mean dangerous, and a high right column does not mean indispensable;
the decision depends on the **ratio** of the two columns. Write permission on the queue opens
twenty paths but also carries one thousand four hundred forty legitimate requests a day. The
validator's write permission on the invoice database also opens twelve paths and carries only
one legitimate request a day. Two items open a similar number of paths, but removing one stops
the network, while removing the other stops one reconciliation job a day.

Comparing the same pair of rows makes the principle concrete. The branch network's manage
permission on the management station opens thirteen paths and carries two requests a day; the
maintenance endpoint's opens thirteen paths too and carries one. Nine of the twenty-two items
carry fewer than five requests a day, and together these are the items that open most of the
paths. This is exactly where least privilege pays off: items that are rarely used but open wide.

The action column explains the table even though it is not the one being read. Six items carry
the `manage` action, and together the six carry six daily legitimate requests; against that,
four of them open ten or more paths. Manage permission is wide by its nature: a subject that can
manage a node can also use all of that node's outgoing steps. In the table, the management
station's three separate manage permissions sit as separate rows, but all three sit in the same
subject's hands. Counting permission items one by one hides this pileup; counting paths does
not, because the same subject's permissions merge on the same paths.

The last row gives the cheapest part of the principle. Two items carry no legitimate request at
all: the field endpoint's write permission on the work-order service, and the management
station's manage permission on the work-order service. Both were granted, neither is used.

#### The Limit of Narrowing

The order of removal matters. Cost-free items are removed first, then items ranked by how many
requests they stop per path removed. The threshold is the lowest `opened path / carried request`
value at which removing an item is accepted.

```js
// run2.mjs — narrowing: cost-free items first, then the path/request threshold is swept
import { FLOW, EDGE, edgeName, steps, paths } from './network.mjs';

const y = paths(), total = y.length, item = EDGE.map(([a, b]) => edgeName(a, b));
const opens = (k) => y.filter((p) => steps(p).includes(k)).length;
const request = (k) => FLOW.filter(([, d]) => steps(d).includes(k)).reduce((s, [, , n]) => s + n, 0);
const idle = item.filter((k) => request(k) === 0);
console.log(`cost-free narrowing: ${idle.length} items removed, remaining path ${paths(new Set(idle)).length}, stopped legitimate request 0`);
console.log('\nthreshold  removed  left  stopped  stopped  stopped request');
console.log('           item     path  flow     daily    per closed path');
for (const t of [Infinity, 10, 4, 1, 0.4, 0.1, 0]) {
  const closed = new Set([...idle, ...item.filter((k) => request(k) > 0 && opens(k) / request(k) >= t)]);
  const left = paths(closed).length;
  const cut = FLOW.filter(([, d]) => steps(d).some((k) => closed.has(k)));
  const stopped = cut.reduce((s, [, , n]) => s + n, 0);
  console.log(`${(t === Infinity ? '-' : String(t)).padEnd(11)}${String(closed.size).padEnd(12)}${String(left).padEnd(7)}` +
    `${String(cut.length).padEnd(9)}${String(stopped).padEnd(9)}${(stopped / (total - left)).toFixed(1)}`);
}
```

```
cost-free narrowing: 2 items removed, remaining path 40, stopped legitimate request 0

threshold  removed  left  stopped  stopped  stopped request
           item     path  flow     daily    per closed path
-          2           40     0        0        0.0
10         5           21     2        2        0.1
4          8           16     4        5        0.1
1          10          12     5        11       0.3
0.4        12          7      7        91       1.9
0.1        14          6      8        123      2.5
0          22          0      12       3513     63.9
```
The first row is the principle's free part. Removing the two unused items closes fifteen of the
fifty-five paths and stops no legitimate work. Twenty-seven percent of the paths went for zero
cost; these two items were the kind nobody notices, and since nobody uses them, nobody ever
will.

The second and third rows are the principle's cheap part. At a threshold of ten, five items are
removed, the remaining path drops from forty to twenty-one, and in exchange two requests a day
stop. At a threshold of four, the remaining path is sixteen, the stopped requests five.
Thirty-nine paths closed for the cost of five daily requests — one-tenth of a request per closed
path.

The stopped-flow column shows which jobs fall in what order, and the order is not random. At a
threshold of ten, the two stopped flows are the maintenance session and the reconciliation,
carrying one request a day each; at a threshold of four, queue maintenance and the
configuration change are added. Because the items removed at a high threshold are the rarely
used ones, the first jobs to stop are the rarely run ones too: maintenance, reconciliation,
manual configuration. These are invisible in daily volume, but they are needed exactly when they
are needed, and that moment is usually an incident. The remaining eight flows — reading uploads,
validation, queries, billing — only start falling once the threshold drops below zero point
four.

#### The Price of Forty-Three Paths

The table draws a curve, and that curve's shape is least privilege's true shape. Forty-three of
the fifty-five paths can be closed for the cost of eleven daily requests; the remaining twelve
paths cost three thousand five hundred two requests. Seventy-eight percent of the paths close
for zero point three percent of the cost.

The same curve also explains why least privilege is usually left half done. Once the easy part
is finished, what remains requires accepting that a legitimate flow will stop at every step;
that is not a security decision, it is a business decision, and one the security team cannot
make alone. Where the threshold ends up is therefore decided not by measurement but by which
work people agree to stop.

This ratio also says how the principle should be applied. Starting narrowing from the question
"which permission is excessive" is the wrong order; the right order is "which permission opens
many paths while carrying little work." The two questions do not give the same answer: write
permission on the queue sits at the top of the list with twenty paths, but because it carries
one thousand four hundred forty requests a day, it is among the last items to be removed. The
management station's manage permission on the work-order service sits in the middle of the list
with ten paths and is the first item to go, because it carries no work at all.

Why the remaining twelve paths are expensive is also readable from the table. Those paths are
made of edges that legitimate flows pass over: field endpoint to collector, collector to queue,
queue to validator. An attack path and a work flow share the same edge here, and a permission
item cannot tell them apart, because a permission item only answers "who can do what to which
item." This is the limit of least privilege. Narrowing brings paths down to the width of the
legitimate channel and stops there — the same place the five layers stopped in the previous
lesson, reached by a different route.

Two things the table does not show are also part of the decision. First, narrowing is not a
one-time job: when a service is added to the network, the edge count grows, the paths the new
edges open are given wide by default on day one, and stay open until the threshold is swept
again. The measurement is a snapshot, not a steady state. Second, every item removed is a
configuration change, and that change itself can be written wrong. The first two items that look
free really are free, but once the threshold drops below one, most of the items removed belong
to rarely run jobs — when an item is removed by mistake, finding out has to wait for that job's
next run. The path count drops instantly, the stoppage shows up weeks later.

#### Summary

- The graph's twenty-two edges correspond to twenty-two permission items. Every item carries two
  numbers: the path it opens and the daily legitimate request it carries; the decision is made
  from their ratio.
- Two items opening a similar number of paths can be very different: write permission on the
  queue opens 20 paths and carries 1,440 requests a day, the validator's invoice-write permission
  opens 12 paths and carries 1 request a day.
- Two permission items carry no legitimate request at all. Removing them closes 15 of the 55
  paths for zero cost — 27% of the paths came from two items nobody had noticed.
- The threshold sweep gives the point where the curve breaks: 43 paths close for the cost of 11
  daily requests, the remaining 12 paths need 3,502. 78% of the paths close for 0.3% of the cost.
- When every permission is removed, the remaining path is 0 and the stopped daily requests are
  3,513: all twelve legitimate flows. Full narrowing is the same thing as removing the network's
  function.
- Narrowing order is set not by "which permission is excessive" but by "which permission opens
  many paths while carrying little work"; the two questions produce different rankings.

#### Next Step

This lesson removed permission items one by one and counted the cost of every removal. But the
whole measurement rested on one assumption: when a step closes, only that step closes, the nodes
themselves stay in place. In a real incident, what closes is not a step but a node — a service
gets compromised, and every permission that service holds passes into the attacker's hands. The
narrowing that closes forty-three paths does not close the paths the compromised node opens with
its own permissions. The next lesson's question is therefore how nodes are grouped: when the
network is divided into compartments, how many items are reached when one compartment falls, how
that number shrinks as the compartment count grows, and how many gates the legitimate traffic
running between compartments costs.

### Isolation

Source: https://academia.sh/en/courses/introduction-to-cybersecurity/isolation

The same network is built with seven separate compartment schemes, and the reached item is counted when a node falls. Gate count, the daily legitimate request passing through the gate, and how much the compartment count narrows the reached asset are all measured together; the location of the boundary is shown to be more decisive than the compartment count.

The previous lesson removed permission items one by one and closed forty-three paths for the
cost of eleven daily requests. The whole measurement rested on one assumption: what closes is a
step, the nodes stay in place. In a real incident, what closes is not a step but a node — a
service gets compromised, and from that moment the attacker moves from where that service stood.
Where they can go from there is decided not by the permission set but by **the shape of the
network**.

**Isolation** is dividing nodes into compartments (compartmentalization) and restricting the
crossing between compartments. The Containers course covered the same concept through runtime
boundaries; what is measured here is not the mechanism but the outcome: **how many items are
reached** when one compartment falls. This lesson's three columns are the reached item, the gate
count, and the daily legitimate request passing through the gate.

- **DP12.** Movement inside a compartment is free. A fallen node reaches every node in its own
  compartment; this models the fact that a flat network also has undeclared connections between
  nodes.
- **DP13.** Crossing between compartments happens only through a **gate**. The gate set is
  derived from legitimate flows: if a flow's step crosses a compartment boundary, that step is
  opened as a gate.
- **DP14.** A gate is open for a fallen node in the direction it was opened, too. The gate itself
  carries no control; if it did, that would be what the previous two lessons already measured.
- **DP15.** The reached item is the number of nodes the fallen node can reach; the node itself is
  not counted. The asset column counts the four protected items among these separately.
- **DP16.** The compartment schemes are chosen model decisions. In reality a compartment boundary
  is drawn together with network topology, physical placement, and operational boundaries; this
  lesson measures only the boundary's **outcome**.

#### Compartment Schemes and Gates

Four schemes group the same node set in different ways: a single compartment, an outer/inner
split, a four-layer split, and one compartment per node. None of the schemes change the graph;
the only thing that changes is which crossing cuts a boundary. A step that cuts a boundary stays
open as a gate if it belongs to a legitimate flow, and closes if it does not — meaning the
compartmentalization decision is set by the legitimate flow list. This dependency is also the
source of the result waiting at the end of the lesson.

```js
// network.mjs — fictional regional measurement network's access graph and legitimate flows (model)
export const ENTRY = ['field-endpoint', 'branch-network', 'public-endpoint', 'maintenance-endpoint'];
export const ASSET = ['measurement-store', 'invoice-db', 'subscriber-registry', 'configuration'];
export const EDGE = [
  ['field-endpoint', 'collector'], ['field-endpoint', 'work-order'], ['branch-network', 'collector'],
  ['branch-network', 'work-order'], ['branch-network', 'management'], ['public-endpoint', 'collector'],
  ['maintenance-endpoint', 'management'], ['maintenance-endpoint', 'queue'], ['collector', 'queue'],
  ['collector', 'measurement-store'], ['queue', 'validator'], ['queue', 'billing'],
  ['validator', 'measurement-store'], ['validator', 'invoice-db'], ['billing', 'invoice-db'],
  ['billing', 'subscriber-registry'], ['work-order', 'queue'], ['work-order', 'subscriber-registry'],
  ['management', 'collector'], ['management', 'validator'], ['management', 'work-order'],
  ['management', 'configuration'],
];
// Legitimate flow: work that runs daily; the step sequence passes through the same graph's edges.
export const FLOW = [
  ['field-reading', ['field-endpoint', 'collector', 'queue'], 1440],
  ['validation', ['queue', 'validator', 'measurement-store'], 1440],
  ['public-reading', ['public-endpoint', 'collector', 'measurement-store'], 300],
  ['branch-query', ['branch-network', 'collector', 'measurement-store'], 210],
  ['invoice-write', ['queue', 'billing', 'invoice-db'], 48],
  ['work-order-opening', ['branch-network', 'work-order', 'subscriber-registry'], 32],
  ['work-order-queuing', ['branch-network', 'work-order', 'queue'], 32],
  ['subscriber-update', ['billing', 'subscriber-registry'], 6],
  ['configuration-change', ['branch-network', 'management', 'configuration'], 2],
  ['maintenance-session', ['maintenance-endpoint', 'management', 'collector'], 1],
  ['queue-maintenance', ['maintenance-endpoint', 'queue'], 1],
  ['reconciliation', ['management', 'validator', 'invoice-db'], 1],
];
export const edgeName = (a, b) => `${a}>${b}`;
export const steps = (d) => d.slice(0, -1).map((x, i) => edgeName(x, d[i + 1]));
export function paths(closed = new Set()) {
  const neighbor = new Map();
  for (const [a, b] of EDGE) if (!closed.has(edgeName(a, b))) neighbor.set(a, [...(neighbor.get(a) || []), b]);
  const out = [];
  const walk = (d, y) => {
    if (ASSET.includes(d)) return void out.push(y);
    for (const k of neighbor.get(d) || []) if (!y.includes(k)) walk(k, [...y, k]);
  };
  for (const g of ENTRY) walk(g, [g]);
  return out;
}
export const blocked = (closed) => FLOW.filter(([, d]) => steps(d).some((k) => closed.has(k)));
```

```js
// compartment.mjs — compartment schemes and the reached-item count when a node falls
import { EDGE, FLOW, ENTRY, ASSET, edgeName, steps } from './network.mjs';
export const NODE = [...new Set(EDGE.flat())];
export const INTERMEDIATE = NODE.filter((d) => !ENTRY.includes(d) && !ASSET.includes(d));
const SERVICE = ['collector', 'queue', 'validator', 'billing', 'work-order'];
const build = (f) => Object.fromEntries(NODE.map((d) => [d, f(d)]));
export const SCHEME = [
  ['single-compartment', build(() => 'network')],
  ['two-compartment', build((d) => (ENTRY.includes(d) ? 'outer' : 'inner'))],
  ['four-compartment', build((d) => (ENTRY.includes(d) ? 'outer'
    : SERVICE.includes(d) ? 'service' : d === 'management' ? 'management' : 'data'))],
  ['per-node', build((d) => d)],
];
const LEGITIMATE = new Set(FLOW.flatMap(([, d]) => steps(d)));
// Free movement inside a compartment; crossing between compartments only through an open gate.
export function resolve(map) {
  const cross = EDGE.map(([a, b]) => edgeName(a, b)).filter((k) => map[k.split('>')[0]] !== map[k.split('>')[1]]);
  const gate = cross.filter((k) => LEGITIMATE.has(k)), member = {}, neighbor = new Map();
  for (const d of NODE) (member[map[d]] ||= []).push(d);
  for (const g of gate) {
    const [a, b] = g.split('>');
    neighbor.set(map[a], [...(neighbor.get(map[a]) || []), map[b]]);
  }
  const reached = (d) => {
    const seen = new Set([map[d]]), stack = [map[d]];
    while (stack.length) for (const x of neighbor.get(stack.pop()) || []) if (!seen.has(x)) { seen.add(x); stack.push(x); }
    return [...seen].flatMap((b) => member[b]).filter((x) => x !== d);
  };
  return { gate, closed: cross.filter((k) => !LEGITIMATE.has(k)), reached };
}
```

```js
// run1.mjs — scheme by scheme: gate count, reached item, and legitimate request through the gate
import { FLOW, ASSET, steps } from './network.mjs';
import { SCHEME, INTERMEDIATE, resolve } from './compartment.mjs';

const avg = (a) => (a.reduce((s, x) => s + x, 0) / a.length).toFixed(1);
console.log('scheme               compartments  gate  closed  avg item  worst  avg asset  gate request');
for (const [nm, map] of SCHEME) {
  const { gate, closed, reached } = resolve(map);
  const re = INTERMEDIATE.map(reached);
  const gr = FLOW.filter(([, d]) => steps(d).some((k) => gate.includes(k))).reduce((s, [, , n]) => s + n, 0);
  console.log(`${nm.padEnd(21)}${String(new Set(Object.values(map)).size).padEnd(14)}${String(gate.length).padEnd(7)}` +
    `${String(closed.length).padEnd(8)}${avg(re.map((s) => s.length)).padEnd(11)}` +
    `${String(Math.max(...re.map((s) => s.length))).padEnd(9)}` +
    `${avg(re.map((s) => s.filter((x) => ASSET.includes(x)).length)).padEnd(12)}${gr}`);
}
```

```
scheme               compartments  gate  closed  avg item  worst  avg asset  gate request
single-compartment   1             0      0       13.0       13       4.0         0
two-compartment      2             7      1       9.0        9        4.0         2018
four-compartment     4             16     2       8.2        9        4.0         3513
per-node             14            20     2       4.8        8        2.8         3513
```
The first row is the measure of the network without compartments. Whichever service falls, all
thirteen of the other nodes and all four of the four assets are reached. On a flat network, the
answer to "which service was compromised" does not change the outcome; every compromised node
opens the same radius.

The second row draws the first compartment boundary, and the average reached item drops from
thirteen to nine — a thirty-one percent narrowing. But the asset column does not move: still
four. The boundary separated the entry points from the inside, but every asset stayed inside.
Seven gates opened, and two thousand eighteen requests a day now pass through them. The cost of
isolation was paid, the count of protected items did not change.

The third row is this lesson's most expensive row. Four compartments require sixteen gates, and
the whole of the daily legitimate request, three thousand five hundred thirteen, passes through
at least one gate. In exchange, the average reached item drops from nine to eight point two, and
the asset count is still four. Nine extra gates bought zero point eight of an item. Increasing
the compartment count alone does not produce isolation.

The last row shows what actually works. When every node is in its own compartment, the average
reached item drops to four point eight, the reached asset to two point eight. The asset column
only moves here. The difference comes not from the compartment count but from the
**granularity** of the gates: in the four-compartment scheme, one gate opening from the service
compartment into the data compartment opened every asset in that compartment; in the per-node
scheme, the same gate opens onto only one asset.

#### Radius per Fallen Node

The average alone is misleading. When the same measurement is done node by node, it becomes
visible which node narrows and which does not.

```js
// run2.mjs — reached item per fallen node; how it narrows as the scheme changes
import { ASSET } from './network.mjs';
import { SCHEME, INTERMEDIATE, resolve } from './compartment.mjs';

const c = SCHEME.map(([nm, h]) => [nm, resolve(h)]);
console.log(`fallen node   ${c.map(([nm]) => nm.split('-')[0].padEnd(9)).join('')}`);
for (const d of INTERMEDIATE)
  console.log(`${d.padEnd(14)}${c.map(([, x]) => String(x.reached(d).length).padEnd(9)).join('')}`);
console.log('\nthe same rows counting only assets:');
for (const d of INTERMEDIATE)
  console.log(`${d.padEnd(14)}${c.map(([, x]) => String(x.reached(d).filter((y) => ASSET.includes(y)).length).padEnd(9)).join('')}`);
const last = c[3][1];
console.log(`\nwidest radius in the per-node scheme: ${INTERMEDIATE.map((d) => [d, last.reached(d).length]).sort((a, b) => b[1] - a[1])[0].join(' -> ')} items`);
```

```
fallen node   single   two      four     per      
collector     13       9        8        6        
work-order    13       9        8        6        
management    13       9        9        8        
queue         13       9        8        5        
validator     13       9        8        2        
billing       13       9        8        2        

the same rows counting only assets:
collector     4        4        4        3        
work-order    4        4        4        3        
management    4        4        4        4        
queue         4        4        4        3        
validator     4        4        4        2        
billing       4        4        4        2        

widest radius in the per-node scheme: management -> 8 items
```

The first two columns are completely flat: in the no-compartment and two-compartment schemes, it
does not matter which node falls. The distinction only opens up in the last column. When the
validator or billing falls, the reached item drops from thirteen to two; these two services sit
near the end of the graph, and behind them there is only the asset they write to. The
measurement collector and the work-order service drop to six, the queue to five.

The management station does not narrow. Nine items in the four-compartment scheme, eight in the
per-node scheme — and four in the asset column, meaning every asset is reached regardless of
scheme. The reason is where the gate set comes from: the management station's legitimate work
already touches the collector, the validator, the work-order service, and the configuration
store. Because gates are derived from legitimate flows, a node whose legitimate work is wide also
gets wide gates.

This is isolation's structural limit: **compartmentalization narrows nodes whose legitimate work
is narrow, not ones whose work is wide.** The node whose compromise would cost the most is the
node that benefits least from isolation. The same observation can be turned into a decision — the
way to narrow the management node's radius is not adding a compartment, it is splitting that
node's legitimate work: putting the subject that does maintenance and the subject that changes
configuration on separate nodes splits eight items into two smaller, separate sets.

The schemes' cost must be read together with this table. The per-node scheme requires twenty
gates, and each of these gates is a hand-written item that has to be updated whenever legitimate
flow changes. The whole of the daily three thousand five hundred thirteen requests passes through
at least one gate; every gate is both an operational item and a point of failure. This is the
price of bringing the average reached item down from thirteen to four point eight, and that price
shows up nowhere unless an incident happens.

#### The Location of the Boundary

If the compartment count alone is not decisive, where the boundary is drawn is. Three schemes
answer the same question differently: should the boundary be drawn around the assets, between
the assets, or between the services?

```js
// run3.mjs — the location of the boundary: what changes when the same compartment count is placed differently
import { FLOW, ENTRY, ASSET, steps } from './network.mjs';
import { NODE, INTERMEDIATE, resolve } from './compartment.mjs';

const build = (f) => Object.fromEntries(NODE.map((d) => [d, f(d)]));
const avg = (a) => (a.reduce((s, x) => s + x, 0) / a.length).toFixed(1);
const PLACEMENT = [
  ['around-assets', build((d) => (ASSET.includes(d) ? 'data' : 'rest'))],
  ['per-asset', build((d) => (ASSET.includes(d) ? d : 'rest'))],
  ['per-service', build((d) => (ASSET.includes(d) ? 'data' : ENTRY.includes(d) ? 'outer' : d))],
];
console.log('scheme            compartments  gate  avg item  avg asset  gate request');
for (const [nm, map] of PLACEMENT) {
  const { gate, reached } = resolve(map), re = INTERMEDIATE.map(reached);
  const gr = FLOW.filter(([, d]) => steps(d).some((k) => gate.includes(k))).reduce((s, [, , n]) => s + n, 0);
  console.log(`${nm.padEnd(18)}${String(new Set(Object.values(map)).size).padEnd(14)}${String(gate.length).padEnd(7)}` +
    `${avg(re.map((s) => s.length)).padEnd(11)}${avg(re.map((s) => s.filter((x) => ASSET.includes(x)).length)).padEnd(12)}${gr}`);
}
```

```
scheme            compartments  gate  avg item  avg asset  gate request
around-assets     2             7      13.0       4.0         2039
per-asset         5             7      13.0       4.0         2039
per-service       8             20     6.0        4.0         3513
```
The first two rows give zero gain. Putting every asset in a single compartment and giving every
asset its own separate compartment both leave the average reached item at thirteen and the
reached asset at four. The reason comes from the gate rule: because all the services stay in the
same compartment, falling in one reaches all of them, and because the gate to every asset is open
from that same shared compartment, every asset is reached too. The five-compartment scheme is no
different from the two-compartment scheme — three extra boundaries were drawn, no path narrowed.
Reporting isolation by compartment count is completely misleading here.

The third row shows a gain, but not the expected one. Separating the services brings the average
reached item down from thirteen to six; lateral movement genuinely narrowed. The reached asset is
still four, because every service gets a gate into the shared data compartment for its own
legitimate work, and movement is free inside that compartment. Twenty gates and the whole of the
daily request were paid, and the count of protected assets did not change.

Read together, the three rows make the rule visible: **a service boundary narrows lateral
movement, an asset boundary narrows asset access, and neither substitutes for the other.** The
reason the per-node scheme brings the reached asset down to two point eight is that it does both
at once. An isolation decision is measured not by how many compartments were built, but by which
pair of items the boundary was placed between.

#### Summary

- On a flat network, whichever service falls, all 13 other nodes and all 4 assets are reached.
  On a network without compartments, "which node was compromised" does not change the outcome.
- Two compartments bring the average reached item down from 13 to 9 (31%), but the reached asset
  stays at 4; the count of protected assets did not change for the cost of 7 gates and 2,018
  daily requests.
- Four compartments bring the average down from only 9 to 8.2 for the cost of 16 gates and the
  whole of the daily request. Increasing the compartment count alone does not produce isolation.
- The reached asset only moves in the per-node compartment scheme: average 4 down to 2.8. What
  is decisive is not the compartment count but the gate's granularity.
- The management station is the least-narrowed node: 13 items fall only to 8, and all 4 assets stay
  reachable in every scheme. Because gates are derived from legitimate flow, a node whose
  legitimate work is wide cannot benefit from isolation.
- The per-node scheme's cost is 20 hand-managed gates and 3,513 daily requests passing through
  them; this cost is invisible unless an incident happens.

#### Next Step

This lesson divided the network into compartments and counted how many items a fallen
compartment reaches. Every compartment ran on the same silent rule: **movement inside a
compartment is free.** This is also why the reached item count started at thirteen — on a flat
network, being inside was enough to pass. Adding compartments did not remove this rule, it only
shrank the area where the rule applies; in every scheme, being inside was still a privilege, and
gates carried that privilege from compartment to compartment. The next lesson's question is the
rule itself: if trust based on location is removed entirely and every step is verified separately
regardless of where it came from, what happens to the path count, where does the reached item
land, and how many extra steps per daily request does verifying every request separately cost.

### Zero Trust

Source: https://academia.sh/en/courses/introduction-to-cybersecurity/zero-trust

As location-based trust is removed, verification scope is widened in five steps, and at each step the path count, daily verification, and extra step per request are counted together. The reason the paths left open under zero trust stay open is found, and how many internal flows carrying identity end to end would change is measured.

The previous lesson divided the network into compartments and counted the reached item when a
node falls. Every scheme ran on the same silent rule: movement inside a compartment is free.
This is also why the reached item count started at thirteen; adding compartments did not remove
the rule, it only shrank the area where it applies. This lesson removes the rule itself.

**Zero trust** is giving up trust based on where a request comes from entirely and verifying
every step separately. Measuring the principle requires something to stand in for "being
inside": which identity each step **belongs to**. A step passes when it is taken by the identity
that owns it; when another identity tries to take the same step, it stops no matter where on the
network it happens. The three things measured are path count, daily verification, and **extra
step per request**.

- **DP17.** Step ownership is derived from the legitimate flow list: if a flow starts from an
  entry point, that flow's steps belong to that identity.
- **DP18.** Four legitimate flows do not start from an entry point; they are triggered from the
  queue or the management station. These flows' steps are treated as **shared**, meaning which
  identity triggered them is unknown. This is the model's most decisive choice.
- **DP19.** Verification scope is the set of steps where identity is checked. A step outside the
  scope trusts location and lets it through; a step inside the scope checks the ownership set.
- **DP20.** One verification is one extra step. The verification's own latency, duration, and
  failure rate are not modeled; only how many times it happens is counted.
- **DP21.** Verification always gives the correct result. Forged identity, a stolen token, and
  replay are not modeled; these would lower the count of closed paths, not change the direction
  of the measure.

#### Identity in Place of Location

The graph stays the same; the only thing that changes is the condition under which a step is
crossed.

```js
// network.mjs — fictional regional measurement network's access graph and legitimate flows (model)
export const ENTRY = ['field-endpoint', 'branch-network', 'public-endpoint', 'maintenance-endpoint'];
export const ASSET = ['measurement-store', 'invoice-db', 'subscriber-registry', 'configuration'];
export const EDGE = [
  ['field-endpoint', 'collector'], ['field-endpoint', 'work-order'], ['branch-network', 'collector'],
  ['branch-network', 'work-order'], ['branch-network', 'management'], ['public-endpoint', 'collector'],
  ['maintenance-endpoint', 'management'], ['maintenance-endpoint', 'queue'], ['collector', 'queue'],
  ['collector', 'measurement-store'], ['queue', 'validator'], ['queue', 'billing'],
  ['validator', 'measurement-store'], ['validator', 'invoice-db'], ['billing', 'invoice-db'],
  ['billing', 'subscriber-registry'], ['work-order', 'queue'], ['work-order', 'subscriber-registry'],
  ['management', 'collector'], ['management', 'validator'], ['management', 'work-order'],
  ['management', 'configuration'],
];
// Legitimate flow: work that runs daily; the step sequence passes through the same graph's edges.
export const FLOW = [
  ['field-reading', ['field-endpoint', 'collector', 'queue'], 1440],
  ['validation', ['queue', 'validator', 'measurement-store'], 1440],
  ['public-reading', ['public-endpoint', 'collector', 'measurement-store'], 300],
  ['branch-query', ['branch-network', 'collector', 'measurement-store'], 210],
  ['invoice-write', ['queue', 'billing', 'invoice-db'], 48],
  ['work-order-opening', ['branch-network', 'work-order', 'subscriber-registry'], 32],
  ['work-order-queuing', ['branch-network', 'work-order', 'queue'], 32],
  ['subscriber-update', ['billing', 'subscriber-registry'], 6],
  ['configuration-change', ['branch-network', 'management', 'configuration'], 2],
  ['maintenance-session', ['maintenance-endpoint', 'management', 'collector'], 1],
  ['queue-maintenance', ['maintenance-endpoint', 'queue'], 1],
  ['reconciliation', ['management', 'validator', 'invoice-db'], 1],
];
export const edgeName = (a, b) => `${a}>${b}`;
export const steps = (d) => d.slice(0, -1).map((x, i) => edgeName(x, d[i + 1]));
export function paths(closed = new Set()) {
  const neighbor = new Map();
  for (const [a, b] of EDGE) if (!closed.has(edgeName(a, b))) neighbor.set(a, [...(neighbor.get(a) || []), b]);
  const out = [];
  const walk = (d, y) => {
    if (ASSET.includes(d)) return void out.push(y);
    for (const k of neighbor.get(d) || []) if (!y.includes(k)) walk(k, [...y, k]);
  };
  for (const g of ENTRY) walk(g, [g]);
  return out;
}
export const blocked = (closed) => FLOW.filter(([, d]) => steps(d).some((k) => closed.has(k)));
```

```js
// identity.mjs — step ownership and verification scopes (model)
import { EDGE, FLOW, ENTRY, ASSET, edgeName, steps } from './network.mjs';
export const EDGE_NAME = EDGE.map(([a, b]) => edgeName(a, b));
// Internal flows do not start from an entry point; their steps are shared by every identity.
const SHARED = new Set(FLOW.filter(([, d]) => !ENTRY.includes(d[0])).flatMap(([, d]) => steps(d)));
const own = (g) => FLOW.filter(([, d]) => d[0] === g).flatMap(([, d]) => steps(d));
export const OWNER = Object.fromEntries(ENTRY.map((g) => [g, new Set([...SHARED, ...own(g)])]));
export const END_TO_END = Object.fromEntries(ENTRY.map((g) => [g, new Set(own(g))]));
const SERVICE = ['collector', 'queue', 'validator', 'billing', 'work-order'];
const compartment = (d) => (ENTRY.includes(d) ? 'outer'
  : SERVICE.includes(d) ? 'service' : d === 'management' ? 'management' : 'data');
export const PERIMETER = EDGE_NAME.filter((k) => ENTRY.includes(k.split('>')[0]));
export const TO_ASSET = EDGE_NAME.filter((k) => ASSET.includes(k.split('>')[1]));
export const GATE = EDGE_NAME.filter((k) => compartment(k.split('>')[0]) !== compartment(k.split('>')[1]));
const neighbor = new Map();
for (const [a, b] of EDGE) neighbor.set(a, [...(neighbor.get(a) || []), b]);
// A step in scope checks identity; a step outside scope trusts location and lets it through.
export function path(g, scope, owner = OWNER) {
  const s = new Set(scope), out = [];
  const walk = (d, y) => {
    if (ASSET.includes(d)) return void out.push(y);
    for (const k of neighbor.get(d) || []) {
      const e = edgeName(d, k);
      if (s.has(e) && !owner[g].has(e)) continue;
      if (!y.includes(k)) walk(k, [...y, k]);
    }
  };
  walk(g, [g]);
  return out;
}
```

```js
// run1.mjs — as verification scope widens, path count and extra step per request
import { FLOW, ENTRY, steps } from './network.mjs';
import { EDGE_NAME, PERIMETER, TO_ASSET, GATE, path } from './identity.mjs';

const unique = (a) => [...new Set(a)];
const SCOPE = [['none', []], ['perimeter', PERIMETER], ['perimeter+asset', unique([...PERIMETER, ...TO_ASSET])],
  ['perimeter+gate', unique([...PERIMETER, ...GATE])], ['every-step', EDGE_NAME]];
const daily = FLOW.reduce((s, [, , n]) => s + n, 0);
console.log('scope            checked  path  closed  daily verification  extra step per request');
for (const [nm, scope] of SCOPE) {
  const y = ENTRY.reduce((s, g) => s + path(g, scope).length, 0), s = new Set(scope);
  const ver = FLOW.reduce((t, [, d, c]) => t + c * steps(d).filter((k) => s.has(k)).length, 0);
  console.log(`${nm.padEnd(17)}${String(scope.length).padEnd(12)}${String(y).padEnd(6)}` +
    `${String(55 - y).padEnd(9)}${String(ver).padEnd(18)}${(ver / daily).toFixed(2)}`);
}
```

```
scope            checked  path  closed  daily verification  extra step per request
none             0           55    0        0                 0.00
perimeter        8           50    5        2018              0.57
perimeter+asset  15          46    9        4057              1.15
perimeter+gate   18          32    23       4059              1.16
every-step       22          20    35       7019              2.00
```
The second row is the measure of location-based trust. Identity is checked at eight entry steps
and nowhere on the inside. Only five of the fifty-five paths close. The cost is low — two
thousand eighteen daily verifications, zero point five seven extra step per request — but what it
buys is low too: ninety percent of the paths stay open. A control sitting at the perimeter never
sees a request again once it has passed the perimeter. This does not mean perimeter verification
is worthless; it is the control that closes the first five paths most cheaply. It means the
measurement cannot stop there — the path count a perimeter control closes is small regardless of
the shape of the network behind it.

The third and fourth rows have to be read together, because their cost is nearly identical: four
thousand fifty-seven and four thousand fifty-nine daily verifications, one point one five and one
point one six extra step per request. But the paths they close are nine and twenty-three. A
two-and-a-half-fold difference for the same cost. Putting verification in front of the asset
means checking the last step of the path, and the request that reaches that step has already
crossed every intermediate node; putting it at the compartment gates cuts the path through its
middle instead. **Verification's cost is set by how many times it runs, its value by where it
runs.**

The last row is zero trust in full. Identity is checked at all twenty-two steps, thirty-five of
the fifty-five paths close and twenty stay open. The cost is seven thousand nineteen daily
verifications, exactly two extra steps per request: every legitimate request shows its identity
twice on average. Thirty more paths closed compared to location-based trust, and the verification
count rose to three and a half times as much.

Dividing the two columns by each other gives an unexpected result. Perimeter verification costs
four hundred four verifications per closed path; verification at every step costs two hundred
one. Per closed path, zero trust is **cheaper** than location-based trust. The most efficient row
is the combination of perimeter and compartment gates: one hundred seventy-six verifications per
closed path. The least efficient is the combination of perimeter and asset entry: four hundred
fifty-one. Looking at total cost and saying "zero trust is expensive" reads only one column of
the table; read together, what is expensive is not the number of verifications but where they
are placed.

The two in the last column also needs explaining. Verification runs at all twenty-two steps, but
the extra step per request comes out to two, because most legitimate flows are two steps long.
Widening the verification scope grows the number of checked steps; what determines the cost a
request pays is how many steps that request passes through. Long flows grow the cost of zero
trust linearly.

#### Per Entry Point

The total does not show which identity narrows. When the same measurement is done per entry
point, the width of the ownership set becomes decisive.

```js
// run2.mjs — path per entry point; what is left once identity is carried end to end
import { FLOW, ENTRY, ASSET } from './network.mjs';
import { EDGE_NAME, PERIMETER, OWNER, END_TO_END, path } from './identity.mjs';

console.log('entry                 owned step  location-based  zero trust  end-to-end identity');
for (const g of ENTRY)
  console.log(`${g.padEnd(22)}${String(OWNER[g].size).padEnd(12)}${String(path(g, PERIMETER).length).padEnd(16)}` +
    `${String(path(g, EDGE_NAME).length).padEnd(13)}${path(g, EDGE_NAME, END_TO_END).length}`);
const open = ENTRY.flatMap((g) => path(g, EDGE_NAME));
const count = open.reduce((m, p) => m.set(p.at(-1), (m.get(p.at(-1)) || 0) + 1), new Map());
console.log(`\nasset reached by the ${open.length} paths left open under zero trust:`);
for (const v of ASSET) console.log(`  ${v.padEnd(20)}${count.get(v) || 0}`);
const internal = FLOW.filter(([, d]) => !ENTRY.includes(d[0]));
console.log(`\ninternal flow that would need to change to carry identity end to end ${internal.length}, ` +
  `daily request ${internal.reduce((s, [, , n]) => s + n, 0)}`);
```

```
entry                 owned step  location-based  zero trust  end-to-end identity
field-endpoint        9           5               4            0
branch-network        14          23              9            3
public-endpoint       9           5               1            1
maintenance-endpoint  10          17              6            0

asset reached by the 20 paths left open under zero trust:
  measurement-store   7
  invoice-db          8
  subscriber-registry 4
  configuration       1

internal flow that would need to change to carry identity end to end 4, daily request 1495
```
The maintenance endpoint drops from seventeen paths to six. The branch network drops from
twenty-three to nine, and it is the entry point that leaves the most paths open after zero
trust, because it has the widest ownership set — fourteen steps. The identity whose
legitimate work is wide is the identity that benefits least from zero trust; we saw the same
thing for the management node in the previous lesson, a different measure, the same result.

The public endpoint sees the sharpest narrowing: from five paths to one. Even though its
ownership set is nine steps, its own flow is only two steps; the remaining seven steps come from
the shared set. This row is the clue to the next finding.

The lower table shows how the narrowing spreads across the assets, and the spread is not even. In
the first lesson, twenty-two paths went to the invoice database, seventeen to the measurement
store, fourteen to the subscriber registry, and two to the configuration store. After zero trust
these numbers are eight, seven, four, and one. The asset carrying the most paths narrowed the most
in absolute terms — from twenty-two to eight, sixty-four percent. The configuration store dropped
from two to one, fifty percent. The large proportional narrowing is a result of starting from a
high number; the remaining absolute number is still highest at the invoice database. When a
defense report is written by percentage, the item that looks most protected is still the most open
item when written by absolute number. Which column to read depends on what the decision is:
investment priority is read from the absolute number, progress is read from the ratio.

#### Where Identity Gets Lost

Why are twenty paths still open under zero trust? The answer is in the shared steps. Four
legitimate flows do not start from an entry point: validation, invoice writing, subscriber
update, and reconciliation. These are triggered from the queue or the management station, and
who triggered them is not written into the step itself. In the model these seven steps stayed
open, shared by every identity; in reality, too, work taken off a queue does not carry the
identity of whoever put it there.

The last column is the measure of this. If identity were carried end to end, meaning every
internal flow also carried the identity of the request that triggered it, the remaining path
would drop from twenty to four. No path remains from the field endpoint or the maintenance
endpoint; three remain from the branch network, one from the public endpoint. All sixteen paths
come from identity being dropped at the queue.

The price of these sixteen paths can also be written down: rebuilding four internal flows and
changing one thousand four hundred ninety-five daily requests to carry identity. This is not
adding a control, it is a design change; every job the queue delivers has to carry the identity
of its caller too. This is the main reason zero trust is usually left half implemented in
practice: of the fifty-one closable paths, thirty-five close by adding verification, sixteen
close only once the flows are rewritten. The first is a configuration job, the second is a
software job, and the two do not come out of the same budget.

The whole measurement also has a limit. The model assumed verification always gives the correct
result; a stolen identity is valid at every step it owns. Zero trust does not prevent an identity
from being stolen, it shrinks **how many steps a stolen identity is valid for**. The difference
between nine and fourteen in the ownership column is the difference in the area opened up if that
identity is stolen. For the same reason, zero trust should be counted not as an end state but as
an axis of measure: as ownership sets narrow, a stolen identity's value falls, and as they widen,
it rises. What should be measured is not "has zero trust been adopted" but **how many steps are
open per identity**.

#### Summary

- Location-based trust checks identity at eight entry steps and nowhere on the inside: only 5 of
  the 55 paths close, at a cost of 0.57 extra step per request.
- Putting verification in front of the asset and putting it at the compartment gates carry
  nearly the same cost (4,057 and 4,059 daily verifications) but close 9 and 23 paths. Cost is
  set by how many times it runs, value by where it runs.
- Verification at every step closes 35 paths and leaves 20; its cost is 7,019 daily
  verifications, exactly 2 extra steps per request.
- The identity with the widest ownership set narrows less: the branch network, with 14
  steps, drops from 23 paths to 9, while the maintenance endpoint, with 10 steps, drops from 17
  to 6.
- The source of the 20 paths left open is the shared steps of 4 internal flows that do not start
  from an entry point. If identity were carried end to end, the remaining path would be 4.
- The price of the last 16 paths is design, not verification: rebuilding 4 internal flows and
  changing 1,495 daily requests to carry identity.

#### Next Step

This lesson checked identity at every step and found where identity gets lost. The question
asked was always the same: who sent this request? But crossing a step requires answering two
separate questions, and this lesson only asked the first. A request whose identity is verified is
not authorized just because it is verified; authorization is a separate question asked in a
separate place. What happens when the two questions are collapsed into the same layer, or one
substitutes for the other? That is what the next lesson measures: at how many items a
verified-but-unauthorized request gets through, how many paths open when the control is placed in
the wrong layer, and how many items asking the two questions separately means adding extra
verification to.

### Authentication and Authorization Distinction

Source: https://academia.sh/en/courses/introduction-to-cybersecurity/authentication-and-authorization-distinction

Two controls are placed in separate scopes over the same step set and measured against two separate requests: a forged-identity request and a verified-but-unauthorized request. How many paths putting authentication at every step closes against a verified-but-unauthorized request, and how many paths asking authorization only at the data layer opens, are counted.

The previous lesson checked identity at every step and brought the path count down from
fifty-five to twenty. The question asked was always the same: who sent this request? But
crossing a step requires answering two separate questions. **Authentication** determines who
sent the request; **authorization** determines whether that identity can perform this action on
this item. The two questions are different, look at different data, and can be asked in
different places.

The Authentication and Authorization course covered this distinction at the flow and token
level. What is measured here is not the mechanism but **the path a control opens by sitting in
the wrong layer**. The measurement runs against two separate requests: one with a forged
identity, and one **with a real identity but unauthorized for that step**. The second is this
lesson's real subject, because most controls only see the first. A forged-identity request
represents an outside attacker; a verified-but-unauthorized request represents a compromised
legitimate identity or a misconfigured service — and the second case is both more common and
harder to see than the first.

- **DP22.** A step passes through two controls. If it is in the authentication scope, it
  requires the request's identity to be valid; if it is in the authorization scope, it requires
  that identity to be authorized for the step.
- **DP23.** The authorization set is derived from the legitimate flow list; an identity is
  authorized on the steps of its own flows and on the shared internal flow steps.
- **DP24.** The two request profiles are run separately. A forged-identity request cannot pass
  authentication; a verified-but-unauthorized request passes all of authentication and can only
  be stopped by authorization.
- **DP25.** The daily control count is the sum of both controls: if both run on one step, that
  counts as two operations. The controls' duration and latency are not modeled.
- **DP26.** Authorization always makes the correct decision. A wrongly written authorization set,
  a stale record, and a delegated session are not modeled; these would grow the count of opened
  paths.

#### Two Questions, Two Scopes

The graph and the legitimate flow list have not changed. The only thing that changes is which
question is asked at which step.

```js
// network.mjs — fictional regional measurement network's access graph and legitimate flows (model)
export const ENTRY = ['field-endpoint', 'branch-network', 'public-endpoint', 'maintenance-endpoint'];
export const ASSET = ['measurement-store', 'invoice-db', 'subscriber-registry', 'configuration'];
export const EDGE = [
  ['field-endpoint', 'collector'], ['field-endpoint', 'work-order'], ['branch-network', 'collector'],
  ['branch-network', 'work-order'], ['branch-network', 'management'], ['public-endpoint', 'collector'],
  ['maintenance-endpoint', 'management'], ['maintenance-endpoint', 'queue'], ['collector', 'queue'],
  ['collector', 'measurement-store'], ['queue', 'validator'], ['queue', 'billing'],
  ['validator', 'measurement-store'], ['validator', 'invoice-db'], ['billing', 'invoice-db'],
  ['billing', 'subscriber-registry'], ['work-order', 'queue'], ['work-order', 'subscriber-registry'],
  ['management', 'collector'], ['management', 'validator'], ['management', 'work-order'],
  ['management', 'configuration'],
];
// Legitimate flow: work that runs daily; the step sequence passes through the same graph's edges.
export const FLOW = [
  ['field-reading', ['field-endpoint', 'collector', 'queue'], 1440],
  ['validation', ['queue', 'validator', 'measurement-store'], 1440],
  ['public-reading', ['public-endpoint', 'collector', 'measurement-store'], 300],
  ['branch-query', ['branch-network', 'collector', 'measurement-store'], 210],
  ['invoice-write', ['queue', 'billing', 'invoice-db'], 48],
  ['work-order-opening', ['branch-network', 'work-order', 'subscriber-registry'], 32],
  ['work-order-queuing', ['branch-network', 'work-order', 'queue'], 32],
  ['subscriber-update', ['billing', 'subscriber-registry'], 6],
  ['configuration-change', ['branch-network', 'management', 'configuration'], 2],
  ['maintenance-session', ['maintenance-endpoint', 'management', 'collector'], 1],
  ['queue-maintenance', ['maintenance-endpoint', 'queue'], 1],
  ['reconciliation', ['management', 'validator', 'invoice-db'], 1],
];
export const edgeName = (a, b) => `${a}>${b}`;
export const steps = (d) => d.slice(0, -1).map((x, i) => edgeName(x, d[i + 1]));
export function paths(closed = new Set()) {
  const neighbor = new Map();
  for (const [a, b] of EDGE) if (!closed.has(edgeName(a, b))) neighbor.set(a, [...(neighbor.get(a) || []), b]);
  const out = [];
  const walk = (d, y) => {
    if (ASSET.includes(d)) return void out.push(y);
    for (const k of neighbor.get(d) || []) if (!y.includes(k)) walk(k, [...y, k]);
  };
  for (const g of ENTRY) walk(g, [g]);
  return out;
}
export const blocked = (closed) => FLOW.filter(([, d]) => steps(d).some((k) => closed.has(k)));
```

```js
// control.mjs — two separate questions: authentication and authorization, with separate scopes
import { EDGE, FLOW, ENTRY, ASSET, edgeName, steps } from './network.mjs';
export const EDGE_NAME = EDGE.map(([a, b]) => edgeName(a, b));
const SHARED = new Set(FLOW.filter(([, d]) => !ENTRY.includes(d[0])).flatMap(([, d]) => steps(d)));
const own = (g) => FLOW.filter(([, d]) => d[0] === g).flatMap(([, d]) => steps(d));
// AUTHORIZATION[g]: the steps identity g is authorized for; derived from the legitimate flow list.
export const AUTHORIZATION = Object.fromEntries(ENTRY.map((g) => [g, new Set([...SHARED, ...own(g)])]));
export const PERIMETER = EDGE_NAME.filter((k) => ENTRY.includes(k.split('>')[0]));
export const TO_ASSET = EDGE_NAME.filter((k) => ASSET.includes(k.split('>')[1]));
const neighbor = new Map();
for (const [a, b] of EDGE) neighbor.set(a, [...(neighbor.get(a) || []), b]);
// A step passes through two separate controls; each control has its own scope.
export function path(g, idScope, authScope, identityValid) {
  const ID = new Set(idScope), AUTH = new Set(authScope), out = [];
  const walk = (d, y) => {
    if (ASSET.includes(d)) return void out.push(y);
    for (const k of neighbor.get(d) || []) {
      const e = edgeName(d, k);
      if (ID.has(e) && !identityValid) continue;         // authentication: who are you
      if (AUTH.has(e) && !AUTHORIZATION[g].has(e)) continue; // authorization: can you do this
      if (!y.includes(k)) walk(k, [...y, k]);
    }
  };
  walk(g, [g]);
  return out;
}
```

```js
// run1.mjs — with two controls in different layers, which request stops, which passes
import { FLOW, ENTRY, steps } from './network.mjs';
import { EDGE_NAME, PERIMETER, TO_ASSET, path } from './control.mjs';

const SCHEME = [['no-control', [], []], ['identity-perimeter', PERIMETER, []],
  ['identity-every-step', EDGE_NAME, []], ['identity-perimeter-auth-data', PERIMETER, TO_ASSET],
  ['identity-perimeter-auth-everywhere', PERIMETER, EDGE_NAME], ['auth-only-everywhere', [], EDGE_NAME],
  ['both-everywhere', EDGE_NAME, EDGE_NAME]];
const daily = FLOW.reduce((s, [, , n]) => s + n, 0);
console.log('scheme                              forged  verified  asset  daily control  per request');
for (const [nm, idScope, authScope] of SCHEME) {
  const forged = ENTRY.reduce((s, g) => s + path(g, idScope, authScope, false).length, 0);
  const verified = ENTRY.flatMap((g) => path(g, idScope, authScope, true));
  const ID = new Set(idScope), AUTH = new Set(authScope);
  const ctrl = FLOW.reduce((s, [, d, c]) => s + c * steps(d)
    .reduce((t, k) => t + (ID.has(k) ? 1 : 0) + (AUTH.has(k) ? 1 : 0), 0), 0);
  console.log(`${nm.padEnd(36)}${String(forged).padEnd(8)}${String(verified.length).padEnd(13)}` +
    `${String(new Set(verified.map((p) => p.at(-1))).size).padEnd(8)}${String(ctrl).padEnd(16)}${(ctrl / daily).toFixed(2)}`);
}
```

```
scheme                              forged  verified  asset  daily control  per request
no-control                          55      55           4       0               0.00
identity-perimeter                  0       55           4       2018            0.57
identity-every-step                 0       55           4       7019            2.00
identity-perimeter-auth-data        0       50           4       4057            1.15
identity-perimeter-auth-everywhere  0       20           4       9037            2.57
auth-only-everywhere                20      20           4       7019            2.00
both-everywhere                     0       20           4       14038           4.00
```
The second and third rows are this lesson's main finding. When authentication is widened from
the perimeter to every step, daily control rises from two thousand eighteen to seven thousand
nineteen — from zero point five seven to exactly two extra steps per request. A forged-identity
request finds zero paths in both rows; it was already stopped at the perimeter. A
verified-but-unauthorized request, though, uses all fifty-five of the paths in both rows.
**Tripling and a half the authentication control did not close a single path against a verified
request.** This is a widening made in the layer of the wrong question, not the wrong layer.

The fourth row is the placement most commonly seen in practice: identity at the gate,
authorization at the data layer. Five of the fifty-five paths close, fifty stay open, and all
four of the four assets are reached. The reason for asking authorization only at the last step
is understandable — the information the authorization decision needs sits there — but the result
is that the intermediate steps are never questioned at all. A verified identity moves freely
among the intermediate nodes and meets a question only at the asset's door.

The fifth and seventh rows have to be read together. Putting authorization at every step brings
the path down to twenty; putting authentication at every step too gives the same twenty. The
difference is only in cost: two point five seven versus four extra steps per request. Zero paths
close in exchange for one point four three extra steps. Leaving authentication at the perimeter
and spreading authorization gives the same result as spreading both, and costs roughly a third
less.

The sixth row shows the other end of the distinction. When authorization is asked at every step
and identity is never asked, a forged-identity request finds twenty paths. Authorization answers
"can this identity do this"; it does not ask whether the identity really is who it claims. The
moment a claimed identity is accepted as true, authorization grants that claim's every privilege.
Neither control substitutes for the other: without one, the other just hands the open path over
to a different kind of request.

#### When Authorization Is Asked at the Last Step

Whose hands hold the fourth row's fifty? When the same measurement is done per identity, what
authorization sees and does not see comes apart.

```js
// run2.mjs — with authorization only in the data layer, where does a verified-unauthorized request reach
import { ENTRY, ASSET } from './network.mjs';
import { PERIMETER, TO_ASSET, AUTHORIZATION, path } from './control.mjs';

console.log(`identity               authorized step  path  ${ASSET.map((v) => v.slice(0, 8).padEnd(10)).join('')}`);
const closed = [];
for (const g of ENTRY) {
  const y = path(g, PERIMETER, TO_ASSET, true), reached = new Set(y.map((p) => p.at(-1)));
  const open = new Set(y.map((p) => p.join('>')));
  closed.push(...path(g, [], [], true).map((p) => p.join(' > ')).filter((p) => !open.has(p.replaceAll(' > ', '>'))));
  console.log(`${g.padEnd(23)}${String(AUTHORIZATION[g].size).padEnd(17)}${String(y.length).padEnd(6)}` +
    ASSET.map((v) => (reached.has(v) ? 'yes' : 'no').padEnd(10)).join(''));
}
console.log(`\n${closed.length} path the data-layer control closes:`);
for (const p of closed) console.log(`  ${p}`);
const narrow = ENTRY.flatMap((g) => path(g, PERIMETER, [...TO_ASSET, ...PERIMETER], true));
console.log(`\nopen path when authorization is data layer + perimeter: ${narrow.length}`);
```

```
identity               authorized step  path  measurem  invoice-  subscrib  configur  
field-endpoint         9                8     yes       yes       yes       no        
branch-network         14               23    yes       yes       yes       yes       
public-endpoint        9                5     yes       yes       yes       no        
maintenance-endpoint   10               14    yes       yes       yes       no        

5 path the data-layer control closes:
  field-endpoint > collector > measurement-store
  field-endpoint > work-order > subscriber-registry
  maintenance-endpoint > management > collector > measurement-store
  maintenance-endpoint > management > work-order > subscriber-registry
  maintenance-endpoint > management > configuration

open path when authorization is data layer + perimeter: 46
```

The branch network row shows how blind authorization is at the data layer. Twenty-three of the
twenty-three paths are open; not a single one closed. The reason is the shape of the
authorization set: for its own legitimate work, the branch network identity carries one-step
authority on all four of the four assets — it reads the measurement store, writes to the
subscriber registry while opening a work order, makes configuration changes, and writes to the
invoice database through internal flows. Because the data-layer control only looks at the last
step, once that last step is authorized, **how the request got there is never asked at all**. The
authorized-step column is the sign of this result: at the widest authorization set — fourteen
steps — the data-layer control stops zero paths. The identity with the widest
authorization is, in a system checked only at the last step, effectively never checked.

The public endpoint is one of the two narrowest identities and still advances on all five of five
paths; the
field endpoint drops from ten to eight, the maintenance endpoint from seventeen to fourteen. The
five closed paths are listed, and all of them share one trait: every one is a short path, two or
three steps long. None of the long paths close.

The reason is who owns the last step. The long paths enter the asset through the validator or
billing; these two services' asset-writing steps belong to internal flows, and internal flow
steps are shared by every identity. Whenever the data-layer control looks at the last step, it
always finds a step that is authorized. The short paths, by contrast, enter the asset directly,
and their last steps belong to a specific identity; that is where a mismatch becomes visible.
**Lengthening the path is how you escape the data-layer control** — and this is not a gap in the
control, it is the consequence of where it is placed.

The last row measures a middle option. When authorization is placed at the perimeter as well as
the data layer, the open path drops from fifty to forty-six. Four more paths closed — that is the
return on adding an eight-step control. Widening authorization does not close paths linearly; how
much closes depends on **which step** the control sits at, and the middle steps carry the most
paths. Placing authorization at the ends cuts the path's ends and leaves the middle open; the
twenty in the first table is only reached once the middle steps are brought into the check too.

Together the two tables give a rule of placement. Authentication can be done in one place,
because its answer does not change along the path: whoever a request is, it stays that all the
way through. Authorization has to be re-asked at every step, because its answer changes from step
to step: the same identity is authorized at one step and not at the next. This asymmetry explains
why the two controls belong to different layers, and it also justifies the table's cost
difference — leaving identity at the perimeter and spreading authorization gave the same twenty
paths for two point five seven extra steps per request instead of four.

A limit of the measurement also has to be written down here. The model assumed the authorization
set is correct and is evaluated correctly at every step. In reality, making an authorization
decision at an intermediate step requires that step to have the information the decision needs:
on whose behalf the work is being done, which item is requested, and which action is being
taken. If this information is not carried along the path, authorization cannot be asked at an
intermediate step — and if it is asked anyway, the answer is "I do not know," and that answer
turns into letting the request through. The shared-step problem from the previous lesson and the
intermediate-step problem here are two faces of the same gap: if who a request is acting for and
what it is doing are not carried between steps, no intermediate control can make the right
decision. The twenty in the table is therefore not a target, it is a lower bound reachable only
once the flows carry this information.

#### Summary

- Crossing a step depends on two separate questions, and the two questions stop different kinds
  of requests: authentication stops a forged identity, authorization stops a
  verified-but-unauthorized request.
- Widening authentication from the perimeter to every step raises daily control from 2,018 to
  7,019 (0.57 to 2.00 per request) and closes 0 paths against a verified-but-unauthorized
  request.
- With identity at the gate and authorization at the data layer, 50 of the 55 paths stay open,
  and all 4 of the 4 assets are reached. If the last step is authorized, how the request got
  there is never asked.
- For the branch network identity, the data-layer control closes none of the 23 paths; by its
  own legitimate work it carries one-step authority on all 4 of the 4 assets.
- Putting authorization at every step brings the path to 20 (2.57 per request). Putting identity
  at every step too gives the same 20 but rises to 4.00 per request: 0 paths for 1.43 extra
  steps.
- When authorization is asked at every step and identity is never asked, a forged-identity
  request finds 20 paths. Neither control substitutes for the other; if one is missing, the open
  path is handed to a different kind of request.

#### Next Step

This lesson separated the two controls and counted which request each one stops. Every scheme
shared one assumption: the scope list is written correctly, and every step that should be in
scope is in scope. In reality this list is hand-written, gets updated as the network changes, and
one item gets forgotten. What happens to a forgotten item depends on the control's default: if a
step outside scope lets requests through, forgetting it opens a path; if it stops them,
forgetting it stops a job. That is what the next lesson measures: what the path count is under an
open default and a closed default, what forgetting a single item costs in paths and stopped
jobs, and which of the two errors shows itself.

### Secure Defaults

Source: https://academia.sh/en/courses/introduction-to-cybersecurity/secure-defaults

What happens when one of twenty-two configuration items is forgotten is counted separately under two defaults: the path opened under an open default, the daily work stopped under a closed default. The count of forgotten items is swept, and resilience to misconfiguration is measured by comparing the visibility rate of the two error types.

The previous lesson separated the two controls and counted which request each one stops. Every
scheme shared one assumption: the scope list is written correctly, and every step that should be
in scope is in scope. In reality this list is hand-written, gets updated as the network changes,
and one item gets forgotten. This lesson's question is what happens to the forgotten item.

**Secure by default** is treating an unconfigured item as closed. Its opposite is letting an
unconfigured item through. The two options only diverge at the moment of error: when everything
is written correctly, both give the same network. This lesson measures that moment of error —
when one item is forgotten, **how many paths open** under an open default, **how much
legitimate work stops** under a closed default, and which of the two errors shows itself. The
path count is again done on the fictional network's model; scanning configuration items on a
real network cannot be carried out without **written authorization and a defined scope**.

- **DP27.** Each of the twenty-two steps is a configuration item. At full configuration, every
  item checks authorization, and the network narrows to the twenty paths of the previous
  lesson's narrowest scheme.
- **DP28.** A forgotten item performs no check. Under an open default it lets everyone through;
  under a closed default it stops everyone, legitimate flow included.
- **DP29.** Items are forgotten independently of each other, and the probability of being
  forgotten is equal for every item. In reality, newly added items get forgotten more often;
  this grows the cost of the open default.
- **DP30.** Sampling is done with a hand-written linear congruential generator. The seed is
  visible and fixed; the output stays the same run to run.
- **DP31.** The "invisible" column is the share of samples where the error leaves no measurable
  trace: unchanged path count under open, no legitimate work stopped under closed.

#### The Cost of One Item

Twenty-two items are forgotten one at a time, and each forgetting's outcome under the two
defaults is written down separately.

```js
// network.mjs — fictional regional measurement network's access graph and legitimate flows (model)
export const ENTRY = ['field-endpoint', 'branch-network', 'public-endpoint', 'maintenance-endpoint'];
export const ASSET = ['measurement-store', 'invoice-db', 'subscriber-registry', 'configuration'];
export const EDGE = [
  ['field-endpoint', 'collector'], ['field-endpoint', 'work-order'], ['branch-network', 'collector'],
  ['branch-network', 'work-order'], ['branch-network', 'management'], ['public-endpoint', 'collector'],
  ['maintenance-endpoint', 'management'], ['maintenance-endpoint', 'queue'], ['collector', 'queue'],
  ['collector', 'measurement-store'], ['queue', 'validator'], ['queue', 'billing'],
  ['validator', 'measurement-store'], ['validator', 'invoice-db'], ['billing', 'invoice-db'],
  ['billing', 'subscriber-registry'], ['work-order', 'queue'], ['work-order', 'subscriber-registry'],
  ['management', 'collector'], ['management', 'validator'], ['management', 'work-order'],
  ['management', 'configuration'],
];
// Legitimate flow: work that runs daily; the step sequence passes through the same graph's edges.
export const FLOW = [
  ['field-reading', ['field-endpoint', 'collector', 'queue'], 1440],
  ['validation', ['queue', 'validator', 'measurement-store'], 1440],
  ['public-reading', ['public-endpoint', 'collector', 'measurement-store'], 300],
  ['branch-query', ['branch-network', 'collector', 'measurement-store'], 210],
  ['invoice-write', ['queue', 'billing', 'invoice-db'], 48],
  ['work-order-opening', ['branch-network', 'work-order', 'subscriber-registry'], 32],
  ['work-order-queuing', ['branch-network', 'work-order', 'queue'], 32],
  ['subscriber-update', ['billing', 'subscriber-registry'], 6],
  ['configuration-change', ['branch-network', 'management', 'configuration'], 2],
  ['maintenance-session', ['maintenance-endpoint', 'management', 'collector'], 1],
  ['queue-maintenance', ['maintenance-endpoint', 'queue'], 1],
  ['reconciliation', ['management', 'validator', 'invoice-db'], 1],
];
export const edgeName = (a, b) => `${a}>${b}`;
export const steps = (d) => d.slice(0, -1).map((x, i) => edgeName(x, d[i + 1]));
export function paths(closed = new Set()) {
  const neighbor = new Map();
  for (const [a, b] of EDGE) if (!closed.has(edgeName(a, b))) neighbor.set(a, [...(neighbor.get(a) || []), b]);
  const out = [];
  const walk = (d, y) => {
    if (ASSET.includes(d)) return void out.push(y);
    for (const k of neighbor.get(d) || []) if (!y.includes(k)) walk(k, [...y, k]);
  };
  for (const g of ENTRY) walk(g, [g]);
  return out;
}
export const blocked = (closed) => FLOW.filter(([, d]) => steps(d).some((k) => closed.has(k)));
```

```js
// default.mjs — the default state of an unconfigured item and the path count
import { EDGE, FLOW, ENTRY, ASSET, edgeName, steps } from './network.mjs';
export const EDGE_NAME = EDGE.map(([a, b]) => edgeName(a, b));
const SHARED = new Set(FLOW.filter(([, d]) => !ENTRY.includes(d[0])).flatMap(([, d]) => steps(d)));
const own = (g) => FLOW.filter(([, d]) => d[0] === g).flatMap(([, d]) => steps(d));
export const AUTHORIZATION = Object.fromEntries(ENTRY.map((g) => [g, new Set([...SHARED, ...own(g)])]));
const neighbor = new Map();
for (const [a, b] of EDGE) neighbor.set(a, [...(neighbor.get(a) || []), b]);
// A configured item checks authorization; a forgotten item lets through or stops per the fallback.
export function pathCount(forgotten, fallback) {
  const F = new Set(forgotten);
  let n = 0;
  for (const g of ENTRY) {
    const walk = (d, y) => {
      if (ASSET.includes(d)) return void n++;
      for (const k of neighbor.get(d) || []) {
        const e = edgeName(d, k);
        if (F.has(e)) { if (fallback === 'closed') continue; }
        else if (!AUTHORIZATION[g].has(e)) continue;
        if (!y.includes(k)) walk(k, [...y, k]);
      }
    };
    walk(g, [g]);
  }
  return n;
}
export function generator(seed) {
  let s = seed >>> 0;
  return () => { s = (Math.imul(s, 1664525) + 1013904223) >>> 0; return s / 4294967296; };
}
```

```js
// run1.mjs — when a single item is forgotten: path opened under open default, job stopped under closed
import { FLOW, steps } from './network.mjs';
import { EDGE_NAME, pathCount } from './default.mjs';

const full = pathCount([], 'open');
const stopped = (k) => FLOW.filter(([, d]) => steps(d).includes(k)).reduce((s, [, , n]) => s + n, 0);
const row = EDGE_NAME.map((k) => [k, pathCount([k], 'open') - full, stopped(k)]);
console.log(`path at full configuration ${full}, configuration item ${EDGE_NAME.length}\n`);
console.log('open default — item opening the most path when forgotten');
for (const [k, a] of [...row].sort((x, y) => y[1] - x[1]).slice(0, 5))
  console.log(`  ${k.padEnd(33)}+${a} path`);
console.log('\nclosed default — item stopping the most job when forgotten');
for (const [k, , c] of [...row].sort((x, y) => y[2] - x[2]).slice(0, 5))
  console.log(`  ${k.padEnd(33)}${c} daily request stopped`);
console.log('');
for (const [label, v] of [
  ['average path opened under open', (row.reduce((s, x) => s + x[1], 0) / row.length).toFixed(2)],
  ['item opening no path under open', `${row.filter((x) => x[1] === 0).length} / ${row.length}`],
  ['average daily request stopped under closed', Math.round(row.reduce((s, x) => s + x[2], 0) / row.length)],
  ['item stopping no job under closed', `${row.filter((x) => x[2] === 0).length} / ${row.length}`],
]) console.log(`${label.padEnd(45)}${v}`);
```

```
path at full configuration 20, configuration item 22

open default — item opening the most path when forgotten
  collector>queue                  +12 path
  management>work-order            +5 path
  collector>measurement-store      +2 path
  management>collector             +1 path
  management>configuration         +1 path

closed default — item stopping the most job when forgotten
  field-endpoint>collector         1440 daily request stopped
  collector>queue                  1440 daily request stopped
  queue>validator                  1440 daily request stopped
  validator>measurement-store      1440 daily request stopped
  collector>measurement-store      510 daily request stopped

average path opened under open               0.95
item opening no path under open              17 / 22
average daily request stopped under closed   319
item stopping no job under closed            2 / 22
```

The shapes of the two lists are different. Under the open default the distribution is extremely
uneven: one item opens twelve paths, one opens five, three open one or two each, and the
remaining seventeen items open nothing. The average is zero point nine five paths, but the
average is meaningless here — the cost is concentrated in one item. If the check on the queue's
write step is forgotten, the path count rises from twenty to thirty-two, meaning a third of all the
work that took the network from fifty-five paths down to twenty is handed back by forgetting a
single item.

Under the closed default the distribution is balanced the other way. Forgetting twenty of the
twenty-two items stops at least one legitimate job; the average is three hundred nineteen
requests a day. Four separate items stop one thousand four hundred forty requests a day, because
the field-reading and validation flows are the network's busiest work. In one sense this is the
cost of the closed default; in another sense it is **the guarantee that the error shows up
immediately**.

The overlap between the two lists also deserves attention. The queue's write step sits at the top of
both lists: it is the item that opens the most paths under the open default, and one of the four
items that stop the most work under the closed default. The most heavily used step is also the
step carrying the most paths, and it is among the most expensive items no matter which default
is chosen. The first place a
configuration check should look is the step carrying the most traffic. This item deserves
separate verification under either default; regardless of which default is chosen, an item that
alone carries a third of the achieved narrowing needs to be specifically tested for being written
correctly.

#### However Many Items Are Forgotten

One item is not a realistic error count. The same measurement is repeated when k items are
forgotten, and a third column is added: the rate at which the error leaves no trace at all.

```js
// run2.mjs — the cost of the two defaults and the visibility of the error when k items are forgotten
import { FLOW, steps } from './network.mjs';
import { EDGE_NAME, pathCount, generator } from './default.mjs';

const SEED = 20260802, SAMPLE = 200, full = pathCount([], 'open');
const r = generator(SEED);
const pick = (k) => {
  const a = [...EDGE_NAME];
  for (let i = a.length - 1; i > 0; i--) { const j = Math.floor(r() * (i + 1)); [a[i], a[j]] = [a[j], a[i]]; }
  return a.slice(0, k);
};
const stopped = (u) => FLOW.filter(([, d]) => steps(d).some((x) => u.includes(x))).reduce((s, [, , n]) => s + n, 0);
console.log(`seed ${SEED}, sample ${SAMPLE}, path at full configuration ${full}`);
console.log('forgotten  open: avg path worst  invisible  closed: avg stopped  worst  invisible');
for (const k of [1, 2, 3, 5, 8, 22]) {
  const count = k === 22 ? 1 : SAMPLE, o = { a: 0, am: 0, ag: 0, c: 0, cm: 0, cg: 0 };
  for (let i = 0; i < count; i++) {
    const u = k === 22 ? EDGE_NAME : pick(k);
    const ya = pathCount(u, 'open'), dur = stopped(u);
    o.a += ya; o.am = Math.max(o.am, ya); if (ya === full) o.ag++;
    o.c += dur; o.cm = Math.max(o.cm, dur); if (dur === 0) o.cg++;
  }
  console.log(`${String(k).padEnd(11)}${(o.a / count).toFixed(1).padEnd(15)}${String(o.am).padEnd(9)}` +
    `${`${Math.round((o.ag / count) * 100)}%`.padEnd(11)}${(o.c / count).toFixed(0).padEnd(21)}` +
    `${String(o.cm).padEnd(9)}${Math.round((o.cg / count) * 100)}%`);
}
```

```
seed 20260802, sample 200, path at full configuration 20
forgotten  open: avg path worst  invisible  closed: avg stopped  worst  invisible
1          21.3           32       73%        347                  1440     9%
2          22.3           37       53%        644                  2880     1%
3          23.2           41       44%        969                  2944     0%
5          24.9           42       22%        1343                 3423     0%
8          29.4           50       8%         2050                 3510     0%
22         55.0           55       0%         3513                 3513     0%
```
The invisible columns are this lesson's answer. When a single item is forgotten, seventy-three
percent of the errors under the open default leave no trace at all: the path count does not
change, nobody notices, no record drops. The same error under the closed default stops a job
with ninety-one percent probability and is seen the moment it stops. It is not the size of the
two errors that differs, it is their **discoverability**.

The gap closes as the number of forgotten items grows, but in the wrong direction. Under the
open default, forgetting three items still leaves forty-four percent silent; at eight items it
is eight percent. Under the closed default, silence drops to one percent at two items, zero at
three. This means a system running on the open default can carry accumulated misconfiguration
for years; a system running on the closed default announces itself by the second error.

The worst-case columns are not symmetric either. Under the open default, forgetting a single item
raises the path count to thirty-two, eight items to fifty; forgetting all twenty-two raises it to
fifty-five — a network as if no defense existed at all. Under the closed default, at the same
extreme the network stops entirely: all three thousand five hundred thirteen requests a day. The
result of both errors is catastrophic, but one is a silent opening, the other a loud outage. The
case for secure by default is that the second kind of error **is known to get fixed**; the first
kind is never fixed, because its existence is never known.

A bias in the model also has to be written down. Items were assumed to be forgotten with equal
probability. In reality the forgotten item is not chosen at random: a newly added service's
steps get forgotten more than old steps that rarely change. The two items that stop no job in
the table are steps that carry no legitimate traffic yet; newly added steps also fall into the
same group in their first days. Put together, these two observations mean the open default's
real cost comes out larger than the table shows: the place forgetting is most likely is the same
place the closed default stays most silent, and where the error is therefore seen latest. The
way to bring the measurement closer to reality is to tie the probability of being forgotten to
the item's age; this lesson does not take that step, but which way the result would shift once
it is taken is clear.

The average columns also give a balance point. When five items are forgotten, the open default
leaves an average of twenty-five paths open — five more than the twenty at full configuration.
The same five items under the closed default stop one thousand three hundred forty-three
requests a day, thirty-eight percent of the daily work. The closed default's cost is clearly
larger, and that cost lands directly on operations. The decision is made between these two
numbers — but a decision made without reading the invisible column is a decision that reads only
the first column.

#### Summary

- At full configuration all 22 items check authorization and the network narrows to 20 paths.
  The forgotten item's outcome depends only on the default; the two defaults give the same
  network when there is no error.
- Under the open default, the distribution of forgetting a single item is uneven: one item opens
  12 paths, 17 items open nothing. The average is 0.95 paths, but the cost is concentrated in
  one item.
- Under the closed default, 20 of the 22 items stop at least one job when forgotten; the average
  is 319 requests a day, the worst 1,440. This is both the cost and the guarantee that the error
  is seen immediately.
- The invisibility rate gives the real difference: when a single item is forgotten, 73% of the
  errors under the open default leave no trace at all, versus only 9% under the closed default.
- The gap closes in the wrong direction as the error count grows: at 3 items, the open default
  is still 44% silent, the closed default 0%. A system running on the open default accumulates
  misconfiguration.
- At the worst extreme, forgetting all 22 items turns the network into 55 paths under the open
  default — as if no defense existed; under the closed default it stops all 3,513 daily
  requests.

#### Next Step

Six principles were measured on the same path set, and the cost of each was counted. Defense in
depth closed fifty-two of the fifty-five paths and brought sixty-three items of maintenance
load; least privilege closed forty-three paths for the cost of eleven daily requests; isolation
brought the reached item down from thirteen to four point eight and required twenty gates; zero
trust brought the path down to twenty and added two extra steps per request; the
authentication-authorization distinction showed that a control sitting in the wrong layer leaves
thirty paths open; secure by default revealed that forgetting one item stays silent with
seventy-three percent probability under the open default.

There is a question none of these tables asked. **Who** is going to run the measurements? Who
will notice that a layer closes no new path, who will test that a gate is unnecessary, who will
scan for a forgotten item? The numbers did not appear by themselves; each one requires testing
independent of whoever built the defense. The next topic takes up this question: what is the
function of the roles that build defenses, think like an attacker, and combine both; which job
descriptions these tasks are divided into; and what determines the written boundary within which
a test can be carried out.

## Field and Roles

Who does the measuring and within what boundary it can be done: separating what blue, red, and purple teams each see within the same set of paths, counting the information gap, splitting security roles by responsibility item, measuring the item left unowned and the item claimed twice, establishing authorization, scope, and responsibility as countable items, and comparing the skill set professional competency paths measure.

### Blue, Red, and Purple Teams

Source: https://academia.sh/en/courses/introduction-to-cybersecurity/blue-red-and-purple-teams

How three teams differ over the same set of paths: in a 30-path access graph, the 11 paths the defense knows, the 15 the attacking side finds, the 8 items where the two do not overlap, and the 13 paths neither sees; how purple work drives the information gap from 6 down to 0, how the closed paths climb from 17 to 22 while the legitimate requests they stop fall from 2,764 to 2,037, and how 8 out-of-scope paths remain.

The previous topic measured six defense principles over the same set of paths and wrote, side
by side, how many paths each one closed and how much legitimate work it stopped. Every one of
those measurements carried a quiet assumption: **the set of paths itself is known.** If a closed
path could be counted, that count was already written down somewhere beforehand. This lesson
lifts that assumption and asks a single question: how many items separate the set of paths an
organization knows from the set of paths that actually exists, and who finds that gap.

There are three working modes. **The blue team** runs the defense: it keeps the inventory,
builds the controls, reads the log. **The red team** looks for paths to the same asset within
written authorization and a defined scope; every path it finds may be an item that is not on the
defense's list. **Purple work** is not a separate roster — it is the two sides working together
over the same set of paths. This lesson separates all three into one number: **who sees how many
paths within the same set of paths.**

#### Three Teams, One Set of Paths

The example is the course's fictional **regional measurement network**. The graph below is a
**model**; there is no real host, real address, or real service endpoint.

- **FR1.** The access graph is 12 nodes and 20 steps: 3 entry points, 2 protected assets. A
  **path** is a walk from an entry node to an asset with no repeated node.
- **FR2.** The defense knows a path only if **every** node on it is written in the asset
  inventory. A single node missing from the inventory makes every path through that node
  invisible.
- **FR3.** The attacking side finds a path only if every node is within **the written scope** and
  the step count does not exceed **the step budget**. In separate work the budget is 4 steps.
- **FR4.** In purple work the inventory is shared: every node the red side touches enters the
  inventory, and because no step is spent on discovery, the budget rises to 6.
- **FR5.** Each step carries a fixed number of legitimate requests per day; the numbers are
  fictional.
- **FR6.** A path closes when one step on it is cut. On a path it **understands**, the defense
  cuts the step carrying the fewest legitimate requests.
- **FR7.** On a path learned only from a report, whose intermediate nodes are not in the
  inventory, the defense cuts the one step it can name — the **entry step**.
- **FR8.** The cost to legitimate work is the total legitimate requests carried by the **distinct**
  set of cut steps; if the same step closes more than one path, it is counted once.

```js
// team/network.mjs — the access graph of a fictional regional measurement network (MODEL). There is
// no real host, real address, or real service endpoint; every number is fictional.

// node: inventory = is it listed in the defense's asset list;
//       scope     = is it within the bounds of written authorization (a third-party endpoint is outside).
export const NODE = `
  field-terminal       entry  yes  yes
  subscriber-portal    entry  yes  yes
  supplier-endpoint     entry  no   no
  branch-switch          mid   no   yes
  collector               mid   yes  yes
  validator                mid   yes  yes
  identity-directory        mid   yes  yes
  management-network         mid   yes  yes
  work-order                   mid   yes  yes
  backup-endpoint                mid   no   yes
  reading-store                   asset yes  yes
  billing                          asset yes  yes`
  .trim().split("\n").map((r) => {
    const [name, type, inv, scp] = r.trim().split(/\s+/);
    return { name, type, inventory: inv === "yes", scope: scp === "yes" };
  });

// edge: a step, and how many legitimate requests pass through it per day (fictional).
export const EDGE = `
  field-terminal     collector             420
  field-terminal     branch-switch          35
  subscriber-portal  validator             900
  subscriber-portal  identity-directory    760
  supplier-endpoint  management-network     18
  supplier-endpoint  backup-endpoint         6
  branch-switch      management-network     24
  branch-switch      collector             110
  collector          validator             380
  collector          reading-store         540
  validator          reading-store         610
  validator          billing               290
  identity-directory management-network    44
  identity-directory work-order           150
  management-network collector             60
  management-network backup-endpoint       12
  management-network identity-directory    38
  backup-endpoint    reading-store           9
  work-order         validator             130
  work-order         billing                95`
  .trim().split("\n").map((r) => {
    const [a, b, m] = r.trim().split(/\s+/);
    return { a, b, legit: Number(m) };
  });

const find = (name) => NODE.find((d) => d.name === name);
export const ENTRY = NODE.filter((d) => d.type === "entry").map((d) => d.name);
export const ASSET = NODE.filter((d) => d.type === "asset").map((d) => d.name);

// All simple paths: from an entry node to an asset, with no repeated node.
export function paths() {
  const out = {};
  for (const e of EDGE) (out[e.a] ??= []).push(e.b);
  const all = [];
  const walk = (d, trail) => {
    if (ASSET.includes(d)) { all.push([...trail]); return; }
    for (const s of out[d] ?? []) if (!trail.includes(s)) walk(s, [...trail, s]);
  };
  for (const g of ENTRY) walk(g, [g]);
  return all.map((trail) => ({
    trail,
    step: trail.length - 1,
    edges: trail.slice(0, -1).map((_, i) => `${trail[i]}>${trail[i + 1]}`),
  }));
}

export const edgeLegitimate = (e) => {
  const [a, b] = e.split(">");
  return EDGE.find((k) => k.a === a && k.b === b).legit;
};

// The defense knows a path only if every node in it is listed in its inventory.
export const blueKnows = (y) => y.trail.every((d) => find(d).inventory);
// The attacking side finds a path only if every node is in scope and the step count does not
// exceed its budget; touching an out-of-scope node steps outside the authorization.
export const redFinds = (y, budget) =>
  y.trail.every((d) => find(d).scope) && y.step <= budget;
```

#### Who Knows What

```js
// team/measure.mjs — within the same set of paths, the path the defense knows, the path the
// attacking side finds, and the path neither sees; then the legitimate-work cost of closing them.
import { paths, NODE, EDGE, edgeLegitimate, blueKnows, redFinds } from "./network.mjs";

const P = paths();
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 INV0 = new Set(NODE.filter((d) => d.inventory).map((d) => d.name));
const knows = (y, inv) => y.trail.every((d) => inv.has(d));
const BUDGET = 4, PURPLE_BUDGET = 6;   // fictional: purple work spends no step on discovery

// Purple work: every node the red side touches enters the inventory, and red reads the inventory.
const TOUCHED = new Set();
for (const y of P) if (redFinds(y, BUDGET)) y.trail.forEach((d) => TOUCHED.add(d));
const INV1 = new Set([...INV0, ...TOUCHED]);

const status = (inv, b) => {
  const m = P.filter((y) => knows(y, inv)), k = P.filter((y) => redFinds(y, b));
  const cut = m.filter((y) => k.includes(y));
  return { blue: m.length, red: k.length, common: cut.length, blueOnly: m.length - cut.length,
    redOnly: k.length - cut.length, neither: P.length - (m.length + k.length - cut.length) };
};
const SOLO = status(INV0, BUDGET), PURPLE = status(INV1, PURPLE_BUDGET);

console.log(`access graph MODEL: ${NODE.length} nodes, ${EDGE.length} steps, ` +
  `${P.length} simple paths; step budget is ${BUDGET} working separately, ${PURPLE_BUDGET} in purple work.`);
const W1 = [-38, 10, 8];
console.log("\n1. who knows what in the same set of paths");
write(W1, "measure", "separate", "purple");
for (const [ad, f] of [["path the defense knows", (r) => r.blue],
  ["path the attacking side finds", (r) => r.red], ["  known by both", (r) => r.common],
  ["  known by defense only", (r) => r.blueOnly],
  ["  known by attack only (gap)", (r) => r.redOnly],
  ["path neither sees", (r) => r.neither]]) write(W1, ad, f(SOLO), f(PURPLE));

// Closing: the step carrying the fewest legitimate requests is cut on a known path; on a path
// learned only from the report, the defense cuts the one step it can name — the entry step.
const cheapest = (y) => y.edges.reduce((a, e) => (edgeLegitimate(e) < edgeLegitimate(a) ? e : a));
const cost = (cut) => ({ step: cut.size,
  legit: [...cut].reduce((a, e) => a + edgeLegitimate(e), 0) });
const knownSolo = P.filter((y) => knows(y, INV0));
const knownPaired = P.filter((y) => knows(y, INV0) || redFinds(y, BUDGET));
const knownPurple = P.filter((y) => knows(y, INV1) || redFinds(y, PURPLE_BUDGET));
const CUT1 = new Set(knownSolo.map(cheapest));
const CUT2 = new Set([...knownSolo.map(cheapest), ...knownPaired.filter((y) => !knows(y, INV0)).map((y) => y.edges[0])]);
const CUT3 = new Set(knownPurple.map(cheapest));

const W2 = [-30, 10, 8, 10, 16];
console.log("\n2. legitimate-work cost of closing (legitimate requests: per day, fictional)");
write(W2, "work mode", "closed", "open", "cut", "legit stopped");
for (const [ad, y, k] of [["defense only", knownSolo, CUT1], ["defense + separate testing", knownPaired, CUT2],
  ["purple work", knownPurple, CUT3]]) {
  const b = cost(k);
  write(W2, ad, y.length, P.length - y.length, b.step, b.legit);
}
const costliest = [...CUT2].filter((e) => !CUT3.has(e))
  .reduce((a, e) => (edgeLegitimate(e) > edgeLegitimate(a) ? e : a));
const thatPath = knownPaired.find((y) => y.edges[0] === costliest && !knows(y, INV0));
console.log(`\nmost expensive cut in separate testing: ${costliest} (${edgeLegitimate(costliest)} legitimate requests),`);
console.log(`  path it closes: ${thatPath.trail.join(" > ")}`);
console.log(`  in purple work the same path closes by cutting ${cheapest(thatPath)}: ` +
  `${edgeLegitimate(cheapest(thatPath))} legitimate requests.`);
const outOfScope = P.filter((y) => !y.trail.every((d) => NODE.find((x) => x.name === d).scope));
console.log(`of the ${PURPLE.neither} paths still remaining after purple work, ${outOfScope.length} ` +
  `pass through an out-of-scope node.`);
```

```
access graph MODEL: 12 nodes, 20 steps, 30 simple paths; step budget is 4 working separately, 6 in purple work.

1. who knows what in the same set of paths
measure                                 separate  purple
path the defense knows                        11      22
path the attacking side finds                 15      22
  known by both                                9      22
  known by defense only                        2       0
  known by attack only (gap)                   6       0
path neither sees                             13       8

2. legitimate-work cost of closing (legitimate requests: per day, fictional)
work mode                         closed    open       cut   legit stopped
defense only                          11      19         7            1969
defense + separate testing            17      13         9            2764
purple work                           22       8        10            2037

most expensive cut in separate testing: subscriber-portal>identity-directory (760 legitimate requests),
  path it closes: subscriber-portal > identity-directory > management-network > backup-endpoint > reading-store
  in purple work the same path closes by cutting backup-endpoint>reading-store: 9 legitimate requests.
of the 8 paths still remaining after purple work, 8 pass through an out-of-scope node.
```

The `separate` column of the first table reads the setup where the three teams work
independently. The graph has 30 paths. The defense knows **11** of them; the attacking side
finds **15**. The two numbers look close, but they do not point at the same paths: the
intersection is only **9** items.

Two non-overlapping items are not the same kind of gap. **The 2 paths known only to the defense**
are harmless: they are 5 steps long, and the attacking side's budget is 4 steps. The defense knows
them and can close them; they do not stop existing just because testing did not find them. **The 6
paths found only by the attacking side**, though, are the **information gap**: they pass through
two nodes — the branch switch and the backup endpoint — that are not in the inventory, so the
defense's list never had them. The size of what an organization does not know about its own
network can only be written down once an outside test measures that number.

The last row gives what neither team sees: **13 paths**. This is nearly half the total, and it
has two separate causes. Part of it runs through nodes the defense's inventory does not have and
is too long for the attacking side's budget to reach; the rest starts from the third-party
endpoint, which is **outside the scope**. Testing a system without written authorization and a
defined scope is both illegal and unprofessional; the reason no one touches the out-of-scope node
is not a technical barrier but this boundary.

#### What Purple Work Closes

The `purple` column is the result of two changes, and both are an exchange of information. The
defense adds the nodes testing touches to its inventory; the branch switch and the backup
endpoint are now on the list. Because the testing side reads the inventory, it spends no step on
discovery, and its budget rises from 4 to 6. No new control was built, no new tool was bought, not
a single step in the graph changed.

The result changes across four rows at once. The path the defense knows goes from 11 to **22**,
the path the attacking side finds goes from 15 to **22**, and both are **the same 22 paths**: the
non-overlapping item drops to zero. The information gap falls **from 6 to 0**. The path neither
sees drops from 13 to **8**.

This is the measured contribution of purple work: **11 paths** the defense could not reach alone
enter the shared list. None of these paths are new — all 30 were in the graph from the start.
What closes is not a vulnerability; it is **an information gap**.

#### The Cost of Closing

The second table reads the same three setups in terms of the defense's actual job: how many
paths close, and how many legitimate requests are stopped in exchange.

When the defense works alone, 11 paths close, 19 stay open, and cutting 7 steps stops **1,969**
legitimate requests a day. Once testing is added, closed paths rise to 17, but stopped legitimate
requests jump to **2,764**: closing 6 more paths costs 795 more requests, or 132 per path. In
purple work, closed paths are **22** and stopped legitimate requests are **2,037**: compared to
the paired setup, **5 more paths close while 727 fewer legitimate requests are stopped.**

This reversal is explained in the last three rows. In a setup working separately, the defense has
to close a path it reads from a report, but that path's intermediate nodes are not in its
inventory; the only step it can name is the entry step. It cuts the step from the subscriber
portal to the identity directory, and **760** legitimate requests are stopped a day. The same
path closes in purple work by cutting the step from the backup endpoint to the reading store: **9**
legitimate requests. The path that closes is identical; the cost differs by a factor of **84**.

The rule follows from this: **a path closed without being understood closes, but closes
expensively.** A finding list says which path is open; it does not say which step should be cut.
Choosing that step requires knowing what job every node on the path does, and that knowledge sits
with the defense. What purple work closes is a two-way gap: the defense learns that the path
exists, and the testing side learns the legitimate work running over it.

#### Where Testing Is Not Enough

Even after purple work, **8 paths** are not on anyone's list, and the output's last line writes
that all 8 of these pass through an **out-of-scope** node. The third-party endpoint is neither in
the defense's inventory nor within the area testing is authorized for.

This remainder does not close with better testing. Raising the budget from 6 to 10 finds no new
path, because the obstacle is not the step count. Widening the inventory does not help either,
because that endpoint belongs to another organization, and authority to test it is granted only
by a separate written agreement. Part of the set of paths stays unmeasured not from a technical
gap but from **the boundary of authorization** — and an unmeasured path is an unclosed path.

#### Summary

- In the 30-path graph, the defense sees 11 paths and the attacking side sees 15; the
  intersection is 9. Of the 8 non-overlapping items, 2 are the long path known to the defense, and
  **6 are the information gap** — paths the defense never knew about because they pass through
  two nodes missing from the inventory.
- Working separately, 13 paths sit outside both teams' view; after purple work that number falls
  to 8, and the information gap drops from 6 to 0. Not one step in the graph changes — only
  information is shared.
- Purple work raises closed paths from 17 to 22 while lowering stopped legitimate requests from
  2,764 to 2,037: more paths, less cost.
- A path that is not understood closes expensively. On a path learned from a report, the defense
  cuts the entry step (760 legitimate requests); the same path in purple work closes with a step
  carrying 9 legitimate requests.
- All of the remaining 8 paths pass through an out-of-scope node. This remainder can only be
  measured by changing the boundary of authorization, not by growing the budget.

#### Next Step

This lesson separated three working modes and tied the difference between them to a single
number: who sees how many paths within the same set of paths. The measurement showed one thing —
the defense is not a roster, it is the sum of complementary **jobs**. The job that keeps the
inventory and the job that looks for the path sit in separate hands, and when the two do not talk,
11 paths never enter the shared list.

Still, "blue" and "red" are team names, not job names. Who keeps the inventory, who chooses which
step to cut, who reads the record left after a cut, who makes the architectural decision? The
next lesson divides the same set of paths this time by **role**, and looks for two numbers: how
many paths sit on no role at all, and how many sit on two roles at once.

### Security Roles

Source: https://academia.sh/en/courses/introduction-to-cybersecurity/security-roles

Splitting roles by responsibility item: of 9 item types and 3 duties across 27 cells, 15 have a single owner, 7 are unowned, 3 have two owners, and 2 sit past the boundary; the unowned item forces 12 paths to close from a more costly step, 2 paths never close, 15 closures require two approvals, and writing the boundary drops stopped legitimate requests from 2,321 to 2,135.

The previous lesson separated three working modes over the same set of paths and counted the
information gap that purple work closed. That measurement left one question open: once the
paths are known, **who** cuts which step. "Blue" and "red" are names for a working mode; what
does the work in an organization is not a mode, it is a responsibility item with a name written
down.

This lesson takes the paths as already known and measures ownership. Security work gathers into
five roles: **security operations** reads the log and runs the incident; **security testing**
looks for the path and confirms it is closed; **security architecture** sets the design decision
and the default; **governance** carries the policy, the risk, and the third-party contract;
**systems and network operations** actually applies the change. Two numbers are asked: in the
same set of paths, how many items sit on **no** role at all, and how many sit on **two** roles at
once.

#### Item, Duty, and Owner

Counting roles by name does not give a measure of responsibility; the same name covers different
work in two organizations. The measurable unit is the **responsibility item**: the thing that has
to change for a step to be cut.

- **FR9.** The set of paths comes from the previous lesson's graph: 20 steps, 30 paths. Each step
  belongs to an **item type**; there are 9 item types.
- **FR10.** Each item type has three duties: **track** (knowing the item exists and keeping it
  listed), **close** (applying the change), **sustain** (verifying it stays closed). The matrix
  is 9 x 3 = 27 cells.
- **FR11.** A cell's owner can be 0, 1, or 2 roles.
- **FR12.** A cell marked `outside` sits past the boundary of shared responsibility. Where that
  boundary runs is not this lesson's question; it is an **input**, not an internal gap.
- **FR13.** A path closes when one step on it is cut. A step can be cut only if its item has an
  internal owner in both its **track** and **close** cells: an item on no one's list never
  comes up, and even if it does, no one applies it.
- **FR14.** The defense picks the cuttable step carrying the fewest legitimate requests. A path
  with no cuttable step stays open.
- **FR15.** If a **close** cell has two owners, that closure earns two separate approval steps.
- **FR16.** Writing the role boundary means assigning one owner to every unowned cell and leaving
  a single owner in every two-owner cell; `outside` cells do not change.

```js
// role/item.mjs — the same access graph, this time labeled with a RESPONSIBILITY ITEM (MODEL).
// Each step belongs to an item type; each item type has three duties. Numbers are fictional.
export const EDGE = `
  field-terminal     collector             420 field-hardware
  field-terminal     branch-switch          35 inventory-record
  subscriber-portal  validator             900 application-control
  subscriber-portal  identity-directory    760 identity-authority
  supplier-endpoint  management-network     18 third-party-clause
  supplier-endpoint  backup-endpoint         6 third-party-clause
  branch-switch      management-network     24 network-rule
  branch-switch      collector             110 network-rule
  collector          validator             380 application-control
  collector          reading-store         540 data-classification
  validator          reading-store         610 data-classification
  validator          billing               290 application-control
  identity-directory management-network    44 identity-authority
  identity-directory work-order           150 identity-authority
  management-network collector             60 network-rule
  management-network backup-endpoint       12 backup-route
  management-network identity-directory    38 identity-authority
  backup-endpoint    reading-store           9 backup-route
  work-order         validator             130 application-control
  work-order         billing                95 patch-matrix`
  .trim().split("\n").map((r) => {
    const [a, b, m, k] = r.trim().split(/\s+/);
    return { a, b, legit: Number(m), item: k };
  });

export const ENTRY = ["field-terminal", "subscriber-portal", "supplier-endpoint"];
export const ASSET = ["reading-store", "billing"];
export const DUTY = ["track", "close", "sustain"];
export const ROLE = ["operations", "testing", "architecture", "governance", "systems"];

// Responsibility cell: item type x duty -> list of owning roles. "outside" = the other side of
// the shared-responsibility boundary; measured in the previous course, not an internal gap.
export const OWNER = {
  "network-rule":         { track: ["systems"], close: ["systems", "architecture"], sustain: ["operations"] },
  "identity-authority":   { track: ["operations"], close: ["systems"], sustain: ["operations", "governance"] },
  "application-control":  { track: ["testing"], close: ["architecture"], sustain: [] },
  "inventory-record":     { track: [], close: ["systems"], sustain: [] },
  "data-classification":  { track: ["governance"], close: ["architecture", "governance"], sustain: ["operations"] },
  "backup-route":         { track: [], close: ["systems"], sustain: [] },
  "patch-matrix":         { track: ["operations"], close: ["systems"], sustain: ["operations"] },
  "field-hardware":       { track: [], close: ["systems"], sustain: [] },
  "third-party-clause":   { track: ["governance"], close: ["outside"], sustain: ["outside"] },
};

export function paths() {
  const out = {};
  for (const k of EDGE) (out[k.a] ??= []).push(k.b);
  const all = [];
  const walk = (d, trail) => {
    if (ASSET.includes(d)) { all.push([...trail]); return; }
    for (const s of out[d] ?? []) if (!trail.includes(s)) walk(s, [...trail, s]);
  };
  for (const g of ENTRY) walk(g, [g]);
  return all.map((trail) => ({ trail, edges: trail.slice(0, -1).map((_, i) =>
    EDGE.find((k) => k.a === trail[i] && k.b === trail[i + 1])) }));
}
```

#### Unowned Cells and Overlapping Cells

```js
// role/measure.mjs — responsibility cell count, load per role, and the path-level result.
import { EDGE, OWNER, DUTY, ROLE, paths } from "./item.mjs";

const P = paths(), ITEM = Object.keys(OWNER);
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 internal = (s) => s.filter((r) => r !== "outside");
const classify = (s) => (s[0] === "outside" ? "outside" : s.length === 0 ? "NONE"
  : s.length > 1 ? "BOTH" : "one");

console.log(`responsibility matrix MODEL: ${ITEM.length} item types x ${DUTY.length} duties ` +
  `= ${ITEM.length * DUTY.length} cells; ${ROLE.length} roles, ${EDGE.length} steps, ` +
  `${P.length} paths.`);
const T = [-22, 12, 25, 23];
console.log("\n1. which cell has an owner (outside = the other side of the shared-responsibility boundary)");
write(T, "item type", ...DUTY);
for (const k of ITEM) write(T, k, ...DUTY.map((g) => OWNER[k][g].join("+") || "-"));
const count = (c) => ITEM.reduce((a, k) =>
  a + DUTY.filter((g) => classify(OWNER[k][g]) === c).length, 0);
console.log(`cell distribution: one owner ${count("one")}, unowned ${count("NONE")}, ` +
  `two owners ${count("BOTH")}, outside ${count("outside")}.`);

// A step is cuttable only if its item has an internal owner in both its track and close cells.
const cuttable = (e) => internal(OWNER[e.item].track).length > 0 && internal(OWNER[e.item].close).length > 0;
const R = [-20, 10, 10];
console.log("\n2. load per role");
write(R, "role", "cells", "paths");
for (const r of ROLE) {
  const h = ITEM.reduce((a, k) => a + DUTY.filter((g) => OWNER[k][g].includes(r)).length, 0);
  const y = P.filter((p) => p.edges.some((e) =>
    cuttable(e) && DUTY.some((g) => OWNER[e.item][g].includes(r)))).length;
  write(R, r, h, y);
}

// Writing the role boundary: one owner for every unowned cell, a single owner in every shared cell.
const WRITTEN = Object.fromEntries(ITEM.map((k) => [k, Object.fromEntries(DUTY.map((g) => {
  const s = OWNER[k][g];
  return [g, s[0] === "outside" ? s : s.length === 0 ? ["systems"] : [s[0]]];
}))]));
const decisions = ITEM.reduce((a, k) =>
  a + DUTY.filter((g) => ["NONE", "BOTH"].includes(classify(OWNER[k][g]))).length, 0);

function run(M) {
  const fit = (e) => internal(M[e.item].track).length > 0 && internal(M[e.item].close).length > 0;
  const cut = new Set(); let closed = 0, approvals = 0, unverified = 0;
  for (const y of P) {
    const u = y.edges.filter(fit).sort((a, b) => a.legit - b.legit);
    if (u.length === 0) continue;
    closed++; cut.add(u[0]);
    if (internal(M[u[0].item].close).length > 1) approvals++;
    if (internal(M[u[0].item].sustain).length === 0) unverified++;
  }
  return { closed, open: P.length - closed, steps: cut.size, unverified, approvals,
    legit: [...cut].reduce((a, e) => a + e.legit, 0) };
}
const CURRENT = run(OWNER), REVISED = run(WRITTEN);
const W3 = [-32, 12, 12];
console.log(`\n3. path-level result (writing the role boundary takes ${decisions} cell decisions)`);
write(W3, "measure", "current", "written");
for (const [ad, f] of [["closed path", (r) => r.closed], ["open path", (r) => r.open],
  ["step cut", (r) => r.steps], ["legit request stopped (day)", (r) => r.legit],
  ["closure needing two approvals", (r) => r.approvals],
  ["closed but unverified", (r) => r.unverified]]) write(W3, ad, f(CURRENT), f(REVISED));

const shifted = P.filter((y) => {
  const u = y.edges.filter(cuttable).sort((a, b) => a.legit - b.legit);
  return u.length > 0 && u[0] !== [...y.edges].sort((a, b) => a.legit - b.legit)[0];
}).length;
const unownedItem = ITEM.filter((k) => !internal(OWNER[k].track).length || !internal(OWNER[k].close).length);
console.log(`\npath closed from a more costly step because its cheapest step is not cuttable: ${shifted};`);
console.log(`item types that cannot be cut: ${unownedItem.join(", ")}.`);
const openPaths = P.filter((y) => y.edges.every((e) => !cuttable(e)));
for (const y of openPaths) console.log(`path left open: ${y.trail.join(" > ")}`);
```

```
responsibility matrix MODEL: 9 item types x 3 duties = 27 cells; 5 roles, 20 steps, 30 paths.

1. which cell has an owner (outside = the other side of the shared-responsibility boundary)
item type                    track                    close                sustain
network-rule               systems     systems+architecture             operations
identity-authority      operations                  systems  operations+governance
application-control        testing             architecture                      -
inventory-record                 -                  systems                      -
data-classification     governance  architecture+governance             operations
backup-route                     -                  systems                      -
patch-matrix            operations                  systems             operations
field-hardware                   -                  systems                      -
third-party-clause      governance                  outside                outside
cell distribution: one owner 15, unowned 7, two owners 3, outside 2.

2. load per role
role                     cells     paths
operations                   6        26
testing                      1        18
architecture                 3        25
governance                   4        22
systems                      7        23

3. path-level result (writing the role boundary takes 10 cell decisions)
measure                              current     written
closed path                               28          30
open path                                  2           0
step cut                                  11          12
legit request stopped (day)             2321        2135
closure needing two approvals             15           0
closed but unverified                      5           0

path closed from a more costly step because its cheapest step is not cuttable: 12;
item types that cannot be cut: inventory-record, backup-route, field-hardware, third-party-clause.
path left open: supplier-endpoint > management-network > backup-endpoint > reading-store
path left open: supplier-endpoint > backup-endpoint > reading-store
```

The first table splits the 27 cells into four classes: **15 with a single owner, 7 unowned, 3
with two owners, 2 outside.** Of the 25 internal cells, only 15 have exactly one owner; **not
just a tenth but a full 40% are either undefined or doubly defined.**

The seven unowned cells are spread over four item types, and all of them are missing in the same
direction: `inventory-record`, `backup-route`, and `field-hardware` have empty **track** and
**sustain** cells while their **close** cells are filled. This means the organization knows how
to change these three items but has given no one the job of **tracking** them: who keeps the
inventory record, who knows the backup route exists, who lists the field hardware has no written
answer. Being able to change an item is not the same as managing it — the item that will change
has to be on the list first. `application-control` also has an empty sustain cell: the control
gets built, but no role is written down to notice if it is removed.

The three two-owner cells are a different kind of gap. The `network-rule` close cell sits with
systems and architecture, the `data-classification` close cell with architecture and governance,
the `identity-authority` sustain cell with operations and governance. These cells **are not
empty**, but every change to them passes through two separate approvals.

The second table gives the load per role, and its two columns do not track each other. `systems`
is the busiest role with 7 cells, but it is not part of the security team — it is the hand that
actually cuts the step. `testing` owns only **1** cell; in exchange, 18 paths' closure depends
on the item it tracks. Cell count does not measure how much work a role does, nor how many paths
run through it either — the two columns answer different questions. This is also why the same
path is counted under more than one role: closing a path usually needs an owner in all three
duties at once.

#### The Path-Level Cost of Being Unowned

The third table brings the cell count down to the set of paths. In the current distribution,
**28 paths close and 2 stay open**. The two open paths are the output's last lines, and both run
from the third-party endpoint through the backup endpoint to the reading store. Every step on
them is either past the boundary or on no role's list — a path stays open only if every step on
it is unowned, and that is exactly what has happened here.

The real cost is not in the open paths — it is in **how the closed paths close.** The output's
third-from-last line writes this: **12 paths cannot close from their cheapest step**, because that
step's item is on no role's list. The defense shifts to the next cuttable step, and that step
carries more legitimate work. Stopped legitimate requests are **2,321** a day in the current
distribution and **2,135** once the role boundary is written. The **186-request** gap between
them buys no security at all; it is purely the cost of not being able to choose the step to cut.

The `closure needing two approvals` row gives the cost of overlap: **15 closures** earn two
separate approval steps. Half of the thirty paths close through an item both roles claim, and
each one requires two decisions. Overlap does not leave a path open; **it slows closure down and
doubles every change.**

The `closed but unverified` row gives the third item: **5 paths** close, but no role verifies
that they stay closed. These paths count as closed in the table. If they reopen at the next
configuration change, no written owner is there to notice — an empty sustain cell means closure
is **unmeasured**.

#### The Cost of Writing the Boundary

The `written` column is the result of ten cell decisions: seven unowned cells each get an owner,
three overlapping cells each keep a single owner. Closed paths rise from 28 to **30**, open paths
drop to zero, stopped legitimate requests fall from 2,321 to **2,135**, closures needing two
approvals fall from 15 to **0**, and unverified paths fall from 5 to **0**.

These ten decisions are not a technical effort. No new control is built, no new step is cut,
nothing in the graph changes; only ten cells get a name written into them. In exchange, two more
paths close and 186 fewer legitimate requests are stopped a day.

The measurement also carries a warning. Every unowned cell sits in the **track** and **sustain**
columns, none in the **close** column. Organizations find it easy to name the hand that does the
work; naming the hand that keeps the list and confirms the result stays behind is the part they
skip. The most commonly skipped cell in a responsibility matrix is not the one where the work
happens — it is **the one that notices the work is needed and checks the outcome.**

#### Summary

- In the 27-cell responsibility matrix, 15 cells have a single owner; 7 cells are unowned, 3 have
  two owners, and 2 sit past the shared-responsibility boundary. 40% of the 25 internal cells are
  either undefined or doubly defined.
- All seven unowned cells sit in the **track** and **sustain** columns. The hand that changes the
  item is written; the hand that keeps it listed and verifies it stays closed is not.
- Cell count per role does not track path count: testing owns a single cell, yet 18 paths' closure
  depends on the item it tracks.
- In the current distribution 28 paths close and 2 stay open; 12 of the closed paths cannot close
  from their cheapest step, so they close from a more costly one and stop 186 more legitimate
  requests a day.
- 15 closures earn a second approval step, and 5 paths count as closed without their staying
  closed ever being verified. Writing the boundary is ten cell decisions, and it turns these three
  numbers into 30, 0, and 0 respectively.

#### Next Step

Writing the role boundary closed ten cells and turned two more paths closable. Still, two cells in
the matrix stayed `outside` from start to finish, and this lesson never touched them: the
third-party clause's close and sustain cells take no internal owner.

The reason for those two cells is not technical. Cutting a step, testing a path, adding an item to
the list — all of it depends on someone's authority to act on that system, and that authority is
granted not by a table but by a document. The next lesson builds that authority as a countable
item: how many assets sit within the written scope, how many out-of-scope assets sit on the same
network, and how many items scope ambiguity leaves open to dispute.

### Legal Framework and Ethics

Source: https://academia.sh/en/courses/introduction-to-cybersecurity/legal-framework-and-ethics

Establishing authorization, scope, and responsibility as countable items: in an 18-asset inventory, four scope wordings cover 8, 13, 17, and 15 assets; the disputed item count is 0, 6, 9, and 0; in the wording written as a named list, 9 neighboring assets sit in the same segment; two assets require separate written permission each; and the undisputedly testable path count is 8, 5, 8, and 22.

The previous lesson measured responsibility in a 27-cell matrix and left two cells unowned within
the organization: the third-party clause's close and sustain cells. The reason for those two
cells is not technical. Cutting a step, testing a path, or adding an asset to the list all depend
on someone's authority to act on that system, and that authority is granted not by a table but by
**a document**.

Testing a system without written authorization and a defined scope is both illegal and
unprofessional; a path being technically walkable does not make walking it a legitimate act. This
lesson treats that document itself as a measurable object. **Rules of engagement** are the text
that writes what a test covers, when, and up to what boundary; **scope** is that text's set of
assets. What's asked is not the text's tone — it is how many items it defines with a single
meaning.

#### Writing Scope as an Item

- **FR17.** The inventory is 18 assets: the 12 nodes of the set of paths plus 6 neighboring
  assets sitting in the same network segments. Each asset's owner (organization, partner,
  supplier) and network segment are written down. Both the inventory and the network are
  fictional.
- **FR18.** Scope is written in four forms: a **named list** (every asset by name), a
  **segment range** (every asset in the written segments), an **exclusion** (everything except a
  given segment), **revised wording**.
- **FR19.** An asset is **undisputedly** in scope only if the text covers it both by form **and**
  by name. An asset covered only by form is **disputed**: the text's narrow reading leaves it
  out, the broad reading leaves it in.
- **FR20.** A path is undisputedly testable only if every node on it is undisputedly in scope.
- **FR21.** An asset not owned by the organization still requires **separate written
  permission** even if it falls inside the scope text; a text an organization writes for itself
  does not produce authority over another organization's asset.
- **FR22.** In a scope written as a named list, a single operation at the segment level would
  touch an out-of-scope neighbor, so every asset is tested in its own window; in a range-written
  scope, one window per segment is enough.
- **FR23.** In the test window, the legitimate requests carried by the tested assets are
  stopped.
- **FR24.** Revised wording names every asset it covers and explicitly excludes assets owned
  outside the organization.

```js
// scope/authority.mjs — four written-scope formats and the asset inventory (MODEL).
// There is no real organization, real contract, or real asset; every number is fictional.
export const ASSET = `
  field-terminal       org      field      yes
  subscriber-portal    org      subscriber yes
  collector            org      core       yes
  validator            org      core       yes
  work-order           org      core       yes
  reading-store        org      core       yes
  billing              org      core       yes
  identity-directory   org      management yes
  branch-switch        org      field      no
  field-crew-tablet    org      field      no
  branch-printer       org      field      no
  subscriber-record-copy org    subscriber no
  partner-analytics-endpoint partner core   no
  supplier-portal       supplier subscriber no
  management-network   org      management no
  backup-endpoint       org     management no
  test-copy             org     management no
  supplier-endpoint     supplier outside   no`
  .trim().split("\n").map((r) => {
    const [name, owner, segment, listed] = r.trim().split(/\s+/);
    return { name, owner, segment, named: listed === "yes" };
  });

export const EDGE = `
  field-terminal     collector             420
  field-terminal     branch-switch          35
  subscriber-portal  validator             900
  subscriber-portal  identity-directory    760
  supplier-endpoint  management-network     18
  supplier-endpoint  backup-endpoint         6
  branch-switch      management-network     24
  branch-switch      collector             110
  collector          validator             380
  collector          reading-store         540
  validator          reading-store         610
  validator          billing               290
  identity-directory management-network    44
  identity-directory work-order           150
  management-network collector             60
  management-network backup-endpoint       12
  management-network identity-directory    38
  backup-endpoint     reading-store          9
  work-order          validator            130
  work-order           billing              95`
  .trim().split("\n").map((r) => {
    const [a, b, m] = r.trim().split(/\s+/);
    return { a, b, legit: Number(m) };
  });

export const ENTRY = ["field-terminal", "subscriber-portal", "supplier-endpoint"];
export const TARGET = ["reading-store", "billing"];
export const FORMAT = {
  "named-list": (v) => v.named,
  "segment": (v) => ["field", "subscriber", "core"].includes(v.segment),
  "exclusion": (v) => v.segment !== "outside",
  "revised": (v) => v.owner === "org" && v.segment !== "outside",
};
// In range-written formats, a single test window per segment is enough.
export const RANGE = ["segment", "exclusion", "revised"];
// These two formats also write every asset they cover BY NAME; the other two rely on the old named list.
export const NAMED = ["named-list", "revised"];

export function paths() {
  const out = {};
  for (const k of EDGE) (out[k.a] ??= []).push(k.b);
  const all = [];
  const walk = (d, trail) => {
    if (TARGET.includes(d)) { all.push([...trail]); return; }
    for (const s of out[d] ?? []) if (!trail.includes(s)) walk(s, [...trail, s]);
  };
  for (const g of ENTRY) walk(g, [g]);
  return all.map((trail) => ({ trail }));
}
```

#### Four Wordings, One Inventory

```js
// scope/measure.mjs — four scope formats read over the same inventory and the same set of paths.
import { ASSET, EDGE, FORMAT, RANGE, NAMED, paths } from "./authority.mjs";

const P = paths(), FMT = Object.keys(FORMAT);
const write = (g, ...s) => console.log(s.map((v, i) =>
  (g[i] < 0 ? String(v).padEnd(-g[i]) : String(v).padStart(g[i]))).join(""));

function measure(b) {
  const covered = ASSET.filter(FORMAT[b]);
  const named = (v) => v.named || NAMED.includes(b);        // does the text name the asset
  const undisputed = covered.filter(named);                 // both named and inside the format
  const disputed = covered.filter((v) => !named(v));        // inside only under the broad reading
  const conflicting = ASSET.filter((v) => v.named && !FORMAT[b](v));
  const permission = covered.filter((v) => v.owner !== "org");   // needs separate written permission
  const segments = new Set(covered.map((v) => v.segment));
  const neighbor = ASSET.filter((v) => !FORMAT[b](v) && segments.has(v.segment));
  const u = new Set(undisputed.map((v) => v.name)), c = new Set(covered.map((v) => v.name));
  return { covered: covered.length, undisputed: undisputed.length, disputed: disputed.length,
    conflicting: conflicting.length, permission: permission.length, neighbor: neighbor.length,
    window: RANGE.includes(b) ? segments.size : covered.length,
    testable: P.filter((y) => y.trail.every((x) => u.has(x))).length,
    contested: P.filter((y) => y.trail.every((x) => c.has(x)) && !y.trail.every((x) => u.has(x))).length,
    outside: P.filter((y) => !y.trail.every((x) => c.has(x))).length,
    legit: EDGE.filter((k) => u.has(k.a)).reduce((a, k) => a + k.legit, 0) };
}
const R = Object.fromEntries(FMT.map((b) => [b, measure(b)]));

console.log(`inventory MODEL: ${ASSET.length} assets, ${EDGE.length} steps, ${P.length} paths; ` +
  `four scope formats read over the same set.`);
const T = [-44, 13, 9, 11, 14];
console.log("\n1. how many items the written scope covers, how many are open to dispute");
write(T, "measure", ...FMT);
for (const [ad, f] of [["asset counted in scope", (r) => r.covered],
  ["  inside under both readings", (r) => r.undisputed],
  ["  inside only under the broad reading", (r) => r.disputed],
  ["asset conflicting with the named list", (r) => r.conflicting],
  ["asset needing separate written permission", (r) => r.permission],
  ["out of scope, same segment", (r) => r.neighbor]]) write(T, ad, ...FMT.map((b) => f(R[b])));

console.log("\n2. counterpart in the set of paths, and the legitimate-work cost");
write(T, "measure", ...FMT);
for (const [ad, f] of [["undisputed testable path", (r) => r.testable],
  ["disputed path", (r) => r.contested], ["path left out of scope", (r) => r.outside],
  ["separate test window", (r) => r.window],
  ["legit request stopped (day)", (r) => r.legit]]) write(T, ad, ...FMT.map((b) => f(R[b])));

const t = R["segment"], s = R["revised"];
console.log(`\nin the segment format, ${t.disputed} assets and ${t.contested} paths are open to dispute; ` +
  `also ${t.conflicting} asset is in the named list but falls outside the range.`);
console.log(`the revised wording leaves ${s.disputed} disputed assets and makes ${s.testable} ` +
  `paths testable; the remaining ${s.outside} paths pass through an asset owned outside the org.`);
```

```
inventory MODEL: 18 assets, 20 steps, 30 paths; four scope formats read over the same set.

1. how many items the written scope covers, how many are open to dispute
measure                                        named-list  segment  exclusion       revised
asset counted in scope                                  8       13         17            15
  inside under both readings                            8        7          8            15
  inside only under the broad reading                   0        6          9             0
asset conflicting with the named list                   0        1          0             0
asset needing separate written permission               0        2          2             0
out of scope, same segment                              9        0          0             2

2. counterpart in the set of paths, and the legitimate-work cost
measure                                        named-list  segment  exclusion       revised
undisputed testable path                                8        5          8            22
disputed path                                           0        3         14             0
path left out of scope                                 22       22          8             8
separate test window                                    8        3          4             4
legit request stopped (day)                          4354     4160       4354          4607

in the segment format, 6 assets and 3 paths are open to dispute; also 1 asset is in the named list but falls outside the range.
the revised wording leaves 0 disputed assets and makes 22 paths testable; the remaining 8 paths pass through an asset owned outside the org.
```
The first table reads four texts over the same 18-asset inventory. Assets covered are 8, 13, 17,
and 15. Read on its own, this row makes exclusion look widest and the named list look narrowest,
as if a scope text's strength were measured by this first row alone.

The second row breaks that impression. **Assets undisputedly in scope are 8, 7, 8, and 15.** The
exclusion wording covers nine more assets than the named list, but **none** of those nine are
inside under both readings; anyone reading the text narrowly counts them as outside. The sentence
that widens scope has not added a single undisputed item.

The segment wording gives a sharper result. Assets covered rise from 8 to 13, but the undisputed
item **drops from 8 to 7**. The reason is in the fourth row: **one asset is named in the list but
falls outside the written segment range.** Two sentences in the same text cut against each other,
and whether that asset is in scope cannot be read off the text. Writing a range meant to widen
scope has instead lowered the undisputed item count.

#### Where Authority Is Not the Same Thing as Scope

The fifth row draws a separate boundary: in the segment and exclusion wordings, **two** of the
covered assets are owned outside the organization — one is a partner organization's analytics
endpoint, one is a supplier's portal. A text the organization writes for itself can count these
two assets as in scope; that text does not produce **authority** over another organization's
asset. Each one needs a separate written permission, and until that permission arrives these
items sit not in scope but **in dispute**.

The sixth row gives the boundary in the other direction. In the named-list wording, **9 assets**
are out of scope but sit in the **same network segment** as the assets in scope. A single
operation carried out at the segment level touches all nine of them; touching an out-of-scope
asset means stepping outside written authority. The named list zeroes out ambiguity, but that
precision has a coordination cost: testing runs not in one window but in a separate window per
asset.

These two rows show the same text answering three separate questions. **Scope** writes which
asset is tested; **authority** writes whose signature grants that test; **responsibility** writes
who answers when harm results. The three are separate items, and a text can write one while
skipping the other two. The two assets owned outside the organization have scope written but lack
authority. The nine named-list neighbors have scope written openly — they are outside — but when a
segment-level operation touches them, who is responsible is not written anywhere. A text's measure
is not how many assets it counts; it is **whether it answers all three of these questions for every
asset.**

Ethical obligation sits outside these three items and is broader than them. An out-of-scope path
can show itself during testing; not testing it is a legal requirement, **reporting** it is a
professional one. Likewise, not overrunning the window, not attempting more than the scope text
authorizes, and passing findings only to the signing party are obligations of the party running
the test even where the written text is silent. Written authority draws a ceiling; the
professional standard also covers staying under that ceiling.

#### Scope's Counterpart in the Set of Paths

The second table brings the same four texts down to the set of paths and gives scope's real
measure.

Undisputed testable paths are 8, 5, 8, and **22**. The named list and the exclusion wording give
the same number: the nine extra assets the exclusion wording covers are disputed, so they do not
make a single path undisputed. The segment wording **drops to 5** — the one conflicting asset
alone turns three paths through it disputed.

The disputed path row is 0, 3, 14, and 0. In the exclusion wording, **nearly half** of the thirty
paths have no textual answer to whether testing them is legitimate. Each of these fourteen paths
is a responsibility question: if it is tested and an outage results, who does the written text
point to. Leaving the question unanswered leaves both the party running the test and the party
requesting it exposed. **A disputed item is an unmeasured path if it is not tested, and an
unwritten act if it is.**

Paths left out of scope are 22, 22, 8, and 8. In revised wording, all eight remaining paths pass
through an asset owned outside the organization. These eight paths are **the same** eight paths
that remained after purple work in the first lesson: there they went unseen because of the
authorization boundary; here that same boundary keeps them untestable. The length of the set of
paths an organization cannot measure on its own network is set not by its own text but by
**another organization's signature.**

#### The Cost of Correct Wording

Revised wording names every item and explicitly excludes assets owned outside the organization:
15 assets, 0 disputed items, 0 separate permissions, 22 testable paths. The last two rows give
this outcome's cost. Testing runs in 4 separate windows, and the legitimate requests stopped
across those windows are **4,607** a day — the **highest** of the four wordings.

This reversal is deliberate. The named list stops 4,354 legitimate requests, the segment wording
4,160; both stop less work because they undisputedly cover fewer assets. Writing scope correctly
does not make testing cheaper — **it makes it more expensive.** What's bought in exchange is the
tested-path count rising from 8 to 22 and the disputed item dropping to zero. If testing never
stops any work at all, there is likely also an asset it never touches.

#### Summary

- In the 18-asset inventory, four scope wordings cover 8, 13, 17, and 15 assets; assets
  undisputedly in scope are 8, 7, 8, and 15. The two sentences that widen scope — the segment
  range and the exclusion — do not increase the undisputed item; the only wording that does is
  the one naming every asset.
- In the segment wording, one asset is named in the list yet falls outside the range; this single
  conflict alone drops undisputed testable paths from 8 to 5.
- Two assets owned outside the organization fall inside the scope text, but the text produces no
  authority over them; each needs separate written permission. In the named-list wording, 9
  out-of-scope assets sit in the same segment as the assets in scope, splitting testing into 8
  separate windows.
- Disputed paths number 0, 3, 14, and 0. A disputed item is an unmeasured path if untested, and an
  unwritten act if tested.
- Revised wording makes 22 paths testable and zeroes out the disputed item; the remaining 8 paths
  pass through an asset owned outside the organization and can only be measured with another
  organization's written permission.
- Correct wording costs the most: 4,607 legitimate requests are stopped a day, against 4,160 and
  4,354. Testing that never stops any legitimate work has probably also never touched the assets
  it does not reach.

#### Next Step

Authority is a document, and that document has now been measured: how many items it defines with
a single meaning, how many it opens to dispute, how many paths it makes testable, and how much
legitimate work it stops in exchange. Once the text is written, testing becomes executable.

One question remains: how the person **able** to do the work the text authorizes is told apart
from everyone else. When an organization wants a path tested, two separate measurements face it —
knowledge measured in an exam room and skill measured in a lab. The final lesson compares the set
of skills these two measure over the same item list: how many items they agree on, how many they
diverge on, and which item neither measures.

### Professional Competency Paths

Source: https://academia.sh/en/courses/introduction-to-cybersecurity/professional-competency-paths

Separating competency paths by what they measure: in a 15-item competency set, multiple-choice certification measures 2 items, the practical exam 9, and the lab environment 6; the two exam formats agree on 2 items and diverge on 7; the three paths together measure 10 items; and the 5 items none of them measure carry 39.2% of the total path load.

The previous lesson measured authority as a document: how many items it defines with a single
meaning, how many it opens to dispute, and how many paths it makes testable. Once the text is
written, the work becomes executable; what remains is how the person **able** to do that work is
told apart.

An organization hands this question to three separate measurements. A **multiple-choice
certification** measures recognizing an answer in an exam room. A **practical exam** measures
reaching an end state within a set time. A **lab environment** lets a person work through a
setup they build themselves, one with no real consequence, again and again. All three measure
something, but not the same thing. This lesson separates the three on a single question: **how
many of the competency items this course requires do they measure.**

#### Writing Competency as an Item

- **FR25.** The competency set is 15 items, drawn from the work the course's fifteen lessons
  genuinely required: counting paths, adding a layer, narrowing privilege, choosing which step to
  cut, and the like.
- **FR26.** Each item has three properties. **Form:** is the item demonstrated by knowledge (`k`)
  or by practice (`p`). **Single meaning:** is there a single correct end state that can be
  tested. **Consequence:** does getting it wrong carry a real cost — legitimate work stops,
  another team is affected.
- **FR27.** Multiple-choice certification measures only an item whose form is knowledge and
  which has a single meaning. The practical exam measures every item with a single meaning; it
  cannot grade one with no defined end state. The lab environment covers every item with no real
  consequence; an item with a real consequence does **not** occur in a lab.
- **FR28.** Each item's **path load** is the number of paths, in this course's 30-path model,
  that cannot close without that item. Loads can be summed, but the same path is counted under
  more than one item; the total is a **load**, not a distinct path count.
- **FR29.** The model is fictional; there is no real exam, certification, organization, or lab
  product.

```js
// competency/measure.mjs — the 15 competency items this course requires and the coverage of the
// three measurement pathways (MODEL). There is no real exam, certification, organization, or lab.
// form: k = demonstrated by knowledge, p = demonstrated by practice
// single: is there a testable single correct end state | cost: does being wrong have a real cost
// path: in this course's 30-path model, the number of paths that cannot close without this item
const ITEM = `
  term-separation         k yes no  30
  risk-ranking            k yes no  12
  path-counting           p yes no  30
  layer-adding            p yes yes 22
  privilege-narrowing     p yes yes 17
  bulkhead-design         p yes yes 14
  default-review          p yes no   8
  log-analysis            p yes no   6
  incident-response       p yes yes  5
  environment-setup       p no  no   0
  step-selection          p no  yes 22
  legit-work-cost         p no  yes 22
  inventory-keeping       p no  yes 19
  scope-writing           k no  yes 22
  boundary-recognition    k no  yes  8`
  .trim().split("\n").map((r) => {
    const [name, form, single, cost, path] = r.trim().split(/\s+/);
    return { name, form, single: single === "yes", cost: cost === "yes", path: Number(path) };
  });

// Three measurement pathways, defined as predicates over item properties.
const WAY = {
  "multiple-choice": (k) => k.form === "k" && k.single,   // a recognizable single right answer
  "practical-exam": (k) => k.single,                       // a testable end state
  "lab": (k) => !k.cost,                                    // an item with a real cost does not occur here
};
const NAME = Object.keys(WAY);
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 load = (s) => s.reduce((a, k) => a + k.path, 0);
const TOTAL = load(ITEM), share = (x) => `${((x / TOTAL) * 100).toFixed(1)}%`;

console.log(`competency MODEL: ${ITEM.length} items, total path load ${TOTAL} ` +
  `(the same path is counted under more than one item; this is a load total, not a distinct path count).`);
const T = [-26, 18, 20, 16];
console.log("\n1. how many items each measurement pathway measures");
write(T, "measure", ...NAME);
for (const [ad, f] of [["item measured", (b) => `${ITEM.filter(WAY[b]).length}/${ITEM.length}`],
  ["path load measured", (b) => load(ITEM.filter(WAY[b]))],
  ["share of path load", (b) => share(load(ITEM.filter(WAY[b])))]]) write(T, ad, ...NAME.map(f));

const both = ITEM.filter((k) => WAY["multiple-choice"](k) && WAY["practical-exam"](k));
const onlyOne = ITEM.filter((k) => WAY["multiple-choice"](k) !== WAY["practical-exam"](k));
const none = ITEM.filter((k) => !NAME.some((b) => WAY[b](k)));
const union = ITEM.filter((k) => NAME.some((b) => WAY[b](k)));
const W2 = [-38, 10, 13];
console.log("\n2. where the two exam formats agree, where they diverge");
write(W2, "set", "items", "path load");
for (const [ad, s] of [["measured by both", both], ["measured by only one", onlyOne],
  ["measured by neither", ITEM.filter((k) => !k.single)],
  ["not covered by the lab", ITEM.filter((k) => k.cost)],
  ["measured by the three together", union], ["measured by none of them", none]])
  write(W2, ad, s.length, `${load(s)} (${share(load(s))})`);

console.log("\n3. items no measurement pathway measures");
write([-26, 8, 8], "item", "path", "form");
for (const k of none) write([-26, 8, 8], k.name, k.path, k.form);
console.log(`what these ${none.length} items share: no single correct end state, ` +
  `and being wrong has a real cost.`);
```

```
competency MODEL: 15 items, total path load 237 (the same path is counted under more than one item; this is a load total, not a distinct path count).

1. how many items each measurement pathway measures
measure                      multiple-choice      practical-exam             lab
item measured                           2/15                9/15            6/15
path load measured                        42                 144              86
share of path load                     17.7%               60.8%           36.3%

2. where the two exam formats agree, where they diverge
set                                        items    path load
measured by both                               2   42 (17.7%)
measured by only one                           7  102 (43.0%)
measured by neither                            6   93 (39.2%)
not covered by the lab                         9  151 (63.7%)
measured by the three together                10  144 (60.8%)
measured by none of them                       5   93 (39.2%)

3. items no measurement pathway measures
item                          path    form
step-selection                  22       p
legit-work-cost                 22       p
inventory-keeping               19       p
scope-writing                   22       k
boundary-recognition             8       k
what these 5 items share: no single correct end state, and being wrong has a real cost.
```

The first table lines the three pathways up by coverage. Multiple-choice certification measures
**2** of the 15 items and covers 17.7% of the total path load. The practical exam covers **9
items** and 60.8% of the load. The lab environment covers **6 items** and 36.3%.

The gap between the two exam formats is not a difference of degree. The two items multiple-choice
certification measures — term separation and risk ranking — sit **inside** the nine the practical
exam measures. The second table's first row writes this: the intersection is 2 items, the
divergence is 7, and all seven of the diverging items sit on the practical-exam side. These two
measurements are not siblings; they are **nested**: one measures a narrow subset of what the other
measures.

#### What the Lab Does Not Cover

The lab environment diverges on a different axis from the two exams. Which items it covers is not
chosen by whether they have a single meaning, but by whether they **carry a consequence**. The
`not covered by the lab` row writes 9 items and **63.7%** of the total load.

The reason is in the lab's own definition. A lab environment carries no legitimate traffic; no
one uses the billing system, so a wrongly cut step never stops anyone. There is no other team; a
handoff done or skipped never has a consequence. There is no scope; stepping outside a boundary
carries no cost. A lab is an environment **where being wrong is free**, and that is exactly why it
cannot measure items that carry a cost. The lab's value is in repetition, not in the reality of
the outcome.

Taken together, the three cover 10 items and 60.8% of the load. The lab's only addition is
`environment-setup`, and its path load is **0**: the lab contributes the most on the item that
carries the least path load.

#### The Five Items Nothing Measures

The third table is this lesson's central finding. The three measurement pathways together **never
measure 5 items**, and these five carry **39.2%** of the total path load.

What the five share is in the output's last line: no single correct end state, and being wrong
carries a real cost. The two properties together push an item outside all three measurements at
once. With no single correct answer, it cannot be graded; with a real cost, it does not occur in a
lab.

Reading the items one by one sharpens the table further. `step-selection` and `legit-work-cost`
are this course's two central jobs, and both carry **22 paths**: what gets measured from this
course's first lesson to its last is which step gets cut and how much legitimate work that cut
stops. `inventory-keeping` carries 19 paths — the count of paths the defense did not know about in
the first lesson. `scope-writing` carries 22, `boundary-recognition` 8. All five are items this
course **measures**; none of them are items this course can be **assessed on**.

This does not mean the three pathways are worthless. Certification genuinely measures term
separation, and without term separation no path can be named; the practical exam covers 60.8% of
the load, and that is no ordinary threshold. The measurement says only this: **the area a
competency measure does not cover does not mean that measure is invalid; it means that area stays
unmeasured.** A certificate or an exam result carries exactly as much information as the path
load it covers — no more, no less.

#### Matching the Measure to the Role

When the three pathways' coverage is dropped onto the five roles separated in the second lesson,
the question turns into one of matching. The ten items measured are weighted heavily toward the
**close** duty: adding a layer, narrowing privilege, designing a bulkhead, running an incident.
The five unmeasured items fall into the **track** and **sustain** duties: keeping the inventory,
writing the scope, recognizing the boundary. The same gap that turned up separately in two
lessons is here again: every one of the responsibility matrix's seven unowned cells sat in these
same two columns, and here too the five unmeasured items in the competency count fall into the
same two columns. **Organizations know how to both name and measure the hand that does the work;
naming and measuring the hand that keeps the list and checks the outcome is the part they skip in
both.**

The practical takeaway is not a ranking — it is a **pairing**. A practical exam shows that a
candidate can do the work of cutting; it does not show which step should be cut or how much
legitimate work that cut will stop. Those two items are **purple work itself**, the very thing
the first lesson measured: it shows up only when the defense and the testing side walk the same
set of paths together, and there too it is measured not by an exam score but by the ratio of
closed paths to stopped legitimate requests. The unmeasured part of competency is not
unmeasurable; **it is measured on top of the work itself, with this course's own three columns.**

#### Summary

- In the 15-item competency set, multiple-choice certification measures 2 items, the practical
  exam 9, and the lab environment 6; the path-load share they cover is 17.7%, 60.8%, and 36.3%
  respectively.
- The two exam formats are not siblings, they are nested: the intersection is 2 items, the
  divergence is 7, and all seven diverging items sit on the practical-exam side.
- The lab environment does not cover 9 items, and they carry 63.7% of the total load. The reason is
  the environment's own definition: with no legitimate traffic, no other team, and no scope
  boundary, being wrong carries no cost.
- The three pathways together measure 10 items; the lab's only addition carries a path load of 0.
- The 5 items no pathway measures carry 39.2% of the load, and what they share is identical: no
  single correct end state, and being wrong has a real cost. Two of them are this course's central
  jobs, and each carries 22 paths.

#### Course Wrap-Up

| Lesson | How many paths | Closed paths | Legitimate-work cost |
|---|---|---|---|
| Confidentiality, Integrity and Availability | 8 assets × 3 axes | 6 decisions: 27 increases, 22 decreases | confidentiality −6 in the net-zero decision |
| Asset, Threat and Vulnerability | 19 valid out of 576 triples | 5 vulnerabilities, 12 triples | 3, 2, 1, 0 triples as the zone narrows |
| Risk Definition | 12 items, 176.05 units | 4-item budget, 77.00 units | 9.40 units by scoring format |
| Exploit and Payload | 12 chains, 53 steps | single step, up to 8 chains | middle steps broken, 1 chain remains |
| Attack Surface | 35 paths, 21 edges | first three removals 68.6% | 7 jobs, 24 jobs for the remaining 4 paths |
| Defense in Depth | 55 paths | 52 close, 3 remain | 63 managed items, 1,793 extra steps |
| Least Privilege | 55 paths | 43 paths, 15 free | from 11 requests a day to 3,502 |
| Isolation | 14 nodes, 4 assets | assets reached 4 → 2.8 | 20 gateways, 3,513 requests a day |
| Zero Trust | 55 paths | 5 by location, 35 at every step | 7,019 verifications a day, +2.00 steps |
| Authentication and Authorization Distinction | 55 paths, two request types | 0 at the wrong layer, 35 at the right one | 2.57 per request; 0 paths at +1.43 steps |
| Secure Defaults | 22 items, 20 paths | one forgotten item opens 12 paths | 319 a day when closed, 1,440 at worst |
| Blue, Red, and Purple Teams | 30 paths | 17 → 22 | from 2,764 requests a day to 2,037 |
| Security Roles | 30 paths | 28 → 30 | from 2,321 requests a day to 2,135 |
| Legal Framework and Ethics | 30 paths | 8 → 22 testable | 4,607 requests a day, 4 windows |
| Professional Competency Paths | 237 path load | 144 measured (60.8%) | 5 items, 93 load unmeasured |

The course's rule is the same across all fifteen rows: **defense is not a list of prohibitions;
it is the narrowing of a countable set of paths, and a defense whose cost is never written down
counts as unmeasured.** Without the middle column, the left column is an inventory listing;
without the right column, the middle column is a claim of success.

The right column collapses into four families across the fifteen rows. **Legitimate work
stopped** repeats in six lessons (least privilege, isolation, secure defaults, and the last
three): the cost lands directly in front of the user, which is why it is the family noticed
fastest. **A step added to every request** shows up in three lessons (defense in depth, zero
trust, authentication versus authorization): unnoticed, but paid on every single request. **A
manually managed item** shows up in three lessons (63 managed items, 20 gateways, 4 test
windows): no one counts it until an incident happens. **A cost stated as a raw number** appears
in the first four lessons, measured before the path unit even existed.

The left column tells its own story. The first four lessons never use the path unit at all: what
gets counted there is an axis, a triple, an item, a chain. The unit is established in the fifth
lesson and never changes across the ten lessons that follow. The path count itself is not
constant, though — 35, 55, and 30 — because the number depends on how the graph is drawn, how many
entry points and how many targets are defined. **A path count is not a fact; it is a model's
output**, and for it to be comparable, the graph itself has to be written down too.

The middle column, in two lessons, writes that defense **falls short**. Defense in depth closes
52 of fifty-five paths, and the remaining three paths are the legitimate channel itself: adding a
layer does not close them. Zero trust verifies at every step and leaves twenty paths open, and the
source of those twenty paths is not a missing control — it is the design of four internal flows. A
set of paths is never zeroed out; what gets written is **why** what remains, remains.

Paths were counted, principles were measured, and who measures within which boundary was written
down. Still, all fifteen of these lessons treated paths as abstract nodes and steps. **M24/K02
Information Technology and Systems Fundamentals** opens that abstraction: what components build
the **machine itself** that every one of these paths runs across, what boundary an operating
system draws between one user and another, how a file permission can cut a step, and how sturdy a
wall virtualization puts between a guest and a host.

