---
title: 'Attack Techniques and the Threat Landscape'
source: 'https://academia.sh/en/courses/attack-techniques'
language: en
updated: '2026-09-26T03:02:48+00:00'
license: 'CC BY-SA 4.0'
lessons: 22
outcomes:
    - 'Models the attack lifecycle as a sequence of steps and calculates the early-break gain'
    - 'Classifies human-focused attacks by distinguishing indicators and measures the cost of the threshold choice'
    - 'Counts at which defense layer each application-layer vulnerability class is broken'
---

# Attack Techniques and the Threat Landscape

Modeling an attack as a sequence of steps and the link at which the chain can be broken: the motivation and capability levels of threat actors, the characteristics of long-running targeted campaigns, the attack lifecycle from reconnaissance to reaching the objective, the coverage measure of tactics and techniques matrices, the relationship between actor, infrastructure, capability, and victim, the concept of a zero-day, how the exploitation of trust and urgency plays out in the human layer, phishing types and their distinguishing indicators, text message and voice call attacks, physical techniques, detecting typosquatting and homograph domains, watering hole attacks, measuring awareness programs, injection, cross-site scripting, request forgery, path traversal, buffer overflow, the cost differences between password attacks, and the place of reverse engineering in defense.

## Threat Models

The attack modeled as a sequence of steps and which link in the chain can be broken: how many steps of the chain a threat actor can get through based on capability and motivation level, the windows of opportunity that duration creates in long-running campaigns together with the item count that dwell time grows, the attack lifecycle built from preconditions and the calculation of the gain from breaking early, the tactic-and-technique matrix surfacing the step with no control at all, how much knowing one corner in the four-corner relationship model narrows the others, and, under a zero-day threat, the separation between the links a signature-based control misses and the links a behavior-based control catches.

### Threat Actors

Source: https://academia.sh/en/courses/attack-techniques/threat-actors

Separating the actor by capability and motivation level: of five actor levels, only one clears all eight steps of an eight-step chain, two levels get stuck at the capability threshold on the fifth step, the capability wall sits in the middle of the chain rather than at its ends, one of the two actors that reach their target is the lowest-capability actor, if the chain is never broken the five actors touch 64 items and if it is broken at reconnaissance 0 items, and the cost of breaking a step one step late ranges from 0 to 17 items.

Across five courses, the view was always from the defender's side: how many paths each control
closed, what it missed, and its legitimate-work cost were all counted. What was never
addressed was **who** used the missed paths, **in what order, and why**. This course crosses to
the other side — but it treats an attack not as an event, but as **a sequence of steps with
preconditions**.

The chain model was built in the Introduction to Cybersecurity course: the `Exploit and Payload`
lesson broke the sequence from vulnerability to effect into steps, and the `Attack Surface` lesson
converted the access step into a path count. Here that model is inherited, and a single question
is asked: **at which link can this chain be broken, and what does breaking it late cost.** The
first step is to make measurable who is walking the chain.

#### An Actor Is a Capability-Motivation Pair

The structure below is a **model**: there is no real group, campaign, or incident name; actors
are identified only by **capability level** and **motivation**. Trying the steps here against a
real system **is both illegal and unprofessional without written authorization and a defined
scope**.

**TM1:** the chain has eight steps; each step has a capability threshold (1–5), a duration cost,
and a count of fictional items reached once it is completed. **TM2:** an actor is identified by
three numbers — capability level, patience (a day budget), and **target step**, the chain's
counterpart to motivation. **TM3:** the actor walks the chain from the start and stops if the
capability threshold is exceeded or the day budget runs out.

```js
// threat/actor.mjs — model of actor capability against the chain steps: there is no running
// attack step here, only a data structure made of step names and numbers.
// No real actor, group, or campaign name appears; actors are identified by capability and motivation.

// TM1: the chain has eight steps; each step has a capability threshold (1..5), a duration
// cost (days), and a count of fictional items reached once it is completed. The item count is cumulative.
export const STEP = [
  { name: "recon", capability: 1, days: 3, items: 0 },
  { name: "initial-access", capability: 2, days: 2, items: 1 },
  { name: "execution", capability: 3, days: 1, items: 2 },
  { name: "persistence", capability: 3, days: 2, items: 3 },
  { name: "privilege-escalation", capability: 4, days: 5, items: 9 },
  { name: "lateral-movement", capability: 4, days: 14, items: 26 },
  { name: "collection", capability: 2, days: 21, items: 41 },
  { name: "exfiltration", capability: 3, days: 4, items: 48 },
];

// TM2: an actor is identified by three numbers: capability level (1..5), patience (day budget),
// and target step (the chain's counterpart to motivation). `skipped` is the steps the actor's position grants for free.
export const ACTOR = [
  { name: "low-capability-opportunist", capability: 2, patience: 6, target: "execution", skipped: [] },
  { name: "mid-capability-ideological", capability: 3, patience: 10, target: "persistence", skipped: [] },
  { name: "mid-capability-profit-driven", capability: 3, patience: 45, target: "exfiltration", skipped: [] },
  { name: "mid-capability-insider-access", capability: 3, patience: 120, target: "collection",
    skipped: ["recon", "initial-access", "privilege-escalation"] },
  { name: "high-capability-targeted", capability: 5, patience: 400, target: "exfiltration", skipped: [] },
];

// TM3: the actor walks the chain from the start; it stops if the capability threshold is exceeded
// or the day budget runs out. The item count reached at the point it stops is the previous step's item count.
export function walk(a, threshold = {}) {
  let days = 0, i = 0, blocker = "none";
  for (; i < STEP.length; i++) {
    if (a.skipped.includes(STEP[i].name)) continue;
    const y = STEP[i].capability + (threshold[STEP[i].name] || 0);
    if (y > a.capability) { blocker = "capability"; break; }
    if (days + STEP[i].days > a.patience) { blocker = "patience"; break; }
    days += STEP[i].days;
  }
  const targetIndex = STEP.findIndex((s) => s.name === a.target);
  return { stop: i, days, blocker, items: i === 0 ? 0 : STEP[i - 1].items,
    passed: STEP.slice(0, i).filter((s) => !a.skipped.includes(s.name)).length,
    haltedAt: i < STEP.length ? STEP[i].name : "-", succeeded: i > targetIndex };
}

// If the chain is broken at step k, the actor's reachable items are the smaller of its own stop and k.
export const itemsIfBroken = (a, k) => {
  const d = Math.min(walk(a).stop, k);
  return d === 0 ? 0 : STEP[d - 1].items;
};
export const totalItems = (k) => ACTOR.reduce((s, a) => s + itemsIfBroken(a, k), 0);
```

```js
// threat/measure.mjs — which actor gets through how many steps of the chain, and which link eliminates whom.
import { STEP, ACTOR, walk, totalItems } from "./actor.mjs";

const print = (w, ...s) => console.log(s.map((v, i) =>
  (w[i] < 0 ? String(v).padEnd(-w[i]) : String(v).padStart(w[i]))).join(""));
const S = ACTOR.map((a) => ({ a, r: walk(a) }));

console.log(`${STEP.length}-step chain, ${ACTOR.length} actor levels; ` +
  `capability thresholds ${STEP.map((s) => s.capability).join("")}, ` +
  `${STEP.reduce((t, s) => t + s.days, 0)} days start to finish, ` +
  `items reached by the full chain ${STEP.at(-1).items}`);

const A = [-31, 12, 10, 18, 8, 6, 23, 8];
console.log("\n1. how many steps of the chain each actor level passes");
print(A, "actor", "capability", "patience", "target step", "passed", "days", "halted at", "items");
for (const { a, r } of S)
  print(A, a.name, a.capability, a.patience, a.target, r.passed, r.days, r.haltedAt, r.items);
console.log(`actors reaching target: ${S.filter((x) => x.r.succeeded).length}/${ACTOR.length} ` +
  `(${S.filter((x) => x.r.succeeded).map((x) => x.a.name).join(", ")})`);

const B = [-21, 11, 6, 16, 15, 8];
console.log("\n2. which link eliminates whom");
print(B, "step", "capability", "days", "actors passing", "stopped here", "items");
for (let i = 0; i < STEP.length; i++) {
  const passing = S.filter((x) => x.r.stop > i).length;
  const stopped = S.filter((x) => x.r.stop === i);
  print(B, STEP[i].name, STEP[i].capability, STEP[i].days, passing,
    stopped.length ? `${stopped.length} (${stopped[0].r.blocker})` : "-", STEP[i].items);
}

const C = [-21, 14, 18, 14, 20];
console.log("\n3. where the chain breaks (total items across five actors)");
print(C, "broken at", "total items", "items saved", "targets lost", "one step later");
const baseline = totalItems(STEP.length);
for (let k = 0; k <= STEP.length; k++) {
  const t = totalItems(k);
  const lost = S.filter((x) => x.r.succeeded &&
    k <= STEP.findIndex((s) => s.name === x.a.target)).length;
  print(C, k < STEP.length ? STEP[k].name : "never broken", t, baseline - t, lost,
    k < STEP.length ? totalItems(k + 1) - t : "-");
}

const D = [-21, 15, 14, 18, 14];
console.log("\n4. if a step's capability threshold is raised by one point");
print(D, "raised step", "new stoppers", "total items", "items saved", "targets lost");
for (const s of STEP) {
  const y = ACTOR.map((a) => walk(a, { [s.name]: 1 }));
  const t = y.reduce((n, r) => n + r.items, 0);
  print(D, s.name, y.filter((r, i) => r.stop < S[i].r.stop).length, t, baseline - t,
    y.filter((r, i) => S[i].r.succeeded && !r.succeeded).length);
}
```

```
8-step chain, 5 actor levels; capability thresholds 12334423, 52 days start to finish, items reached by the full chain 48

1. how many steps of the chain each actor level passes
actor                            capability  patience       target step  passed  days              halted at   items
low-capability-opportunist                2         6         execution       2     5              execution       1
mid-capability-ideological                3        10       persistence       4     8   privilege-escalation       3
mid-capability-profit-driven              3        45      exfiltration       4     8   privilege-escalation       3
mid-capability-insider-access             3       120        collection       2     3       lateral-movement       9
high-capability-targeted                  5       400      exfiltration       8    52                      -      48
actors reaching target: 2/5 (mid-capability-ideological, high-capability-targeted)

2. which link eliminates whom
step                  capability  days  actors passing   stopped here   items
recon                          1     3               5              -       0
initial-access                 2     2               5              -       1
execution                      3     1               4 1 (capability)       2
persistence                    3     2               4              -       3
privilege-escalation           4     5               2 2 (capability)       9
lateral-movement               4    14               1 1 (capability)      26
collection                     2    21               1              -      41
exfiltration                   3     4               1              -      48

3. where the chain breaks (total items across five actors)
broken at               total items       items saved  targets lost      one step later
recon                             0                64             2                   0
initial-access                    0                64             2                   5
execution                         5                59             2                   4
persistence                       9                55             2                   4
privilege-escalation             13                51             1                  12
lateral-movement                 25                39             1                  17
collection                       42                22             1                  15
exfiltration                     57                 7             1                   7
never broken                     64                 0             0                   -

4. if a step's capability threshold is raised by one point
raised step             new stoppers   total items       items saved  targets lost
recon                              0            64                 0             0
initial-access                     1            63                 1             0
execution                          3            52                12             1
persistence                        3            55                 9             1
privilege-escalation               0            64                 0             0
lateral-movement                   0            64                 0             0
collection                         0            64                 0             0
exfiltration                       0            64                 0             0
```

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

#### One of Five Levels Passes All Eight Steps

The first table identifies the actor not by a single label but by **how many steps** it passes.
Of five levels, only one passes all eight steps of the chain. The low-capability opportunist stops
after two steps, two mid-capability levels get stuck on the fifth step, and the level with insider
access goes as far as the fifth step — thanks to the three steps its position grants for free — but
stops at the sixth.

The reason for stopping is written on every row, and it is the same on all of them: **capability**.
The patience budget stops no level, because the chain's total duration is 52 days and the only
level whose budget that would exceed is the six-day opportunist — and that one is already stopped
by the capability threshold before it gets there. Duration is not a binding constraint in this
lesson; the next lesson will turn duration itself into the measurement axis.

The level with insider access is a separate row. Its capability is mid-level, but its position
grants three steps for free: recon, initial access, and privilege escalation. This level reaches
nine items by walking **only two steps**; an outside level with the same capability walks four
steps and reaches three items. How many steps are passed is not, by itself, the measure —
**where you start from** multiplies it by three.

#### The Wall Sits in the Middle of the Chain

The second table lines the thresholds up in order: 1, 2, 3, 3, **4, 4**, 2, 3. The two highest
thresholds sit on the chain's fifth and sixth steps; the last two steps then drop below them —
collection at 2, exfiltration at 3. Carrying collected data out demands less capability than
crossing the middle of the chain.

The consequence of this shows up in the actors-passing column. All five of the five levels pass
recon and initial access; four pass execution, two pass privilege escalation, and **one** passes
lateral movement. The chain is a sequence of elimination, and all of that elimination happens on
the two middle steps. The steps at the ends eliminate no one: the first two steps are the ones
everyone passes, and the last two are the ones only whoever already got there passes.

This bears directly on the question of where to place a defense. The two middle steps eliminate
actors on their own; a control placed there already faces an already-thinned set. A control placed
on one of the first two steps, by contrast, sees all five of the five levels at once. **If the
chain's natural elimination and the defense's elimination collect at the same place, the defense
only stops the actors who would have been eliminated on their own.**

#### The Target Step Redefines Success

The row of actors that reach their target lists two levels, and one of them is the mid-capability
ideological level, with ten days of patience. It passes only four of the eight steps; even so, it
reaches its target, because its target step is the fourth in line. The profit-driven level, with
the same capability, passes exactly the same four steps, reaches the same three items, and **does
not reach its target**, because its target step is the eighth.

The two rows are identical as a measurement and opposite as an outcome. Motivation here is not an
adjective but **a position number in the chain**. An assessment that defines success as reaching
the end of the chain would count the level that reaches its target on the fourth step as a failure
and leave it out of the measurement. The same mistake also runs the other way: the
highest-capability level passes all eight steps in 52 days and reaches all 48 items — while
spending only an eighth of its patience budget.

#### The Cost of Breaking Late Is a Number

The third table gives this lesson's final column. If the chain is never broken, the five actors'
combined reach is **64 items**. If it is broken at recon or initial access, **0**; at execution, 5;
at privilege escalation, 13; at lateral movement, 25; at collection, 42 items. The last column reads
the same numbers as a difference: the cost of breaking one step late is, in order, 0, 5, 4, 4,
**12, 17**, 15, and 7 items.

These numbers are the total across five actors, not a single attack. Levels with different
capability on the same chain reach different depths; a single control point meets all of them at
once. This is why the value of a breaking point is written not against a single actor but against
**the combined reach of every level that can get that far**.

The distribution of the cost is not flat. The combined cost of being late across the first four
steps is 13 items; across the three steps between privilege escalation and collection it is 44
items. **Breaking one step late in the middle of the chain costs three times as much as breaking
four steps late at the start.** The fourth column, in turn, gives the effect of the break on the
target: breaking at persistence or earlier lowers two actors' targets at once, breaking after it
lowers only one.

The fourth table tries the defender's actual move: raising a step's capability threshold **by one
point**. On execution, this stops three actors earlier and saves 12 items; on persistence, it stops
three actors and saves 9 items. On the chain's two hardest steps — privilege escalation and lateral
movement — the same move **stops no actor and saves zero items**: only one level already passes
lateral movement, one of the two levels that pass privilege escalation never walks that step at all
because of its position, and the other clears the threshold even after it is raised by one point.
Piling hardening effort onto the hardest step is, once measured, a decision with no payoff.

#### Summary

- A threat actor is identified not by a name but by three numbers: capability level, patience
  budget, and target step. Of five levels, only one passes all eight steps of the eight-step chain.
- The capability wall sits in the middle of the chain: the thresholds follow the order 1, 2, 3, 3,
  4, 4, 2, 3; all five of the five levels pass the first two steps, four pass the third, two pass
  the fifth, and one passes the sixth.
- Position changes how many steps are walked: the level with insider access takes three steps for
  free, walks only two steps, and reaches 9 items; an outside level with the same capability walks
  four steps and reaches 3 items.
- Motivation is a position number in the chain: of two levels that pass the same four steps, the
  one whose target is on the fourth step succeeds, and the one whose target is on the eighth does
  not.
- The breaking point determines the cost: 64 items if never broken, 0 if broken at recon. The cost
  of breaking one step late is 0, 5, 4, 4, 12, 17, 15, 7 items; the three middle steps carry
  two-thirds of the total.
- Raising a step's threshold by one point saves 12 items at execution and 9 at persistence; on the
  chain's two hardest steps it saves 0 items: one level is already the only one passing, and the
  two levels passing the other are unaffected by a one-point threshold rise.

#### Next Step

In every measurement in this lesson, duration was never binding. The patience budget stopped no
actor, because the chain's total cost was written as 52 days and the most patient level's budget
was 400 days. That level passed all eight steps, reached all 48 items, and spent an eighth of its
budget — what it did with the remaining 348 days was never asked.

Yet in a long-running campaign, the real measure is duration itself. A step can take not days but
**months**; over that month, a record is born on the defender's side every day, and every record is
a window of opportunity. The next lesson sets the chain on the time axis: how many days each step
takes, how many windows of opportunity are born over that time, how many of those windows no
control ever looks at, and how much the item count grows for every week that goes undetected.

### Advanced Persistent Threat

Source: https://academia.sh/en/courses/attack-techniques/advanced-persistent-threat

Measuring a long-running campaign on the time axis: in a 330-day, eight-step sequence, 261 days (79.1%) are days no control sees; only two of five controls reach their record threshold; the two longest steps carry 65.5% of the duration and two-thirds of the item growth; first detection arrives on day 92 with 7 items; raising a single record threshold from 180 to 300 pushes detection to day 325 and raises dwell time from 47 days to 280 days and the item count from 7 to 45; and a control placed on the recon step saves all 48 items without foothold ever being established.

In every measurement in the previous lesson, duration was never binding: the chain's total cost
was written as 52 days, the most patient level's budget was 400 days, and no actor ever stopped
because of its day budget. Patience never came into play as a constraint.

In a long-running campaign, the measure itself is duration. A step can take not days but
**months**, and over that month a record is born on the defender's side every day. Every record is
a **window of opportunity**: a moment at which a control could alert. This lesson sets the chain on
the time axis and gives three numbers — how many days it takes, how many days no control ever
looks at, and how much the detection delay grows the item count.

#### A Campaign Is a Sequence in Time

The structure below is a **model**: there is no real campaign, actor, or incident name; steps are
identified only by duration, daily trace, and item count. The work is done on a fictional
measurement network.

**TM4:** each step has a duration, a count of fictional records it produces per day (`trace`), a
control that sees that record, and the record threshold the control waits for before it alerts.
**TM5:** a control only alerts once records accumulate up to the threshold on its own step; if the
step ends before that many records are produced, the control never alerts even if it exists.
**TM6:** the item count accumulates linearly within a step.

```js
// threat/campaign.mjs — the duration model of a long-running campaign: there is no running
// attack step here, only a data structure made of day, trace, and item counts. No real
// campaign, actor, or incident name appears. There is no randomness.

// TM4: each step has a duration (days), a count of fictional records produced per day (trace),
// a control that sees that record (or none), and the record threshold the control waits for
// before it alerts. `items` is the cumulative fictional item count reached once the step ends.
export const STEP = [
  { name: "recon", days: 45, trace: 1, control: null, threshold: 0, items: 0 },
  { name: "initial-access", days: 12, trace: 4, control: "identity-log", threshold: 60, items: 1 },
  { name: "execution", days: 2, trace: 9, control: "process-log", threshold: 30, items: 2 },
  { name: "persistence", days: 3, trace: 2, control: "startup-inventory", threshold: 10, items: 3 },
  { name: "privilege-escalation", days: 38, trace: 6, control: "privilege-change", threshold: 180, items: 9 },
  { name: "lateral-movement", days: 96, trace: 3, control: null, threshold: 0, items: 26 },
  { name: "collection", days: 120, trace: 2, control: null, threshold: 0, items: 41 },
  { name: "exfiltration", days: 14, trace: 12, control: "egress-volume", threshold: 100, items: 48 },
];

export const START = STEP.map((_, i) =>
  STEP.slice(0, i).reduce((t, s) => t + s.days, 0));
export const DURATION = STEP.reduce((t, s) => t + s.days, 0);
export const FOOTHOLD = START[1]; // day the initial-access step starts

// TM5: a control only alerts once records accumulate to the threshold on its own step; if the
// step ends before that many records are produced, the control never alerts even if it exists.
export function alertDay(i, override = {}) {
  const s = { ...STEP[i], ...(override[STEP[i].name] || {}) };
  if (!s.control || s.days * s.trace < s.threshold) return null;
  return START[i] + Math.ceil(s.threshold / s.trace);
}
export const firstDetection = (override = {}) => STEP
  .map((_, i) => alertDay(i, override)).filter((g) => g !== null).sort((a, b) => a - b)[0] ?? null;

// TM6: the item count accumulates linearly within a step; the items reached on day d are as follows.
export function itemsOnDay(d) {
  let previous = 0;
  for (let i = 0; i < STEP.length; i++) {
    const start = START[i], end = start + STEP[i].days;
    if (d < end) return Math.floor(previous + ((d - start) / STEP[i].days) * (STEP[i].items - previous));
    previous = STEP[i].items;
  }
  return STEP.at(-1).items;
}
```

```js
// threat/duration.mjs — the time axis: which step runs for months, how many windows of
// opportunity are born, how many items every day of dwell time grows.
import { STEP, START, DURATION, FOOTHOLD, alertDay, firstDetection, itemsOnDay }
  from "./campaign.mjs";

const print = (w, ...s) => console.log(s.map((v, i) =>
  (w[i] < 0 ? String(v).padEnd(-w[i]) : String(v).padStart(w[i]))).join(""));
const monitored = STEP.filter((s) => s.control);
const windowDays = monitored.reduce((t, s) => t + s.days, 0);
const windowRecords = monitored.reduce((t, s) => t + s.days * s.trace, 0);

console.log(`${STEP.length} steps, ${DURATION} days; ${monitored.length} steps have a control, ` +
  `${STEP.length - monitored.length} steps no control sees at all; ` +
  `monitored days ${windowDays} (${((windowDays / DURATION) * 100).toFixed(1)}%), ` +
  `monitored records ${windowRecords}`);

const A = [-23, 6, 7, 22, 11, 10, 14, 8];
console.log("\n1. step-by-step duration and window of opportunity");
print(A, "step", "days", "trace", "control", "threshold", "produced", "alert day", "items");
for (let i = 0; i < STEP.length; i++) {
  const s = STEP[i], u = alertDay(i);
  print(A, s.name, s.days, s.trace, s.control || "-", s.threshold || "-", s.days * s.trace,
    u === null ? (s.control ? "no threshold" : "-") : u, s.items);
}
console.log(`controls reaching threshold: ${STEP.filter((_, i) => alertDay(i) !== null).length}/` +
  `${monitored.length}; first detection day ${firstDetection()}`);

const B = [-24, 8, 18, 9, 13];
console.log("\n2. where the duration collects");
print(B, "group", "days", "duration share %", "records", "item growth");
for (const [label, k] of [["steps with a control", monitored],
  ["steps no control sees", STEP.filter((s) => !s.control)],
  ["the two longest steps", [...STEP].sort((x, y) => y.days - x.days).slice(0, 2)]]) {
  const g = k.reduce((t, s) => t + s.days, 0);
  const growth = k.reduce((t, s) => {
    const i = STEP.indexOf(s);
    return t + s.items - (i === 0 ? 0 : STEP[i - 1].items);
  }, 0);
  print(B, label, g, ((g / DURATION) * 100).toFixed(1), k.reduce((t, s) => t + s.days * s.trace, 0), growth);
}

const C = [-56, 15, 13, 7, 7];
console.log("\n3. when the detection day falls, and how many items by then");
print(C, "override", "detection day", "dwell days", "items", "saved");
const SCENARIO = [
  ["thresholds as-is", {}],
  ["privilege threshold 180 -> 300", { "privilege-escalation": { threshold: 300 } }],
  ["privilege threshold 300 + control on lateral movement", { "privilege-escalation": { threshold: 300 },
    "lateral-movement": { control: "session-hopping", threshold: 60 } }],
  ["control on recon (threshold 30)", { recon: { control: "external-query-volume", threshold: 30 } }],
  ["no control alerts", { "privilege-escalation": { threshold: 300 }, exfiltration: { threshold: 300 } }],
];
for (const [label, override] of SCENARIO) {
  const g = firstDetection(override) ?? DURATION;
  print(C, label, firstDetection(override) ?? "-", g < FOOTHOLD ? "no foothold" : g - FOOTHOLD,
    itemsOnDay(g), STEP.at(-1).items - itemsOnDay(g));
}

const D = [-21, 6, 13, 11, 14];
console.log("\n4. the item rate of the delay");
print(D, "step", "days", "item growth", "items/day", "30-day delay");
for (let i = 0; i < STEP.length; i++) {
  const previous = i === 0 ? 0 : STEP[i - 1].items, growth = STEP[i].items - previous;
  print(D, STEP[i].name, STEP[i].days, growth, (growth / STEP[i].days).toFixed(3),
    Math.min(growth, Math.floor((growth / STEP[i].days) * 30)));
}
const t0 = firstDetection(), t1 = firstDetection({ "privilege-escalation": { threshold: 300 } });
console.log(`raising a single threshold from 180 to 300 pushes detection from day ${t0} to day ${t1}: ` +
  `dwell days ${t0 - FOOTHOLD} -> ${t1 - FOOTHOLD}, items ${itemsOnDay(t0)} -> ${itemsOnDay(t1)}`);
```

```
8 steps, 330 days; 5 steps have a control, 3 steps no control sees at all; monitored days 69 (20.9%), monitored records 468

1. step-by-step duration and window of opportunity
step                     days  trace               control  threshold  produced     alert day   items
recon                      45      1                     -          -        45             -       0
initial-access             12      4          identity-log         60        48  no threshold       1
execution                   2      9           process-log         30        18  no threshold       2
persistence                 3      2     startup-inventory         10         6  no threshold       3
privilege-escalation       38      6      privilege-change        180       228            92       9
lateral-movement           96      3                     -          -       288             -      26
collection                120      2                     -          -       240             -      41
exfiltration               14     12         egress-volume        100       168           325      48
controls reaching threshold: 2/5; first detection day 92

2. where the duration collects
group                       days  duration share %  records  item growth
steps with a control          69              20.9      468           16
steps no control sees        261              79.1      573           32
the two longest steps        216              65.5      528           32

3. when the detection day falls, and how many items by then
override                                                  detection day   dwell days  items  saved
thresholds as-is                                                     92           47      7     41
privilege threshold 180 -> 300                                      325          280     45      3
privilege threshold 300 + control on lateral movement               120           75     12     36
control on recon (threshold 30)                                      30  no foothold      0     48
no control alerts                                                     -          285     48      0

4. the item rate of the delay
step                   days  item growth  items/day  30-day delay
recon                    45            0      0.000             0
initial-access           12            1      0.083             1
execution                 2            1      0.500             1
persistence               3            1      0.333             1
privilege-escalation     38            6      0.158             4
lateral-movement         96           17      0.177             5
collection              120           15      0.125             3
exfiltration             14            7      0.500             7
raising a single threshold from 180 to 300 pushes detection from day 92 to day 325: dwell days 47 -> 280, items 7 -> 45
```

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

#### Seventy-Nine Percent of Three Hundred Thirty Days in the Dark

The chain's total duration is **330 days**, and this duration is not spread evenly across the
steps. Execution takes two days, persistence three; lateral movement is 96, collection 120. Two
steps alone carry **65.5%** of the duration.

The distribution of control coverage is the inverse of duration. Five of the eight steps have a
control, three do not — and the three uncontrolled steps include the two longest. The steps that
have a control run for a combined **69 days**, that is 20.9% of the duration; the steps no control
ever sees run for **261 days**, that is 79.1%. The length of a campaign does not by itself buy the
defense time: the only time that is gained is monitored time.

The second table's last column sharpens this. Across the monitored 69 days, the item growth reached
is **16**; across the unmonitored 261 days, it is **32**. Two-thirds of the items accumulate in the
stretch of time that is never looked at. The record count runs the same way: the unmonitored steps
produce 573 records, and none of those records ever reach a control.

#### A Control That Exists Can Still Be a Control That Never Alerts

The first table's **alert day** column writes the difference between a control existing and a
control working. Three of the five controls carry a "no threshold" row: the control is defined, it
sees the step's records, but the step ends before enough records are produced to reach the
threshold. Initial access produces 48 records in 12 days; its threshold is 60. Execution produces
18 records in two days; its threshold is 30. Persistence produces 6 records in three days; its
threshold is 10.

These three rows are three instances of the same flaw: **the threshold was set against record
volume, not against the step's duration**. Short steps can never reach a high threshold. Five items
appear in the control inventory; two of them actually work. If coverage is counted by step count,
it reads 5/8; a coverage measure that also counts reaching the threshold reads **2/8**.

The first alert falls on day 92, and it comes from the control on the privilege-escalation step:
the step starts on day 62, produces 6 records a day, and reaches its 180-record threshold in 30
days. The item count reached by that day is **7**, and the campaign has been in place for 47 days.
**Dwell time** is the difference between the day the initial-access step starts and the day of
detection.

#### The Two-Hundred-Thirty-Three-Day Cost of One Threshold

The third table changes a single setting. If the privilege-change control's threshold is raised
from 180 to 300 — an ordinary adjustment made to cut down on alert volume — that step, which
produces 228 records, never reaches the threshold at all. The job of alerting then falls to the
chain's **last step**: the egress-volume control alerts on day 325.

The result shows up in three numbers at once. The detection day rises from 92 to 325, dwell time
from 47 days to **280 days**, and items reached from 7 to **45**. The items saved drop from 41 to 3.
A single threshold value, on a single control, makes a **38-item**, 233-day difference. A threshold
adjustment looks like a decision about alert volume; once measured, it is a decision about items.

The third row tries a remedy: the threshold stays at 300, but a control is added to the
lateral-movement step. That step produces 3 records a day, reaches its 60-record threshold in 20
days, and pulls detection back to day 120 — 12 items, 36 saved. Of the 38 items that were lost, 33
are recovered, not all of them. **Placing a control on a step that was never looked at does not
fully make up for a broken threshold.**

The fourth row goes to the start of the chain. If a control with a 30-record threshold is placed on
the recon step, its alert comes on day 30 — since the initial-access step does not start until day
45, **foothold is never even established**, and all 48 items are saved. The recon step takes 45 days and
produces only 1 record a day; the cost of monitoring this step is building a measure that separates
out incoming external query volume. The fifth row is the opposite extreme: if no control ever
alerts, the campaign stays in place for 285 days and reaches all 48 items.

#### Every Day of Dwell

The fourth table turns the delay into a rate. The highest item rate is on the execution and
exfiltration steps: 0.500 items a day. The lowest rate, 0.083, is on initial access; on recon there
is no item growth at all. But the rate alone does not give the cost, because the duration it is
multiplied by varies by a factor of sixty from step to step.

The last column combines the two: the item cost of a thirty-day delay on each step. On execution,
1; on persistence, 1; on privilege escalation, 4; on lateral movement, **5**; on collection, 3
items. Waiting thirty days is not even possible on short steps — the step already only lasts two
days and its growth is capped at 1 item. On long steps, thirty days is a small slice of the step,
and even that slice carries several items.

The order that comes out of this is the time-axis reading of where to place a control. Placing a
control on a short step caps its alerting window at that step's duration: a control that does not
reach its threshold within two days never alerts at all. Placing it on a long step widens the
window but also grows the cost of any delay. **The most valuable control on the time axis is one
that can reach its threshold near the start of a long step** — the 20-day alert time on the
lateral-movement row is an example of this.

#### Summary

- Across the 330-day, eight-step sequence, the steps with a control run for 69 days (20.9%) and the
  steps no control sees run for 261 days (79.1%); the two longest steps carry 65.5% of the duration
  and both are uncontrolled.
- Two-thirds of the item growth accumulates on uncontrolled days: 16 items across the 69 monitored
  days, 32 items across the 261 unmonitored days.
- A control existing does not mean it alerts: three of five controls never alert at all, because
  the step ends before enough records are produced to reach the threshold. Coverage by step count
  is 5/8; coverage by reaching the threshold is 2/8.
- First detection falls on day 92; by that day 7 items have been reached and dwell time has become
  47 days.
- If a single control's threshold rises from 180 to 300, detection shifts to day 325: dwell days
  rise from 47 to 280 and items from 7 to 45. Adding a control to lateral movement, without undoing
  that setting, saves 33 items.
- The cost of a thirty-day delay ranges from 0 to 7 items depending on the step; a control placed
  on the recon step saves all 48 items without foothold ever being established.

#### Next Step

Two lessons' worth of chain have now been measured on two separate axes: the actor's capability
determined how many steps it passed, and the step's duration determined how many windows of
opportunity were born. In both, the chain itself was taken as given — eight steps, a fixed order,
each step following the one before it.

Yet the chain's real structure is not in its ordering but in its **preconditions**. What a step
requires, why the next step cannot happen without that requirement being met, which step can be
skipped and which cannot. Once the preconditions are written down, a control point can be set
against every step, and then the real question can be asked: at which step does the chain break,
what happens when it does not break, and **how many items does breaking one step early gain**. The
next lesson builds this course's unit of measure and calculates the early-break gain.

### Attack Lifecycle

Source: https://academia.sh/en/courses/attack-techniques/attack-lifecycle

Building the course's unit of measure: writing the eight steps by their preconditions, removing any one of five environment conditions breaking the chain with certainty, reaching 48 items if the chain runs to the end unbroken while the expected item count is 20.14, the share that reaches the end never broken (0.2384) carrying more than half of the expected total, removing an environment condition yielding a certain gain that ranges from 20.14 to 1.67 items, and placing the same control item on each of the eight steps in turn producing an early-break gain that falls from 7.05 to 0.58 items.

Two lessons measured the chain along two separate axes: the actor's capability determined how many
steps were passed, the step's duration determined how many windows of opportunity were born. In
both, the chain itself was taken as given — eight steps, a fixed order, each step following the one
before it.

The chain's real structure is not in its ordering but in its **preconditions**. What a step needs,
and why the next step cannot happen if that need is not met. This lesson builds the chain by its
preconditions and defines the course's unit of measure: **early-break gain**, the number of items
saved by moving the same defense one step earlier in the chain.

#### What a Step Needs

The structure below is a **model**; how a step is carried out is not written anywhere, only **what
it needs** and **what it makes available** are written. The work is done on a fictional measurement
network.

**TM7:** environment conditions are conditions the attacker does not produce and the defense can
remove. **TM8:** each step is reachable once its preconditions are satisfied; if a control exists,
it breaks the chain there with some catch probability. **TM9:** a step is reachable only if all its
preconditions are satisfied; the first unsatisfied step structurally cuts the chain and none of the
following steps are reachable.

```js
// threat/lifecycle.mjs — the precondition model of the attack lifecycle: there is no running attack step
// here. Every step is written by "what it needs" (precondition) and "what it makes available" (produces);
// how the step is carried out is not written. There is no randomness; the expected value is calculated.

// TM7: environment conditions are conditions the attacker does not produce and the defense can remove.
export const ENVIRONMENT = ["externally-visible-item", "open-ingress-endpoint", "local-weak-configuration",
  "flat-network", "external-egress-path"];

// TM8: each step is reachable once its preconditions are satisfied; if a control exists, it breaks the chain
// there with a `catch` probability. `items` is the cumulative fictional item count reached once the step completes.
export const STEP = [
  { name: "recon", precondition: ["externally-visible-item"], produces: "target-list",
    control: null, catch: 0, items: 0 },
  { name: "initial-access", precondition: ["target-list", "open-ingress-endpoint"], produces: "valid-credential",
    control: "identity-log", catch: 0.15, items: 1 },
  { name: "execution", precondition: ["valid-credential"], produces: "code-execution",
    control: "process-log", catch: 0.25, items: 2 },
  { name: "persistence", precondition: ["code-execution"], produces: "persists-across-restart",
    control: "startup-inventory", catch: 0.2, items: 3 },
  { name: "privilege-escalation", precondition: ["code-execution", "local-weak-configuration"],
    produces: "admin-privilege", control: "privilege-change", catch: 0.15, items: 9 },
  { name: "lateral-movement", precondition: ["admin-privilege", "flat-network"], produces: "neighbor-access",
    control: null, catch: 0, items: 26 },
  { name: "collection", precondition: ["neighbor-access"], produces: "bulk-data",
    control: null, catch: 0, items: 41 },
  { name: "exfiltration", precondition: ["bulk-data", "external-egress-path"], produces: "impact",
    control: "egress-volume", catch: 0.45, items: 48 },
];

// TM9: a step is reachable only if all its preconditions are satisfied; the first unsatisfied
// step structurally cuts the chain and none of the following steps are reachable.
export function reachable(removed = []) {
  const satisfied = new Set(ENVIRONMENT.filter((k) => !removed.includes(k)));
  let n = 0;
  for (const s of STEP) {
    if (!s.precondition.every((k) => satisfied.has(k))) break;
    satisfied.add(s.produces);
    n++;
  }
  return n;
}

// Expected items: if the chain breaks at step i, the items reached up to that point are items(i-1).
// The break probability is the product of the earlier steps' miss probabilities and that step's catch.
export function expectedItems(removed = [], extra = {}) {
  const last = reachable(removed);
  let remaining = 1, total = 0;
  for (let i = 0; i < last; i++) {
    const p = 1 - (1 - STEP[i].catch) * (1 - (extra[STEP[i].name] || 0));
    total += remaining * p * (i === 0 ? 0 : STEP[i - 1].items);
    remaining *= 1 - p;
  }
  return total + remaining * (last === 0 ? 0 : STEP[last - 1].items);
}
```

```js
// threat/gain.mjs — at which link the chain can be broken, and how many items breaking early gains.
import { ENVIRONMENT, STEP, reachable, expectedItems } from "./lifecycle.mjs";

const print = (w, ...s) => console.log(s.map((v, i) =>
  (w[i] < 0 ? String(v).padEnd(-w[i]) : String(v).padStart(w[i]))).join(""));
const BASELINE = expectedItems();
const uncontrolled = STEP.filter((s) => !s.control);

console.log(`${STEP.length} steps, ${ENVIRONMENT.length} environment conditions; ` +
  `${STEP.length - uncontrolled.length} steps have a control, ${uncontrolled.length} steps ` +
  `no control sees at all (${uncontrolled.map((s) => s.name).join(", ")}); ` +
  `items reached unbroken ${STEP.at(-1).items}, expected items ${BASELINE.toFixed(2)}`);

const A = [-23, -43, -26, 20, 7, 7];
console.log("\n1. what each step needs, what it makes available");
print(A, "step", "precondition", "produces", "control", "catch", "items");
for (const s of STEP)
  print(A, s.name, s.precondition.join(" + "), s.produces, s.control || "-",
    s.control ? s.catch.toFixed(2) : "-", s.items);

const B = [-26, -23, 14, 16, 7];
console.log("\n2. structural breaking: if one environment condition is removed");
print(B, "removed condition", "impossible step", "reached step", "expected items", "gain");
for (const k of ENVIRONMENT) {
  const n = reachable([k]), e = expectedItems([k]);
  print(B, k, STEP[n].name, n, e.toFixed(2), (BASELINE - e).toFixed(2));
}

const C = [-23, 7, 14, 13, 15, 14];
console.log("\n3. probabilistic breaking: at which step the chain breaks");
print(C, "step", "catch", "reaches here", "breaks here", "items by then", "contribution");
let remaining = 1, total = 0;
for (let i = 0; i < STEP.length; i++) {
  const p = STEP[i].catch, broken = remaining * p, before = i === 0 ? 0 : STEP[i - 1].items;
  total += broken * before;
  print(C, STEP[i].name, p.toFixed(2), remaining.toFixed(4), broken.toFixed(4), before,
    (broken * before).toFixed(3));
  remaining -= broken;
}
console.log(`share reaching the end unbroken ${remaining.toFixed(4)} x ${STEP.at(-1).items} items = ` +
  `${(remaining * STEP.at(-1).items).toFixed(3)}; total expected items ` +
  `${(total + remaining * STEP.at(-1).items).toFixed(2)}`);

const D = [-23, 10, 16, 8, 18];
console.log("\n4. early-break gain: where the same control item (catch 0.35) is placed");
print(D, "placed step", "existing", "expected items", "gain", "vs previous step");
let previousGain = null;
for (const s of STEP) {
  const e = expectedItems([], { [s.name]: 0.35 }), gain = BASELINE - e;
  print(D, s.name, s.control ? s.catch.toFixed(2) : "none", e.toFixed(2), gain.toFixed(2),
    previousGain === null ? "-" : (gain - previousGain).toFixed(2));
  previousGain = gain;
}
const best = STEP.map((s) => ({ name: s.name, g: BASELINE - expectedItems([], { [s.name]: 0.35 }) }))
  .sort((x, y) => y.g - x.g);
console.log(`highest gain ${best[0].name} (${best[0].g.toFixed(2)} items), lowest ` +
  `${best.at(-1).name} (${best.at(-1).g.toFixed(2)}); best of the three uncontrolled steps is ` +
  `${best.find((x) => uncontrolled.some((s) => s.name === x.name)).name}`);
```

```
8 steps, 5 environment conditions; 5 steps have a control, 3 steps no control sees at all (recon, lateral-movement, collection); items reached unbroken 48, expected items 20.14

1. what each step needs, what it makes available
step                   precondition                               produces                               control  catch  items
recon                  externally-visible-item                    target-list                                  -      -      0
initial-access         target-list + open-ingress-endpoint        valid-credential                  identity-log   0.15      1
execution              valid-credential                           code-execution                     process-log   0.25      2
persistence            code-execution                             persists-across-restart      startup-inventory   0.20      3
privilege-escalation   code-execution + local-weak-configuration  admin-privilege               privilege-change   0.15      9
lateral-movement       admin-privilege + flat-network             neighbor-access                              -      -     26
collection             neighbor-access                            bulk-data                                    -      -     41
exfiltration           bulk-data + external-egress-path           impact                           egress-volume   0.45     48

2. structural breaking: if one environment condition is removed
removed condition         impossible step          reached step  expected items   gain
externally-visible-item   recon                               0            0.00  20.14
open-ingress-endpoint     initial-access                      1            0.00  20.14
local-weak-configuration  privilege-escalation                4            2.00  18.14
flat-network              lateral-movement                    5            4.60  15.54
external-egress-path      exfiltration                        7           18.47   1.67

3. probabilistic breaking: at which step the chain breaks
step                     catch  reaches here  breaks here  items by then  contribution
recon                     0.00        1.0000       0.0000              0         0.000
initial-access            0.15        1.0000       0.1500              0         0.000
execution                 0.25        0.8500       0.2125              1         0.212
persistence               0.20        0.6375       0.1275              2         0.255
privilege-escalation      0.15        0.5100       0.0765              3         0.229
lateral-movement          0.00        0.4335       0.0000              9         0.000
collection                0.00        0.4335       0.0000             26         0.000
exfiltration              0.45        0.4335       0.1951             41         7.998
share reaching the end unbroken 0.2384 x 48 items = 11.444; total expected items 20.14

4. early-break gain: where the same control item (catch 0.35) is placed
placed step              existing  expected items    gain  vs previous step
recon                        none           13.09    7.05                 -
initial-access               0.15           13.09    7.05              0.00
execution                    0.25           13.39    6.75             -0.30
persistence                  0.20           13.61    6.53             -0.22
privilege-escalation         0.15           13.79    6.35             -0.18
lateral-movement             none           14.70    5.44             -0.91
collection                   none           17.28    2.86             -2.58
exfiltration                 0.45           19.56    0.58             -2.28
highest gain recon (7.05 items), lowest exfiltration (0.58); best of the three uncontrolled steps is recon
```

The numbers are in the **measurement** class; their inputs are TM7–TM9.

#### The Precondition Column Is the Chain Itself

The first table's second column carries this lesson. Every step is written not by what it does but
by **what it requires to exist**. The execution step requires a valid credential; the persistence
step requires code execution; lateral movement requires both admin privilege and a flat network.
Which method the step is carried out with never appears here and does not need to — the only
information the defense side needs for its decision is the list of requirements.

The third column closes the match: every step produces a condition, and that condition sits in the
next step's precondition. The chain is therefore not an ordering but a **dependency sequence**. The
order cannot be broken, because every step's input condition comes from the one before it.

Preconditions are filled from two sources. Some are conditions the chain itself produces; the rest
are **environment conditions** — the externally-visible item, the open ingress endpoint, the local
weak configuration, the flat network, and the external egress path. The attacking side produces
none of these five environment conditions; all of them are in the defense side's hands. This
distinction determines the whole of the next table.

#### Two Separate Forms of Breaking

The chain can be broken in two ways, and the two do not resemble each other as measures.

**Structural breaking** removes a precondition. The second table removes each of the five
environment conditions in turn and writes which step becomes impossible. If the externally-visible
item is removed, the recon step is not reachable and the chain never starts; if the open ingress
endpoint is removed, initial access is not reachable. In both cases the expected items are **0.00**
and the gain is the full 20.14 items. Removing the local weak configuration cuts the chain after the
fourth step (2.00 items), removing the flat network after the fifth step (4.60), and removing the
external egress path after the seventh step — expected items **18.47** and a gain of only 1.67.

The value of the same kind of move varies by **a factor of twelve** depending on where in the chain
it is applied. Closing the external egress path has already let 41 items be lost, because it allows
collection to complete.

**Probabilistic breaking**, on the other hand, leaves the step reachable and waits for a control to
see it. The third table works this through: the share reaching each step, the share broken there,
and the items reached by that point. The five controls' catch rates lie between 0.15 and 0.45; none
of these is certain. The share reaching the end of the chain never broken is **0.2384**, and it
alone carries 11.44 items — more than half of the expected 20.14 items. **In a chain with five
controls, the single largest item is the share that never breaks at all.**

The expected item count is not a guess but a sum derived from the inputs: each row's contribution is
the product of the break share at that step and the items reached by that point, and the sum of the
eight rows is 20.14. The model assumes two things — that the controls are independent of one
another, and that a control stops the chain the moment it sees it. Both are optimistic assumptions;
in a real setup, the time that passes between a warning being seen and being responded to grows the
item count further. This is why the number 20.14 is not an upper bound but a **lower bound**.

#### Three Uncontrolled Steps

The third table's most striking rows are lateral movement and collection. Both have a catch rate of
0.00, neither ever breaks the chain — yet together they take the chain's item count from 3 to
**41**. 79% of the item growth happens on the two steps no control ever looks at.

This is the result of a distribution common on the defense side. Controls get placed on steps rich
in records: identity logins, process launches, privilege changes. The lateral-movement and
collection steps do not give rise to a new record type on their own; they run through privileges
that already exist, over paths that already exist. **A control follows the noise of an event; items
accumulate on the silent steps.**

The recon step is the third uncontrolled step, but it sits at the opposite end: it never raises the
item count at all, because nothing has been touched yet. "Uncontrolled" is the same word; its
consequence is two different things in two different places.

#### Early-Break Gain

The fourth table defines this course's unit of measure. There is a single control item in hand —
catch rate 0.35 — and it is placed on each of the eight steps in turn. Expected items are
recalculated on every row; the gain is the difference from the baseline.

The gain sequence runs: 7.05, 7.05, 6.75, 6.53, 6.35, 5.44, 2.86, 0.58. **The same control saves
twelve times as many items at the start of the chain as at the end.** The equality of the first two
rows is structural: breaking at recon or at initial access gives the same result, because in neither
case has any item been touched yet.

The last column gives the cost of a one-step delay separately. Over the chain's first five steps,
this cost is small — between 0.00 and 0.30. At lateral movement it is 0.91, at collection **2.58**,
at exfiltration 2.28. The loss is not evenly distributed: delaying is nearly free over the first five
steps, then the cost jumps fivefold to 0.91 on the sixth, reaches 2.58 on the seventh, and stays at
2.28 on the eighth. The ordering is not a coincidence; the item count also jumps from 3 to 26 at
exactly that point.

Among the three uncontrolled steps, the best choice is recon (7.05), the worst is collection (2.86).
For a team with a single control item in hand, the decision is written right here: placing it on the
**earliest** of the three never-looked-at steps saves 2.5 times as many items as placing it on the
latest.

Even so, the fourth table's highest row reaches only a third of the second table's strongest row.
The best control placement saves 7.05 items; the weakest structural break saves 1.67, but the
strongest saves **20.14**. Removing a precondition is certain; placing a control is probabilistic.
The unit of measure is the same; the order of magnitude is not.

#### Summary

- The attack lifecycle is written not by how steps are carried out but by **what they need**; every
  step produces a condition, and that condition is the next step's precondition.
- Five preconditions are environment conditions the attacker does not produce; removing one breaks
  the chain with **certainty**, and the gain ranges from 20.14 to 1.67 items — the same move is worth
  twelve times as much at the start of the chain.
- The five controls' catch rates lie between 0.15 and 0.45; the share reaching the end of the chain
  never broken is 0.2384 and carries 11.44 items, that is, more than half of the expected 20.14
  items.
- Three steps are steps no control sees at all; two of them (lateral movement, collection) take the
  item count from 3 to 41, that is, they carry 79% of the growth.
- Early-break gain is this course's unit of measure: placing the same control item on each of the
  eight steps in turn gives a gain sequence falling from 7.05 to 0.58 items.
- The cost of breaking one step late is under 0.30 items for the first five steps, and 0.91, 2.58,
  and 2.28 items after the fifth; the cost jumps at exactly the point where the item count jumps
  from 3 to 26.

#### Next Step

This lesson measured eight steps and the single control set against each. In reality, a step is not
matched against a single control; it needs as many controls as the **number of distinct forms** in
which that step can be carried out. Lateral movement is one row here, yet underneath it there are
many independent techniques, and a single control sees only a portion of them.

Works that classify adversary behavior are built exactly to fill this gap: steps are arranged as
columns, techniques as rows, and a control is mapped to each cell. The next lesson builds this
structure and asks a single question: as the coverage percentage rises, does the number of **steps
with no control at all** fall, or does coverage only grow on the steps that were already seen.

### Adversary Behavior Classifications

Source: https://academia.sh/en/courses/attack-techniques/adversary-behavior-classifications

Mapping techniques onto steps and counting the step with no control at all: 8 tactics and 27 technique classes producing 10,800 chains; five control items covering 40.7% of the techniques; three steps remaining steps no control sees at all; 80% of chains breaking within the first two steps while 72 chains (0.67%) cannot be broken at any link; the latest-breaking 1.33% slice carrying 42.6% of the average items; and two interventions that yield the same coverage percentage (51.9%) producing gains that differ threefold, 1.39 versus 0.47 items.

The previous lesson measured eight steps and the single control set against each. In reality, a
step is not matched against a single control; it needs as many controls as the **number of
distinct forms** in which that step can be carried out. Lateral movement was one row there;
underneath it there are many independent technique classes, and a single control sees only a
portion of them.

Works that classify adversary behavior are built exactly to fill this gap. The structure is
always the same: **steps become columns, technique classes become rows**, and a control is
mapped to each cell. This lesson builds that structure and asks a single question: as the
coverage percentage rises, does the number of **steps with no control at all** fall, or does
coverage only grow on the steps that were already seen.

#### The Matrix Is a Step–Technique Mapping

The structure below is a **model**: techniques are written only by class name and by which
control sees them; how any of them is carried out never appears. The work is done on a fictional
measurement network.

**TM10:** every step (tactic) has more than one technique class under it, and a chain picks one
technique from each step. **TM11:** a control item sees particular techniques; a step carried out
through a technique it does not see cannot be broken on that chain. **TM12:** the earliest link at
which a chain can be broken is the step where the first seen technique among the chosen ones sits;
if none is seen, the chain cannot be broken.

```js
// threat/matrix.mjs — the model of the tactic and technique matrix: rows are the technique
// class, columns are the chain step. There is no running technique here; techniques are
// written only by class name and by which control sees them. There is no randomness.

// TM10: every step (tactic) has more than one technique class under it; a chain picks one technique
// from each step. `items` is the cumulative fictional item count reached once the step completes.
export const TACTIC = [
  { name: "recon", items: 0, technique: ["open-source-compilation", "external-surface-enumeration",
    "employee-list-harvesting", "service-fingerprinting"] },
  { name: "initial-access", items: 1, technique: ["valid-credential-login", "attachment-based",
    "external-service-exploitation", "supply-chain-connection", "removable-media"] },
  { name: "execution", items: 2, technique: ["interpreter-invocation", "scheduled-trigger",
    "user-interaction"] },
  { name: "persistence", items: 3, technique: ["startup-entry", "service-definition", "scheduled-task",
    "account-creation", "configuration-hook"] },
  { name: "privilege-escalation", items: 9, technique: ["permission-misuse", "token-takeover",
    "path-hijacking"] },
  { name: "lateral-movement", items: 26, technique: ["remote-service-session", "share-write",
    "credential-reuse"] },
  { name: "collection", items: 41, technique: ["local-data-collection", "share-scanning"] },
  { name: "exfiltration", items: 48, technique: ["encrypted-channel", "split-transfer"] },
];

// TM11: a control item sees particular techniques; a step carried out through a technique it
// does not see cannot be broken on that chain.
export const CONTROL = {
  "identity-log": ["valid-credential-login", "external-service-exploitation", "account-creation"],
  "process-log": ["interpreter-invocation", "scheduled-trigger"],
  "startup-inventory": ["startup-entry", "service-definition", "scheduled-task"],
  "privilege-change": ["permission-misuse", "token-takeover"],
  "egress-volume": ["encrypted-channel"],
};

export const seenSet = (extra = {}) =>
  new Set(Object.values({ ...CONTROL, ...extra }).flat());
export const TECHNIQUE_COUNT = TACTIC.reduce((t, k) => t + k.technique.length, 0);
export const CHAIN_COUNT = TACTIC.reduce((t, k) => t * k.technique.length, 1);

// TM12: a chain picks one technique from each tactic; the earliest link at which the chain can be
// broken is the step where the first seen technique among the chosen ones sits. If none is seen, it cannot be broken.
export function distribution(extra = {}) {
  const g = seenSet(extra), count = new Array(TACTIC.length + 1).fill(0);
  const walk = (i, first) => {
    if (i === TACTIC.length) return count[first === -1 ? TACTIC.length : first]++;
    for (const t of TACTIC[i].technique) walk(i + 1, first === -1 && g.has(t) ? i : first);
  };
  walk(0, -1);
  return count;
}
export const itemsBefore = (i) => (i === 0 ? 0 : TACTIC[i - 1].items);
export function averageItems(extra = {}) {
  const s = distribution(extra);
  return s.reduce((t, n, i) =>
    t + n * (i === TACTIC.length ? TACTIC.at(-1).items : itemsBefore(i)), 0) / CHAIN_COUNT;
}
```

```js
// threat/coverage.mjs — as the coverage percentage rises, does the count of steps with no control at all fall?
import { TACTIC, CONTROL, seenSet, TECHNIQUE_COUNT, CHAIN_COUNT, distribution, itemsBefore,
  averageItems } from "./matrix.mjs";

const print = (w, ...s) => console.log(s.map((v, i) =>
  (w[i] < 0 ? String(v).padEnd(-w[i]) : String(v).padStart(w[i]))).join(""));
const G = seenSet();
const covered = TECHNIQUE_COUNT - TACTIC.reduce((t, k) =>
  t + k.technique.filter((x) => !G.has(x)).length, 0);
const emptyTactic = TACTIC.filter((k) => k.technique.every((x) => !G.has(x)));

console.log(`${TACTIC.length} tactics x ${TECHNIQUE_COUNT} techniques, ` +
  `${Object.keys(CONTROL).length} control items; covered techniques ${covered} ` +
  `(${((covered / TECHNIQUE_COUNT) * 100).toFixed(1)}%), steps with no control at all ` +
  `${emptyTactic.length} (${emptyTactic.map((k) => k.name).join(", ")}); ` +
  `chain count ${CHAIN_COUNT}, average items ${averageItems().toFixed(2)}`);

const A = [-23, 11, 6, 11, 12, 8];
console.log("\n1. matrix: how many techniques per step, how many are seen");
print(A, "tactic", "technique", "seen", "unseen", "coverage %", "items");
for (const k of TACTIC) {
  const seen = k.technique.filter((x) => G.has(x)).length;
  print(A, k.name, k.technique.length, seen, k.technique.length - seen,
    ((seen / k.technique.length) * 100).toFixed(1), k.items);
}

const B = [-20, 11, 30, 16];
console.log("\n2. techniques seen per control item");
print(B, "control", "technique", "which step", "matrix share %");
for (const [d, t] of Object.entries(CONTROL)) {
  const step = [...new Set(t.map((x) => TACTIC.find((k) => k.technique.includes(x)).name))];
  print(B, d, t.length, step.join("/"), ((t.length / TECHNIQUE_COUNT) * 100).toFixed(1));
}

const C = [-23, 8, 10, 15, 19];
console.log("\n3. the earliest link a chain can be broken at");
print(C, "first seen step", "chains", "chains %", "items by then", "item contribution");
const S = distribution();
for (let i = 0; i <= TACTIC.length; i++) {
  if (S[i] === 0) continue;
  const items = i === TACTIC.length ? TACTIC.at(-1).items : itemsBefore(i);
  print(C, i === TACTIC.length ? "cannot be broken" : TACTIC[i].name, S[i],
    ((S[i] / CHAIN_COUNT) * 100).toFixed(2), items,
    ((S[i] * items) / CHAIN_COUNT).toFixed(2));
}

const D = [-43, 12, 12, 16, 9, 13];
console.log("\n4. if one more control item is added");
print(D, "added control", "coverage %", "empty step", "average items", "gain", "unbreakable");
const EXTRA = [
  ["recon: two techniques", { newControl: ["open-source-compilation", "external-surface-enumeration"] }],
  ["initial-access: all three techniques", { newControl: ["attachment-based", "supply-chain-connection",
    "removable-media"] }],
  ["persistence: the one remaining technique", { newControl: ["configuration-hook"] }],
  ["lateral-movement: all three techniques", { newControl: ["remote-service-session", "share-write",
    "credential-reuse"] }],
  ["collection: both techniques", { newControl: ["local-data-collection", "share-scanning"] }],
];
const baseline = averageItems();
for (const [name, extra] of EXTRA) {
  const g = seenSet(extra);
  const cov = TECHNIQUE_COUNT - TACTIC.reduce((t, k) =>
    t + k.technique.filter((x) => !g.has(x)).length, 0);
  const empty = TACTIC.filter((k) => k.technique.every((x) => !g.has(x))).length;
  const o = averageItems(extra);
  print(D, name, ((cov / TECHNIQUE_COUNT) * 100).toFixed(1), empty, o.toFixed(2),
    (baseline - o).toFixed(2), distribution(extra)[TACTIC.length]);
}
const late = S[TACTIC.length - 1] + S[TACTIC.length];
console.log(`baseline: unbreakable ${S[TACTIC.length]} chains; the ${late} chains in the two latest rows ` +
  `(${((late / CHAIN_COUNT) * 100).toFixed(2)}%) carry ` +
  `${(((S[TACTIC.length - 1] * TACTIC.at(-2).items + S[TACTIC.length] * TACTIC.at(-1).items) /
    CHAIN_COUNT / baseline) * 100).toFixed(1)}% of the average items`);
```

```
8 tactics x 27 techniques, 5 control items; covered techniques 11 (40.7%), steps with no control at all 3 (recon, lateral-movement, collection); chain count 10800, average items 1.39

1. matrix: how many techniques per step, how many are seen
tactic                   technique  seen     unseen  coverage %   items
recon                            4     0          4         0.0       0
initial-access                   5     2          3        40.0       1
execution                        3     2          1        66.7       2
persistence                      5     4          1        80.0       3
privilege-escalation             3     2          1        66.7       9
lateral-movement                 3     0          3         0.0      26
collection                       2     0          2         0.0      41
exfiltration                     2     1          1        50.0      48

2. techniques seen per control item
control               technique                    which step  matrix share %
identity-log                  3    initial-access/persistence            11.1
process-log                   2                     execution             7.4
startup-inventory             3                   persistence            11.1
privilege-change              2          privilege-escalation             7.4
egress-volume                 1                  exfiltration             3.7

3. the earliest link a chain can be broken at
first seen step          chains  chains %  items by then  item contribution
initial-access             4320     40.00              0               0.00
execution                  4320     40.00              1               0.40
persistence                1728     16.00              2               0.32
privilege-escalation        288      2.67              3               0.08
exfiltration                 72      0.67             41               0.27
cannot be broken             72      0.67             48               0.32

4. if one more control item is added
added control                                coverage %  empty step   average items     gain  unbreakable
recon: two techniques                              48.1           2            0.70     0.70           36
initial-access: all three techniques               51.9           3            0.00     1.39            0
persistence: the one remaining technique           44.4           3            0.80     0.59            0
lateral-movement: all three techniques             51.9           2            0.92     0.47            0
collection: both techniques                        48.1           2            1.15     0.25            0
baseline: unbreakable 72 chains; the 144 chains in the two latest rows (1.33%) carry 42.6% of the average items
```

The numbers are in the **measurement** class; their inputs are TM10–TM12.

#### Inside Forty Percent Coverage

Under the eight steps there are **27 technique classes**, and five control items see **11 of
them** — 40.7% coverage. This number alone carries no decision; the decision emerges once the
first table breaks it down step by step.

Coverage is not evenly distributed: four of the persistence step's five techniques are seen (80%),
two-thirds of execution and privilege escalation are seen, two-fifths of initial access. Coverage
is **zero** on three steps — recon, lateral movement, and collection. These three steps are the
same three uncontrolled steps from the previous lesson; the matrix confirms that observation at
the technique level and writes why: none of these steps' techniques falls in the set the five
control items see.

The matrix structure's function in this lesson is not the classification itself. The
classification only provides a countable grid: every cell answers yes or no to "can this step be
carried out through this technique," and whether a control is mapped to every yes cell is written
separately. Without the grid, coverage stays an opinion; with the grid, coverage becomes the
quotient of two numbers.

The second table reads the same number from the control side. The widest of the five controls
sees three techniques and covers 11.1% of the matrix; the narrowest sees one technique, 3.7%. Two
controls overlap on the same step: the identity log sees one technique that falls under the
persistence step, while the startup inventory sees all three. **Adding more controls is not the
same as covering more steps.**

#### Ten Thousand Eight Hundred Chains

Multiplying the technique counts per step gives **10,800 distinct chains**. The third table finds
the **earliest link** at which each chain can be broken and groups chains by that link.

This multiplication writes the difference between counting techniques and counting chains. The
technique count is additive: adding two techniques to a step takes 27 to 29. The chain count is
multiplicative: if those same two techniques take collection's count from two to four, the chain
count goes from 10,800 to 21,600. Coverage percentage is calculated over the technique count and
so grows additively, while the probability set the defense faces grows multiplicatively. There is
no reason for the two measures to move in the same direction.

The distribution piles up at the start: 40% of chains can be broken at initial access, 40% at
execution, 16% at persistence. A total of 96% collects in the first three breakable links, and on
those chains the items reached are 0, 1, or 2. Average items are **1.39**. Even though the coverage
percentage looks low, coverage sitting **at the start of the chain** keeps the average down.

The tail runs the opposite way. 72 chains can only be broken at the exfiltration step — by that
point 41 items have been reached. A further 72 chains **cannot be broken at any link**: an unseen
technique was picked at all eight of the eight steps. Together these two groups are only **1.33%**
of the chains, yet they carry **42.6%** of the average items. A matrix evaluated by coverage
averages never sees this 1.33% at all.

This weight in the tail comes directly from the item sequence. Item counts run 0, 1, 2, 3, 9, 26,
41, 48 across the steps; the first four steps' combined contribution is 3, the last four steps' is
45. While the large majority of chains break in the cheap region, the unbreakable minority carries
the whole of the expensive region.

The 72 unbreakable chains are a situation appearing for the first time in this course: the chain
has no break point at any link. In this case the measure shifts from breaking to **detection** —
the question is no longer "at which link does it break" but "once the event has happened, from
which record is it seen."

#### Same Coverage, a Threefold Difference

The fourth table compares five separate interventions on the same scale and gives this lesson's
central finding.

Two rows produce **exactly the same coverage percentage**: covering initial access's three unseen
techniques, and covering lateral movement's three techniques — both take coverage from 40.7% to
**51.9%**. Their gains, though, are 1.39 and 0.47 items; the first is **three times** the second.
The same size of effort, the same percentage increase, a threefold difference in outcome. The
difference comes only from the position in the chain of the step it lands on.

The second row is even sharper: once initial access is fully covered, every chain becomes
breakable right after the first step and average items drop to **0.00** — yet **the empty-step
count stays at 3**. An assessment that looks at the coverage table finds this intervention
lacking, saying "there are still three blind steps"; an assessment that looks at the item measure
records it as the best move.

The opposite example is in the same table. Covering lateral movement and collection brings the
empty-step count from 3 to 2 — the only improvement visible in the coverage table — and their
gains are 0.47 and 0.25 items, the two lowest on the list. **Lowering the empty-step count and
raising the early-break gain are separate goals, and they do not pick the same intervention.**

The last column brings a third criterion: the count of unbreakable chains. The baseline is 72.
Covering persistence's one remaining technique raises coverage only to 44.4% and does not change
the empty-step count at all, but it takes the unbreakable chain count **from 72 to 0** — because
that technique is the single common gap every unbreakable chain passes through. Covering two
techniques at recon brings the same number down by only half. Three criteria pick three different
rows; a coverage decision made without stating which criterion was used counts as not measured at
all.

#### Summary

- Adversary behavior classifications build a matrix that makes steps columns and technique
  classes rows: 8 steps, 27 techniques, and the product of the per-step technique counts is
  **10,800 chains**.
- Five control items see 11 of the 27 techniques (40.7%); coverage is zero on three steps, and
  these are the same three steps that came out uncontrolled in the previous lesson.
- 96% of chains break at the first three breakable links, and average items are 1.39; coverage
  collecting at the start of the chain keeps the average low.
- 1.33% of chains (144 chains) carry 42.6% of the average items; 72 of these **cannot be broken at
  any link**, and on those chains the measure shifts from breaking to detection.
- Two interventions that yield the same coverage percentage (51.9%) save 1.39 and 0.47 items; the
  difference comes only from the covered step's position in the chain.
- Three criteria pick three different interventions: coverage percentage and item gain point to
  initial access, empty-step count to lateral movement, unbreakable-chain count to persistence's
  one remaining technique.

#### Next Step

The matrix leaves one question unanswered. Cells say "this step can be carried out through this
technique" and record whether a control sees that technique. But once an observation is in hand —
a record, a connection, a file trace — **which chain that observation belongs to** cannot be read
from the matrix. The same technique class occurs in many independent events.

There is a second structure that fills this gap and asks a different question from the matrix: an
event's four corners — who did it, which infrastructure was used, which capability it carried, and
who it targeted — are linked to one another. Knowing one corner narrows the others. The next lesson
measures this narrowing as a **link count**: how many candidates knowing one corner eliminates from
the other three, and which corner becomes visible at the chain's earliest link.

### Diamond Model

Source: https://academia.sh/en/courses/attack-techniques/diamond-model

Translating the four-corner relationship model into a link count: the actor, infrastructure, capability, and victim corners fill 77 of 134 possible links across 20 observations; the narrowing ratio of knowing one corner comes out exactly equal to that corner pair's link sparsity; the strongest corner (50%) becomes visible at the chain's eighth step and only after 41 items; the earliest-visible corner narrows only 30.7%; the capability corner gives 43.1% narrowing at a cost of 1 item; and adding a second corner adds only 10 to 13.1 points to the narrowing.

The matrix left one question unanswered. Cells say "this step can be carried out through this
technique" and record whether a control sees that technique. But once an observation is in hand —
a record, a connection, a file trace — **which event that observation belongs to** cannot be read
from the matrix. The same technique class occurs in many independent events.

A second structure fills this gap and names an event by four corners: **who** (actor profile),
**through which infrastructure**, **with which capability**, and **against whom**. Four corners
produce six pairwise links, and the claim the model carries is this: when one corner is known, the
others narrow. This lesson measures that narrowing as a **link count**.

#### Four Corners, Six Links

The observation set below is **fictional**: no real actor, infrastructure item, campaign, or
organization name appears; the actor corner is written only by capability and motivation level.

**TM13:** an observation records four corners together, and the four corners produce six pairwise
links. **TM14:** knowing a corner means staying confined to the observations that carry that
value; the remaining candidate count is the number of distinct values of the target corner in that
subset. **TM15:** every corner becomes visible at a particular step of the chain, and the item
count reached by that step is the cost of using that corner.

```js
// threat/corner.mjs — data structure of the four-corner relationship model: every observation ties together an
// event's four corners. The observations are fictional; no real actor, infrastructure item, campaign, or
// organization name appears. The actor corner is written only by capability and motivation level.

// TM13: an observation records four corners together: actor profile, infrastructure item,
// capability (technique class), and victim item. Four corners produce 6 pairwise links.
export const CORNER = ["actor", "infrastructure", "capability", "victim"];
export const OBSERVATION = `
  capability-2-revenue          shared-hosting          attachment-based                branch-endpoint
  capability-2-revenue          shared-hosting          attachment-based                subscriber-portal
  capability-2-revenue          open-proxy              valid-credential-login          subscriber-portal
  capability-2-revenue          shared-hosting          valid-credential-login          branch-endpoint
  capability-2-revenue          open-proxy              attachment-based                field-application
  capability-3-revenue          shared-hosting          external-service-exploitation   subscriber-portal
  capability-3-revenue          rented-endpoint         external-service-exploitation   billing-service
  capability-3-revenue          rented-endpoint         interpreter-invocation          billing-service
  capability-3-revenue          open-proxy              attachment-based                field-application
  capability-3-revenue          shared-hosting          valid-credential-login          subscriber-portal
  capability-3-insider-access   own-endpoint            credential-reuse                field-application
  capability-3-insider-access   own-endpoint            valid-credential-login          billing-service
  capability-3-insider-access   own-endpoint            credential-reuse                billing-service
  capability-3-insider-access   own-endpoint            encrypted-channel               branch-endpoint
  capability-5-targeted         compromised-endpoint    external-service-exploitation   field-application
  capability-5-targeted         compromised-endpoint    interpreter-invocation          field-application
  capability-5-targeted         compromised-endpoint    credential-reuse                billing-service
  capability-5-targeted         compromised-endpoint    encrypted-channel               branch-endpoint
  capability-5-targeted         rented-endpoint         encrypted-channel               billing-service
  capability-5-targeted         rented-endpoint         interpreter-invocation          subscriber-portal`
  .trim().split("\n").map((r) => {
    const [actor, infrastructure, capability, victim] = r.trim().split(/\s+/);
    return { actor, infrastructure, capability, victim };
  });

export const value = (k) => [...new Set(OBSERVATION.map((g) => g[k]))];

// TM14: knowing a corner means staying confined to observations that carry that value; the remaining
// candidate count is the number of distinct values of the target corner in that subset, averaged over the groups.
export function narrow(known, target) {
  const total = value(target).length;
  const groups = new Map();
  for (const g of OBSERVATION) {
    const key = known.map((k) => g[k]).join("|");
    if (!groups.has(key)) groups.set(key, new Set());
    groups.get(key).add(g[target]);
  }
  const avg = [...groups.values()].reduce((t, s) => t + s.size, 0) / groups.size;
  return { avg, total, ratio: 1 - avg / total, group: groups.size };
}

// TM15: every corner becomes visible at a particular step of the chain; the item count reached by
// that step is the cost of using that corner. The actor corner is never directly visible; it is inferred.
export const VISIBILITY = { victim: { step: "initial-access", items: 0 },
  capability: { step: "execution", items: 1 }, infrastructure: { step: "exfiltration", items: 41 },
  actor: { step: "-", items: null } };
```

```js
// threat/pivot.mjs — how many candidates knowing one corner eliminates from the others.
import { CORNER, OBSERVATION, value, narrow, VISIBILITY } from "./corner.mjs";

const print = (w, ...s) => console.log(s.map((v, i) =>
  (w[i] < 0 ? String(v).padEnd(-w[i]) : String(v).padStart(w[i]))).join(""));
const pair = CORNER.flatMap((a, i) => CORNER.slice(i + 1).map((b) => [a, b]));
const possible = pair.reduce((t, [a, b]) => t + value(a).length * value(b).length, 0);
const seen = pair.reduce((t, [a, b]) =>
  t + new Set(OBSERVATION.map((g) => `${g[a]}|${g[b]}`)).size, 0);

console.log(`${OBSERVATION.length} observations, ${CORNER.length} corners ` +
  `(${CORNER.map((k) => `${k} ${value(k).length}`).join(", ")}); ` +
  `${pair.length} corner pairs, ${possible} possible links, ${seen} present in the observations ` +
  `(${((seen / possible) * 100).toFixed(1)}%)`);

const A = [-16, -16, 10, 11, 13];
console.log("\n1. corner pairs: how many links are present in the observations");
print(A, "corner", "corner", "possible", "present", "sparsity %");
for (const [a, b] of pair) {
  const n = new Set(OBSERVATION.map((g) => `${g[a]}|${g[b]}`)).size;
  const o = value(a).length * value(b).length;
  print(A, a, b, o, n, (100 - (n / o) * 100).toFixed(1));
}

const B = [-16, -16, 8, 16, 13, 8];
console.log("\n2. how many candidates remain in the other corner once one is known");
print(B, "known", "target", "total", "avg candidates", "narrowing %", "group");
for (const b of CORNER) for (const h of CORNER) {
  if (b === h) continue;
  const d = narrow([b], h);
  print(B, b, h, d.total, d.avg.toFixed(2), (d.ratio * 100).toFixed(1), d.group);
}

const C = [-16, -16, 15, 17, 6];
console.log("\n3. which corner becomes visible when, and how much it narrows");
print(C, "corner", "visible step", "items by then", "avg narrowing %", "rank");
const strength = CORNER.map((k) => ({ k, g: CORNER.filter((h) => h !== k)
  .reduce((t, h) => t + narrow([k], h).ratio, 0) / (CORNER.length - 1) }))
  .sort((x, y) => y.g - x.g);
for (const { k, g } of strength)
  print(C, k, VISIBILITY[k].step, VISIBILITY[k].items ?? "inferred",
    (g * 100).toFixed(1), strength.findIndex((x) => x.k === k) + 1);

const D = [-30, -16, 16, 13, 9];
console.log("\n4. what a second corner buys");
print(D, "known corner(s)", "target", "avg candidates", "narrowing %", "added %");
for (const h of CORNER) {
  const single = CORNER.filter((k) => k !== h)
    .map((k) => ({ k, d: narrow([k], h) })).sort((x, y) => x.d.avg - y.d.avg)[0];
  const other = CORNER.filter((k) => k !== h && k !== single.k);
  const pairD = narrow([single.k, other[0]], h);
  print(D, single.k, h, single.d.avg.toFixed(2), (single.d.ratio * 100).toFixed(1), "-");
  print(D, `${single.k} + ${other[0]}`, h, pairD.avg.toFixed(2), (pairD.ratio * 100).toFixed(1),
    ((pairD.ratio - single.d.ratio) * 100).toFixed(1));
}
console.log("if three corners are known, remaining average candidates: " + CORNER.map((h) =>
  `${h} ${narrow(CORNER.filter((k) => k !== h), h).avg.toFixed(2)}`).join(", "));

const best = strength[0];
console.log(`strongest corner ${best.k} (${(best.g * 100).toFixed(1)}%), ` +
  `visible at the ${VISIBILITY[best.k].step} step and by then ` +
  `${VISIBILITY[best.k].items} items have been reached; earliest-visible corner ` +
  `victim (${VISIBILITY.victim.items} items) and its narrowing ` +
  `${(strength.find((x) => x.k === "victim").g * 100).toFixed(1)}%`);
```

```
20 observations, 4 corners (actor 4, infrastructure 5, capability 6, victim 4); 6 corner pairs, 134 possible links, 77 present in the observations (57.5%)

1. corner pairs: how many links are present in the observations
corner          corner            possible    present   sparsity %
actor           infrastructure          20          8         60.0
actor           capability              24         13         45.8
actor           victim                  16         13         18.8
infrastructure  capability              30         15         50.0
infrastructure  victim                  20         12         40.0
capability      victim                  24         16         33.3

2. how many candidates remain in the other corner once one is known
known           target             total  avg candidates  narrowing %   group
actor           infrastructure         5            2.00         60.0       4
actor           capability             6            3.25         45.8       4
actor           victim                 4            3.25         18.8       4
infrastructure  actor                  4            1.60         60.0       5
infrastructure  capability             6            3.00         50.0       5
infrastructure  victim                 4            2.40         40.0       5
capability      actor                  4            2.17         45.8       6
capability      infrastructure         5            2.50         50.0       6
capability      victim                 4            2.67         33.3       6
victim          actor                  4            3.25         18.8       4
victim          infrastructure         5            3.00         40.0       4
victim          capability             6            4.00         33.3       4

3. which corner becomes visible when, and how much it narrows
corner          visible step      items by then  avg narrowing %  rank
infrastructure  exfiltration                 41             50.0     1
capability      execution                     1             43.1     2
actor           -                      inferred             41.5     3
victim          initial-access                0             30.7     4

4. what a second corner buys
known corner(s)               target            avg candidates  narrowing %  added %
infrastructure                actor                       1.60         60.0        -
infrastructure + capability   actor                       1.20         70.0     10.0
actor                         infrastructure              2.00         60.0        -
actor + capability            infrastructure              1.38         72.3     12.3
infrastructure                capability                  3.00         50.0        -
infrastructure + actor        capability                  2.25         62.5     12.5
infrastructure                victim                      2.40         40.0        -
infrastructure + actor        victim                      1.88         53.1     13.1
if three corners are known, remaining average candidates: actor 1.05, infrastructure 1.00, capability 1.33, victim 1.11
strongest corner infrastructure (50.0%), visible at the exfiltration step and by then 41 items have been reached; earliest-visible corner victim (0 items) and its narrowing 30.7%
```

The numbers are in the **measurement** class; their inputs are TM13–TM15.

#### Narrowing Is a Sparsity Measure

The four corners' value counts are 4, 5, 6, and 4. The six corner pairs define a total of **134
possible links**; 20 observations fill **77 of them**. The remaining 57 links are never seen at
all, and the model's whole power comes from there.

Reading the second table alongside the first reveals an equality: every corner pair's **sparsity
percentage** turns out **exactly equal to the narrowing percentage** calculated for that pair. The
actor-infrastructure pair's sparsity is 60.0%; knowing the actor narrows infrastructure by 60.0%,
and knowing infrastructure narrows the actor by 60.0% as well. The actor-victim pair's sparsity is
18.8%, and so is its narrowing.

The equality is not a coincidence. For every value of a corner, the sum of the remaining candidate
counts is exactly the number of distinct links in that pair; dividing by the group count gives the
average candidate, and relating that to the target corner's total value count yields link density.
**Narrowing power is how empty the link matrix is.** The model is therefore not an intuition but
something countable: the fewer combinations that have occurred between two corners, the more
knowing one narrows the other.

The warning that follows from this is on the same scale. As the observation count grows, the link
matrix fills, and narrowing power **falls**. A growing observation pool does not automatically
strengthen inference; once the matrix starts filling, the same corner discriminates less.

#### The Strongest Corner Is the Most Expensive Corner

The third table is this lesson's central comparison: every corner's average narrowing power is
written side by side with the step at which it becomes visible in the chain.

**Infrastructure** heads the ranking: it narrows the other three corners by an average of 50.0%.
But the infrastructure corner only becomes visible at the exfiltration step — the external
endpoint shows up as data leaves — and by that point **41 items** have been reached. The strongest
clue is born at the chain's latest link.

This late birth comes from the model's own structure. The infrastructure corner is strong because
its link matrix with the actor is the emptiest one — every actor profile uses only a few forms of
infrastructure. That same sparsity is also why this corner appears late on the defense side: a
sparsely used item only falls into a record at the chain's end, on an outward-facing step.

**Victim** sits at the bottom of the ranking: 30.7% narrowing. This corner is free on the defense
side, because which item is targeted is already clear at the initial-access step, and at that
point **0 items** have been lost. The free one is the weakest; the strongest one is the most
expensive.

The row in between makes the call. **Capability** narrows 43.1% and becomes visible at the
execution step — the items reached by then are **1**. Infrastructure's 6.9-point edge costs 40
more items. Measured by the chain's own scale, the best corner is not the one that narrows the
most but the one that **narrows the most per item spent**; on that measure the capability corner
is thirty-five times more efficient than infrastructure. The victim corner is free, so no such
ratio can be formed for it; what sets it apart is not efficiency but having the lowest narrowing
of all.

There is also an asymmetry inside the second table itself. Even though the narrowing percentage is
equal in both directions for the same pair, the **remaining candidate count** is not: knowing
infrastructure brings the actor down from 4 candidates to 1.60, knowing the actor brings
infrastructure down from 5 to 2.00. The percentage is the same, the absolute number is different,
and the decision is made with the absolute number. Targeting the corner with the smaller value set
produces a more precise result at the same sparsity.

The actor corner stands apart in the table: it is never directly visible at any step; it is
inferred from the other three corners. Its narrowing power is 41.5%, but this power is only
meaningful as a target of inference — knowing the actor narrows the other corners, but the only
way to know the actor is through the other corners.

#### The Return of a Second Corner

The fourth table finds the best single pivot for every target corner, then adds a second corner to
it.

The best single corner for narrowing the actor is infrastructure: it drops from four candidates to
**1.60** (60.0%). Adding the capability corner brings it down to 1.20 (70.0%) — the second corner's
contribution is **10 points**. In the other three rows the added contribution is 12.3, 12.5, and
13.1 points. In all four cases, the second corner brings roughly **one-sixth** as much extra
narrowing as the first.

The reason for the diminishing return is in the first table. The second corner only works within
the subset the first one leaves behind, and that subset is already sparse; the number of links the
second pivot can still remove is far smaller than what the first removed. The last row shows the
limit: once three corners are known together, the remaining average candidate is between 1.00 and
1.33 — meaning nothing is left to add after the third corner.

This writes how the four-corner model is used in practice. The first pivot delivers nearly all of
the decision; the second pivot is a verification tool. Going from 1.60 to 1.20 on the actor corner
means, on average, "one candidate" instead of "two candidates," and this difference is decisive on
some observation sets — but if it is bought at the cost of waiting 40 more items, **the price paid
is larger than the narrowing gained**.

Put on the chain's scale: waiting for one more corner means advancing one more step in the chain.
With the previous lesson's numbers, that wait — going from execution to exfiltration — means moving
from 1 item to 41 items. **The relationship model delivers narrowing, but the narrowing's cost
carries no decision unless it is written on the chain's own scale.**

#### Summary

- Four corners (actor, infrastructure, capability, victim) produce six pairwise links; 20
  observations fill 77 of 134 possible links, and the remaining 57 gaps supply the model's entire
  power.
- The narrowing percentage of knowing a corner is exactly equal to that corner pair's link sparsity;
  narrowing power is how empty the link matrix is.
- As the observation count grows, the matrix fills and narrowing power falls; growing the pool does
  not strengthen inference by itself.
- The strongest corner is infrastructure (50.0%), but it becomes visible at the exfiltration step,
  after 41 items; the earliest-visible corner is the victim (0 items), and it narrows only 30.7%.
- The capability corner narrows 43.1% at a cost of 1 item; infrastructure's 6.9-point edge costs 40
  more items.
- The second corner's contribution is between 10.0 and 13.1 points — roughly one-sixth of what the
  first pivot provides; the first pivot makes the decision, the second verifies it.

#### Next Step

Everything measured across five lessons stood on the same assumption: what the controls saw were
things **known in advance**. Every cell in the matrix was tied to a technique class, every control
item to a recognized record pattern; every corner in the relationship model rested on a link that
had been seen before.

The moment a technique is used for the first time, though, falls outside this assumption. There is
not yet a pattern to recognize, that row in the link matrix is empty, and no fix has been released
either. The topic's final lesson measures this situation in terms of coverage: of the links that
can be broken under a known threat, how many can still be broken under an unknown threat; how many
steps a signature-based control misses; how many a behavior-based control still catches; and how
many items stay exposed in the window until a fix is published.

### Known and Unknown Threats

Source: https://academia.sh/en/courses/attack-techniques/known-and-unknown-threats

Measuring zero-day in terms of control coverage: eight controls make 7 links breakable under a known threat but drop to 5 under an unknown threat; three signature-based controls leave two steps completely uncovered; three behavior-based controls keep 70% of their catch; two configuration-based controls are not affected at all; expected items rise from 6.42 to 19.69; and the 77-day pre-patch window leaves 115.49 items exposed while the same window under structural breaking gives 0.00.

Everything measured across five lessons stood on the same assumption: what the controls saw were
things **known in advance**. Every cell in the matrix was tied to a recognized technique class,
every control item to a recognized record pattern; every corner in the relationship model rested
on a link that had been seen before.

The moment a technique is used for the first time falls outside this assumption. **Zero-day** is
the condition where a vulnerability is not yet known on the defense side and no fix exists for it.
This lesson measures that condition not as an adjective but as **coverage loss**: how many links
stay breakable, which control type remains standing, and how many items are exposed in the window
until a fix is published.

#### The Control's Type Determines What It Looks At

The structure below is a **model**: there is no running exploit, vulnerability identifier, or
patch record; there are only control types and catch rates. The work runs on a fictional
measurement network.

**TM16:** every control belongs to a type — a signature-based control looks at a recognized
pattern, a behavior-based control looks at deviation, a configuration-based control looks at the
state itself. **TM17:** under an unknown threat, a signature-based control's catch is zero; a
behavior-based control still runs but drops by a multiplier because its threshold is not tuned to
that behavior; a configuration-based control is not affected. **TM18:** if the chain breaks at
step k, the items reached by then are the previous step's item count.

```js
// threat/unknown.mjs — coverage model of controls under a known vs. an unknown threat: there
// is no running exploit or vulnerability identifier here; only a data structure of control
// types and catch rates. No randomness; the expected value is computed.

export const STEP = ["recon", "initial-access", "execution", "persistence",
  "privilege-escalation", "lateral-movement", "collection", "exfiltration"];
export const ITEM = [0, 1, 2, 3, 9, 26, 41, 48];

// TM16: every control belongs to a type. A signature-based control looks at a recognized pattern; a
// behavior-based control looks at deviation; a configuration-based control looks at the state itself.
export const CONTROL = [
  { name: "signature-match", type: "signature", step: "initial-access", catch: 0.35 },
  { name: "file-signature", type: "signature", step: "execution", catch: 0.4 },
  { name: "process-tree-deviation", type: "behavior", step: "execution", catch: 0.2 },
  { name: "startup-inventory", type: "configuration", step: "persistence", catch: 0.3 },
  { name: "privilege-change", type: "configuration", step: "privilege-escalation", catch: 0.25 },
  { name: "session-hopping", type: "behavior", step: "lateral-movement", catch: 0.18 },
  { name: "data-pattern-signature", type: "signature", step: "collection", catch: 0.22 },
  { name: "egress-volume-deviation", type: "behavior", step: "exfiltration", catch: 0.45 },
];

// TM17: under an unknown threat, a signature-based control's catch is zero (there is no pattern yet); a
// behavior-based control still runs but drops by a multiplier because its threshold is not tuned to that
// behavior; a configuration-based control does not depend on whether the threat is known.
export const MULTIPLIER = {
  known: { signature: 1, behavior: 1, configuration: 1 },
  unknown: { signature: 0, behavior: 0.7, configuration: 1 },
  "disclosed-no-patch": { signature: 0, behavior: 1, configuration: 1 },
  "patch-available-not-applied": { signature: 1, behavior: 1, configuration: 1 },
};

export function stepCatch(multiplier) {
  return STEP.map((a) => 1 - CONTROL.filter((d) => d.step === a)
    .reduce((t, d) => t * (1 - d.catch * multiplier[d.type]), 1));
}

// TM18: if the chain breaks at step k, the items reached by then are ITEM[k-1]; under
// structural breaking the chain can never be walked past that step at all.
export function expectedItems(multiplier, through = STEP.length) {
  const p = stepCatch(multiplier);
  let remaining = 1, total = 0;
  for (let i = 0; i < through; i++) {
    total += remaining * p[i] * (i === 0 ? 0 : ITEM[i - 1]);
    remaining *= 1 - p[i];
  }
  return total + remaining * (through === 0 ? 0 : ITEM[through - 1]);
}
export const breakable = (multiplier) => stepCatch(multiplier).filter((x) => x > 0).length;
```

```js
// threat/scope.mjs — how many steps a signature-based control misses under the unknown, how many a
// behavior-based control still catches, how many items stay exposed in the pre-patch window.
import { STEP, ITEM, CONTROL, MULTIPLIER, stepCatch, expectedItems, breakable }
  from "./unknown.mjs";

const print = (w, ...s) => console.log(s.map((v, i) =>
  (w[i] < 0 ? String(v).padEnd(-w[i]) : String(v).padStart(w[i]))).join(""));
const type = (t) => CONTROL.filter((d) => d.type === t).length;

console.log(`${STEP.length} steps, ${CONTROL.length} controls ` +
  `(signature ${type("signature")}, behavior ${type("behavior")}, configuration ${type("configuration")}); ` +
  `breakable links known ${breakable(MULTIPLIER.known)}, unknown ` +
  `${breakable(MULTIPLIER.unknown)}; expected items ` +
  `${expectedItems(MULTIPLIER.known).toFixed(2)} -> ` +
  `${expectedItems(MULTIPLIER.unknown).toFixed(2)}`);

const A = [-25, -15, -22, 11, 13];
console.log("\n1. control items and their catch under the unknown");
print(A, "control", "type", "step", "known", "unknown");
for (const d of CONTROL)
  print(A, d.name, d.type, d.step, d.catch.toFixed(2),
    (d.catch * MULTIPLIER.unknown[d.type]).toFixed(2));

const B = [-22, 10, 12, 8, 17, 33];
console.log("\n2. step by step: which link still breaks under the unknown as well");
print(B, "step", "known", "unknown", "items", "remaining catch", "status");
const pB = stepCatch(MULTIPLIER.known), pU = stepCatch(MULTIPLIER.unknown);
for (let i = 0; i < STEP.length; i++)
  print(B, STEP[i], pB[i].toFixed(3), pU[i].toFixed(3), ITEM[i],
    pB[i] === 0 ? "-" : `${((pU[i] / pB[i]) * 100).toFixed(0)}%`,
    pB[i] === 0 ? "no control at all" : pU[i] === 0 ? "closes once the signature drops" : "stays exposed if breakable");

const C = [-34, 8, 8, 13, 16, 15];
console.log("\n3. the pre- and post-patch window");
print(C, "phase", "days", "events", "breakable", "expected items", "exposed items");
const PHASE = [["known internally, not disclosed", 40, "unknown", 8],
  ["disclosed, no patch", 12, "disclosed-no-patch", 8],
  ["patch available, not applied", 25, "patch-available-not-applied", 8],
  ["patch applied (structural)", 0, "patch-available-not-applied", 1]];
let exposedTotal = 0;
for (const [name, days, c, through] of PHASE) {
  const events = days / 10, e = expectedItems(MULTIPLIER[c], through), exposed = events * e;
  if (days > 0) exposedTotal += exposed;
  print(C, name, days, events.toFixed(1), breakable(MULTIPLIER[c]), e.toFixed(2), exposed.toFixed(2));
}
const daysTotal = PHASE.reduce((t, f) => t + f[1], 0);
console.log(`window ${daysTotal} days, ${(daysTotal / 10).toFixed(1)} events, ` +
  `exposed items ${exposedTotal.toFixed(2)}; the same window under structural breaking gives ` +
  `${((daysTotal / 10) * expectedItems(MULTIPLIER["patch-available-not-applied"], 1)).toFixed(2)}`);

const D = [-24, 13, 16, 11, 12];
console.log("\n4. control types in isolation");
print(D, "control type active", "breakable", "expected items", "vs. known", "coverage %");
const only = (t) => ({ signature: t === "signature" ? 1 : 0, behavior: t === "behavior" ? 1 : 0,
  configuration: t === "configuration" ? 1 : 0 });
const base = expectedItems(MULTIPLIER.known);
for (const [name, c] of [["all (known threat)", MULTIPLIER.known],
  ["all (unknown threat)", MULTIPLIER.unknown], ["signature only", only("signature")],
  ["behavior only", only("behavior")], ["configuration only", only("configuration")],
  ["none", only("none")]]) {
  const e = expectedItems(c);
  print(D, name, breakable(c), e.toFixed(2), (e - base).toFixed(2),
    `${((1 - e / ITEM.at(-1)) * 100).toFixed(1)}`);
}
```

```
8 steps, 8 controls (signature 3, behavior 3, configuration 2); breakable links known 7, unknown 5; expected items 6.42 -> 19.69

1. control items and their catch under the unknown
control                  type           step                        known      unknown
signature-match          signature      initial-access               0.35         0.00
file-signature           signature      execution                    0.40         0.00
process-tree-deviation   behavior       execution                    0.20         0.14
startup-inventory        configuration  persistence                  0.30         0.30
privilege-change         configuration  privilege-escalation         0.25         0.25
session-hopping          behavior       lateral-movement             0.18         0.13
data-pattern-signature   signature      collection                   0.22         0.00
egress-volume-deviation  behavior       exfiltration                 0.45         0.32

2. step by step: which link still breaks under the unknown as well
step                       known     unknown   items  remaining catch                           status
recon                      0.000       0.000       0                -                no control at all
initial-access             0.350       0.000       1               0%  closes once the signature drops
execution                  0.520       0.140       2              27%       stays exposed if breakable
persistence                0.300       0.300       3             100%       stays exposed if breakable
privilege-escalation       0.250       0.250       9             100%       stays exposed if breakable
lateral-movement           0.180       0.126      26              70%       stays exposed if breakable
collection                 0.220       0.000      41               0%  closes once the signature drops
exfiltration               0.450       0.315      48              70%       stays exposed if breakable

3. the pre- and post-patch window
phase                                 days  events    breakable  expected items  exposed items
known internally, not disclosed         40     4.0            5           19.69          78.76
disclosed, no patch                     12     1.2            5           17.23          20.67
patch available, not applied            25     2.5            7            6.42          16.05
patch applied (structural)               0     0.0            7            0.00           0.00
window 77 days, 7.7 events, exposed items 115.49; the same window under structural breaking gives 0.00

4. control types in isolation
control type active         breakable  expected items  vs. known  coverage %
all (known threat)                  7            6.42       0.00        86.6
all (unknown threat)                5           19.69      13.27        59.0
signature only                      3           17.09      10.67        64.4
behavior only                       3           30.92      24.50        35.6
configuration only                  2           26.32      19.90        45.2
none                                0           48.00      41.58         0.0
```

The numbers are in the **measurement** class; their inputs are TM16–TM18.

#### Five of Seven Links

Eight control items make **7 links** breakable under a known threat; of the eight steps, only the
recon step is empty. The same eight controls leave **5 links** under an unknown threat. The loss
is two links, and these two links are not chosen at random: initial-access and collection are the
two steps that have only a signature-based control facing them.

The second table's **remaining catch** column gives the ratio for every step. At the persistence
and privilege-escalation steps the ratio is **100%** — the control at these two steps is
configuration-based, and the presence of a startup entry or a change of privilege is seen
regardless of whether the threat is known. At lateral-movement and exfiltration the ratio is 70%.
At execution the ratio drops to **27%**, because most of that step's 0.520 catch is carried by the
signature-based control; once the signature drops, only the behavior-based control's 0.140
remains.

Expected items rise from 6.42 to **19.69** — more than triple. While the breakable link count
drops from 7 to 5, meaning a 29% decrease, expected items rise 207%. **The link count and the item
count do not move at the same rate**, because one of the two lost links (initial-access) is the
chain's cheapest breaking point, and the other (collection) sits right before 41 items.

The first table writes the same distinction from the control side. Of the eight items, three are
signature-based, three behavior-based, two configuration-based. Looked at by inventory count, this
looks like a balanced three-three-two split; but in the unknown column, three items drop to zero
and **37.5%** of the inventory goes idle at once. There is no link between the size of a control
inventory and its zero-day resilience; the link is built by **which type** the items are and
**which steps** they fall on.

#### Which Control Type Stays Standing

The fourth table isolates the types one by one and gives an unexpected ranking. Under a known
threat, **signature only** — three controls — leaves 17.09 items; **behavior only** — three
controls — leaves 30.92. With the same number of control items, the signature-based set is nearly
twice as good, because its catch rates are higher (0.35, 0.40, 0.22 versus 0.20, 0.18, 0.45) and
two of them sit at the front of the chain.

Under an unknown threat, the contribution of the same three signature items is **zero**. What
remains standing is the five behavior-based and configuration-based items, and together they leave
19.69 items. The two types are weak on their own (30.92 and 26.32), and together they leave about
three times as many items as the known-threat figure — but **neither is zero**. In the row with no
controls at all, expected items are 48.00, the entire chain.

The decision follows from this. A signature-based control is the most efficient tool under a known
threat and there is no reason to drop it; under an unknown threat, it does nothing at all. A
configuration-based control does the same job in both cases and has the narrowest coverage (only 2
links). **A control inventory's zero-day resilience is measured by how many links its
non-signature items are found on** — 5 links in this set.

#### The Pre-Patch Window

The third table splits the window into three phases and counts how many items stay exposed in each
phase. In the fictional network, one event of this class is assumed every ten days.

The first phase is the period when the vulnerability is known but not publicly disclosed: 40 days,
4.0 events, 19.69 items per event, **78.76 items** in total. The second phase runs between
disclosure and the fix being published: 12 days. There is still no signature here, but the
behavior-based controls' thresholds are now tuned to that behavior, and expected items drop from
19.69 to 17.23. The breakable link count, though, **stays at 5** — tuning the threshold does not
open a new link, it only sharpens the existing one a little.

The third phase is the period when the fix has been published but not applied: 25 days. Because
the signature pattern has been published, the breakable link count returns to 7 and expected items
drop to 6.42. Even so, this phase leaves **16.05 items** exposed, because the fix has only been
published.

There is an ordering among the phases, and it runs opposite to what one would expect. The longest
phase by duration is the first (40 days), and it is also the heaviest by item count; but the second
phase carries 17.23 items per event despite lasting only 12 days, while the third phase carries
6.42 per event despite lasting 25 days. **Items do not fall as the window shortens; items fall
according to how many links can still be broken in that phase.**

The total window is 77 days, 7.7 events, and **115.49 items**. The same window would give **0.00**
if the fix had been applied: an applied fix is not a control but the **structural breaking**
measured in the third lesson — it removes the precondition, and the chain can never even reach the
second step. The result repeated across every measurement in this course takes its sharpest form
here: 78.76 of the 115.49 items accumulate in the first phase alone, the 40 days with no disclosure
at all, and the only thing that can be done in that phase is how many links the non-signature
controls are found on.

#### Summary

- Zero-day is not an adjective but a coverage loss: eight controls make 7 links breakable under a
  known threat, and 5 under an unknown one.
- The two lost links are the steps that have only a signature-based control facing them; a
  configuration-based control's catch is preserved at 100%, a behavior-based one keeps 70%, a
  signature-based one keeps 0%.
- While the breakable link count falls 29%, expected items rise from 6.42 to 19.69 — a 207%
  increase; the link count and the item count do not move at the same rate.
- Under a known threat, signature only (three controls) leaves 17.09 items, behavior only (three
  controls) leaves 30.92; under an unknown threat, signature's contribution is zero, and the
  remaining five items leave 19.69.
- The 77-day window leaves 115.49 items exposed; 78.76 of that accumulates in the 40
  pre-disclosure days, and 16.05 while the fix is published but not applied.
- An applied fix is not a control but structural breaking: the same window gives 0.00 items.

#### Next Step

This topic built the chain as a model and counted which links can be broken. Actors were separated
by capability and motivation, and how many steps each level got through was written down; on the
time axis, how many days go unseen by any control was measured; preconditions were written and the
early-break gain was calculated; techniques were mapped to steps and the step with no control
at all was counted; the four-corner relationship model was translated into a link count; and where
coverage falls under an unknown threat was written.

All these measurements had one thing in common. Every chain's first link was written as a
**technical** step: recon, then initial-access. Yet initial-access is opened, more often than not,
not by an exploit but by **a decision a person makes** — a reply given to a message, a sentence
said on a phone call, a moment that lets someone through a door alongside you. That link's
precondition is not a configuration weakness but a sense of trust or urgency, and the control set
against it is not a record pattern. The next topic — **Social Engineering** — measures this first
link of the chain.

## Social Engineering

Measuring the human layer as one link in the chain: the number of verification items at decision time and the budget that time pressure cuts short, the number of distinguishing indicators across phishing types and the cost of threshold choice, the verifiability gap between text messages and voice calls, physical techniques counted in access items, the edit distance and visual similarity used to detect typosquatted domain candidates, watering hole attacks built on a shared external dependency, and the persistence of awareness programs measured on two metrics.

### Fundamentals of Social Engineering

Source: https://academia.sh/en/courses/attack-techniques/fundamentals-of-social-engineering

Modeling the moment of decision as a time budget: how many of seven verification items can be used under pressure, how many of twelve requests are fake and pass anyway, how many of six chain links carry a control, and the requests a callback procedure stops versus the legitimate work it delays.

The previous topic modeled an attack as a sequence of steps and tried to place a control against
each one. In that model the first link in the chain was always technical: an unrecorded account, an
open port, an unmonitored connection. This topic asks a different question. When the first link in
the chain is a decision made by a person, what can be placed against that decision, and how much of
it?

Verifying a request takes time, and every verification item has its own cost. Time pressure is
therefore not a matter of style but a measurable constraint: as the seconds left to the
decision-maker shrink, the number of verification items available shrinks with them. Trust is
measured the same way — an unverifiable internal detail is a shortcut that stands in for an item.
This lesson reduces both factors to a single number, the **count of usable items**, and counts the
link at which the chain breaks. Attempting the steps modeled here outside written authorization and
a defined scope is both illegal and unprofessional.

- **SE1.** The request set, verification items, and their durations belong to the subscriber
  services desk of the fictional regional measurement network. No real request is processed; what is
  measured is the model built with `node`.
- **SE2.** Every verification item has a fixed duration. In reality duration varies by person and by
  current load; a fixed duration preserves the items' ranking from cheap to expensive, and that
  ranking is what the measure rests on.
- **SE3.** The decision-maker applies items from cheapest to most expensive. This is the **most
  optimistic** assumption; a real order is set by intuition and fits fewer items into the same
  budget.
- **SE4.** The request's true class is known in the model. In reality the class is known only after
  the fact; it is assumed known here so that fake requests that slip through and legitimate work
  that is delayed can be counted separately.
- **SE5.** An item either disqualifies a request or it does not. Partial suspicion, asking a second
  question, and putting a request on hold are not modeled.
- **SE6.** Time pressure is reduced to a single number: the seconds left for the decision. Factors
  such as authority, reciprocity, and embarrassment are not modeled separately; all of them are
  carried as the same number that shortens the budget.

#### The Moment of Decision Is a Time Budget

The model has three parts. Verification items are listed with their durations; requests are given
with their class, type, the time budget left to them, and their amount; and each request carries the
items that can **disqualify** it. The list of disqualifying items is not a guess but the model's
definition: if that item is applied, the request stops; if not, it passes.

Request type is carried as a separate column because the scope of a procedure is drawn by type.
There are three types — access, payment, and record — and which type a procedure covers directly
determines which chain it breaks; the cost of the one type left out of scope is counted at the end of
the lesson.

```js
// decisions.mjs — verification items, request set, and decision chain; model
export const ITEM = { // name: cost of applying the item, in seconds
  'compare amount to threshold': 20, 'compare sender address to record': 40,
  'look up work order number in system': 60, 'find requester in authorization list': 90,
  'check past request record': 120, 'call back on recorded number': 180, 'request supervisor approval': 300,
};
const [A, S, W, L, H, C, P] = Object.keys(ITEM);
export const THRESHOLD = 2000; // fictional payment unit
// [request, class, type, pressure budget (s), amount, items that disqualify the request]
export const REQUEST = [
  ['password reset', 'fake', 'access', 60, 0, [C, L]],
  ['payment account change', 'fake', 'payment', 120, 4800, [C, H]],
  ['urgent payment approval', 'fake', 'payment', 30, 9500, [A, P, C]],
  ['field access outside work order', 'fake', 'access', 90, 0, [W, C]],
  ['subscriber record export', 'fake', 'record', 180, 0, [L, P]],
  ['planned maintenance temporary access', 'legitimate', 'access', 300, 0, []],
  ['new field worker account', 'legitimate', 'access', 600, 0, []],
  ['end-of-period invoice approval', 'legitimate', 'payment', 240, 12000, []],
  ['meter replacement payment', 'legitimate', 'payment', 120, 1500, []],
  ['printer setup at branch office', 'legitimate', 'record', 480, 0, []],
  ['subscriber dispute record lookup', 'legitimate', 'record', 90, 0, []],
  ['temporary authorization for outage crew', 'legitimate', 'access', 45, 0, []],
];
// chain steps: [step, precondition, control that sees the step, item dropped once the step completes]
export const CHAIN = [
  ['gathering task information', 'externally visible task list', null, 0],
  ['building the pretext', 'the name of an internal process', null, 0],
  ['making contact', 'a reachable channel', 'is the channel on record', 1],
  ['signaling trust', 'an unverifiable internal detail', 'is the detail checked against the record', 1],
  ['creating urgency', "the decision-maker's time budget", 'is the time budget fixed by procedure', 2],
  ['requesting the action', 'an authorized decision-maker', 'is callback mandatory', 5],
];
export function items(budget) { // verification items that fit the budget, cheapest to most expensive
  let t = 0; const chosen = [];
  for (const [name, s] of Object.entries(ITEM)) { if (t + s > budget) break; t += s; chosen.push(name); }
  return { chosen, time: t };
}
export const procedureScope = (t) => t[2] === 'access' || (t[2] === 'payment' && t[4] > THRESHOLD);
```

#### How the Pressure Budget Cuts the Number of Items

```js
// run1.mjs — number of verification items usable under time pressure, and fake requests that pass
import { ITEM, REQUEST, items } from './decisions.mjs';

console.log(`${Object.keys(ITEM).length} verification items, total ${Object.values(ITEM).reduce((a, b) => a + b, 0)} s; ` +
  `${REQUEST.length} requests, ${REQUEST.filter((t) => t[1] === 'fake').length} fake\n`);
console.log(`${'request'.padEnd(39)} ${'class'.padStart(10)} ${'budget'.padStart(6)} ${'items'.padStart(5)} ${'result'.padStart(7)}`);
let passed = 0, stopped = 0;
for (const t of REQUEST) {
  const { chosen } = items(t[3]);
  const stoppedHere = t[5].some((k) => chosen.includes(k));
  if (t[1] === 'fake') { stoppedHere ? stopped++ : passed++; }
  console.log(`${t[0].padEnd(39)} ${t[1].padStart(10)} ${String(t[3]).padStart(6)} ${String(chosen.length).padStart(5)} ` +
    `${(t[1] === 'fake' ? (stoppedHere ? 'stopped' : 'passed') : 'passed').padStart(7)}`);
}
console.log(`\nusable items by pressure budget:`);
for (const b of [30, 60, 120, 180, 300, 600]) {
  const { chosen, time } = items(b);
  console.log(`  ${String(b).padStart(3)} s -> ${chosen.length}/${Object.keys(ITEM).length} items (${time} s), ` +
    `left out: ${Object.keys(ITEM).filter((k) => !chosen.includes(k)).length}`);
}
console.log(`\ndecision without a procedure: fake requests stopped ${stopped}, passed ${passed}`);
console.log(`fake requests that passed: ${REQUEST.filter((t) => t[1] === 'fake' && !t[5].some((k) => items(t[3]).chosen.includes(k))).map((t) => `${t[0]} (${t[3]} s)`).join('; ')}`);
```

```
7 verification items, total 810 s; 12 requests, 5 fake

request                                      class budget items  result
password reset                                fake     60     2  passed
payment account change                        fake    120     3  passed
urgent payment approval                       fake     30     1 stopped
field access outside work order               fake     90     2  passed
subscriber record export                      fake    180     3  passed
planned maintenance temporary access    legitimate    300     4  passed
new field worker account                legitimate    600     6  passed
end-of-period invoice approval          legitimate    240     4  passed
meter replacement payment               legitimate    120     3  passed
printer setup at branch office          legitimate    480     5  passed
subscriber dispute record lookup        legitimate     90     2  passed
temporary authorization for outage crew legitimate     45     1  passed

usable items by pressure budget:
   30 s -> 1/7 items (20 s), left out: 6
   60 s -> 2/7 items (60 s), left out: 5
  120 s -> 3/7 items (120 s), left out: 4
  180 s -> 3/7 items (120 s), left out: 4
  300 s -> 4/7 items (210 s), left out: 3
  600 s -> 6/7 items (510 s), left out: 1

decision without a procedure: fake requests stopped 1, passed 4
fake requests that passed: password reset (60 s); payment account change (120 s); field access outside work order (90 s); subscriber record export (180 s)
```

The lower table gives the lesson's first number. The desk holds seven verification items, and
applying all of them takes 810 seconds. In a thirty-second budget, only **one** of them can be used;
six are left out. Even in a hundred-and-eighty-second budget, only three items are usable; the two
most expensive items — the callback and the supervisor approval — only fit into a budget of six
hundred seconds. The number of items does not scale in proportion with the budget; it steps up: no
new item is added between one hundred twenty and one hundred eighty seconds, because the next item
costs ninety seconds and the running total reaches two hundred ten.

The upper table shows the consequence. Four of five fake requests pass, and what they share is that
their disqualifying items were left out: an item that would disqualify each fake request **exists**
in the model, but it does not fit into the budget the request was given. The one request that is
stopped is the one given the shortest budget — thirty seconds — and it is stopped by the cheapest
item. Comparing the amount to a threshold takes twenty seconds and flags a nine-thousand-five-hundred
unit payment on its own. This is the measure's first counterintuitive finding: raising the pressure
does not always help the chain, because a budget short enough on its own is conspicuous enough to
trigger even the cheapest item.

A decision made without a procedure therefore lets four fake requests through. What is measured is
not the decision-maker's carelessness but the shortness of the budget. The same person can apply six
items given six hundred seconds.

#### The Chain, Its Links, and the Early-Break Gain

```js
// run2.mjs — chain steps, the breakable link, and the callback procedure's three columns
import { REQUEST, CHAIN, ITEM, items, procedureScope } from './decisions.mjs';

const CALLBACK = 'call back on recorded number';
let cum = 0; const cumul = CHAIN.map((a) => (cum += a[3]));
console.log(`${'#'.padStart(2)} ${'step'.padEnd(28)} ${'precondition'.padEnd(34)} ${'control'.padStart(7)} ${'cum. items'.padStart(10)}`);
CHAIN.forEach((a, i) => console.log(`${String(i + 1).padStart(2)} ${a[0].padEnd(28)} ${a[1].padEnd(34)} ` +
  `${(a[2] ? 'yes' : 'no').padStart(7)} ${String(cumul[i]).padStart(10)}`));
const breakable = CHAIN.map((a, i) => (a[2] ? i : -1)).filter((i) => i >= 0);
const early = breakable[0], late = breakable[breakable.length - 1];
console.log(`\nchain has ${CHAIN.length} steps; with a control ${breakable.length}, with no control at all ${CHAIN.length - breakable.length}`);
console.log(`earliest breakable link ${early + 1} -> items dropped by then ${early ? cumul[early - 1] : 0}`);
console.log(`latest breakable link  ${late + 1} -> items dropped by then ${cumul[late - 1]}`);
console.log(`items dropped if never broken ${cumul[CHAIN.length - 1]}; early-break gain ${cumul[late - 1] - (early ? cumul[early - 1] : 0)} items`);

console.log(`\ncallback procedure (${ITEM[CALLBACK]} s, type 'access' or above-threshold 'payment'):`);
let stopped = 0, slipped = 0, delayed = [], extraTime = 0;
for (const t of REQUEST) {
  const scope = procedureScope(t);
  const applied = items(t[3]).chosen.concat(scope ? [CALLBACK] : []);
  if (scope) extraTime += ITEM[CALLBACK];
  if (t[1] === 'fake') { t[5].some((k) => applied.includes(k)) ? stopped++ : slipped++; }
  else if (scope && t[3] < ITEM[CALLBACK]) delayed.push(`${t[0]} (${t[3]} s)`);
}
console.log(`  requests in scope ${REQUEST.filter(procedureScope).length}/${REQUEST.length}, added verification time ${extraTime} s`);
console.log(`  fake requests stopped ${stopped}/5, slipped through ${slipped}`);
console.log(`  slipped through: ${REQUEST.filter((t) => t[1] === 'fake' && !procedureScope(t) && !t[5].some((k) => items(t[3]).chosen.includes(k))).map((t) => `${t[0]} (type ${t[2]})`).join('; ')}`);
console.log(`  legitimate work delayed ${delayed.length}: ${delayed.join('; ')}`);
```

```
 # step                         precondition                       control cum. items
 1 gathering task information   externally visible task list            no          0
 2 building the pretext         the name of an internal process         no          0
 3 making contact               a reachable channel                    yes          1
 4 signaling trust              an unverifiable internal detail        yes          2
 5 creating urgency             the decision-maker's time budget       yes          4
 6 requesting the action        an authorized decision-maker           yes          9

chain has 6 steps; with a control 4, with no control at all 2
earliest breakable link 3 -> items dropped by then 0
latest breakable link  6 -> items dropped by then 4
items dropped if never broken 9; early-break gain 4 items

callback procedure (180 s, type 'access' or above-threshold 'payment'):
  requests in scope 8/12, added verification time 1440 s
  fake requests stopped 4/5, slipped through 1
  slipped through: subscriber record export (type record)
  legitimate work delayed 1: temporary authorization for outage crew (45 s)
```

The chain has six steps, and the first two carry no control at all. This is not because those steps
are harmless but because they sit outside the defense's field of view: a task list being visible from
the outside and the name of an internal process being known are preparation steps that never touch
any record. The chain's first two links are therefore **uncountable**, only narrowable.

The four breakable links begin at the third step, and at that point no item has dropped yet. Breaking
at the sixth step comes after four items have already dropped. The difference between the two is the
early-break gain: **four items**. If the chain never breaks, the count reaches nine, meaning that
letting the final step through costs five items on its own. Read together, these three numbers turn
where a defense should invest from an opinion into a ranking.

The callback procedure shows this across three columns. The procedure covers eight of the twelve
requests and stops four of the five fake ones; a decision without a procedure had stopped only one.
The one request that slips through falls outside scope: a record-type export request is neither
access nor an above-threshold payment, so the procedure never touches it. This is where the measure
shifts to detection rather than the chain breaking: for this request there is no link to break — the
request passes, but the export can still be counted afterward.

The cost is written in two items. Adding one hundred eighty seconds to each of eight requests
produces a total of 1,440 seconds of added verification. Exactly one piece of legitimate work is
delayed, and which one matters: the request for temporary authorization for the outage crew arrives
with a forty-five-second budget, the callback takes one hundred eighty seconds, and the work does not
fit inside the procedure. The only legitimate work the procedure delays is the most urgent one. This
is not a flaw but the unavoidable result of the procedure's design: any procedure that turns time
pressure into a control also routes genuinely urgent work through that same control.

#### Summary

- All 7 verification items together take 810 seconds. A 30-second budget allows 1 item, a 180-second
  budget allows 3, a 600-second budget allows 6; the item count rises in steps, not in proportion to
  the budget.
- In a decision without a procedure, 4 of 5 fake requests pass. An item that would disqualify each
  one exists in the model, but it does not fit into the budget given; the one request stopped had the
  shortest budget, and the cheapest item stopped it.
- The chain has 6 steps; 4 carry a control, and the first 2 touch no record at all. These two links
  cannot be counted, only made harder by narrowing what is visible from outside.
- The earliest breakable link is step 3, where 0 items have dropped; breaking at step 6 means 4 items
  have already dropped. The early-break gain is 4 items, and the cost of never breaking the chain is
  9 items.
- The callback procedure covers 8 of 12 requests and stops 4 of 5 fake ones; the 1 request outside
  its scope breaks no link, and the measure shifts to detection.
- The procedure's cost is 1,440 seconds of added verification and 1 piece of delayed legitimate work
  — the most urgent request, whose budget is shorter than the procedure's own duration.

#### Next Step

In this lesson a request was assumed to have already reached the decision-maker, and what was
measured was the decision itself. The chain's third link — making contact — was passed over in a
single line, yet the message that contact carries is itself a measurable object. What separates a
legitimate message from a fake one in a message set is not tone but countable indicators: a sender
never seen before, a reply address that diverges from the sender, a link target that differs from the
displayed text. The next lesson's question is this: how many distinguishing indicators can be counted
in a message set, how many fake messages does the chosen threshold miss, how many legitimate messages
does it flag by mistake — and why does the indicator count fall as the target narrows.

### Phishing Types

Source: https://academia.sh/en/courses/attack-techniques/phishing-types

Counting ten indicator types in a fictional set of twenty-six messages: the gap in average indicator count across bulk, spear, and whaling messages, the opposite directions of what a threshold scan misses versus what it falsely flags, legitimate messages flagged independent of any threshold, and the early-break gain of a five-step chain.

The previous lesson assumed the request had already reached the decision-maker and passed over the
chain's third link — making contact — in a single line. Yet the message that contact carries is
itself a measurable object: it carries countable indicators, and those indicators can be summed into
a single number. This lesson produces that number and records, together, what the chosen threshold
misses and what it falsely flags.

What is measured is not the message's text. Text varies endlessly; indicator type does not. A sender
coming from a domain never seen before, a sender verification record that fails, a link target that
diverges from the displayed text — each of these is an item a control **can see**. How sender
verification records are set up was measured in a different course; here, that record's result is
only one of ten indicators. The real question is this: why does phishing carry fewer indicators as
its target narrows, and what does that do to the threshold?

- **SE7.** The message set belongs to the field-crew mailbox of the fictional regional measurement
  network. No real message exists; the model carries only **indicator type and count** — no message
  text, subject line, or link ever appears.
- **SE8.** Every message's true class is known in the model. In reality, class is known only after
  review; it is assumed known here so that missed and falsely flagged messages can be counted
  separately.
- **SE9.** Indicator weights are fixed, and the score is the sum of the weights. In real systems
  weight is learned and drifts; a fixed weight simplifies the curve's shape and does not change the
  direction of either error.
- **SE10.** The set is not realistic in class ratio: there are sixteen fake and ten legitimate
  messages. In a real inbox the number of legitimate messages is far larger, and the number of false
  positives grows in the same proportion. The model therefore **understates** the false-positive
  rate.
- **SE11.** A message is either flagged or it passes. Intermediate states such as quarantine, a
  warning banner, or asking the user are not modeled.
- **SE12.** The chain was reduced to a single pattern: delivery, interaction, credential entry, use.
  Variants that proceed with an attachment or that directly request payment were not counted as
  separate chains.

#### Indicator Types and Classes

```js
// messages.mjs — indicator types and each message's indicator set; model (no message content)
export const INDICATOR = { // indicator type: distinguishing weight
  'sender domain seen for the first time': 3, 'sender verification record fails': 3,
  'sender domain resembles a protected name': 3, 'link target diverges from displayed text': 3,
  'reply address differs from sender': 2, 'unexpected attachment type': 2,
  'wide recipient set': 1, 'unusual send time': 1,
  'first contact outside any reply thread': 1, 'time-pressure phrasing': 1,
};
const [F, V, R, L, P, A, W, U, C, T] = Object.keys(INDICATOR);
export const CLASS = ['bulk', 'spear', 'whaling', 'legitimate'];
// [message id, class, indicators it carries]
export const MESSAGE = [
  ['B1', 'bulk', [F, V, L, P, W, U, T]], ['B2', 'bulk', [F, V, L, A, W, T]],
  ['B3', 'bulk', [F, V, P, W, C, T]], ['B4', 'bulk', [F, R, L, W, U, T]],
  ['B5', 'bulk', [F, V, L, W, T]], ['B6', 'bulk', [V, L, P, W, U]],
  ['B7', 'bulk', [F, V, A, W, C, T]], ['B8', 'bulk', [F, L, W, U, C, T]],
  ['S1', 'spear', [R, L, C, T]], ['S2', 'spear', [F, L, C]],
  ['S3', 'spear', [R, P, T]], ['S4', 'spear', [V, L]], ['S5', 'spear', [R, A, C]],
  ['W1', 'whaling', [R, T]], ['W2', 'whaling', [L]], ['W3', 'whaling', [R, C]],
  ['L1', 'legitimate', [C]], ['L2', 'legitimate', [W, U]], ['L3', 'legitimate', []],
  ['L4', 'legitimate', [F, U, C]], ['L5', 'legitimate', [A]], ['L6', 'legitimate', [U, T]],
  ['L7', 'legitimate', [P]], ['L8', 'legitimate', [W, U, C, T]],
  ['L9', 'legitimate', [F, V, L, P, W]], ['L10', 'legitimate', [F, V, P, C]],
];
export const score = (m) => m[2].reduce((a, g) => a + INDICATOR[g], 0);
export const isFake = (m) => m[1] !== 'legitimate';
export function atThreshold(e) {
  const flagged = MESSAGE.filter((m) => score(m) >= e);
  return { missed: MESSAGE.filter((m) => isFake(m) && score(m) < e), falsePositives: flagged.filter((m) => !isFake(m)),
    caught: flagged.filter(isFake) };
}
// [step, precondition, control that sees the step, item dropped once the step completes]
export const CHAIN = [
  ['preparing the message', 'target address list', null, 0],
  ['delivery', 'a valid mail path', 'indicator threshold', 1],
  ['opening and interaction', "the recipient's interest", 'user reporting', 1],
  ['credential entry', 'a convincing collection point', 'second factor', 3],
  ['use of the collected information', 'a valid session', 'anomalous session detection', 6],
];
```

```js
// run1.mjs — per-indicator class distribution and the indicator-count gap across three attack types
import { INDICATOR, MESSAGE, CLASS, score, isFake } from './messages.mjs';

console.log(`${MESSAGE.length} messages, ${MESSAGE.filter(isFake).length} fake; ${Object.keys(INDICATOR).length} indicator types, ` +
  `highest possible score ${Object.values(INDICATOR).reduce((a, b) => a + b, 0)}\n`);
console.log(`${'indicator'.padEnd(40)} ${'wt.'.padStart(3)} ${'bulk'.padStart(5)} ${'spear'.padStart(7)} ` +
  `${'whaling'.padStart(8)} ${'legitimate'.padStart(10)}`);
for (const [g, a] of Object.entries(INDICATOR)) {
  const c = CLASS.map((s) => MESSAGE.filter((m) => m[1] === s && m[2].includes(g)).length);
  console.log(`${g.padEnd(40)} ${String(a).padStart(3)} ${String(c[0]).padStart(5)} ${String(c[1]).padStart(7)} ` +
    `${String(c[2]).padStart(8)} ${String(c[3]).padStart(10)}`);
}
console.log(`\n${'class'.padEnd(10)} ${'messages'.padStart(8)} ${'avg. indicators'.padStart(15)} ${'avg. score'.padStart(10)} ` +
  `${'min score'.padStart(13)} ${'max score'.padStart(14)}`);
for (const s of CLASS) {
  const k = MESSAGE.filter((m) => m[1] === s), p = k.map(score);
  console.log(`${s.padEnd(10)} ${String(k.length).padStart(8)} ` +
    `${(k.reduce((a, m) => a + m[2].length, 0) / k.length).toFixed(2).padStart(15)} ` +
    `${(p.reduce((a, b) => a + b, 0) / k.length).toFixed(2).padStart(10)} ${String(Math.min(...p)).padStart(13)} ` +
    `${String(Math.max(...p)).padStart(14)}`);
}
const whl = MESSAGE.filter((m) => m[1] === 'whaling'), leg = MESSAGE.filter((m) => m[1] === 'legitimate');
console.log(`\naverage indicator count of whaling messages (${(whl.reduce((a, m) => a + m[2].length, 0) / whl.length).toFixed(2)}), ` +
  `lower than that of legitimate messages (${(leg.reduce((a, m) => a + m[2].length, 0) / leg.length).toFixed(2)})`);
console.log(`4 highest-scoring messages: ${[...MESSAGE].sort((a, b) => score(b) - score(a)).slice(0, 4).map((m) => `${m[0]}/${m[1]} (${score(m)})`).join(', ')}`);
```

```
26 messages, 16 fake; 10 indicator types, highest possible score 20

indicator                                wt.  bulk   spear  whaling legitimate
sender domain seen for the first time      3     7       1        0          3
sender verification record fails           3     6       1        0          2
sender domain resembles a protected name   3     1       3        2          0
link target diverges from displayed text   3     6       3        1          1
reply address differs from sender          2     3       1        0          3
unexpected attachment type                 2     2       1        0          1
wide recipient set                         1     8       0        0          3
unusual send time                          1     4       0        0          4
first contact outside any reply thread     1     3       3        1          4
time-pressure phrasing                     1     7       2        1          2

class      messages avg. indicators avg. score     min score      max score
bulk              8            5.88      11.50            10             14
spear             5            3.00       6.60             6              8
whaling           3            1.67       3.67             3              4
legitimate       10            2.30       3.90             0             12

average indicator count of whaling messages (1.67), lower than that of legitimate messages (2.30)
4 highest-scoring messages: B1/bulk (14), B2/bulk (13), B4/bulk (12), L9/legitimate (12)
```

Read row by row, the indicator table shows which item belongs to which class. A wide recipient set
appears in eight of eight bulk messages and in none of the spear or whaling messages; a sender domain
that resembles a protected name runs the other way — it appears in one bulk message and in five
narrowly targeted ones. Cheap indicators are a scale signal; expensive indicators are a preparation
signal.

The class table gives the lesson's central number. Bulk phishing carries 5.88 indicators per
message, spear phishing 3.00, whaling 1.67. The drop is not chance; it is the definition itself: as
the target narrows, **cheap** indicators such as a wide recipient set, an unusual hour, and generic
sending infrastructure disappear. What remains is only the expensive indicators, and there are few of
those too.

The last line shows the measure's limit. The average indicator count of whaling messages is 1.67,
that of legitimate messages 2.30. A fake message carries **fewer** indicators than a legitimate one.
No threshold that looks only at indicator count can separate these two sets in the right direction; a
threshold that tries flags legitimate messages before it flags fake ones.

The top of the score ranking shows the same problem on the weight side. Fourth place is a legitimate
message, sitting at twelve points in the same band as three bulk phishing messages. This message is a
subscriber notice sent through an external delivery service: it carries a domain seen for the first
time, a verification record that fails, a reply address that diverges from the sender, and a link
target that diverges from the displayed text. Every one of these indicators is counted correctly; the
message not being fake does not change that.

#### Where the Threshold Collapses

```js
// run2.mjs — threshold scan, structural false positives, and the chain's early-break gain
import { MESSAGE, CHAIN, score, isFake, atThreshold } from './messages.mjs';

const FAKE = MESSAGE.filter(isFake).length;
console.log(`${'threshold'.padStart(9)} ${'caught'.padStart(9)} ${'missed'.padStart(6)} ${'false pos.'.padStart(10)} ` +
  `${'total wrong'.padStart(11)} ${'class distribution of missed'.padStart(30)}`);
const result = [];
for (let e = 13; e >= 2; e--) {
  const r = atThreshold(e); result.push({ e, r });
  const d = ['bulk', 'spear', 'whaling'].map((s) => `${s[0].toUpperCase()}${r.missed.filter((m) => m[1] === s).length}`).join(' ');
  console.log(`${String(e).padStart(9)} ${`${r.caught.length}/${FAKE}`.padStart(9)} ${String(r.missed.length).padStart(6)} ` +
    `${String(r.falsePositives.length).padStart(10)} ${String(r.missed.length + r.falsePositives.length).padStart(11)} ${d.padStart(30)}`);
}
const least = result.reduce((a, s) => (s.r.missed.length + s.r.falsePositives.length < a.r.missed.length + a.r.falsePositives.length ? s : a));
const clean = result.filter((s) => s.r.falsePositives.length === 0);
const full = result.filter((s) => s.r.missed.length === 0);
console.log(`\nthreshold minimizing total wrong ${least.e}: missed ${least.r.missed.length}, falsely flagged ${least.r.falsePositives.length}`);
console.log(`lowest threshold with zero false positives ${Math.min(...clean.map((s) => s.e))}: missed there ${clean.reduce((a, s) => (s.e < a.e ? s : a)).r.missed.length}`);
console.log(`highest threshold that misses nothing ${Math.max(...full.map((s) => s.e))}: falsely flagged there ${full.reduce((a, s) => (s.e > a.e ? s : a)).r.falsePositives.length}`);
const always = MESSAGE.filter((m) => !isFake(m) && score(m) >= 9);
console.log(`legitimate messages flagged independent of threshold ${always.length}: ${always.map((m) => `${m[0]} (${score(m)} points, ${m[2].length} indicators)`).join('; ')}`);

let cum = 0; const cumul = CHAIN.map((a) => (cum += a[3]));
const breakable = CHAIN.map((a, i) => (a[2] ? i : -1)).filter((i) => i >= 0);
console.log(`\nchain has ${CHAIN.length} steps, with a control ${breakable.length}, with no control ${CHAIN.length - breakable.length}`);
console.log(`${'#'.padStart(2)} ${'step'.padEnd(34)} ${'precondition'.padEnd(31)} ${'control'.padEnd(29)} ${'cum.'}`);
CHAIN.forEach((a, i) => console.log(`${String(i + 1).padStart(2)} ${a[0].padEnd(34)} ${a[1].padEnd(31)} ${(a[2] ?? '-').padEnd(29)} ${cumul[i]}`));
console.log(`earliest breakable link ${breakable[0] + 1} (items dropped ${breakable[0] ? cumul[breakable[0] - 1] : 0}), ` +
  `latest ${breakable[breakable.length - 1] + 1} (items dropped ${cumul[breakable[breakable.length - 1] - 1]})`);
console.log(`early-break gain ${cumul[breakable[breakable.length - 1] - 1] - (breakable[0] ? cumul[breakable[0] - 1] : 0)} items; if never broken ${cumul[CHAIN.length - 1]}`);
```

```
threshold    caught missed false pos. total wrong   class distribution of missed
       13      2/16     14          0          14                       B6 S5 W3
       12      3/16     13          1          14                       B5 S5 W3
       11      6/16     10          1          11                       B2 S5 W3
       10      8/16      8          1           9                       B0 S5 W3
        9      8/16      8          2          10                       B0 S5 W3
        8      9/16      7          2           9                       B0 S4 W3
        7     10/16      6          2           8                       B0 S3 W3
        6     13/16      3          2           5                       B0 S0 W3
        5     13/16      3          3           6                       B0 S0 W3
        4     15/16      1          4           5                       B0 S0 W1
        3     16/16      0          4           4                       B0 S0 W0
        2     16/16      0          8           8                       B0 S0 W0

threshold minimizing total wrong 3: missed 0, falsely flagged 4
lowest threshold with zero false positives 13: missed there 14
highest threshold that misses nothing 3: falsely flagged there 4
legitimate messages flagged independent of threshold 2: L9 (12 points, 5 indicators); L10 (9 points, 4 indicators)

chain has 5 steps, with a control 4, with no control 1
 # step                               precondition                    control                       cum.
 1 preparing the message              target address list             -                             0
 2 delivery                           a valid mail path               indicator threshold           1
 3 opening and interaction            the recipient's interest        user reporting                2
 4 credential entry                   a convincing collection point   second factor                 5
 5 use of the collected information   a valid session                 anomalous session detection   11
earliest breakable link 2 (items dropped 0), latest 5 (items dropped 5)
early-break gain 5 items; if never broken 11
```

The class-distribution-of-missed column repeats the same thing throughout the scan. As the threshold
falls from ten to six, bulk phishing is already at zero and spear phishing begins to erode, but all
three whaling messages still evade the threshold entirely at six. The W column only moves once the
threshold reaches four, and it hits zero at three. A threshold that drops from thirteen to three
catches every fake message; in exchange, four legitimate messages get flagged — two-fifths of the ten
legitimate messages.

Two of the false positives are not a threshold problem. The messages scoring twelve and nine points
are flagged at every threshold that catches whaling messages — even at most thresholds that fail to
catch them. The two sets are interleaved in the same score band. This is not fixed by adjusting
weights; fixing it requires legitimate bulk mail to **lose its own indicators**: leaving its own
domain, having its verification record pass, having its link target match the text. The thing to fix
is not the threshold but the legitimate flow itself.

The chain table closes three columns. The first of the five steps is a preparation step no control
sees. The earliest of the four breakable links is delivery, where the dropped-item count is zero; the
latest is the fifth step, by which point five items have already dropped. The early-break gain is
five items; if the chain never breaks, the dropped-item count reaches eleven. Raising the threshold to
loosen the second link is therefore not just a matter of one number changing: it shifts the load onto
the third and fourth links — user reporting and the second factor.

#### Summary

- 10 indicator types were counted across a set of 26 messages. Bulk phishing carries 5.88 indicators
  per message, spear phishing 3.00, whaling 1.67; cheap indicators disappear as the target narrows.
- The average indicator count of whaling messages (1.67) is lower than that of legitimate messages
  (2.30). A measure that looks only at indicator count separates these two sets in the wrong
  direction.
- At threshold 13, false positives are zero and 14 of 16 fake messages evade it. At threshold 3, none
  evade it and 4 of 10 legitimate messages are flagged. The threshold that minimizes total error is 3
  (0 + 4).
- The class distribution of missed messages evades all 3 whaling messages entirely up to threshold 6;
  this column only moves at threshold 4.
- 2 legitimate messages are flagged independent of threshold (12 and 9 points). This is not a
  threshold-tuning problem; it is legitimate bulk mail carrying the same indicators.
- The chain has 5 steps, 1 without a control. The earliest breakable link is step 2 (0 items), the
  latest is step 5 (5 items); the early-break gain is 5 items, and 11 items drop if the chain is
  never broken.

#### Next Step

Every indicator in this lesson belonged to a single channel, and that channel had one thing in
common: the message is stored. Headers can be read, the domain can be queried, the link target can be
compared with the text, and all of this can be done before the message is opened. In text messages
and voice calls, most of these items do not exist; in a voice call, the one verifiable item that
remains is usually the caller's number, and even that may not be what it appears to be. That is the
next lesson's question: how many verification items exist per channel, at which link the chain can be
broken in each channel, and how many chains a callback verification procedure breaks, channel by
channel.

### Other Channels

Source: https://academia.sh/en/courses/attack-techniques/other-channels

Measuring text-message- and voice-call-based phishing in terms of verifiability: the strength of ten verification items across four channels, the fact that the number of strong items in a voice call is zero, the number of breakable links per channel, and the link a callback verification procedure gains in only one channel versus the cost it pays across four channels.

Every indicator in the previous lesson belonged to a single channel, and that channel had a defining
property: the message is stored. Headers can be read, the domain can be queried, the link target can
be compared with the text — and all of this can be done before the message is opened. In a text
message, part of these items is missing; in a voice call, nearly all of them are.

This lesson compares channels not by tone but by a single measure: **how many verification items can
be applied per channel.** An item is in one of three states — applicable in the channel, present in
the channel but spoofable, absent from the channel entirely. The same five-step chain is run across
four channels, and the number of links broken in each channel is counted. What emerges is a channel in
which the chain breaks at no link at all; which procedure can break that channel, and what that
procedure costs, is the lesson's closing section.

- **SE13.** The channel list, verification items, and strength values belong to the field crews and
  the subscriber services desk of the fictional regional measurement network. What is measured is the
  model built with `node`.
- **SE14.** The strength value has three tiers (2, 1, 0). Real verifiability is continuous; three
  tiers preserve the ranking across channels and erase the fine differences within a channel.
- **SE15.** An item counted as "spoofable" means that the shown source cannot be verified against an
  independent record as being the source itself. Under what conditions the channel's infrastructure
  provides this was not modeled.
- **SE16.** The chain was taken as **identical** across the four channels. In reality the channel also
  changes the chain's length; a fixed chain makes cross-channel comparison possible.
- **SE17.** Traffic counts are daily and fictional. A request that is in scope is one requesting
  authorization or payment; a request that does not fit the window is one that must pass before the
  callback completes.
- **SE18.** A step is either broken or it is not. Partial breaking — slowing the request, asking a
  second question — was not modeled.

#### Verifiability Per Channel

```js
// channels.mjs — per-channel verification-item strength, the chain, and legitimate traffic; model
export const CHANNEL = ['email', 'text message', 'voice call', 'instant message'];
// item: strength per channel in channel order (2 applicable, 1 weak/spoofable, 0 absent)
export const ITEM = {
  'sender domain lookup': [2, 0, 0, 1],
  'sender verification record': [2, 0, 0, 0],
  'link target comparison': [2, 2, 0, 2],
  'inspection without opening the message': [2, 2, 0, 2],
  'lookup in a saved directory': [2, 1, 1, 1],
  'later re-reading': [2, 2, 0, 2],
  'matching against past correspondence': [2, 1, 0, 2],
  'verifying the source against an independent record': [1, 1, 1, 1],
  'window to consult a second person': [2, 2, 0, 2],
  'retaining a record for audit': [2, 2, 0, 1],
};
// [step, precondition, item that breaks the step (needs strength 2), item dropped once the step completes]
export const CHAIN = [
  ['channel selection and preparation', 'a reachable channel address', null, 0],
  ['contact', 'the recipient responding', 'lookup in a saved directory', 1],
  ['building trust', 'an internal detail that cannot be verified', 'matching against past correspondence', 1],
  ['conveying the request', "the recipient's interest", 'inspection without opening the message', 3],
  ['action', 'the decision-maker being alone', 'window to consult a second person', 6],
];
export const CALLBACK_TIME = 180; // seconds
// [channel, daily legitimate requests, in procedure scope, in scope but does not fit the callback window]
export const TRAFFIC = [['email', 240, 36, 1], ['text message', 60, 15, 3],
  ['voice call', 45, 18, 7], ['instant message', 120, 24, 2]];
export const strength = (item, k) => ITEM[item][k];
export function breakable(k, procedure = false) {
  const d = CHAIN.map((a, i) => (a[2] && strength(a[2], k) === 2 ? i : -1)).filter((i) => i >= 0);
  return procedure && !d.includes(4) ? [...d, 4] : d;
}
```

```js
// run1.mjs — number of verification items per channel and total verifiability strength
import { CHANNEL, ITEM } from './channels.mjs';

console.log(`${Object.keys(ITEM).length} verification items, ${CHANNEL.length} channels ` +
  `(2 applicable, 1 weak, 0 absent)\n`);
console.log(`${'item'.padEnd(52)} ${CHANNEL.map((k) => k.padStart(16)).join('')}`);
for (const [name, v] of Object.entries(ITEM))
  console.log(`${name.padEnd(52)} ${v.map((g) => String(g).padStart(16)).join('')}`);
console.log(`\n${'channel'.padEnd(16)} ${'strong items'.padStart(12)} ${'weak items'.padStart(11)} ` +
  `${'absent'.padStart(7)} ${'total strength'.padStart(14)} ${'% of email'.padStart(18)}`);
const strong = CHANNEL.map((_, k) => Object.values(ITEM).filter((v) => v[k] === 2).length);
const total = CHANNEL.map((_, k) => Object.values(ITEM).reduce((a, v) => a + v[k], 0));
CHANNEL.forEach((name, k) => console.log(`${name.padEnd(16)} ${String(strong[k]).padStart(12)} ` +
  `${String(Object.values(ITEM).filter((v) => v[k] === 1).length).padStart(11)} ` +
  `${String(Object.values(ITEM).filter((v) => v[k] === 0).length).padStart(7)} ${String(total[k]).padStart(14)} ` +
  `${`${Math.round((total[k] / total[0]) * 100)}%`.padStart(18)}`));
const inAll = Object.entries(ITEM).filter(([, v]) => v.every((g) => g >= 1));
console.log(`\nitems present in all 4 channels ${inAll.length}: ${inAll.map(([a, v]) => `${a} (strength ${v.join('/')})`).join('; ')}`);
console.log(`strong items in voice call: ${strong[2]}`);
```

```
10 verification items, 4 channels (2 applicable, 1 weak, 0 absent)

item                                                            email    text message      voice call instant message
sender domain lookup                                                2               0               0               1
sender verification record                                          2               0               0               0
link target comparison                                              2               2               0               2
inspection without opening the message                              2               2               0               2
lookup in a saved directory                                         2               1               1               1
later re-reading                                                    2               2               0               2
matching against past correspondence                                2               1               0               2
verifying the source against an independent record                  1               1               1               1
window to consult a second person                                   2               2               0               2
retaining a record for audit                                        2               2               0               1

channel          strong items  weak items  absent total strength         % of email
email                       9           1       0             19               100%
text message                5           3       2             13                68%
voice call                  0           2       8              2                11%
instant message             5           4       1             14                74%

items present in all 4 channels 2: lookup in a saved directory (strength 2/1/1/1); verifying the source against an independent record (strength 1/1/1/1)
strong items in voice call: 0
```

The channel table converges on a single line: the number of strong verification items in a voice call
is **zero**. Eight of the ten items are entirely absent in this channel, two are present but
spoofable. Total strength is two, against email's nineteen — eleven percent. This is not because the
channel is badly designed; it is that a live, unstored channel has no object left to examine: the
conversation cannot be looked at before it opens, cannot be re-read afterward, cannot be retained for
audit.

The difference between a text message and an instant message is also instructive. Both have five
strong items, but the distribution differs: in an instant message, matching against past
correspondence is strong while in a text message it is weak; in a text message, retaining a record for
audit is strong while in an instant message it is weak. Even though total strength is close, which
link of the chain they break at will differ, because links depend not on a sum but on **specific
items**.

The percentage in the total-strength column ranks channels with a single number, but it can mislead: a
text message appears as sixty-eight percent of email, yet email's two strongest items — the domain
lookup and the sender verification record — are entirely absent from this channel. The percentage does
not say **which** item is missing; its counterpart in the chain will be counted separately in the next
section.

Of the two items present in all four channels, the first is strong in only one channel — email; the
second is not strong in any channel. No verification item works across every channel; a channel-
independent verification procedure can only be added from outside.

#### Where the Chain Breaks by Channel

```js
// run2.mjs — breakable links per channel, and the gain and cost of the callback verification procedure
import { CHANNEL, CHAIN, TRAFFIC, CALLBACK_TIME, breakable } from './channels.mjs';

let s = 0; const cumul = CHAIN.map((a) => (s += a[3]));
console.log(`${'#'.padStart(2)} ${'step'.padEnd(35)} ${'precondition'.padEnd(44)} ${'item that breaks the step'.padEnd(40)} cumulative`);
CHAIN.forEach((a, i) => console.log(`${String(i + 1).padStart(2)} ${a[0].padEnd(35)} ${a[1].padEnd(44)} ` +
  `${(a[2] ?? '-').padEnd(40)} ${cumul[i]}`));
const FULL = cumul[CHAIN.length - 1];
const row = (name, d) => {
  const e = d.length ? (d[0] ? cumul[d[0] - 1] : 0) : FULL, g = d.length ? cumul[d[d.length - 1] - 1] : FULL;
  return `${name.padEnd(16)} ${String(d.length).padStart(6)} ${(d.length ? String(d[0] + 1) : '-').padStart(10)} ` +
    `${String(e).padStart(11)} ${String(g).padStart(10)} ${String(g - e).padStart(6)}`;
};
console.log(`\n${'channel'.padEnd(16)} ${'links'.padStart(6)} ${'earliest'.padStart(10)} ${'early items'.padStart(11)} ` +
  `${'late items'.padStart(10)} ${'gain'.padStart(6)}   (if never broken ${FULL})`);
CHANNEL.forEach((name, k) => console.log(row(name, breakable(k))));
console.log(`\nafter adding the callback verification procedure (callback to the recorded channel, ${CALLBACK_TIME} s):`);
CHANNEL.forEach((name, k) => console.log(row(name, breakable(k, true))));
console.log(`\n${'channel'.padEnd(16)} ${'legitimate requests'.padStart(20)} ${'in scope'.padStart(8)} ${'added time (s)'.padStart(14)} ` +
  `${'delayed jobs'.padStart(12)} ${'link gain +'.padStart(15)}`);
let scope = 0, delayed = 0;
for (const [name, t, k, a] of TRAFFIC) {
  scope += k; delayed += a;
  const i = CHANNEL.indexOf(name), diff = breakable(i, true).length - breakable(i).length;
  console.log(`${name.padEnd(16)} ${String(t).padStart(20)} ${String(k).padStart(8)} ${String(k * CALLBACK_TIME).padStart(14)} ` +
    `${String(a).padStart(12)} ${String(diff).padStart(15)}`);
}
console.log(`total: in scope ${scope} requests, ${scope * CALLBACK_TIME} s (${(scope * CALLBACK_TIME / 3600).toFixed(2)} hours), delayed jobs ${delayed}`);
console.log(`voice call: items dropped before the procedure ${FULL}, after ${cumul[3]}; gain ${FULL - cumul[3]} items, cost 7 delayed jobs`);
const only = TRAFFIC.find((t) => t[0] === 'voice call');
console.log(`if the procedure is applied only to voice call: in scope ${only[2]} requests, ${only[2] * CALLBACK_TIME} s, ` +
  `delayed jobs ${only[3]}; the link-gain is the same (1)`);
```

```
 # step                                precondition                                 item that breaks the step                cumulative
 1 channel selection and preparation   a reachable channel address                  -                                        0
 2 contact                             the recipient responding                     lookup in a saved directory              1
 3 building trust                      an internal detail that cannot be verified   matching against past correspondence     2
 4 conveying the request               the recipient's interest                     inspection without opening the message   5
 5 action                              the decision-maker being alone               window to consult a second person        11

channel           links   earliest early items late items   gain   (if never broken 11)
email                 4          2           0          5      5
text message          2          4           2          5      3
voice call            0          -          11         11      0
instant message       3          3           1          5      4

after adding the callback verification procedure (callback to the recorded channel, 180 s):
email                 4          2           0          5      5
text message          2          4           2          5      3
voice call            1          5           5          5      0
instant message       3          3           1          5      4

channel           legitimate requests in scope added time (s) delayed jobs     link gain +
email                             240       36           6480            1               0
text message                       60       15           2700            3               0
voice call                         45       18           3240            7               1
instant message                   120       24           4320            2               0
total: in scope 93 requests, 16740 s (4.65 hours), delayed jobs 13
voice call: items dropped before the procedure 11, after 5; gain 6 items, cost 7 delayed jobs
if the procedure is applied only to voice call: in scope 18 requests, 3240 s, delayed jobs 7; the link-gain is the same (1)
```

The channel table shows how the chain changes by channel. In email, four links are breakable and the
earliest is the second step: no item has dropped there yet. In a text message the number of breakable
links falls to two, and the earliest is the fourth step — by that point two items have already
dropped. In an instant message there are three links, and the earliest is the third step. In a voice
call the number of breakable links is **zero**, and the table's "late items" column reads eleven: the
number of items dropped when the chain never breaks at all.

This is the course's second encounter with the same situation: when a chain has no control at any
link, the measure shifts from breaking to **detection**. In a voice call, detection comes not from the
call itself but from what follows it — from the record of the item where the request was carried out.
The chain not breaking does not mean what happened goes unseen; it only means that seeing it happens
**afterward**.

The callback verification procedure changes this table in exactly one place. In a voice call, the
fifth link becomes breakable, and dropped items fall from eleven to five: the gain is six items. The
other three channels see no change at all, because the fifth link was already breakable there. Every
gain from the procedure is confined to a single channel.

The cost table writes the counterpart. If the procedure is applied across all four channels at once,
it covers ninety-three requests, adds 16,740 seconds a day — more than
four and a half hours — of verification time, and delays thirteen legitimate jobs. Its contribution to
the breakable-link count is one, and only in the voice call; in the other three channels it is zero.
If the procedure is applied only to the voice call, scope falls to eighteen requests, added time to
3,240 seconds, and delayed jobs to seven; the link gained stays the same. The cost of the same gain
drops to a fifth, and the only thing that makes the difference is **where** the procedure is applied.

The seven delayed jobs are the highest in this channel, and that is not a coincidence. A voice call is
also the channel of urgent legitimate work; seven of the eighteen in-scope requests do not fit the
callback window. The chain's weakest channel is also the channel where the procedure is most
expensive.

#### The Measure of Switching Channels

The tables measure a chain that runs in a single channel, yet a chain's steps can be spread across
different channels. The effect can be read directly from the output. In email the breakable links are
the second and third steps; in a voice call, neither the fourth nor the fifth step is breakable. When
the first three steps pass through email and the last two through a voice call, only two breakable
links remain, and both sit at values one and two of the cumulative column.

The numeric result is this: on the same chain, the early-break gain falls from five items to one, and
the nine-item gap carried by the last two links stays behind no control at all. Switching channels
does not add a new step to the chain; it only moves existing steps to a channel with fewer verification
items. The defense-side counterpart is measured the same way — what matters is not which channel a
request **begins** in, but which items are present in the channel it **completes** in. In the
previous section, applying the procedure only to voice calls looked cheap; switching channels makes
this choice necessary in addition, because the chain's final link can deliberately be moved to that
channel. Per-channel measurement is therefore not enough on its own; the sequence of channels a request
follows must also be recorded as an item.

#### Summary

- The total strength of 10 verification items across the four channels is 19, 13, 2, and 14
  respectively. The number of strong items in a voice call is 0; 8 items are entirely absent, 2 are
  spoofable.
- Only 2 items are present in all four channels, and neither is strong in more than one channel.
  Channel-independent verification can only be added by an external procedure.
- The same 5-step chain breaks at 4 links in email, 3 in an instant message, 2 in a text message, and
  0 in a voice call. In a voice call the chain never breaks, and dropped items stay at 11.
- In an unbreakable chain the measure shifts to detection: in a voice call, seeing happens not during
  the call but in the record of the item where the request was carried out.
- The callback verification procedure gains one link only in a voice call; dropped items fall from 11
  to 5, a gain of 6 items. The other three channels gain 0.
- Applied across all four channels, the procedure costs 16,740 seconds and 13 delayed jobs a day;
  applied only to voice calls, 3,240 seconds and 7 delayed jobs. The link gained is the same in both
  cases.

#### Next Step

All four channels in this lesson worked remotely, and all of them shared one precondition: an address
— an email address, a number, or an account. A person entering a building needs none of these
preconditions. Going through a door, spoofing a card, looking at a screen left unlocked, or collecting
a discarded document draws on a different set of resources: doors, cards, and visual verification.
That is the next lesson's question: how many access items exist in a building model, how many of them
have a control, which chain breaks at which item, and how many steps an escort procedure breaks in
exchange for how many legitimate passages it slows down.

### Physical Techniques

Source: https://academia.sh/en/courses/attack-techniques/physical-techniques

Counting tailgating, shoulder surfing, and dumpster diving in terms of access items: the number of paths to the target zone in a fictional building made up of ten doors, five cards, and two visual verifications, how many doors break the chain on each path, the two paths that never break, and the ten access items the escort procedure gains against its daily cost of 6.8 hours.

Every channel in the previous lesson worked remotely, and all of them shared one precondition: an
address — an email address, a number, or an account. A person entering a building needs none of
these. Going through a door, looking at a screen left unlocked, or collecting a discarded document
draws on a different set of resources, and this set's items are countable: doors, cards, visual
verification.

This lesson measures three techniques on the same building model. Tailgating exploits a door staying
open; shoulder surfing exploits a line of sight; dumpster diving exploits waste that has not been
separated. All three have short chains, and in all three the first step is one no control sees. What
is measured is not how entering the building is done, but **which access item breaks which link**.
Running trials like these outside of written authorization and a defined scope is both illegal and
unprofessional.

- **SE19.** The building, door graph, card authorizations, and waste flow belong to a branch office
  of the fictional regional measurement network. No real building exists; what is measured is the model
  built with `node`.
- **SE20.** A card alone does not stop tailgating: once a card is read, the door opens, and the card
  does not know how many people passed through behind it. The controls that break the chain are a
  control that **sees** the person and a single-person passage.
- **SE21.** The path search counts paths that do not pass through the same door twice. Non-door
  passages such as windows, elevator shafts, and fire exits were not modeled; a real building would
  count these paths too.
- **SE22.** Visual verification depends on a person being present there. Off-shift hours and busy
  passage moments were not modeled; both lower this control's strength.
- **SE23.** The dropped access item is the count of doors, screens, and documents that become
  reachable once that step completes. Item types were not weighted separately.
- **SE24.** The escort procedure rests on the assumption that a person alone in an interior zone is
  seen. An escort losing sight of the person they are escorting was not modeled.

#### Access Items and Paths

```js
// building.mjs — door graph, card authorizations, and the chain of three physical techniques; model
// [door, source zone, target zone, control]
export const DOOR = [
  ['main entrance', 'outside', 'entrance hall', 'visual'],
  ['side entrance', 'outside', 'entrance hall', 'card'],
  ['parking garage elevator', 'outside', 'office floor', 'card'],
  ['loading dock door', 'outside', 'warehouse', 'none'],
  ['lobby turnstile', 'entrance hall', 'office floor', 'card+single person'],
  ['warehouse corridor', 'warehouse', 'office floor', 'none'],
  ['equipment room door', 'office floor', 'equipment room', 'card'],
  ['server room door', 'equipment room', 'server room', 'card+visual'],
  ['archive door', 'office floor', 'archive', 'card'],
  ['waste point', 'warehouse', 'waste area', 'none'],
];
export const CARD = { // card type: doors it opens
  visitor: ['side entrance'], office: ['side entrance', 'lobby turnstile', 'archive door'],
  field: ['side entrance', 'lobby turnstile', 'loading dock door'],
  technical: ['side entrance', 'parking garage elevator', 'lobby turnstile', 'equipment room door', 'server room door'],
  cleaning: ['side entrance', 'lobby turnstile', 'equipment room door', 'archive door'],
};
// a card alone does not stop tailgating; a control that sees the person or a single-person passage does
export const breaks = (control) => control.includes('visual') || control.includes('single person');
export function paths(target, zone = 'outside', visited = []) {
  if (zone === target) return [visited];
  return DOOR.filter((k) => k[1] === zone && !visited.includes(k[0]))
    .flatMap((k) => paths(target, k[2], [...visited, k[0]]));
}
export const controlOf = (name) => DOOR.find((k) => k[0] === name)[3];
// [technique, [step, precondition, control that sees the step, access item dropped once the step completes]]
export const TECHNIQUE = {
  tailgating: [
    ['waiting in front of the door', 'the door being visible from outside', null, 0],
    ['waiting for an authorized passage', 'a busy passage hour', null, 0],
    ['entering on the same passage', 'the door staying open', 'single-person passage', 2],
    ['advancing to the target zone', 'interior doors being uncontrolled', 'visual verification', 5],
  ],
  'shoulder surfing': [
    ['positioning in a shared area', 'visitor access', null, 0],
    ['observing the screen or keypad', 'line of sight', 'screen placement', 1],
    ['using the observed information', 'a valid session', 'second factor', 4],
  ],
  'dumpster diving': [
    ['accessing the waste point', 'the waste area being uncontrolled', null, 0],
    ['collecting documents', 'unseparated waste', 'document destruction procedure', 3],
    ['using the collected information', 'the information being current', 'record retention period', 4],
  ],
};
export const ESCORT = { dailyPassages: 34, averageMinutes: 12 }; // fictional
```

```js
// run1.mjs — count of access items and the control density of paths to the target zone
import { DOOR, CARD, breaks, paths, controlOf } from './building.mjs';

const visual = DOOR.filter((k) => k[3].includes('visual'));
console.log(`${DOOR.length} doors, ${Object.keys(CARD).length} card types, ${visual.length} visual verification points`);
console.log(`uncontrolled doors ${DOOR.filter((k) => k[3] === 'none').length}, card-only ${DOOR.filter((k) => k[3] === 'card').length}, ` +
  `doors that break tailgating ${DOOR.filter((k) => breaks(k[3])).length}\n`);
console.log(`${'door'.padEnd(24)} ${'source'.padEnd(14)} ${'target'.padEnd(16)} ${'control'.padEnd(18)} ${'opening cards'.padStart(13)}`);
for (const [name, a, b, d] of DOOR)
  console.log(`${name.padEnd(24)} ${a.padEnd(14)} ${b.padEnd(16)} ${d.padEnd(18)} ` +
    `${String(Object.values(CARD).filter((v) => v.includes(name)).length).padStart(13)}`);
for (const target of ['server room', 'archive']) {
  const y = paths(target);
  console.log(`\n${target}: ${y.length} paths`);
  console.log(`${'path'.padEnd(83)} ${'doors'.padStart(5)} ${'breaking'.padStart(8)} ${'cardless'.padStart(15)}`);
  for (const p of y)
    console.log(`${p.join(' > ').padEnd(83)} ${String(p.length).padStart(5)} ` +
      `${String(p.filter((k) => breaks(controlOf(k))).length).padStart(8)} ` +
      `${String(p.filter((k) => controlOf(k) === 'none').length).padStart(15)}`);
  const fewest = y.reduce((a, b) => (b.filter((k) => breaks(controlOf(k))).length < a.filter((k) => breaks(controlOf(k))).length ? b : a));
  console.log(`path with fewest controls: ${fewest.join(' > ')} (${fewest.filter((k) => breaks(controlOf(k))).length} breaking doors)`);
}
console.log(`\ncard types that open the server room ${Object.entries(CARD).filter(([, v]) => v.includes('server room door')).length}, ` +
  `that open the equipment room door ${Object.entries(CARD).filter(([, v]) => v.includes('equipment room door')).length}`);
```

```
10 doors, 5 card types, 2 visual verification points
uncontrolled doors 3, card-only 4, doors that break tailgating 3

door                     source         target           control            opening cards
main entrance            outside        entrance hall    visual                         0
side entrance            outside        entrance hall    card                           5
parking garage elevator  outside        office floor     card                           1
loading dock door        outside        warehouse        none                           1
lobby turnstile          entrance hall  office floor     card+single person             4
warehouse corridor       warehouse      office floor     none                           0
equipment room door      office floor   equipment room   card                           2
server room door         equipment room server room      card+visual                    1
archive door             office floor   archive          card                           2
waste point              warehouse      waste area       none                           0

server room: 4 paths
path                                                                                doors breaking        cardless
main entrance > lobby turnstile > equipment room door > server room door                4        3               0
side entrance > lobby turnstile > equipment room door > server room door                4        2               0
parking garage elevator > equipment room door > server room door                        3        1               0
loading dock door > warehouse corridor > equipment room door > server room door         4        1               2
path with fewest controls: parking garage elevator > equipment room door > server room door (1 breaking doors)

archive: 4 paths
path                                                                                doors breaking        cardless
main entrance > lobby turnstile > archive door                                          3        2               0
side entrance > lobby turnstile > archive door                                          3        1               0
parking garage elevator > archive door                                                  2        0               0
loading dock door > warehouse corridor > archive door                                   3        0               2
path with fewest controls: parking garage elevator > archive door (0 breaking doors)

card types that open the server room 1, that open the equipment room door 2
```

The door table counts the access items: ten doors, five card types, two visual verification points.
Three doors have no control at all, four doors open with a card only, and the number of doors that
break tailgating is three. The card column shows a second imbalance: only one card type opens the
server room door, but two types open the equipment room door, and one of those is the cleaning card.
Authorization is narrowest at the last door and doubles at the one before it.

The path table gives the real number. There are four paths to the server room, and the breaking-door
count across these paths ranges from three down to one. The path through the main entrance contains
three breaking doors; the path through the parking garage elevator contains only one. This gap between
two paths to the same target shows that defense is stacked at the main entrance, and side entrances are
not treated with the same measure.

The archive is sharper still. In two of the four paths, the breaking-door count is **zero**. The
loading dock door and the warehouse corridor carry no control at all, and both are passed without a
card; the parking garage elevator requires a card but does not see the person. Counting paths here
does not just produce a surface — it produces a ranking: which route to which target leaves a course
that no control sees.

#### Three Chains and the Escort Procedure

```js
// run2.mjs — the chain of three techniques, the path-dependent breakable link, and the escort procedure's cost
import { TECHNIQUE, ESCORT, paths, controlOf } from './building.mjs';

const cumulOf = (z) => { let s = 0; return z.map((a) => (s += a[3])); };
console.log(`${'technique'.padEnd(16)} ${'steps'.padStart(5)} ${'controlled'.padStart(10)} ${'early items'.padStart(11)} ` +
  `${'late items'.padStart(10)} ${'gain'.padStart(6)} ${'if never broken'.padStart(15)}`);
for (const [name, z] of Object.entries(TECHNIQUE)) {
  const k = cumulOf(z), d = z.map((a, i) => (a[2] ? i : -1)).filter((i) => i >= 0);
  const e = d[0] ? k[d[0] - 1] : 0, g = k[d[d.length - 1] - 1];
  console.log(`${name.padEnd(16)} ${String(z.length).padStart(5)} ${String(d.length).padStart(10)} ${String(e).padStart(11)} ` +
    `${String(g).padStart(10)} ${String(g - e).padStart(6)} ${String(k[z.length - 1]).padStart(15)}`);
}
const T = TECHNIQUE.tailgating, TK = cumulOf(T);
console.log(`\ntailgating chain:`);
T.forEach((a, i) => console.log(`  ${i + 1} ${a[0].padEnd(34)} precondition ${a[1].padEnd(37)} control ${(a[2] ?? '-')}`));
const link = (path, escort) => {
  const d = [];
  if (path.some((k) => controlOf(k).includes('single person'))) d.push(2);
  if (escort || path.some((k) => controlOf(k).includes('visual'))) d.push(3);
  return d;
};
const dropped = (d) => (d.length ? { e: d[0] ? TK[d[0] - 1] : 0, g: TK[d[d.length - 1] - 1] } : { e: TK[3], g: TK[3] });
let unbroken = 0, gain = 0;
// dropped column: access items dropped at the earliest breakable link; if never breakable, the entire chain
for (const target of ['server room', 'archive']) {
  console.log(`\n${target} (${paths(target).length} paths)`.padEnd(84) +
    `${'links'.padStart(5)} ${'dropped'.padStart(7)} ${'escorted'.padStart(9)} ${'dropped'.padStart(7)}`);
  for (const p of paths(target)) {
    const a = link(p, false), b = link(p, true), da = dropped(a), db = dropped(b);
    if (!a.length) unbroken++;
    gain += da.e - db.e;
    console.log(`${p.join(' > ').padEnd(83)} ${String(a.length).padStart(5)} ${String(da.e).padStart(7)} ` +
      `${String(b.length).padStart(9)} ${String(db.e).padStart(7)}`);
  }
}
console.log(`\npaths never broken without escort ${unbroken}/8; total gain from the escort procedure ${gain} access items`);
console.log(`cost: ${ESCORT.dailyPassages} escorted passages a day x ${ESCORT.averageMinutes} min = ` +
  `${ESCORT.dailyPassages * ESCORT.averageMinutes} min (${(ESCORT.dailyPassages * ESCORT.averageMinutes / 60).toFixed(1)} hours/day)`);
```

```
technique        steps controlled early items late items   gain if never broken
tailgating           4          2           0          2      2               7
shoulder surfing     3          2           0          1      1               5
dumpster diving      3          2           0          3      3               7

tailgating chain:
  1 waiting in front of the door       precondition the door being visible from outside   control -
  2 waiting for an authorized passage  precondition a busy passage hour                   control -
  3 entering on the same passage       precondition the door staying open                 control single-person passage
  4 advancing to the target zone       precondition interior doors being uncontrolled     control visual verification

server room (4 paths)                                                              links dropped  escorted dropped
main entrance > lobby turnstile > equipment room door > server room door                2       0         2       0
side entrance > lobby turnstile > equipment room door > server room door                2       0         2       0
parking garage elevator > equipment room door > server room door                        1       2         1       2
loading dock door > warehouse corridor > equipment room door > server room door         1       2         1       2

archive (4 paths)                                                                  links dropped  escorted dropped
main entrance > lobby turnstile > archive door                                          2       0         2       0
side entrance > lobby turnstile > archive door                                          1       0         2       0
parking garage elevator > archive door                                                  0       7         1       2
loading dock door > warehouse corridor > archive door                                   0       7         1       2

paths never broken without escort 2/8; total gain from the escort procedure 10 access items
cost: 34 escorted passages a day x 12 min = 408 min (6.8 hours/day)
```

The three-technique table shows how short these chains are: four, three, and three steps. In all three
the first step is uncontrolled, and in all three the number of controlled links is two. Early-break
gains differ: two access items for tailgating, one for shoulder surfing, three for dumpster diving.
The highest gain is dumpster diving, because its second step — collecting documents — drops three
items, and the control that breaks that step is not at a door but in the waste separation itself. If
the chain never breaks, dropped items reach seven.

The path table runs tailgating over the building and produces, once again, the same situation the
course has now seen twice: in two of eight paths, no link breaks at all. Both paths lead to the
archive, and dropped items are seven — the entire chain. In the remaining six paths, dropped items at
the earliest breakable link are either zero or two; that is, if a control exists it is early, and if
not it is absent entirely. No middle option appears.

Comparing the escort-procedure columns shows exactly where the procedure works. Nothing changes on six
paths, because those paths already have an early-breaking link. The two paths that change are the ones
that never broke at all: dropped items fall from seven to two, and the total gain is ten access items.
Everything the procedure gains sits on the two routes that previously had no control at all.

The cost is written on a single line: thirty-four escorted passages a day, an average of twelve minutes
per passage, 408 minutes total — 6.8 hours a day. This time is close to a full person's
workload and comes out of the escort's own work. Once it is known that the procedure gains something on
only two routes, a measurable alternative appears: instead of escorting, add a visual or single-person
passage control to the three uncontrolled doors on those two routes. Both gain the same ten items; one
is paid every day, the other once.

#### Summary

- The building model has 10 doors, 5 card types, and 2 visual verification points. 3 doors have no
  control, 4 doors open with a card only; the number of doors that break tailgating is 3.
- 4 paths lead to the server room, and the breaking-door count ranges from 3 to 1. Of the 4 paths to
  the archive, 2 have a breaking-door count of 0.
- All three techniques have an uncontrolled first step. Early-break gain is 2 access items for
  tailgating, 1 for shoulder surfing, 3 for dumpster diving; if the chain never breaks, dropped items
  are 7, 5, and 7.
- Tailgating breaks on 6 of 8 paths and never breaks on 2. On the paths that break, dropped items at
  the earliest link are 0 or 2; no middle value appears.
- The escort procedure is effective only on those 2 paths: dropped items fall from 7 to 2, total gain
  10 access items. Gain on the other 6 paths is 0.
- The procedure costs 34 escorted passages a day × 12 minutes = 408 minutes (6.8 hours). The same 10
  items can also be gained by adding a control to the 3 uncontrolled doors on those 2 routes.

#### Next Step

Every chain in this lesson required passing through a door, and every door was a countable item. An
impersonation carried out remotely needs no door at all: a domain one character away from a real name
faces no control the moment it is registered. **Producing** such names is not what is measurable;
**detecting** them is. The edit distance between protected names and candidate names can be computed,
and visually similar characters can be normalized. That is the next lesson's question: how many
impersonation candidates turn up in a registry feed, how many of them are false positives, and how many
days of a head start monitoring the registry gains before one goes into use.

### Domain Spoofing

Source: https://academia.sh/en/courses/attack-techniques/domain-spoofing

Detecting typosquatting and homograph domain names in a fictional registry list: the number of candidates edit distance and visual normalization catch separately and together, each metric's false positive rate, the one spoof that is missed, and the average 11.3-day window that registry monitoring gains.

Every chain in the previous lesson required passing through a door, and every door was a countable
item. A spoof carried out remotely needs no door at all: a name one character away from a protected
name faces no control the moment it is registered. **Detecting** these names is a measurable task, and
this lesson measures only that — no name is generated, an observed registry list is scanned.

Two metrics are used. The first is edit distance: how many insertions, deletions, and substitutions it
takes to turn a label into a protected name. The second is visual similarity: spellings that resemble
each other are reduced to a single form, then distance is measured again. The two catch different spoofs,
and each has its own false positive. The third measure is on the time side: the gap between the day a
name is registered and the day it is first used determines at which link the defense can break the
chain.

- **SE25.** The protected names, the registry list, and the days are fictional, and extensions reserved
  for documentation were used. No real domain, organization, or brand is referenced; what is measured
  is the model built with `node`.
- **SE26.** The records' true class is known in the model. In reality the class is known only after
  investigation; it is assumed known here so that caught and false positive can be counted separately.
- **SE27.** Visual normalization was kept to five confusable forms. In a real browser the set of
  confusable spellings is far wider and also covers characters from other writing systems; the narrow
  set lowers what is caught, it does not change the direction of the measure.
- **SE28.** Label comparison is done independently of the extension. Registering the same label under a
  different extension gives distance zero, and there is such a record in the list.
- **SE29.** The first-use day is known in the model. In reality use is known only once observed; in
  reality, the window is therefore measured **shorter** than it is in the model.
- **SE30.** The registration feed was assumed to be observed in full. An unobserved registration cannot
  be found by any metric, and in that case the chain's first link cannot be counted at all.

#### Output of the Two Metrics

```js
// domain.mjs — protected names, observed registry list, and detection metrics; model (names are not generated, only measured)
export const PROTECTED = ['olcumag', 'aboneportali', 'sahaislem']; // extension .example
// [label, extension, registration day, first-use day, true class]
export const RECORD = [
  ['olcurnag', '.example', 3, 19, 'spoof'], ['o1cumag', '.test', 5, 12, 'spoof'],
  ['olcunag', '.example', 8, 26, 'spoof'], ['ab0neportali', '.example', 11, 21, 'spoof'],
  ['aboneportali-giris', '.test', 12, 15, 'spoof'], ['sahais1em', '.example', 14, 30, 'spoof'],
  ['sahaisiem', '.invalid', 16, 24, 'spoof'], ['sahaislem', '.test', 18, 22, 'spoof'],
  ['olcumak', '.example', 2, 4, 'unrelated'], ['aboneportal', '.test', 6, 9, 'unrelated'],
  ['sahaisleme', '.example', 7, 10, 'unrelated'], ['olcumagaza', '.test', 9, 13, 'unrelated'],
  ['abonelik', '.example', 10, 14, 'unrelated'], ['sahakayit', '.invalid', 13, 17, 'unrelated'],
  ['olcumerkezi', '.example', 15, 20, 'unrelated'], ['portalabone', '.test', 17, 23, 'unrelated'],
  ['veriolcum', '.example', 19, 25, 'unrelated'], ['sayacokuma', '.invalid', 20, 27, 'unrelated'],
  ['faturaservisi', '.example', 21, 28, 'unrelated'], ['isemriakisi', '.test', 22, 29, 'unrelated'],
  ['bolgeolcum', '.example', 23, 31, 'unrelated'], ['aboneislem', '.invalid', 24, 32, 'unrelated'],
  ['sahaportali', '.test', 25, 33, 'unrelated'], ['olcumveri', '.example', 26, 34, 'unrelated'],
  ['olcumaci', '.test', 4, 8, 'unrelated'],
];
export function distance(a, b) { // edit distance
  const d = Array.from({ length: a.length + 1 }, (_, i) => [i, ...Array(b.length).fill(0)]);
  for (let j = 0; j <= b.length; j++) d[0][j] = j;
  for (let i = 1; i <= a.length; i++)
    for (let j = 1; j <= b.length; j++)
      d[i][j] = Math.min(d[i - 1][j] + 1, d[i][j - 1] + 1, d[i - 1][j - 1] + (a[i - 1] === b[j - 1] ? 0 : 1));
  return d[a.length][b.length];
}
// visual similarity: confusable forms are reduced to a single spelling, then distance is measured
export const normalize = (s) => s.replace(/rn/g, 'm').replace(/vv/g, 'w')
  .replace(/1/g, 'l').replace(/0/g, 'o').replace(/5/g, 's');
export const nearest = (label, f = (x) => x) =>
  PROTECTED.reduce((a, k) => Math.min(a, distance(f(label), f(k))), Infinity);
export const window = (k) => k[3] - k[2];
// [step, precondition, control that sees the step, item dropped once the step completes]
export const CHAIN = [
  ['domain registration', 'access to a registration service', 'monitoring the registration feed', 0],
  ['mail and certificate setup', 'a registered domain', 'monitoring the transparency log', 1],
  ['first message', 'a target address list', 'indicator threshold', 2],
  ['credential collection', 'a convincing collection point', 'second factor', 3],
  ['use of the collected information', 'a valid session', 'anomalous session detection', 6],
];
```

```js
// run1.mjs — edit distance and visual similarity measured over the registry list
import { PROTECTED, RECORD, normalize, nearest, window } from './domain.mjs';

console.log(`${PROTECTED.length} protected names (${PROTECTED.map((k) => k + '.example').join(', ')}), ` +
  `${RECORD.length} observed records, ${RECORD.filter((k) => k[4] === 'spoof').length} spoofs\n`);
console.log(`${'label'.padEnd(19)} ${'extension'.padEnd(9)} ${'class'.padStart(9)} ${'distance'.padStart(8)} ` +
  `${'visual'.padStart(6)} ${'reg.'.padStart(5)} ${'used'.padStart(8)} ${'window'.padStart(7)}`);
const near = [...RECORD].sort((a, b) => nearest(a[0]) - nearest(b[0])).filter((k) => nearest(k[0]) <= 3 || k[4] === 'spoof');
for (const k of near)
  console.log(`${k[0].padEnd(19)} ${k[1].padEnd(9)} ${k[4].padStart(9)} ${String(nearest(k[0])).padStart(8)} ` +
    `${String(nearest(k[0], normalize)).padStart(6)} ${String(k[2]).padStart(5)} ${String(k[3]).padStart(8)} ` +
    `${String(window(k)).padStart(7)}`);
const far = RECORD.filter((k) => nearest(k[0]) > 3 && k[4] !== 'spoof');
console.log(`\nrest of the list: ${far.length} records, distance ${Math.min(...far.map((k) => nearest(k[0])))}-${Math.max(...far.map((k) => nearest(k[0])))}, ` +
  `none is a spoof and none has a visual match`);
const visualZero = RECORD.filter((k) => nearest(k[0], normalize) === 0);
console.log(`records with distance 0 after visual normalization ${visualZero.length}: ` +
  `${visualZero.map((k) => `${k[0]} (${k[4]}, raw distance ${nearest(k[0])})`).join('; ')}`);
console.log(`labels changed by normalization ${RECORD.filter((k) => normalize(k[0]) !== k[0]).length}`);
```

```
3 protected names (olcumag.example, aboneportali.example, sahaislem.example), 25 observed records, 8 spoofs

label               extension     class distance visual  reg.     used  window
sahaislem           .test         spoof        0      0    18       22       4
o1cumag             .test         spoof        1      0     5       12       7
olcunag             .example      spoof        1      1     8       26      18
ab0neportali        .example      spoof        1      0    11       21      10
sahais1em           .example      spoof        1      0    14       30      16
sahaisiem           .invalid      spoof        1      1    16       24       8
olcumak             .example  unrelated        1      1     2        4       2
aboneportal         .test     unrelated        1      1     6        9       3
sahaisleme          .example  unrelated        1      1     7       10       3
olcurnag            .example      spoof        2      0     3       19      16
olcumaci            .test     unrelated        2      2     4        8       4
olcumagaza          .test     unrelated        3      3     9       13       4
aboneportali-giris  .test         spoof        6      6    12       15       3

rest of the list: 12 records, distance 4-10, none is a spoof and none has a visual match
records with distance 0 after visual normalization 5: olcurnag (spoof, raw distance 2); o1cumag (spoof, raw distance 1); ab0neportali (spoof, raw distance 1); sahais1em (spoof, raw distance 1); sahaislem (spoof, raw distance 0)
labels changed by normalization 4
```

The table's first row shows the cheapest spoof: **the same** protected label, only with a different
extension. Its edit distance is zero and it contains no spelling change at all. A scan that does not
compare labels independently of the extension never sees this one.

The rows at distance one show the measure's tension: five of eight rows are spoofs, three are unrelated.
`olcumak` and `aboneportal`, sitting in the same band, are not real spoofs — they are other words that
happen to sit one character away from a protected name. Edit distance cannot separate them, because the
information needed to separate them is not in the distance.

The visual column shows what the second metric does. In five records, distance drops to zero after
normalization, and **all five are spoofs**. One of them is `olcurnag`, with a raw distance of two: two
characters that together resemble a single character stretch the edit distance by two steps, even though
there is no visual difference at all. Normalization changes four of the twenty-five labels; this metric
works on a narrow set, and that is exactly why it produces no false positive.

The spoof in the last row falls outside both metrics. `aboneportali-giris` contains the protected name
exactly as it is and adds a word to it; its edit distance is six. Both metrics look for **changes** in
spelling, but here the spelling has not changed at all.

#### Comparing the Metrics and the Window

```js
// run2.mjs — threshold scan, combined metric, monitoring window, and the chain's early-break gain
import { RECORD, CHAIN, normalize, nearest, window } from './domain.mjs';

const SPOOF = RECORD.filter((k) => k[4] === 'spoof').length;
const count = (f) => { const a = RECORD.filter(f); return { candidates: a.length, caught: a.filter((k) => k[4] === 'spoof').length,
  wrong: a.filter((k) => k[4] === 'unrelated').length, missed: RECORD.filter((k) => k[4] === 'spoof' && !f(k)) }; };
console.log(`${'metric'.padEnd(28)} ${'candidates'.padStart(10)} ${'caught'.padStart(9)} ${'false positive'.padStart(14)} ${'missed'.padStart(6)}`);
for (const e of [0, 1, 2, 3]) {
  const r = count((k) => nearest(k[0]) <= e);
  console.log(`${`edit distance <= ${e}`.padEnd(28)} ${String(r.candidates).padStart(10)} ${`${r.caught}/${SPOOF}`.padStart(9)} ` +
    `${String(r.wrong).padStart(14)} ${String(r.missed.length).padStart(6)}`);
}
const g = count((k) => nearest(k[0], normalize) === 0);
console.log(`${'visual similarity = 0'.padEnd(28)} ${String(g.candidates).padStart(10)} ${`${g.caught}/${SPOOF}`.padStart(9)} ` +
  `${String(g.wrong).padStart(14)} ${String(g.missed.length).padStart(6)}`);
const b = count((k) => nearest(k[0], normalize) === 0 || nearest(k[0]) <= 1);
console.log(`${'combined (visual 0 or <=1)'.padEnd(28)} ${String(b.candidates).padStart(10)} ${`${b.caught}/${SPOOF}`.padStart(9)} ` +
  `${String(b.wrong).padStart(14)} ${String(b.missed.length).padStart(6)}`);
console.log(`\nwhat the combined metric misses: ${b.missed.map((k) => `${k[0]} (distance ${nearest(k[0])}, window ${window(k)} days)`).join('; ')}`);
const found = RECORD.filter((k) => k[4] === 'spoof' && (nearest(k[0], normalize) === 0 || nearest(k[0]) <= 1));
const p = found.map(window);
console.log(`monitoring window for the ${found.length} caught spoofs: average ${(p.reduce((a, c) => a + c, 0) / p.length).toFixed(2)} days, ` +
  `shortest ${Math.min(...p)}, longest ${Math.max(...p)}`);
console.log(`manual-review load from false positives: ${b.wrong} records`);

let s = 0; const cumul = CHAIN.map((a) => (s += a[3]));
console.log(`\n${'#'.padStart(2)} ${'step'.padEnd(33)} ${'precondition'.padEnd(33)} ${'control'.padEnd(33)} dropped if broken`);
CHAIN.forEach((a, i) => console.log(`${String(i + 1).padStart(2)} ${a[0].padEnd(33)} ${a[1].padEnd(33)} ${a[2].padEnd(33)} ` +
  `${i ? cumul[i - 1] : 0}`));
console.log(`chain has ${CHAIN.length} steps, all of them controlled; if it never breaks, dropped items ${cumul[4]}`);
console.log(`with monitoring, earliest link 1 (dropped 0), latest link 5 (dropped ${cumul[3]}); early-break gain ${cumul[3]} items`);
console.log(`without registration-feed monitoring, earliest link 3 (dropped ${cumul[1]}) and the average ${(p.reduce((a, c) => a + c, 0) / p.length).toFixed(2)}-day window goes unused`);
```

```
metric                       candidates    caught false positive missed
edit distance <= 0                    1       1/8              0      7
edit distance <= 1                    9       6/8              3      2
edit distance <= 2                   11       7/8              4      1
edit distance <= 3                   12       7/8              5      1
visual similarity = 0                 5       5/8              0      3
combined (visual 0 or <=1)           10       7/8              3      1

what the combined metric misses: aboneportali-giris (distance 6, window 3 days)
monitoring window for the 7 caught spoofs: average 11.29 days, shortest 4, longest 18
manual-review load from false positives: 3 records

 # step                              precondition                      control                           dropped if broken
 1 domain registration               access to a registration service  monitoring the registration feed  0
 2 mail and certificate setup        a registered domain               monitoring the transparency log   0
 3 first message                     a target address list             indicator threshold               1
 4 credential collection             a convincing collection point     second factor                     3
 5 use of the collected information  a valid session                   anomalous session detection       6
chain has 5 steps, all of them controlled; if it never breaks, dropped items 12
with monitoring, earliest link 1 (dropped 0), latest link 5 (dropped 6); early-break gain 6 items
without registration-feed monitoring, earliest link 3 (dropped 1) and the average 11.29-day window goes unused
```

The comparison table shows the two metrics failing in different ways. As the edit-distance threshold
rises from one to three, caught rises from six to seven and false positives from three to five. Visual
similarity alone catches only five spoofs but **produces no false positive at all**. The combined metric
catches seven spoofs and produces three false positives — the same catch as distance threshold two, with
one fewer false positive. Combining two narrow metrics produces fewer errors than loosening a single
metric — this is a design decision that stands in for threshold tuning.

The one missed spoof marks the boundary of both metrics. A label that contains the protected name
exactly and adds a word to it is far from the protected name in terms of distance. A third metric would
catch it: the protected name appearing **as a substring**. That metric would have its own false
positive, and this lesson does not measure it; what it measures is that the two metrics together still
miss one.

The window row gives the time side. Between the registration day and the first-use day of the seven
caught spoofs, there is an average of 11.29 days; the shortest is four, the longest eighteen. This span
is not a detector's accuracy measure — it is the **time the defense has**: if the registration feed is
monitored, the chain can break at the first link, and at that point no item has dropped yet.

The window's distribution says more than its average. The shortest window is four days, and looking at
the table shows which record it belongs to: the protected label registered with a different extension.
The cheapest spoof to set up is also the fastest to go into use. The longest window is eighteen days, a
ratio of four and a half between the two. A monitor that scans the registration feed once a week sees
the records above the average in time but misses the four-day window; what is decisive in this measure
is not the average but the shortest window.

The chain table writes this in terms of items. All five steps have a control; if the chain never breaks,
dropped items reach twelve. Breaking at the first link ends with zero items dropped, breaking at the
fifth with six — the early-break gain is six items. If the registration feed is not monitored, the
earliest breakable link shifts to the third step: one item has already dropped there, and the average
eleven-day window goes entirely unused. The cost of false positives, on the same table, is a
three-record manual review — this is the price paid in exchange for the eleven-day window.

#### Summary

- The fictional 25-record list has 8 spoofs. The 1 record with edit distance 0 is the protected label
  registered with a different extension; a scan that does not compare independently of the extension
  misses it.
- At distance threshold 1, 6/8 are caught with 3 false positives; at threshold 3, caught stays at 7
  while false positives rise to 5.
- Visual normalization changes 4 of the 25 labels and, alone, catches 5 spoofs with 0 false positives.
  One of them is a record with a raw distance of 2.
- The combined metric catches 7/8 with 3 false positives: the same catch as distance threshold 2, with
  1 fewer false positive.
- The one missed spoof is the record that contains the protected name exactly and adds a word
  (distance 6). Both metrics look for a change in spelling; here the spelling did not change.
- The monitoring window for the 7 caught spoofs averages 11.29 days (shortest 4, longest 18). With
  registration monitoring, the chain breaks at link 1 (0 items); without it, at the earliest link 3
  (1 item); early-break gain is 6 items, at a cost of manually reviewing 3 records.

#### Next Step

This lesson's spoof called the target to itself: a name is registered and the target is expected to go
to it. The reverse is also possible — corrupting a place the target **already goes to**. A portion of an
organization's users visit the same external resource every day: a shared document repository, an
industry announcement page, a script or font resource. That resource is outside the organization's
control and is a single point. That is the next lesson's question: how many users go to the same
external resource, how many chains this shared dependency opens at once, and how many of them a
resource-integrity check breaks.

### Watering Hole Attacks

Source: https://academia.sh/en/courses/attack-techniques/watering-hole-attacks

Modeling the watering hole attack as a shared dependency: eight external resources that six internal applications reach, how many users a single resource affects at once, at which link the six-step chain breaks per resource, the three chains that a resource-integrity hash breaks, and the 48-day-a-year delay cost of version pinning.

The previous lesson's spoof called the target to itself: a name is registered and the target is
expected to go to it. The reverse is also possible — corrupting a place the target **already goes to**.
A portion of an organization's users visit the same external resource every day: a shared script
library, a font resource, a document repository, an industry announcement page. These resources are
outside the organization's control, and each one is a single point.

A watering hole attack therefore targets not the target but the target's **dependency**. In its narrow
definition, a watering hole is the corruption of a page the user **goes to directly** — a document
repository and an announcement page fit this definition; the script and font resources an application
embeds and runs overlap with a **supply chain** attack. This lesson combines the two into a single
measure, because the question it asks is the same for both: when an external resource is corrupted, how
many users does it affect at once, and at which link does their chain break? The resource type is
decisive here, because the type determines which control can be applied. A resource whose version can
be pinned can have its hash computed; an external page the user goes to directly has no hash. The step
at the end of the chain — foothold after a drive-by download — was measured in another course; here
it appears only as the chain's last link.

- **SE31.** The applications, user counts, and external resource list belong to the fictional regional
  measurement network. No real resource is contacted; what is measured is the model built with `node`.
- **SE32.** The applications' user sets were counted as disjoint. In reality a user uses more than one
  application; the disjointness makes the total user count look **larger** than it is and makes
  overlapping resource sharing look **smaller** than it is.
- **SE33.** The controls applicable to a resource were hard-coded in the model. In reality, whether a
  resource's version can be pinned depends on the provider and changes over time.
- **SE34.** The chain's second step — the resource being corrupted — is outside the organization's
  control and was therefore not mapped to any control. The provider's own defenses were not modeled.
- **SE35.** The dropped item is the count of sessions, files, and endpoint items that become accessible
  after that step. Item types were not weighted separately.
- **SE36.** The manual-approval delay and the annual update count are fictional. Both vary widely by
  resource type; the fixed value shows the **direction** of the cost, not its magnitude.

#### What Sharing Means in Users

```js
// resource.mjs — internal applications, shared external resources, and the watering hole chain; model
// [application, daily users]
export const APPLICATION = [
  ['field work order app', 180], ['subscriber portal', 1400], ['billing generation interface', 60],
  ['branch endpoint dashboard', 90], ['measurement monitoring dashboard', 240], ['internal announcement page', 620],
];
export const users = (name) => APPLICATION.find((u) => u[0] === name)[1];
const [F, S, B, N, M, I] = APPLICATION.map((u) => u[0]);
// control -> which step of the chain it breaks
export const CONTROL = { hash: 3, policy: 4, signature: 5, endpoint: 6 };
// [external resource, type, applications that use it, applicable controls, annual updates]
export const RESOURCE = [
  ['shared script library', 'script', [F, S, B, N, M, I], ['hash', 'policy', 'endpoint'], 12],
  ['font resource', 'font', [S, M, I], ['hash', 'policy', 'endpoint'], 2],
  ['chart drawing library', 'script', [B, M], ['hash', 'policy', 'endpoint'], 6],
  ['map tile service', 'image', [F, M], ['policy', 'endpoint'], 0],
  ['measurement tool download', 'binary', [F, N], ['signature', 'endpoint'], 4],
  ['calendar feed', 'data', [S, I], ['policy', 'endpoint'], 0],
  ['shared document repository', 'external page', [F, B, N, I], ['endpoint'], 0],
  ['industry announcement page', 'external page', [B, I], ['endpoint'], 0],
];
// [step, precondition, item dropped once the step completes]
export const CHAIN = [
  ['identifying the shared resource', 'the external resource list being visible from outside', 0],
  ['corruption of the resource', 'access on the resource provider side', 0],
  ['delivery', 'the client fetching the resource', 1],
  ['execution on the client', 'the client running the resource', 2],
  ['local download', 'user approval', 3],
  ['foothold', 'permission to execute', 6],
];
export const CUMUL = (() => { let s = 0; return CHAIN.map((a) => (s += a[2])); })();
export const reached = (k) => k[2].reduce((a, u) => a + users(u), 0);
export function links(k, hashOn = true) {
  return k[3].filter((d) => hashOn || d !== 'hash').map((d) => CONTROL[d]).sort((a, b) => a - b);
}
export const dropped = (h) => (h.length ? CUMUL[h[0] - 2] ?? 0 : CUMUL[CHAIN.length - 1]);
```

```js
// run1.mjs — user counts of shared external resources and the chain's earliest-breaking link
import { APPLICATION, RESOURCE, CHAIN, CUMUL, reached, links, dropped } from './resource.mjs';

const total = APPLICATION.reduce((a, u) => a + u[1], 0);
console.log(`${APPLICATION.length} internal applications, ${total} daily users; ${RESOURCE.length} shared external resources`);
console.log(`chain has ${CHAIN.length} steps; if it never breaks, dropped items ${CUMUL[CHAIN.length - 1]}\n`);
console.log(`${'#'.padStart(2)} ${'step'.padEnd(33)} ${'precondition'.padEnd(53)} ${'dropped'.padStart(7)}`);
CHAIN.forEach((a, i) => console.log(`${String(i + 1).padStart(2)} ${a[0].padEnd(33)} ${a[1].padEnd(53)} ${String(CUMUL[i]).padStart(7)}`));
console.log(`\n${'external resource'.padEnd(27)} ${'type'.padEnd(13)} ${'apps'.padStart(8)} ${'users'.padStart(9)} ` +
  `${'controls'.padStart(8)} ${'earliest link'.padStart(14)} ${'dropped'.padStart(7)}`);
const sorted = [...RESOURCE].sort((a, b) => reached(b) - reached(a));
for (const k of sorted) {
  const h = links(k);
  console.log(`${k[0].padEnd(27)} ${k[1].padEnd(13)} ${String(k[2].length).padStart(8)} ${String(reached(k)).padStart(9)} ` +
    `${String(h.length).padStart(8)} ${String(h[0]).padStart(14)} ${String(dropped(h)).padStart(7)}`);
}
const matches = RESOURCE.reduce((a, k) => a + reached(k), 0);
console.log(`\ntotal user-resource matches ${matches}; most shared resource ${sorted[0][0]} ` +
  `(${sorted[0][2].length}/${APPLICATION.length} applications, ${reached(sorted[0])} users)`);
console.log(`the corruption of a single resource affects at most ${reached(sorted[0])} users at once, ` +
  `which is ${Math.round((reached(sorted[0]) / total) * 100)}% of the total users`);
```

```
6 internal applications, 2590 daily users; 8 shared external resources
chain has 6 steps; if it never breaks, dropped items 12

 # step                              precondition                                          dropped
 1 identifying the shared resource   the external resource list being visible from outside       0
 2 corruption of the resource        access on the resource provider side                        0
 3 delivery                          the client fetching the resource                            1
 4 execution on the client           the client running the resource                             3
 5 local download                    user approval                                               6
 6 foothold                          permission to execute                                      12

external resource           type              apps     users controls  earliest link dropped
shared script library       script               6      2590        3              3       0
font resource               font                 3      2260        3              3       0
calendar feed               data                 2      2020        2              4       1
shared document repository  external page        4       950        1              6       6
industry announcement page  external page        2       680        1              6       6
map tile service            image                2       420        2              4       1
chart drawing library       script               2       300        3              3       0
measurement tool download   binary               2       270        2              5       3

total user-resource matches 9490; most shared resource shared script library (6/6 applications, 2590 users)
the corruption of a single resource affects at most 2590 users at once, which is 100% of the total users
```

The resource table's first row carries the whole measure. The shared script library is used in all six
of the six applications and reaches all 2,590 users — one hundred percent of the model's total users.
The corruption of a single external resource affects every user at once, without touching anything
inside the organization. In phishing, a target list is built; here the target list is already built,
and it is the organization's own dependency choice that built it.

User count and application count do not give the same ranking. The calendar feed is used in only two
applications but reaches 2,020 users; the shared document repository is used in four applications and
reaches 950. The measure of sharing is not how many applications use a resource but how **many people**
those applications serve. A study that ranks the dependency inventory by application count brings the
wrong three items to the top.

The last two columns show where the chain breaks. In three resources, the earliest link is the third
step, and zero items have dropped there. In two external pages, the earliest link is the sixth step —
the chain's last step — and by the time it is reached, six items have dropped. The difference between
them is in the control count: one has three applicable controls, the other has one.

#### The Gain and Cost of Integrity Checking

```js
// run2.mjs — how many chains resource-integrity checking breaks, the user-weighted gain, and the cost
import { APPLICATION, RESOURCE, CUMUL, reached, links, dropped } from './resource.mjs';

const APPROVAL_DAYS = 2; // fictional: manual-approval delay for an update under version pinning
console.log(`${'external resource'.padEnd(27)} ${'users'.padStart(9)} ${'no-hash link'.padStart(14)} ${'dropped'.padStart(7)} ` +
  `${'hash link'.padStart(13)} ${'dropped'.padStart(7)} ${'gain'.padStart(6)}`);
let breaking = 0, weighted = 0;
for (const k of [...RESOURCE].sort((a, b) => reached(b) - reached(a))) {
  const y = links(k, false), v = links(k, true), dy = dropped(y), dv = dropped(v);
  if (dy !== dv) { breaking++; weighted += (dy - dv) * reached(k); }
  console.log(`${k[0].padEnd(27)} ${String(reached(k)).padStart(9)} ${String(y[0]).padStart(14)} ${String(dy).padStart(7)} ` +
    `${String(v[0]).padStart(13)} ${String(dv).padStart(7)} ${String(dy - dv).padStart(6)}`);
}
const hashed = RESOURCE.filter((k) => k[3].includes('hash'));
const matches = RESOURCE.reduce((a, k) => a + reached(k), 0);
console.log(`\nresource-integrity hashing breaks ${breaking}/${RESOURCE.length} chains earlier; ` +
  `user-resource matches it covers ${hashed.reduce((a, k) => a + reached(k), 0)}/${matches} (${Math.round(hashed.reduce((a, k) => a + reached(k), 0) / matches * 100)}%)`);
console.log(`user-weighted early-break gain ${weighted} item-users`);
const latest = RESOURCE.filter((k) => links(k)[0] === 6);
console.log(`chains that break latest (link 6, dropped ${CUMUL[4]}): ${latest.map((k) => `${k[0]} (${reached(k)} users)`).join('; ')}`);
console.log(`hashing cannot be applied to these two resources; both are external pages outside the organization's control`);
const signed = RESOURCE.filter((k) => k[3].includes('signature'));
const approvals = [...hashed, ...signed].reduce((a, k) => a + k[4], 0); // annual updates of pinned resources
console.log(`\ncost: ${hashed.length + signed.length} resources need version pinning, ${approvals} manual approvals a year, ` +
  `${APPROVAL_DAYS} days of delay per approval = ${approvals * APPROVAL_DAYS} days/year of resource staleness`);
console.log(`out-of-scope ${RESOURCE.length - hashed.length - signed.length} resources, ` +
  `${RESOURCE.filter((k) => !k[3].includes('hash') && !k[3].includes('signature')).reduce((a, k) => a + reached(k), 0)} user-resource matches`);
console.log(`total daily users ${APPLICATION.reduce((a, u) => a + u[1], 0)}; out-of-scope matches are ` +
  `${(RESOURCE.filter((k) => !k[3].includes('hash') && !k[3].includes('signature')).reduce((a, k) => a + reached(k), 0) / APPLICATION.reduce((a, u) => a + u[1], 0)).toFixed(2)} times that`);
```

```
external resource               users   no-hash link dropped     hash link dropped   gain
shared script library            2590              4       1             3       0      1
font resource                    2260              4       1             3       0      1
calendar feed                    2020              4       1             4       1      0
shared document repository        950              6       6             6       6      0
industry announcement page        680              6       6             6       6      0
map tile service                  420              4       1             4       1      0
chart drawing library             300              4       1             3       0      1
measurement tool download         270              5       3             5       3      0

resource-integrity hashing breaks 3/8 chains earlier; user-resource matches it covers 5150/9490 (54%)
user-weighted early-break gain 5150 item-users
chains that break latest (link 6, dropped 6): shared document repository (950 users); industry announcement page (680 users)
hashing cannot be applied to these two resources; both are external pages outside the organization's control

cost: 4 resources need version pinning, 24 manual approvals a year, 2 days of delay per approval = 48 days/year of resource staleness
out-of-scope 4 resources, 4070 user-resource matches
total daily users 2590; out-of-scope matches are 1.57 times that
```

The hash columns count what resource-integrity checking does: it breaks three of eight chains one link
earlier. The gain column is only one item per resource, but the user count it covers is large — the
user-weighted gain is 5,150 item-users, covering fifty-four percent of the total matches. The control's
value is not in the item count but in how many users that one item does not drop for, all at once.

For the remaining five resources, hashing changes nothing. In the two external pages, the reason is
clear: the user goes there directly, and what is fetched is not a pinned version but the page as it is
at that moment. These two resources are the two whose chains break latest — the sixth link, with six
items already dropped — and together they reach 1,630 users. There is no relationship between how
**shared** a resource is and how **checkable** it is; the most shared resource happened to be the most
checkable, while the two latest-breaking resources sit in the middle of the ranking.

The cost collects into two items. The four resources that need version pinning produce twenty-four
manual approvals a year; at a two-day delay per approval, that means the resource is kept on a stale
version for forty-eight days a year. This delay produces an exposure of its own: an issue the provider
has closed stays open inside the organization until the approval completes. The cost of the control here
is not a labor item — it is a **risk running in the opposite direction**.

The four out-of-scope resources carry 4,070 user-resource matches; that is 1.57 times the total daily
user count. In other words, the average user visits one and a half external resources with no integrity
check at all every day. Breaking the chain through these resources is left to endpoint protection alone,
and the dropped item there is six.

#### The First Two Links That Cannot Be Broken

Neither of the chain table's first two steps has a control across from it, and the reasons differ. The
first step — identifying the shared resource — is invisible from inside the organization, because the
external resource list is already written into everything the client fetches; hiding it is not an
option. The second step takes place on the resource's own side and is entirely outside the
organization's control.

Since these two links cannot be broken, only one lever remains: **reducing how many resources are
depended on.** The table gives the measure for this too. If the shared script library is brought
in-house, the 2,590-user match drops off the external list, but the item moves into the organization's
inventory and keeping it current becomes the organization's job. The 4,070 matches of the four
out-of-scope resources cannot be moved this way: two of them are external pages the user goes to
directly, and they have no version to bring in-house.

This is where the measure shifts. If the first two links cannot be broken, the remaining work is
**detection**: a record that sees which resource changed and when does not break the chain, but it shows
when the control at the third link kicks in. The dependency inventory is therefore not a document but a
measurement item: how many resources, how many users, how many have a control.

#### Summary

- 6 internal applications serve 2,590 users a day and share 8 external resources. The most shared
  resource is used in all 6 applications and reaches 100% of users.
- User count and application count do not give the same ranking: a resource used in 2 applications
  reaches 2,020 users, while another used in 4 applications reaches 950.
- The chain has 6 steps; if it never breaks, dropped items reach 12. In 3 resources the earliest link
  is step 3 (0 items); in 2 external pages it is step 6 (6 items).
- Resource-integrity hashing breaks 3 of 8 chains one link earlier. The user-weighted gain is 5,150
  item-users, 54% of the total matches.
- The 2 external pages where hashing cannot be applied reach 1,630 users and are the two whose chains
  break latest. No relationship emerges between the degree of sharing and checkability.
- Cost: version pinning on 4 resources, 24 manual approvals a year, 48 days/year on a non-current
  version. The 4 out-of-scope resources carry 4,070 matches — 1.57 times the daily user count.

#### Next Step

Every control measured across six lessons was technical: a procedure, a threshold, a door, a hash. Yet
the breakable link in most of the chains depended on a decision one person made — whether to call back,
whether to notice and report an indicator, whether to hold a door. That decision itself is a trainable
behavior, and training's effect can be measured. That is the next lesson's question: how many points
does an awareness program gain on two metrics — reporting rate and click rate — how many weeks does that
gain take to decay back to its old value, and what is the program's own cost. It is the topic's closing
lesson.

### Awareness Programs

Source: https://academia.sh/en/courses/attack-techniques/awareness-programs

Running the awareness program with two metrics: the weekly path of the reporting and click rate after training, which week the gain decays back to its old value, the eighteen-fold difference in the number of people who click before the first report, and the per-hour return of four reinforcement intervals.

Every control measured across six lessons was technical: a procedure, a threshold, a door, a hash. Yet
the breakable link in most of the chains depended on a decision one person made — whether to call back,
whether to notice and report an indicator, whether to hold a door. This lesson treats that decision as a
**trainable behavior** and measures training's effect with two metrics.

Both metrics are rates, and they read in opposite directions: the reporting rate should rise, the click
rate should fall. Both improve right after a program, then decay back to their old value. This lesson's
real question is not the size of the improvement but its **persistence**: in which week does the gain
decay away, and what does reinforcement cost? A third number ties the two rates to a single operating
size — how many recipients have clicked the message by the time the first report arrives.

- **SE37.** The headcount, rates, and minutes belong to the field and subscriber services teams of the
  fictional regional measurement network. No real exercise is run; what is measured is the model built with
  `node`.
- **SE38.** Decay was assumed to be exponential: a fixed fraction of the gain is retained each week. In
  reality decay is not smooth and jumps with periodic events; the exponential form gives the return week
  only approximately.
- **SE39.** The training effect was counted as the same for everyone. In reality the effect varies by
  person and role; working with the average hides the tail of the distribution — the segment that never
  reports.
- **SE40.** The number of recipients processed before the first report is an expected value and is the
  inverse of the reporting rate. The time for the report to reach the defense and be processed was not
  modeled; in reality this time makes the number larger.
- **SE41.** It was assumed that as soon as a report arrives, the campaign can be stopped for the
  remaining recipients. This depends on the report-handling side being ready, and it does not hold in
  every organization.
- **SE42.** The measurement runs themselves also affect behavior. This feedback was not modeled;
  measurement frequency was held fixed.

#### The Weekly Path of the Two Metrics

```js
// program.mjs — the awareness program's two metrics, decay model, and chain connection; model
export const BASE = { report: 8, click: 31 };           // pre-training percentage, fictional
export const EFFECT = { report: 34, click: -22 };        // point change in the training week
export const DECAY = { report: 0.75, click: 0.80 };      // fraction of the gain retained each week
export const PEOPLE = 250, TRAINING_MIN = 25, MEASURE_MIN = 90; // fictional: headcount and minutes
// metric value: base + that week's remaining gain
export const value = (m, weeksSince) => BASE[m] + EFFECT[m] * DECAY[m] ** weeksSince;
export const gain = (m, weeksSince) => Math.abs(EFFECT[m]) * DECAY[m] ** weeksSince;
// number of recipients processed before the first report, and how many had clicked by then
export const firstReport = (r) => 1 / (r / 100);
export const priorClicks = (r, c) => firstReport(r) * (c / 100);
// the message chain from lesson 2: item dropped once the step completes
export const CHAIN_DROP = [0, 1, 1, 3, 6];
export const CUMUL = (() => { let s = 0; return CHAIN_DROP.map((d) => (s += d)); })();
export function week(n, interval) { // weeks elapsed since the last training
  return interval ? n % interval : n;
}
```

```js
// run1.mjs — the weekly path of the two metrics after a single training, and the week they return to baseline
import { BASE, EFFECT, DECAY, PEOPLE, value, gain, firstReport, priorClicks } from './program.mjs';

console.log(`base report ${BASE.report}%, base click ${BASE.click}%; ` +
  `training effect ${EFFECT.report >= 0 ? '+' : ''}${EFFECT.report} and ${EFFECT.click} points`);
console.log(`weekly retained fraction of the gain: report ${DECAY.report}, click ${DECAY.click}; campaign recipients ${PEOPLE}\n`);
console.log(`${'week'.padStart(5)} ${'report %'.padStart(10)} ${'gain'.padStart(6)} ${'click %'.padStart(9)} ` +
  `${'gain'.padStart(6)} ${'recipients until report'.padStart(24)} ${'clicked by then'.padStart(20)}`);
for (let n = 0; n <= 16; n += 2) {
  const r = value('report', n), c = value('click', n);
  console.log(`${String(n).padStart(5)} ${r.toFixed(1).padStart(10)} ${gain('report', n).toFixed(1).padStart(6)} ` +
    `${c.toFixed(1).padStart(9)} ${gain('click', n).toFixed(1).padStart(6)} ` +
    `${firstReport(r).toFixed(1).padStart(24)} ${priorClicks(r, c).toFixed(2).padStart(20)}`);
}
for (const m of ['report', 'click']) {
  let n = 0; while (gain(m, n) >= 1) n++;
  console.log(`\n${m}: gain drops below 1 point in week ${n} (that week's value ${value(m, n).toFixed(1)}%, base ${BASE[m]}%)`);
}
const r0 = value('report', 0), c0 = value('click', 0);
console.log(`\nweek 0: ${firstReport(r0).toFixed(1)} recipients until first report, ${priorClicks(r0, c0).toFixed(2)} clicks`);
console.log(`base level: ${firstReport(BASE.report).toFixed(1)} recipients until first report, ` +
  `${priorClicks(BASE.report, BASE.click).toFixed(2)} clicks; ratio ` +
  `${(priorClicks(BASE.report, BASE.click) / priorClicks(r0, c0)).toFixed(1)}x`);
```

```
base report 8%, base click 31%; training effect +34 and -22 points
weekly retained fraction of the gain: report 0.75, click 0.8; campaign recipients 250

 week   report %   gain   click %   gain  recipients until report      clicked by then
    0       42.0   34.0       9.0   22.0                      2.4                 0.21
    2       27.1   19.1      16.9   14.1                      3.7                 0.62
    4       18.8   10.8      22.0    9.0                      5.3                 1.17
    6       14.1    6.1      25.2    5.8                      7.1                 1.80
    8       11.4    3.4      27.3    3.7                      8.8                 2.39
   10        9.9    1.9      28.6    2.4                     10.1                 2.89
   12        9.1    1.1      29.5    1.5                     11.0                 3.25
   14        8.6    0.6      30.0    1.0                     11.6                 3.49
   16        8.3    0.3      30.4    0.6                     12.0                 3.64

report: gain drops below 1 point in week 13 (that week's value 8.8%, base 8%)

click: gain drops below 1 point in week 14 (that week's value 30.0%, base 31%)

week 0: 2.4 recipients until first report, 0.21 clicks
base level: 12.5 recipients until first report, 3.88 clicks; ratio 18.1x
```

The weekly table shows the two metrics decaying in the same direction but at different speeds. The
reporting rate is forty-two percent in week zero; it falls to nine percent by week twelve, and the gain
drops below one point in week thirteen. The click rate starts at nine percent and returns to its old
value in week fourteen. The one-week difference between the two metrics comes from their decay
coefficients; the result the measure carries is shared: **a single training's effect lasts less than a
quarter.**

The speed of decay is not evenly distributed either. The reporting gain falls from 34 points to 19.1
points in the first two weeks — forty-four percent of the gain is gone within two weeks — and the rest
decays over the remaining eleven weeks. A study that measures a program's effect in the training week
ends up reporting a value that, in reality, will not survive two more weeks. When the measurement is
taken matters as much as the value it measures.

The last two columns convert the rates into an operating number. When the reporting rate is forty-two
percent, the first report arrives after an average of 2.4 recipients, and 0.21 people have clicked by
then. With no program at all, the first report arrives after 12.5 recipients, and 3.88 people have
clicked by then. The ratio between them is **18.1x**, even though each rate's own change is only three
to five times. When two metrics work in the same direction, their effects multiply: fewer people click,
and the number who do click is also stopped earlier.

This also answers the question of which of the two metrics matters more. The click rate determines the
**speed** of the harm, the reporting rate determines its **duration**. A program that tracks only the
click rate ends up never measuring when the campaign was stopped.

#### The Return on Reinforcement

```js
// run2.mjs — reinforcement interval comparison, program cost, and its counterpart in the chain
import { BASE, PEOPLE, TRAINING_MIN, MEASURE_MIN, CUMUL, value, firstReport, priorClicks, week } from './program.mjs';

const PERIOD = 26;
const avg = (v) => v.reduce((a, b) => a + b, 0) / v.length;
console.log(`${PERIOD}-week period, ${PEOPLE} people\n`);
console.log(`${'reinforcement'.padEnd(18)} ${'trainings'.padStart(9)} ${'avg. report %'.padStart(15)} ${'avg. click %'.padStart(14)} ` +
  `${'avg. clicks until first report'.padStart(31)}`);
const results = [];
for (const interval of [0, 13, 6, 3]) {
  const r = [], c = [], x = [];
  for (let n = 0; n < PERIOD; n++) {
    const w = week(n, interval), rr = value('report', w), cc = value('click', w);
    r.push(rr); c.push(cc); x.push(priorClicks(rr, cc));
  }
  const trainings = interval ? Math.ceil(PERIOD / interval) : 1;
  results.push({ interval, trainings, r: avg(r), c: avg(c), x: avg(x) });
  console.log(`${(interval ? `every ${interval} weeks` : 'single training').padEnd(18)} ${String(trainings).padStart(9)} ` +
    `${avg(r).toFixed(1).padStart(15)} ${avg(c).toFixed(1).padStart(14)} ${avg(x).toFixed(2).padStart(31)}`);
}
console.log(`${'no program'.padEnd(18)} ${'0'.padStart(9)} ${BASE.report.toFixed(1).padStart(15)} ` +
  `${BASE.click.toFixed(1).padStart(14)} ${priorClicks(BASE.report, BASE.click).toFixed(2).padStart(31)}`);

console.log(`\n${'reinforcement'.padEnd(18)} ${'training min'.padStart(12)} ${'measure min'.padStart(11)} ${'total hours'.padStart(11)} ` +
  `${'click drop vs. base'.padStart(26)} ${'per hour'.padStart(11)}`);
const baseline = priorClicks(BASE.report, BASE.click);
for (const s of results) {
  const min = s.trainings * PEOPLE * TRAINING_MIN, measure = PERIOD * MEASURE_MIN, hours = (min + measure) / 60;
  console.log(`${(s.interval ? `every ${s.interval} weeks` : 'single training').padEnd(18)} ${String(min).padStart(12)} ` +
    `${String(measure).padStart(11)} ${hours.toFixed(1).padStart(11)} ${(baseline - s.x).toFixed(2).padStart(26)} ` +
    `${((baseline - s.x) / hours).toFixed(4).padStart(11)}`);
}
console.log(`\nin the message chain (5 steps, ${CUMUL[4]} items if never broken):`);
console.log(`  reporting behavior breaks link 3 -> items dropped by then ${CUMUL[1]}`);
console.log(`  if no report arrives, the next control is link 4 -> items dropped ${CUMUL[2]}`);
console.log(`  latest-breakable link is 5 -> items dropped ${CUMUL[3]}; early-break gain ${CUMUL[3] - CUMUL[1]} items`);
```

```
26-week period, 250 people

reinforcement      trainings   avg. report %   avg. click %  avg. clicks until first report
single training            1            13.2           26.8                            2.74
every 13 weeks             2            18.2           23.0                            1.77
every 6 weeks              5            27.5           17.0                            0.76
every 3 weeks              9            34.5           13.0                            0.40
no program                 0             8.0           31.0                            3.88

reinforcement      training min measure min total hours        click drop vs. base    per hour
single training            6250        2340       143.2                       1.14      0.0079
every 13 weeks            12500        2340       247.3                       2.11      0.0085
every 6 weeks             31250        2340       559.8                       3.12      0.0056
every 3 weeks             56250        2340       976.5                       3.47      0.0036

in the message chain (5 steps, 11 items if never broken):
  reporting behavior breaks link 3 -> items dropped by then 1
  if no report arrives, the next control is link 4 -> items dropped 2
  latest-breakable link is 5 -> items dropped 5; early-break gain 4 items
```

The upper table gives the absolute result, and it runs in the expected direction: as reinforcement gets
more frequent, the average reporting rate rises from 13.2 percent to 34.5 percent, and the average click
rate falls from 26.8 percent to 13.0 percent. With no program at all, the number of people who click
before the first report is 3.88; with reinforcement every three weeks, it is 0.40. A decision based on
this table alone would be to shorten the interval as much as possible.

The lower table upsets that decision. The gain per hour spent is highest for reinforcement every
thirteen weeks (0.0085) and falls as the interval shortens: 0.0056 for every six weeks, 0.0036 for every
three weeks. Reinforcement every three weeks spends four times the hours of reinforcement every thirteen
weeks and raises the gain only from 2.11 to 3.47. The shape of the curve explains this — the gain is
largest in the training week, and each reinforcement is stacked on a point where the gain **has not yet
decayed away**.

The distribution of the cost is worth reading too. The measurement minutes are the same across all four
rows — 2,340 minutes over twenty-six weeks — and the whole difference between rows comes from training
minutes. In single training, 104.2 of the total 143.2 hours are training; in reinforcement every three
weeks, 937.5 of the total 976.5 hours. Measurement's share falls from twenty-seven percent to four
percent. This means measurement runs are cheap and training sessions are expensive; the two metrics can
be measured often, but they cannot be reinforced often.

The chain rows write the program's counterpart in items. Reporting behavior breaks the third link of the
five-step message chain, and the dropped item at that point is one. If no report arrives, the next
control is the fourth link, where the dropped item is two. The latest-breakable link is the fifth step:
five items. The early-break gain is four items, and that is the human layer's total measured contribution
on this topic.

#### What the Metric Does Not See

Both metrics are averages, and what the average hides is the tail of the distribution. When the
reporting rate rises to forty-two percent, fifty-eight percent remains, and **who** makes up that
remainder is not in the table. Reaching the chain's fourth link — credential entry — takes only one
person; while the metric rises, whether that one person's behavior changed may not have been measured
at all.

The second limit is that the measurement itself is a form of training. A measurement program that runs
weekly also feeds the behavior it measures; the decay in the model does not include this feedback, so
the real decay may be slower than the model's. The return week coming out to thirteen is not an upper
bound — it is the result of a fixed coefficient.

The third has to do with where persistence is measured. The callback procedure in the first lesson
stopped four spoofed requests independent of the person, at a cost of 1,440 seconds of added
verification; the awareness program, by contrast, depends on the person and its cost is paid again every
period. The two do not break the same link: the procedure operates at the chain's last step, reporting
behavior at its third step. An organization does not choose one over the other — it counts, separately,
the link each one breaks. What this topic's seven lessons measured was never which one is better, but at
**which link** and with **how many items** each one breaks it.

#### Summary

- A single training raises the reporting rate from 8% to 42% and lowers the click rate from 31% to 9%.
  The reporting gain drops below 1 point in week 13, the click gain in week 14.
- The number of people who click before the first report is 0.21 in the training week and 3.88 with no
  program at all — an 18.1x difference. While each rate's own change is only 3 to 5x, their effects
  multiply.
- The click rate determines the speed of the harm, the reporting rate its duration. A program that
  tracks only the click rate never measures when the campaign was stopped.
- Over a 26-week period, as the reinforcement interval shortens from 13 to 3 weeks, the average
  reporting rate rises from 18.2% to 34.5%, and clicks before the first report fall from 1.77 to 0.40.
- The gain per hour is highest for reinforcement every 13 weeks (0.0085) and falls as the interval
  shortens. Reinforcement every 3 weeks spends 4x the hours and raises the gain only from 2.11 to 3.47.
- Reporting behavior breaks the message chain's 3rd link (1 item); the latest-breakable link is the 5th
  (5 items). The early-break gain is 4 items.

#### Next Step

This topic measured the human layer across seven lessons and filled the same three columns in each one.
At the moment of decision, one of seven verification items held up under pressure; in one message set,
executive-targeted attacks carried fewer indicators than legitimate messages; in a voice call, the
strong-verification item count was zero; in one building, two of eight paths broke no link at all; spoof
candidates bought an eleven-day window; a single external resource reached the entirety of the users; and
an awareness program delivered a four-item gain at the chain's third link.

All of these measurements share one limit: every one of them counts the links **before entry**. How the
chain continues once entry has occurred — the links the software itself opens — was never counted. A
field with no input validation, an item running in the user's browser, a copy with no bounds check, or
an identity attempt with no threshold sets up its own chains, and the links of those chains never pass
through a human decision at all. The next topic, **Technical Attack Classes**, counts those links: how
many steps each class is, which defense layer breaks which step, and how many steps no control sees at
all.

## Technical Attack Classes

Measuring application- and system-layer attack classes as a step-to-control mapping: how common risk lists are read and why the ranking changes by organization, which defense layer breaks which step in the injection, cross-site scripting, request forgery, and path traversal classes, how buffer overflow corrupts the neighboring item in the bounded-array model, the cost difference between brute-force, dictionary, and spraying forms against lockout and rate limiting, the precondition chain of browser-based infection, and the place of reverse engineering in defense.

### Web Application Risks

Source: https://academia.sh/en/courses/attack-techniques/web-application-risks

Measuring that common risk lists are a way of reading, not a ranking: six of ten fictional items changing places between the overall ranking and the organizational ranking, the largest shift being four places, twenty-two of the thirty-nine steps across nine chains remaining uncontrolled, one chain never being breakable at runtime, and the defense's break order overlapping the organizational ranking in only one of three items.

The previous topic measured the human layer: from pretext to message, from message to domain,
the chain's first links rested on a decision made by a person, and that decision was measured
by counting how many links it broke. Once the attacker is inside, though, the chain continues
from somewhere else — from the links the software itself opens. This topic counts those links.

Before counting begins, there is a reading problem. Common lists prepared for application-layer
risks exist, and in most organizations these lists are read like a to-do order. This lesson
measures that way of reading: what the list is ranked by, why the ranking changes within an
organization, and why a third ranking that none of the list's items carries is needed. No
technique in this lesson or this topic may be tried on a system outside **written authorization
and a defined scope**; trying it is both illegal and unprofessional.

#### The Structure of the List

Common risk lists rank their items by four criteria: **how often** the item is seen in the
field, **how readily** an attacker can exploit it, **how readily** the defender finds it, and the
**technical impact** that results when it succeeds. All four belong to the item; none belongs
to the organization.

One of the four criteria works in the opposite direction. The defender finding the item readily
**lowers** the risk, because a readily found item is one with a high chance of being fixed. This
is the criterion that feeds directly into the list's order and is most often misread: an item
being lower on the list does not mean it is rarely seen — it means it has **already been found**
in most organizations. For an organization that has not done its own inventory, this information
means nothing.

A fifth criterion is deliberately left out: **business impact**. What an item costs a given
organization depends on what that organization protects, which work it considers unstoppable,
and whether that item even has a counterpart in its architecture. The list cannot know this, so
it does not account for it. The list's order is therefore not a recommendation — it is **a
starting point**.

**TA1:** the item set below is a **model** — not a real risk list, a real organization, or a
real measurement; the criteria values are fictional. **TA2:** each item is a step sequence;
`controlled` is the index of the steps that carry a control point. **TA3:** the overall ranking
is the average of the four criteria. **TA4:** the organizational ranking uses the same criteria,
sums the technical impact with its own weights, and scales an item that has no counterpart in
its architecture by exposure. **TA5:** the breakable link is the lowest-indexed step that
carries a control point.

```js
// risk/item.mjs — fictional risk item set (model): not a real risk list, a real
// organization, or a real measurement. The four criteria are fictional values between 0..10.
// prev: prevalence, expl: exploitability, det: the defender's ease of detection,
// impact: technical impact component {c: confidentiality, i: integrity, a: availability}
export const ITEMS = [
  { name: "access-control",         prev: 9, expl: 7, det: 4, impact: { c: 9, i: 8, a: 3 }, exposure: 1.0 },
  { name: "injection",              prev: 6, expl: 8, det: 6, impact: { c: 9, i: 9, a: 5 }, exposure: 0.8 },
  { name: "identity-management",    prev: 7, expl: 6, det: 5, impact: { c: 8, i: 6, a: 4 }, exposure: 1.0 },
  { name: "configuration",          prev: 8, expl: 5, det: 8, impact: { c: 6, i: 5, a: 6 }, exposure: 1.0 },
  { name: "unmaintained-component", prev: 8, expl: 4, det: 9, impact: { c: 7, i: 7, a: 6 }, exposure: 1.0 },
  { name: "cryptographic-decision", prev: 5, expl: 3, det: 3, impact: { c: 9, i: 4, a: 1 }, exposure: 0.3 },
  { name: "logging-gap",            prev: 7, expl: 2, det: 2, impact: { c: 3, i: 3, a: 2 }, exposure: 1.0 },
  { name: "server-side-request",    prev: 3, expl: 5, det: 5, impact: { c: 7, i: 4, a: 3 }, exposure: 0.2 },
  { name: "design-flaw",            prev: 5, expl: 6, det: 2, impact: { c: 6, i: 8, a: 5 }, exposure: 0.9 },
  { name: "integrity-verification", prev: 4, expl: 4, det: 4, impact: { c: 4, i: 9, a: 4 }, exposure: 1.0 },
];

// TA2: each item is a step sequence. steps = number of steps in the chain;
// controlled = 1-based indices of the steps that carry a control point;
// cost = number of fictional items affected if the chain is only broken at the last step.
export const CHAIN = {
  "access-control":         { steps: 4, controlled: [2, 4],    cost: 240 },
  "injection":               { steps: 6, controlled: [2, 3, 5], cost: 480 },
  "identity-management":     { steps: 5, controlled: [1, 3, 4], cost: 180 },
  "configuration":           { steps: 3, controlled: [1, 2, 3], cost: 120 },
  "unmaintained-component":  { steps: 5, controlled: [1, 2],    cost: 300 },
  "cryptographic-decision":  { steps: 4, controlled: [4],       cost: 360 },
  "logging-gap":             { steps: 0, controlled: [],        cost: 0 },
  "server-side-request":     { steps: 5, controlled: [3, 4],    cost: 90 },
  "design-flaw":             { steps: 4, controlled: [],        cost: 200 },
  "integrity-verification":  { steps: 3, controlled: [2],       cost: 150 },
};

export const write = (w, ...s) => console.log(s.map((v, i) =>
  (w[i] < 0 ? String(v).padEnd(-w[i]) : String(v).padStart(w[i]))).join(""));
```

```js
// risk/rank.mjs — the same item set ranked three different ways: overall list, org ranking,
// defense's break order. No randomness; every number is derived from fictional criteria values.
import { ITEMS, CHAIN, write } from "./item.mjs";

const overall = (k) => (k.prev + k.expl + (10 - k.det) + (k.impact.c + k.impact.i + k.impact.a) / 3) / 4;

// For the fictional measurement network, integrity and availability are heavy, confidentiality is lighter.
const W = { c: 0.1, i: 0.5, a: 0.4 };
const org = (k) => k.exposure * (k.prev + k.expl +
  (10 - k.det) + (k.impact.c * W.c + k.impact.i * W.i + k.impact.a * W.a)) / 4;

const rankBy = (f) => [...ITEMS].sort((a, b) => f(b) - f(a)).map((k) => k.name);
const OV = rankBy(overall), ORG = rankBy(org);
const A = [-24, 8, 6, 8, 6, 8];

console.log(`${ITEMS.length} fictional risk items, the same four criteria, two weight sets\n`);
write(A, "item", "overall", "rank", "org", "rank", "shift");
for (const name of OV) {
  const k = ITEMS.find((x) => x.name === name);
  const g = OV.indexOf(name) + 1, u = ORG.indexOf(name) + 1;
  write(A, name, overall(k).toFixed(2), g, org(k).toFixed(2), u,
    u === g ? "-" : (u < g ? "+" : "") + (g - u));
}
console.log(`\nitems that changed rank: ${OV.filter((name, i) => ORG.indexOf(name) !== i).length}` +
  `/${ITEMS.length}; largest shift: ` +
  `${Math.max(...OV.map((name, i) => Math.abs(ORG.indexOf(name) - i)))} places; shared in top three: ` +
  `${OV.slice(0, 3).filter((name) => ORG.slice(0, 3).includes(name)).length}/3`);

const B = [-24, 7, 12, 14, 11];
console.log(`\nchain columns (by org rank)`);
write(B, "item", "steps", "controlled", "first break", "late cost");
const chained = ORG.filter((name) => CHAIN[name].steps > 0);
for (const name of ORG) {
  const z = CHAIN[name];
  if (z.steps === 0) { write(B, name, "-", "-", "not a chain", "-"); continue; }
  write(B, name, z.steps, z.controlled.length,
    z.controlled.length ? `step ${z.controlled[0]}` : "unbreakable", z.cost);
}
const sum = (f) => chained.reduce((s, name) => s + f(CHAIN[name]), 0);
console.log(`\n${chained.length} chains, ${sum((z) => z.steps)} steps total, ` +
  `${sum((z) => z.controlled.length)} steps have a control, ` +
  `${sum((z) => z.steps - z.controlled.length)} steps have no control`);
const first = (name) => CHAIN[name].controlled[0] ?? 0;
console.log(`broken at step 1: ${chained.filter((a) => first(a) === 1).length}; ` +
  `step 3 or later: ${chained.filter((a) => first(a) >= 3).length}; ` +
  `never breakable: ${chained.filter((a) => first(a) === 0).length}`);

// Defense's order: earliest-breakable first, ties broken by higher cost.
const breakOrder = chained.filter((a) => first(a) > 0)
  .sort((a, b) => first(a) - first(b) || CHAIN[b].cost - CHAIN[a].cost);
console.log(`\ntop three by org rank   : ${ORG.slice(0, 3).join(", ")}`);
console.log(`top three by break order: ${breakOrder.slice(0, 3).join(", ")}`);
console.log(`shared items: ${ORG.slice(0, 3).filter((a) => breakOrder.slice(0, 3).includes(a)).length}/3`);
```

```
10 fictional risk items, the same four criteria, two weight sets

item                     overall  rank     org  rank   shift
access-control              7.17     1    7.03     1       -
injection                   6.42     2    5.08     6      -4
design-flaw                 6.33     3    5.76     3       -
identity-management         6.00     4    5.85     2      +2
configuration               5.17     5    5.13     4      +1
unmaintained-component      4.92     6    4.90     7      -1
cryptographic-decision      4.92     7    1.37     9      -2
logging-gap                 4.92     8    4.90     8       -
integrity-verification      4.92     9    5.13     5      +4
server-side-request         4.42    10    0.84    10       -

items that changed rank: 6/10; largest shift: 4 places; shared in top three: 2/3

chain columns (by org rank)
item                      steps  controlled   first break  late cost
access-control                4           2        step 2        240
identity-management           5           3        step 1        180
design-flaw                   4           0   unbreakable        200
configuration                 3           3        step 1        120
integrity-verification        3           1        step 2        150
injection                     6           3        step 2        480
unmaintained-component        5           2        step 1        300
logging-gap                   -           -   not a chain          -
cryptographic-decision        4           1        step 4        360
server-side-request           5           2        step 3         90

9 chains, 39 steps total, 17 steps have a control, 22 steps have no control
broken at step 1: 3; step 3 or later: 2; never breakable: 1

top three by org rank   : access-control, identity-management, design-flaw
top three by break order: unmaintained-component, identity-management, configuration
shared items: 1/3
```

#### Ranking Changes by Organization

The first table ranks the same ten items twice. The criteria values are the same in both
rankings; what changes is only how the technical impact is summed and whether the item has a
counterpart in this architecture. The result: **six of the ten items changed rank**, and the
largest shift is four places.

Two extreme examples show where the difference comes from. `injection` is second in the overall
ranking but drops to sixth in the organizational ranking; on part of the fictional measurement
network's endpoints, this item has no counterpart, and exposure scales it by 0.8.
`integrity-verification`, by contrast, rises from ninth to fifth: proving that a meter reading
is correct is this network's **actual job**, and the integrity weight is 0.5. In the general list,
this item sits in the middle ranks because, in an average organization, integrity does not
weigh as heavily as confidentiality.

Exposure's most visible effect on the list is in the `cryptographic-decision` item: from 4.92
to 1.37, from seventh to ninth.
The cause is not weight but **exposure**: on this fictional network, the relevant surface is
narrowly defined, and the score is scaled by 0.3. Exposure is a harsher multiplier than weight:
weight moves an item a few places, exposure drops it off the list.

The direction of the shifts also carries information. Two of the three items that rise —
`identity-management` and `integrity-verification` — are items that touch this network's own work;
of the three items that fall, two fall because of exposure, one because of weight. When an
organization produces its own ranking, the real output it generates is not the order itself but
**the reason for the shift**: if an item has no counterpart in the architecture, the effort
spent tracking it is wasted; if it fell because of weight, the item is still there, and only its
rank has changed.

Two items are shared in the top three. This does not mean the list is useless; it means the
list correctly says **which items exist** but does not say **which one comes first**. The list
is an inventory; the order is the organization's own measurement.

#### Chain Columns

The second table names each item by this course's three columns. There are a total of **39
steps** across nine chains; 17 of them carry a control point, and **22 steps are uncontrolled**.
The number of uncontrolled steps is not a measure of the surface — it is a measure of the
defense: every uncontrolled step is a point where the chain passes through cleanly.

Three chains can be broken at the first step. These are the three cheapest items: the chain
ends before it starts, and the late-break cost is never paid. `cryptographic-decision`, by
contrast, can only be broken at the **fourth step**, and by that point 360 fictional items have
already been affected. The same item is ninth in the organizational ranking — meaning an item
counted as low priority carries the second most expensive late-break cost.

Two rows in the table show the limit of the measure. `design-flaw` has four steps and **carries
no control point at any of them**: this chain cannot be broken at runtime, because its control
point lies outside the chain — at design time. For an item like this, the measure shifts from
breaking to **detection**: the chain will complete; the only question left to ask is how late it
will be seen.

The `logging-gap` row, meanwhile, is not even a chain. This item does not define an attack step
sequence; it defines the **invisibility** of the other chains. Two different kinds of item stand
side by side on the same list, and a single ranking shows both on the same scale. This is the
most concrete problem with reading the list like a to-do order.

#### The Defense's Third Ranking

The last three lines are the lesson's conclusion. The organizational ranking gives risk from
largest to smallest; the defense's order, on the other hand, goes by **the earliest breakable
link**. Only one item is shared between the top three of the two rankings.

`unmaintained-component` is seventh in the organizational ranking but first in the break order:
its chain breaks at the first step, and its late-break cost of 300 is the third-highest value.
In other words, an item that looks low priority is the cheapest and highest-return first move.
`access-control`, in contrast, is first in both rankings, but its chain can only be broken at
the second step; what decides whether something is the first move is not its place on the list
but the place of its link.

The rule that follows from this governs the rest of this topic: prioritizing a risk item
requires the answer to three separate questions — does the item exist in this architecture, how
heavy is its impact in this organization, and **at which step** can its chain be broken. The
list only supplies the inventory for the first.

The answer to the third question cannot be read from the list, because the list does not write
out an item's steps when it defines it. The remaining eight lessons of this topic fill exactly
that missing column: each class's step sequence is written out with its preconditions, the
defense layers are mapped onto those steps, and the breakable link falls out as an index.

#### Summary

- Common risk lists rank items by prevalence, exploitability, ease of detection, and technical
  impact; they deliberately leave out business impact. The list is not an ordering
  recommendation — it is an inventory.
- When the same ten items were reordered with organizational weights and exposure, 6 items
  changed rank, the largest shift was 4 places, and only 2 items stayed shared in the top three.
- Exposure is a harsher multiplier than weight: one item dropped from 4.92 to 1.37 and fell to
  the bottom of the list; weight changes moved items by at most four places.
- Across nine chains, 17 of 39 steps carry a control point, and 22 steps are uncontrolled. Three
  chains break at the first step; one chain can never be broken at runtime, and there the
  measure shifts to detection.
- The top three of the organizational ranking and the top three of the break order overlap in
  only 1 item; an item ranked seventh becomes the first move because it breaks at the first step.

#### Next Step

This lesson counted the items but opened none of them up. Among the ten items, the chain
carrying the highest late-break cost had six steps and three of them carried a control point —
which three, and **which step** those controls cut off, was not written. The next lesson opens
that chain: the injection class's six steps are written out one by one with their preconditions,
the same input set is run through five defense layers, and the step each layer breaks is
counted. What gets measured is not the input itself, but which layer the input stops at, and,
when it does not stop, how far into the chain it advances.

### Injection Attacks

Source: https://academia.sh/en/courses/attack-techniques/injection-attacks

Measuring the injection class as a six-step chain: which step each of the six defense layers breaks, the absence of a control in forty-two of the seventy-two step-site combinations across twelve call sites, the three-layer paper design being complete at only one call site, six of the one hundred ninety-two input-site combinations completing all six steps, and those six concentrating at two call sites.

The previous lesson covered ten risk items across three columns and gave the injection class
the highest late-break cost: a six-step chain, a control point at three steps, 480 fictional
items if never broken. This lesson opens up that row. Each of the six steps is written with
its **precondition**, the six defense layers are mapped to steps, and the same input set is
run through the fictional measurement network's twelve call sites.

The measurement runs entirely on formal properties. No input value is written out, no query
text is constructed, and no fix example is given. Inputs are defined only by four binary
properties: does it comply with the field contract, does it carry that context's **delimiter
character class**, does the context produce a return, is its length long. What is measured is
not the input itself, but **which step the input stops at**.

#### The Chain's Six Steps

Injection is not a single moment; it requires all six steps to hold in sequence.

1. **Entry.** The value enters the system through an endpoint. Precondition: the endpoint is
   reachable from outside and the field accepts this value.
2. **Concatenation.** The value is written **into** the text that will be handed to an
   interpreter. Precondition: the call site does not pass the value as a separate operand.
3. **Parsing.** The interpreter reads part of the value as **syntax**, not data. Precondition:
   the value carries that context's delimiter character class and that class has not been
   escaped.
4. **Authority.** The altered expression runs with the calling identity's privileges.
   Precondition: that identity can access the targeted items.
5. **Return.** The result reaches the caller. Precondition: the response body or error text
   carries detail and the context produces a return.
6. **Exit.** The obtained items are carried out. Precondition: there is no limit on the exit
   volume and destination.

None of the steps was written with **how** it is done; each was written with what it needs.
That is exactly the defense's job: to remove a step's precondition.

**TA6:** the step sequence and the `cost` field are a model; `cost` is the number of fictional
items affected if the chain passes that step unbroken. **TA7:** each layer breaks a single
step; a conditional layer breaks only for a specific input class. **TA8:** step 3 holds only
if the input carries the delimiter class, step 5 only if the context produces a return.
**TA9:** the same input is run separately through every call site; a chain broken at one site
is not counted as broken at another.

```js
// inj/chain.mjs — the injection class's step sequence and defense layers (model).
// There is no real system, no real endpoint, and no real input value; inputs are
// defined only by their formal properties.
export const STEP = [
  { n: 1, name: "entry",         cost: 0 },
  { n: 2, name: "concatenation", cost: 0 },
  { n: 3, name: "parsing",       cost: 40 },
  { n: 4, name: "authority",     cost: 120 },
  { n: 5, name: "return",        cost: 300 },
  { n: 6, name: "exit",          cost: 480 },
];

export const LAYER = [
  { name: "field-contract",   breaks: 1, cond: (g) => !g.contract, legit: 3 },
  { name: "separate-operand", breaks: 2, cond: () => true,         legit: 0 },
  { name: "context-escaping", breaks: 3, cond: () => true,         legit: 1 },
  { name: "least-privilege",  breaks: 4, cond: () => true,         legit: 6 },
  { name: "plain-response",   breaks: 5, cond: () => true,         legit: 4 },
  { name: "exit-limit",       breaks: 6, cond: () => true,         legit: 9 },
];

export const PRECOND = { 3: (g) => g.delimiter, 5: (g) => g.readback };

// 16 fictional inputs: all combinations of four binary properties. No value is ever written out.
export const INPUT = [];
for (const contract of [true, false])
  for (const delimiter of [true, false])
    for (const readback of [true, false])
      for (const long of [true, false])
        INPUT.push({ no: INPUT.length + 1, contract, delimiter, readback, long });

export const classify = (g) => !g.contract ? "off-contract"
  : g.delimiter ? "delimiter-bearing" : g.long ? "long-but-compliant" : "plain-compliant";

// The fictional measurement network's 12 call sites and the layers actually deployed at each.
export const CALL = [
  ["subscriber-portal.search",  ["field-contract", "separate-operand", "context-escaping", "plain-response"]],
  ["subscriber-portal.billing", ["field-contract", "separate-operand", "plain-response"]],
  ["subscriber-portal.dispute", ["field-contract", "separate-operand", "context-escaping"]],
  ["collector.read-write",      ["separate-operand", "least-privilege"]],
  ["collector.bulk-transfer",   ["separate-operand", "least-privilege", "exit-limit"]],
  ["validator.threshold",       ["field-contract", "separate-operand"]],
  ["validator.archive-search",  ["field-contract"]],
  ["billing-generation.period", ["separate-operand", "plain-response", "exit-limit"]],
  ["billing-generation.report", ["context-escaping", "plain-response"]],
  ["work-order.create",         ["field-contract", "separate-operand", "least-privilege"]],
  ["work-order.field-note",     []],
  ["branch-endpoint.query",     ["field-contract", "separate-operand", "context-escaping", "least-privilege"]],
];

export const write = (g, ...s) => console.log(s.map((v, i) =>
  (g[i] < 0 ? String(v).padEnd(-g[i]) : String(v).padStart(g[i]))).join(""));
```

```js
// inj/measure.mjs — the same 16 inputs are run through every call site: at each step,
// was the chain broken by a control, or did the precondition never hold. No randomness.
import { STEP, LAYER, PRECOND, INPUT, classify, CALL, write } from "./chain.mjs";

const cost = (n) => STEP[n - 2]?.cost ?? 0;   // items accumulated if broken at step n

function run(input, deployed) {
  for (const a of STEP) {
    const k = LAYER.find((x) => x.breaks === a.n && deployed.includes(x.name) && x.cond(input));
    if (k) return { stop: a.n, reason: "control", cost: cost(a.n) };
    if (PRECOND[a.n] && !PRECOND[a.n](input))
      return { stop: a.n, reason: "precond", cost: cost(a.n) };
  }
  return { stop: 7, reason: "completed", cost: STEP[5].cost };
}

console.log(`${STEP.length}-step chain, ${LAYER.length} layers, ` +
  `${CALL.length} call sites, ${INPUT.length} fictional inputs\n`);

const A = [-17, 9, 25, 13, 12];
console.log("1. layer-to-step mapping");
write(A, "layer", "breaks", "cond", "legit cost", "call sites");
for (const k of LAYER)
  write(A, k.name, `step ${k.breaks}`,
    k.cond({ contract: true }) ? "unconditional" : "depends on input class",
    k.legit, CALL.filter(([, d]) => d.includes(k.name)).length);

const B = [-29, 8, 14, 14, 13];
console.log("\n2. chain per call site");
write(B, "call site", "layers", "first break", "uncontrolled", "completing");
let uncontrolled = 0;
for (const [name, d] of CALL) {
  const broken = STEP.filter((a) => LAYER.some((x) => x.breaks === a.n && d.includes(x.name)));
  const unconditional = broken.filter((a) => LAYER.find((x) => x.breaks === a.n).cond({ contract: true }));
  const done = INPUT.filter((g) => run(g, d).stop === 7).length;
  uncontrolled += STEP.length - broken.length;
  write(B, name, d.length, unconditional.length ? `step ${unconditional[0].n}` : "none",
    STEP.length - broken.length, `${done}/${INPUT.length}`);
}
console.log(`no control exists in ${uncontrolled} of the ${CALL.length * STEP.length} step-site combinations`);
const PAPER = ["separate-operand", "context-escaping", "plain-response"];
console.log(`sites where the paper design (${PAPER.join(" + ")}) is complete: ` +
  `${CALL.filter(([, d]) => PAPER.every((k) => d.includes(k))).length}/${CALL.length}`);

const C = [-20, 7, 15, 15, 13, 12];
console.log("\n3. stops per input class (16 inputs x 12 sites)");
write(C, "input class", "inputs", "control stop", "precond stop", "completed", "cost");
for (const s of [...new Set(INPUT.map(classify))]) {
  const set = INPUT.filter((g) => classify(g) === s);
  const r = set.flatMap((g) => CALL.map(([, d]) => run(g, d)));
  write(C, s, set.length, r.filter((x) => x.reason === "control").length,
    r.filter((x) => x.reason === "precond").length,
    r.filter((x) => x.reason === "completed").length, r.reduce((t, x) => t + x.cost, 0));
}
const all = INPUT.flatMap((g) => CALL.map(([name, d]) => ({ name, ...run(g, d) })));
const complete = all.filter((x) => x.reason === "completed");
console.log(`${complete.length} of ${all.length} combinations complete all six steps, ` +
  `all at these two sites: ${[...new Set(complete.map((x) => x.name))].join(", ")}`);

const D = [-14, 17, 18, 13];
console.log("\n4. the cost of breaking late");
write(D, "step broken", "items accumulated", "breaking layer", "legit cost");
for (const a of STEP) {
  const k = LAYER.find((x) => x.breaks === a.n);
  write(D, `step ${a.n}`, cost(a.n), k.name, k.legit);
}
console.log(`${STEP[5].cost} items if never broken; the six layers' total legitimate-work cost is ` +
  `${LAYER.reduce((t, k) => t + k.legit, 0)} items/day, the two that break the first two steps total ` +
  `${LAYER.slice(0, 2).reduce((t, k) => t + k.legit, 0)}`);
```

```
6-step chain, 6 layers, 12 call sites, 16 fictional inputs

1. layer-to-step mapping
layer               breaks                     cond   legit cost  call sites
field-contract      step 1   depends on input class            3           7
separate-operand    step 2            unconditional            0           9
context-escaping    step 3            unconditional            1           4
least-privilege     step 4            unconditional            6           4
plain-response      step 5            unconditional            4           4
exit-limit          step 6            unconditional            9           2

2. chain per call site
call site                      layers   first break  uncontrolled   completing
subscriber-portal.search            4        step 2             2         0/16
subscriber-portal.billing           3        step 2             3         0/16
subscriber-portal.dispute           3        step 2             3         0/16
collector.read-write                2        step 2             4         0/16
collector.bulk-transfer             3        step 2             3         0/16
validator.threshold                 2        step 2             4         0/16
validator.archive-search            1          none             5         2/16
billing-generation.period           3        step 2             3         0/16
billing-generation.report           2        step 3             4         0/16
work-order.create                   3        step 2             3         0/16
work-order.field-note               0          none             6         4/16
branch-endpoint.query               4        step 2             2         0/16
no control exists in 42 of the 72 step-site combinations
sites where the paper design (separate-operand + context-escaping + plain-response) is complete: 1/12

3. stops per input class (16 inputs x 12 sites)
input class          inputs   control stop   precond stop    completed        cost
delimiter-bearing         4             40              4            4        2400
long-but-compliant        2             20              4            0           0
plain-compliant           2             20              4            0           0
off-contract              8             88              6            2        1200
6 of 192 combinations complete all six steps, all at these two sites: validator.archive-search, work-order.field-note

4. the cost of breaking late
step broken   items accumulated    breaking layer   legit cost
step 1                        0    field-contract            3
step 2                        0  separate-operand            0
step 3                        0  context-escaping            1
step 4                       40   least-privilege            6
step 5                      120    plain-response            4
step 6                      300        exit-limit            9
480 items if never broken; the six layers' total legitimate-work cost is 23 items/day, the two that break the first two steps total 3
```

#### The Layer's Condition

The first table separates the six layers by a single column: **condition**. Five layers are
unconditional — wherever they are deployed, they break the same step for every input. One is
conditional: the field contract only breaks the chain for input that violates the contract.
For an input that complies with the contract and carries the delimiter class, the contract
layer **breaks no step at all**.

This distinction is the point most often misread in practice. Contract validation is cited as
an injection defense; what measurement shows is that it only stops inputs that are formally
out of bounds. The delimiter character class inside a legitimate subscriber name still
complies with that field's contract.

The second distinction is in the cost column. The layer that breaks the second step has a
legitimate-work cost of **zero**: passing the value as a separate operand rejects no legitimate
request. The exit limit that breaks the sixth step has a cost of 9 items/day. The six layers'
total cost is 23, while the two that break the first two steps total 3. **Breaking early
does not just lose fewer items; breaking itself is also cheaper.**

#### Breaking Per Call Site

The second table is this lesson's real measurement. The class is one, the chain is one — but
the defense is deployed per call site, and twelve sites produce twelve different outcomes.

At nine of the twelve sites, the chain breaks at the **second step**, and a single layer does
it. At one site, the first break shifts to the third step. **At two sites there is no
unconditional layer at all**: there, the chain only stops when the input's preconditions fail
to hold, not because of the defense. Of the 72 combinations formed by the twelve call sites
and six steps, **42 have no control at all**.

The distribution of uncontrolled combinations is not uniform either. Four layers are present at
four or fewer of the twelve sites; the most common layer is at nine sites. The count shows that
coverage must be measured **per layer**: a layer existing does not mean it exists everywhere,
and the chain runs through its weakest call site.

The last line gives the difference between paper and the field. The previous lesson recorded
the injection chain as having a "control at three steps"; that record assumes the three layers
are present everywhere. The measurement says that design is complete at **only 1 of the
organization's 12 call sites**. No defense claim written at the system level counts as verified
until it is counted per call site.

#### Input Class and Stop

The third table splits the 16 inputs into four classes and counts where the 192 input-site
combinations stop. Two columns are kept separate: did **a control** stop the chain, or did the
input's **precondition** fail to hold.

Inputs that do not carry the delimiter class stop on their own at the third step — with no
defense doing any work. That is what makes the `plain-compliant` and `long-but-compliant`
classes' cost column zero. A defense saying "no problem found" often just means the chain was
already without a precondition for that input.

The four inputs that do carry the delimiter class, on the other hand, are stopped by a control
in 40 of 48 combinations, the precondition fails to hold in 4, and the chain completes in 4.
Those same four inputs alone carry a cost of 2,400 items at the two sites with incomplete
deployment.

Of the 192 combinations, only **6** complete all six steps, and all six are at the same two
call sites. Those two sites carry a cost of 2,400 and 1,200 items. Risk does not depend on the
item itself, but on **how many sites it is uncontrolled at**: the class is the same at all
twelve sites, the count piles up at two.

#### The Cost of Breaking Late

The fourth table gives the cost step by step. A chain broken in the first three steps has zero
cost: parsing never happened. If it breaks at the fourth step, 40 fictional items are affected;
at the fifth, 120; at the sixth, 300; if never broken, 480.

The three zero-cost steps do not mean breaking at those steps is **without payoff**. Breaking
there means an incident never happens at all; the only thing that lands in the record is a
rejected request. Breaking after the fourth step, on the other hand, always produces an
incident record, a review, and a scope question — that is the second cost the cost column
does not write down.

The jump in cost is not even. The move from the third step to the fourth goes from 0 to 40; from
the fifth to the sixth, from 120 to 300. **Toward the end of the chain, each step is more
expensive than the last**, because each step multiplies what the previous one gained. Least
privilege and the exit limit are "late" defenses for this reason: they work, but by the time
they engage, part of the loss has already happened.

#### Summary

- The injection class is a six-step chain, and each step depends on a precondition: a
  reachable endpoint, a concatenating call site, a value carrying the delimiter class, broad
  privilege, a detailed return, unlimited exit.
- Five of the six layers break unconditionally, one is conditional: the field contract only
  breaks for input outside the contract, and breaks no step at all for input that complies
  with the contract while carrying the delimiter class.
- The layer that breaks the second step has a legitimate-work cost of 0, the one that breaks the
  sixth has 9 items/day; the six layers total 23, the two that break the first two steps
  total 3.
- At 9 of the twelve call sites the chain breaks at the second step, at 1 at the third, at 2
  there is no unconditional layer at all. 42 of the 72 step-site combinations have no control,
  and the three-layer paper design is complete at only 1 of the 12 sites.
- 6 of the 192 input-site combinations complete all six steps, and all six are at the same two
  call sites; from the fourth step on, the cost is 40, 120, 300 items, and 480 if never broken.

#### Next Step

In this lesson's chain, the value was written into an interpreter's text, and the interpreter
was inside the server. The same step sequence takes a different shape when the interpreter is
**in the user's browser**: the value passes through the server, lands on a page, and parsing
happens on another machine. The next lesson measures that shift — why the three separate forms
have different step counts, how far the number of steps broken by server-side layers falls
when a form never routes the value through the server at all, and what it means for the
measure to shift from breaking to detection in that case.

### Cross-Site Scripting

Source: https://academia.sh/en/courses/attack-techniques/cross-site-scripting

Measuring the three forms as separate step sequences: reflected five, stored six, DOM-based four steps; the number of steps the three server-side layers break in the DOM-based form being zero, no link breaking in three of the twenty-four combinations across eight pages, the daily cost of one hundred ninety sessions for the seven combinations broken only at the last step, and a single browser-side layer closing off all three forms at once.

In the previous lesson's chain, the interpreter was inside the server, and three of the six
steps passed through the server. This lesson measures the same step sequence when the
interpreter is **in the user's browser**. The only thing that changes is where parsing
happens; the result is that the chain's length and the breakable link land in three separate
places across three separate forms.

The three forms are separated: the **reflected** form, where the value comes back in the same
response; the **stored** form, where the value is first written to storage and then served to
every viewer; and the **DOM-based** form, where the value never touches the server at all.
Throughout the measurement, no script body, no value, and no page source is ever written out;
the forms are referred to only by their step sequences.

#### Three Step Sequences

**Reflected form, five steps.** (1) The value reaches the endpoint in a request field —
precondition: the endpoint writes that field back into its response. (2) The value lands in a
position that will be read as code in the response — precondition: no encoding is applied for
that position's context. (3) The link reaches the victim and the victim opens it with their
own session — precondition: a delivery channel exists. (4) The browser executes the placed
value — precondition: the page's content policy allows this source. (5) The running code
accesses the session item and carries it out — precondition: the cookie is script-accessible.

**Stored form, six steps.** The reflected form's first step splits in two: the value first
enters **storage** through a write endpoint, then lands in the response along a view path. In
exchange, the third step's precondition **disappears**: nobody needs to be directed to a link,
anyone who opens the page for their own work completes the chain themselves. The chain gets
one step longer and the delivery link comes free.

**DOM-based form, four steps.** (1) The value is read from a client-side source — precondition:
a source that never reaches the server is used, such as the URL fragment or local storage.
(2) The client script writes the value to a target that is interpreted as code — precondition:
the target selection is not fixed. (3) The browser executes it. (4) Access and exit. This
sequence has **no step that passes through the server**; the server never sees the request, it
never lands in its log, and it never alters its response.

What the three sequences share is the last two steps: execution and exit. That is why the
defense can stand in two separate places — one blocks the value from landing in the response,
the other blocks the browser from executing it. Where they diverge is the start: the reflected
and stored forms pass through the server, the third does not.

**TA10:** each form is a separate step sequence. **TA11:** each layer breaks a specific step in
each form; in a form it does not break, it has no value. **TA12:** for per-link delivery the
cost is a fictional 12 people, for per-view delivery it is the page's daily view count.
**TA13:** the first broken link is the smallest step index broken by the layers deployed on
that page.

```js
// xss/form.mjs — cross-site scripting's three forms, their step sequences, and layers
// (model). There is no real page, no real value, and no script body at all.
export const FORM = {
  reflected: { steps: ["entry", "placement", "delivery", "execution", "exit"],
    side: ["server", "server", "victim", "browser", "browser"], delivery: "per link" },
  stored: { steps: ["write", "storage", "placement", "view", "execution", "exit"],
    side: ["server", "server", "server", "victim", "browser", "browser"],
    delivery: "per view" },
  domBased: { steps: ["client-source", "write-to-target", "execution", "exit"],
    side: ["browser", "browser", "browser", "browser"], delivery: "per link" },
};

// breaks[form] = 1-based step index, or 0 (does not break any step in that form).
export const LAYER = [
  { name: "input-contract", side: "server",
    breaks: { reflected: 1, stored: 1, domBased: 0 }, legit: 3 },
  { name: "storage-sanitization", side: "server",
    breaks: { reflected: 0, stored: 2, domBased: 0 }, legit: 2 },
  { name: "output-encoding", side: "server",
    breaks: { reflected: 2, stored: 3, domBased: 0 }, legit: 0 },
  { name: "content-policy", side: "browser",
    breaks: { reflected: 4, stored: 5, domBased: 3 }, legit: 7 },
  { name: "client-target-selection", side: "client-code",
    breaks: { reflected: 0, stored: 0, domBased: 2 }, legit: 1 },
  { name: "cookie-flag", side: "browser",
    breaks: { reflected: 5, stored: 6, domBased: 4 }, legit: 4 },
];

export const LINK = 12;

// The fictional measurement network's eight pages: deployed layers and daily view count.
export const PAGE = [
  ["subscriber-portal.search", ["input-contract", "output-encoding", "content-policy", "cookie-flag"], 210],
  ["subscriber-portal.dispute", ["input-contract", "storage-sanitization", "output-encoding", "cookie-flag"], 90],
  ["subscriber-portal.billing", ["output-encoding", "content-policy", "cookie-flag"], 340],
  ["field-app.work-order", ["input-contract", "output-encoding", "cookie-flag"], 160],
  ["field-app.field-note", ["cookie-flag"], 75],
  ["branch-endpoint.dashboard", ["output-encoding", "content-policy"], 40],
  ["branch-endpoint.report-link", ["client-target-selection", "cookie-flag"], 55],
  ["admin.metric-search", [], 20],
];

export const write = (g, ...s) => console.log(s.map((v, i) =>
  (g[i] < 0 ? String(v).padEnd(-g[i]) : String(v).padStart(g[i]))).join(""));
```

```js
// xss/measure.mjs — the three forms are run across eight pages: the first broken link,
// how many steps the server side breaks, and the sessions affected if unbroken. No randomness.
import { FORM, LAYER, PAGE, LINK, write } from "./form.mjs";

const forms = Object.keys(FORM);
const LABEL = { reflected: "reflected", stored: "stored", domBased: "DOM-based" };
const firstBreak = (b, d) => Math.min(...LAYER
  .filter((k) => d.includes(k.name) && k.breaks[b] > 0).map((k) => k.breaks[b]), Infinity);
const cost = (b, views) => (b === "stored" ? views : LINK);

console.log(`${forms.length} forms, ${LAYER.length} layers, ${PAGE.length} fictional pages\n`);

const A = [-16, 7, 11, 12, 20];
console.log("1. chain per form");
write(A, "form", "steps", "on server", "breakable", "delivery");
for (const b of forms)
  write(A, LABEL[b], FORM[b].steps.length, FORM[b].side.filter((t) => t === "server").length,
    LAYER.filter((k) => k.breaks[b] > 0).length, FORM[b].delivery);

const B = [-25, -14, 10, 8, 15];
console.log("\n2. layer-to-step mapping");
write(B, "layer", "side", "reflected", "stored", "DOM-based");
for (const k of LAYER)
  write(B, k.name, k.side, k.breaks.reflected || "-", k.breaks.stored || "-", k.breaks.domBased || "-");
const server = LAYER.filter((k) => k.side === "server");
console.log(`the ${server.length} server-side layers break this many steps in the DOM-based form: ` +
  `${server.filter((k) => k.breaks.domBased > 0).length}`);

const C = [-29, 7, 12, 12, 15];
console.log("\n3. first broken link per page");
write(C, "page", "layers", "reflected", "stored", "DOM-based");
let open = 0, sessions = 0;
for (const [name, d, views] of PAGE) {
  const h = forms.map((b) => firstBreak(b, d));
  h.forEach((v, i) => { if (v === Infinity) { open++; sessions += cost(forms[i], views); } });
  write(C, name, d.length, ...h.map((v) => (v === Infinity ? "not broken" : `step ${v}`)));
}
console.log(`no link breaks in ${open} of the ${PAGE.length * forms.length} page-form combinations; ` +
  `those combinations' daily cost is ${sessions} fictional sessions`);
const lateBreaks = PAGE.flatMap(([, d, views]) => forms.filter((b) =>
  firstBreak(b, d) === FORM[b].steps.length).map((b) => cost(b, views)));
console.log(`combinations broken only at the last step: ${lateBreaks.length}, daily cost ` +
  `${lateBreaks.reduce((a, x) => a + x, 0)} fictional sessions`);

const D = [-25, 10, 8, 15, 9, 20];
console.log("\n4. if a single layer covered every page");
write(D, "single layer", "reflected", "stored", "DOM-based", "legit", "remaining sessions");
for (const k of LAYER) {
  const remaining = PAGE.reduce((t, [, , views]) => t + forms.reduce((s, b) =>
    s + (k.breaks[b] > 0 ? 0 : cost(b, views)), 0), 0);
  write(D, k.name, k.breaks.reflected || "-", k.breaks.stored || "-", k.breaks.domBased || "-",
    k.legit, remaining);
}
const allOpen = PAGE.reduce((t, [, , views]) => t + forms.reduce((s, b) => s + cost(b, views), 0), 0);
console.log(`daily cost with no layer at all: ${allOpen} fictional sessions`);
```

```
3 forms, 6 layers, 8 fictional pages

1. chain per form
form              steps  on server   breakable            delivery
reflected             5          2           4            per link
stored                6          3           5            per view
DOM-based             4          0           3            per link

2. layer-to-step mapping
layer                    side           reflected  stored      DOM-based
input-contract           server                 1       1              -
storage-sanitization     server                 -       2              -
output-encoding          server                 2       3              -
content-policy           browser                4       5              3
client-target-selection  client-code            -       -              2
cookie-flag              browser                5       6              4
the 3 server-side layers break this many steps in the DOM-based form: 0

3. first broken link per page
page                          layers   reflected      stored      DOM-based
subscriber-portal.search           4      step 1      step 1         step 3
subscriber-portal.dispute          4      step 1      step 1         step 4
subscriber-portal.billing          3      step 2      step 3         step 3
field-app.work-order               3      step 1      step 1         step 4
field-app.field-note               1      step 5      step 6         step 4
branch-endpoint.dashboard          2      step 2      step 3         step 3
branch-endpoint.report-link        2      step 5      step 6         step 2
admin.metric-search                0  not broken  not broken     not broken
no link breaks in 3 of the 24 page-form combinations; those combinations' daily cost is 44 fictional sessions
combinations broken only at the last step: 7, daily cost 190 fictional sessions

4. if a single layer covered every page
single layer              reflected  stored      DOM-based    legit  remaining sessions
input-contract                    1       1              -        3                  96
storage-sanitization              -       2              -        2                 192
output-encoding                   2       3              -        0                  96
content-policy                    4       5              3        7                   0
client-target-selection           -       -              2        1                1086
cookie-flag                       5       6              4        4                   0
daily cost with no layer at all: 1182 fictional sessions
```

#### The Chain the Server Never Sees

The last line of the second table is this lesson's main finding: the number of steps the three
server-side layers break in the DOM-based form is **zero**. The input contract never sees a
request, storage sanitization never sees a write, output encoding never sees a value written
into the response. The value never touches the server at all.

This has a second consequence, heavier than the first: the server **records none of this
chain's steps**. When a chain cannot be broken, the measure shifts to detection; here, detection
cannot happen server-side either, because there is no log line to look at. The only places this
form can be seen are a policy-violation report that describes browser behavior and the client
code's own logging.

The same line also highlights a layer class: `client-target-selection` only carries value in
this form, and it alone breaks the **second step**. No decision about hardening the server
affects this step.

This also determines where the measure moves. A defense report might count the three
server-side layers and write "three-layer protection," and that is true for two forms. For the
third form, the count is three, but the number of steps it breaks is zero — and the difference
stays invisible unless the report separates the two. This line is the reason for counting
**broken steps** instead of counting layers.

#### Chain Length and Delivery

The first table puts the three forms side by side: 5, 6, and 4 steps. The longest chain is not
the safest one. The stored form has six steps, and the extra step **favors the attacker**: the
fourth step requires no delivery effort, anyone who opens the page for their own work completes
that step.

The number of layers that can break it reads the same way: four layers can break the reflected
form, five the stored form, three the DOM-based form. The stored form is the one seen by the
most layers because most of its steps pass through the server; the one seen by the fewest
layers is the one that never passes through the server at all. **A chain's visibility is
measured not by its length, but by how many steps it spends on the side where the defense
sits.**

The delivery column says the unit of cost differs too. In the reflected and DOM-based forms,
the cost is per link — a fictional 12 people. In the stored form, the cost is per view and
ranges from 20 to 340 depending on the page. Same class, same page, different form: **the cost
of breaking late can run up to twenty-eight times as high.**

#### Breaking Per Page

The third table measures deployment. In **3** of the 24 combinations formed by eight pages and
three forms, no link breaks at all; all three are concentrated on a single page. That page has
no layer deployed at all, and its daily cost is 44 fictional sessions.

The subtler result is in seven combinations: there, the chain does break, but **only at the
last step**. On a page that carries the cookie flag but has no other layer, code runs, the page
changes, requests go out — the only link that breaks is the access at the very end. These seven
combinations have a daily cost of 190 sessions, and they show up as "protected" in the defense
report.

The table's left column gives the reason for this result. On the two pages with four layers,
the chain breaks at the first step; on pages with three layers, at the second or third step; on
the page with a single layer, at the fifth and sixth step. In this set, the layer count and the
index of the broken link move together, but not one to one: the three-layer
`subscriber-portal.billing` breaks at the second step, and the two-layer
`branch-endpoint.dashboard` also breaks at the second step. What matters is not the count, but
**which steps** the layers fall on.

The `branch-endpoint.report-link` row shows the directional difference between two layers. In
the DOM-based form, the chain breaks at the **second step**, because client target selection
sits there; in the reflected and stored forms, it only breaks at the fifth and sixth step. Same
page, same two layers, three different links depending on the form.

#### A Single Layer's Coverage

The fourth table assumes each layer alone, deployed on every page, and counts the remaining
daily cost. With no layer at all, the cost is 1,182 fictional sessions.

Two rows give zero: the content policy and the cookie flag break all three forms, because both
are **browser-side** and all three forms converge there. Their legitimate-work cost is 7 and 4 —
the most expensive of the six layers.

The storage-sanitization row gives the limit of a layer tied to a single form: it breaks the
stored form at the second step, does nothing in the other two forms, and leaves 192 sessions
remaining — not the highest of the six rows, but one of the two layers tied to only a single
form.

Output encoding's legitimate-work cost is **zero**, and it breaks the reflected and stored forms at
the second and third step; but it does nothing in the DOM-based form and leaves 96 sessions
remaining. The cheapest layer breaks early but never sees one of the three forms; the layer
that covers all three forms at once is the most expensive one. **Coverage and earliness do not
converge in the same layer** — that is the measured reason both layers are needed.

#### Summary

- The three forms are three separate chains: reflected 5, stored 6, DOM-based 4 steps. The
  stored form's extra step favors the attacker, because it removes the delivery link's
  precondition.
- The number of steps the 3 server-side layers break in the DOM-based form is 0, and none of
  that chain's steps land in a server log; there, the measure shifts from breaking to
  detection, and detection can only happen browser-side.
- The unit of cost changes by form: 12 fictional people per link, 20 to 340 per view. On the
  same page, the cost of breaking late can run up to twenty-eight times as high depending on
  the form.
- Of the 24 combinations formed by eight pages and three forms, 3 have no link breaking at all
  (44 sessions/day), and 7 break only at the last step (190 sessions/day).
- Each of the two browser-side layers alone closes off all three forms and brings the remaining
  cost to 0; their legitimate-work cost is 7 and 4. Output encoding, with a cost of 0, breaks early but
  leaves 96 sessions remaining because it never sees one of the forms.

#### Next Step

In all three of this lesson's forms, the code ran in the victim's browser and accessed the
session item at the last step. There is a quieter thing the same browser can do: send a request
**with the victim's identity**, without running any code at all. The next lesson measures that
class — the direction in which a request travels through the victim's browser and the
direction in which it travels with the server's own identity are written as separate chains,
how many steps each direction has, which layer breaks which step, and why the two directions'
late-break costs cannot be measured in the same unit.

### Request Forgery

Source: https://academia.sh/en/courses/attack-techniques/request-forgery

Measuring the two directions of request forgery as separate chains: in the browser direction, the five steps' preconditions are split between the victim, the browser, and the server; in the server direction, forty percent of the cost is realized at the third step; two of eleven endpoints cannot be broken at all; removing a single layer produces a forty-five-transaction difference; and the two directions' costs are measured in separate units.

In the previous lesson, code ran in the victim's browser. There is a quieter thing the same
browser can do without running any code at all: send a request with the victim's **identity**.
Request forgery is that class, and it has two directions.

In the first direction, what is forged is the **victim's identity**: the request leaves the
victim's browser and automatically carries the session item along with it. In the second
direction, what is forged is the **server's network position**: the request leaves the server
and reaches an internal endpoint that cannot be accessed from outside. The two directions share
the same class name because both involve sending a request with someone else's authority; but
their chains, precondition owners, and cost units are separate.

#### Two Chains, Two Precondition Owners

**Browser direction, five steps.** (1) The victim has an open session on the target —
precondition: the session item sits in the browser and has not expired. (2) The victim reaches
a page chosen by the attacker — precondition: a delivery channel exists. (3) A request leaves
that page for the target, and the session item is automatically attached to it — precondition:
the same-site policy does not exclude this request. (4) The endpoint accepts the request —
precondition: the endpoint does not require an item that proves the request's origin. (5) The
transaction becomes permanent — precondition: the transaction completes in a single step.

**Server direction, five steps.** (1) An endpoint sends a request to an address given from
outside — precondition: the target address comes from a request field. (2) Address resolution
lands on an address inside the internal network — precondition: there is no check after
resolution. (3) The request reaches the internal endpoint — precondition: the internal
endpoint is reachable at the network layer. (4) The internal endpoint answers the request
without asking for identity — precondition: internal endpoints trust based on location. (5)
The response returns to the caller — precondition: the response body or an error difference is
reflected back outward.

The first chain's first two steps have their precondition **in the victim's hands**; there is
no foothold for the defense there. In the second chain, no step's precondition belongs to the
victim — two are in the defender's own infrastructure, two in the network, one in an internal
service. This explains why the two directions' defense responsibility falls to different
places.

**TA14:** the two directions are separate chains; `owner` is the party that holds the step's
precondition. **TA15:** each layer breaks a single step in a single direction. **TA16:** the
unit of cost changes by direction, and the two units are not added together. **TA17:**
`curve[i]` gives how much of the cost is realized once step i is complete.

```js
// rf/direction.mjs — request forgery's two directions, step sequences, and layers (model).
// No real endpoint, no real address, and no request body at all.
export const DIRECTION = {
  browser: {
    steps: ["open-session", "delivery", "auto-attach", "endpoint-acceptance", "persistence"],
    owner: ["victim", "victim", "browser", "server", "server"],
    unit: "transaction", curve: [0, 0, 0, 0, 1],
  },
  server: {
    steps: ["address-from-outside", "resolution", "reach-internal-endpoint", "unauthenticated-response", "reflect-back"],
    owner: ["server", "network", "network", "internal-service", "server"],
    unit: "internal endpoint", curve: [0, 0, 0.4, 0.8, 1],
  },
};

export const LAYER = [
  { name: "same-site-cookie",         direction: "browser", breaks: 3, legit: 2 },
  { name: "request-token",            direction: "browser", breaks: 4, legit: 5 },
  { name: "origin-check",             direction: "browser", breaks: 4, legit: 3 },
  { name: "re-authentication",        direction: "browser", breaks: 5, legit: 11 },
  { name: "egress-allowlist",         direction: "server",  breaks: 1, legit: 4 },
  { name: "resolution-check",         direction: "server",  breaks: 2, legit: 1 },
  { name: "network-segmentation",     direction: "server",  breaks: 3, legit: 8 },
  { name: "internal-endpoint-identity", direction: "server", breaks: 4, legit: 6 },
  { name: "plain-response",           direction: "server",  breaks: 5, legit: 2 },
];

// The fictional measurement network's eleven endpoints: direction, deployed layers, and cost.
export const ENDPOINT = [
  ["subscriber-portal.plan-change", "browser", ["same-site-cookie", "request-token"], 60],
  ["subscriber-portal.address-update", "browser", ["same-site-cookie"], 45],
  ["subscriber-portal.dispute-open", "browser", ["request-token", "origin-check"], 30],
  ["field-app.work-order-close", "browser", ["same-site-cookie", "re-authentication"], 120],
  ["admin.threshold-setting", "browser", [], 8],
  ["admin.user-permission", "browser", ["request-token", "re-authentication"], 4],
  ["billing-generation.template-fetch", "server", ["egress-allowlist", "resolution-check"], 9],
  ["collector.feedback-endpoint", "server", ["resolution-check", "network-segmentation"], 14],
  ["work-order.map-preview", "server", ["plain-response"], 14],
  ["report.external-source-fetch", "server", [], 17],
  ["branch-endpoint.connection-test", "server", ["network-segmentation", "internal-endpoint-identity", "plain-response"], 6],
];

export const write = (g, ...s) => console.log(s.map((v, i) =>
  (g[i] < 0 ? String(v).padEnd(-g[i]) : String(v).padStart(g[i]))).join(""));
```

```js
// rf/measure.mjs — the two directions are run across eleven endpoints: the first broken
// link, the uncontrolled step count, and the direction-specific unit cost of breaking late. No randomness.
import { DIRECTION, LAYER, ENDPOINT, write } from "./direction.mjs";

const firstBreak = (y, d) => Math.min(...LAYER
  .filter((k) => k.direction === y && d.includes(k.name)).map((k) => k.breaks), Infinity);
// share of the cost realized in a chain that breaks at step n
const ratio = (y, n) => (n === Infinity ? 1 : DIRECTION[y].curve[n - 2] ?? 0);

console.log(`2 directions, ${LAYER.length} layers, ${ENDPOINT.length} fictional endpoints\n`);

console.log("1. the two directions' step sequences");
for (const [name, z] of Object.entries(DIRECTION)) {
  console.log(`${name} direction — ${z.steps.length} steps, cost unit "${z.unit}", ` +
    `${LAYER.filter((k) => k.direction === name).length} layers`);
  z.steps.forEach((a, i) => console.log(`  ${i + 1}. ${a.padEnd(26)}precondition owner: ` +
    `${z.owner[i].padEnd(18)}cost realized on completion: ${(z.curve[i] * 100).toFixed(0)}%`));
}

const B = [-37, -12, 8, 13, 14, 8, 14];
console.log("\n2. chain per endpoint");
write(B, "endpoint", "direction", "layers", "first break", "uncontrolled", "cost", "realized");
const status = ENDPOINT.map(([name, y, d, cost]) => {
  const h = firstBreak(y, d);
  const broken = new Set(LAYER.filter((k) => k.direction === y && d.includes(k.name)).map((k) => k.breaks));
  return { name, y, d, cost, h, uncontrolled: DIRECTION[y].steps.length - broken.size, real: cost * ratio(y, h) };
});
for (const u of status)
  write(B, u.name, u.y, u.d.length, u.h === Infinity ? "not broken" : `step ${u.h}`,
    u.uncontrolled, u.cost, u.real.toFixed(1));
for (const y of Object.keys(DIRECTION)) {
  const k = status.filter((u) => u.y === y);
  console.log(`${y} direction: ${k.length} endpoints, total cost ` +
    `${k.reduce((t, u) => t + u.cost, 0)} ${DIRECTION[y].unit}s, realized ` +
    `${k.reduce((t, u) => t + u.real, 0).toFixed(1)}; endpoints unbroken ` +
    `${k.filter((u) => u.h === Infinity).length}`);
}

const C = [-29, -12, 12, 8, 17, 21];
console.log("\n3. if a layer is removed from every endpoint");
write(C, "layer", "direction", "deployment", "legit", "newly-unbroken", "realized difference");
for (const k of LAYER) {
  const set = status.filter((u) => u.y === k.direction);
  let newlyOpen = 0, diff = 0;
  for (const u of set) {
    const h2 = firstBreak(k.direction, u.d.filter((x) => x !== k.name));
    if (u.h !== Infinity && h2 === Infinity) newlyOpen++;
    diff += u.cost * ratio(k.direction, h2) - u.real;
  }
  write(C, k.name, k.direction, set.filter((u) => u.d.includes(k.name)).length, k.legit, newlyOpen,
    `+${diff.toFixed(1)}`);
}

const D = [-8, 13, 13, 20, 19];
console.log("\n4. shape of the cost curve");
write(D, "step", "browser %", "server %", "browser endpoints", "server endpoints");
for (let n = 1; n <= 5; n++)
  write(D, `${n}.`, (DIRECTION.browser.curve[n - 1] * 100).toFixed(0),
    (DIRECTION.server.curve[n - 1] * 100).toFixed(0),
    status.filter((u) => u.y === "browser" && u.h === n).length,
    status.filter((u) => u.y === "server" && u.h === n).length);
console.log(`in the browser direction, cost is realized only at the last step: breaking at ` +
  `the fourth step keeps the cost at zero`);
console.log(`in the server direction, the third step produces 40% of the cost: breaking late does not zero out the cost`);
```

```
2 directions, 9 layers, 11 fictional endpoints

1. the two directions' step sequences
browser direction — 5 steps, cost unit "transaction", 4 layers
  1. open-session              precondition owner: victim            cost realized on completion: 0%
  2. delivery                  precondition owner: victim            cost realized on completion: 0%
  3. auto-attach               precondition owner: browser           cost realized on completion: 0%
  4. endpoint-acceptance       precondition owner: server            cost realized on completion: 0%
  5. persistence               precondition owner: server            cost realized on completion: 100%
server direction — 5 steps, cost unit "internal endpoint", 5 layers
  1. address-from-outside      precondition owner: server            cost realized on completion: 0%
  2. resolution                precondition owner: network           cost realized on completion: 0%
  3. reach-internal-endpoint   precondition owner: network           cost realized on completion: 40%
  4. unauthenticated-response  precondition owner: internal-service  cost realized on completion: 80%
  5. reflect-back              precondition owner: server            cost realized on completion: 100%

2. chain per endpoint
endpoint                             direction     layers  first break  uncontrolled    cost      realized
subscriber-portal.plan-change        browser            2       step 3             3      60           0.0
subscriber-portal.address-update     browser            1       step 3             4      45           0.0
subscriber-portal.dispute-open       browser            2       step 4             4      30           0.0
field-app.work-order-close           browser            2       step 3             3     120           0.0
admin.threshold-setting              browser            0   not broken             5       8           8.0
admin.user-permission                browser            2       step 4             3       4           0.0
billing-generation.template-fetch    server             2       step 1             3       9           0.0
collector.feedback-endpoint          server             2       step 2             3      14           0.0
work-order.map-preview               server             1       step 5             4      14          11.2
report.external-source-fetch         server             0   not broken             5      17          17.0
branch-endpoint.connection-test      server             3       step 3             2       6           0.0
browser direction: 6 endpoints, total cost 267 transactions, realized 8.0; endpoints unbroken 1
server direction: 5 endpoints, total cost 60 internal endpoints, realized 28.2; endpoints unbroken 1

3. if a layer is removed from every endpoint
layer                        direction     deployment   legit   newly-unbroken  realized difference
same-site-cookie             browser                3       2                1                +45.0
request-token                browser                3       5                0                 +0.0
origin-check                 browser                1       3                0                 +0.0
re-authentication            browser                2      11                0                 +0.0
egress-allowlist             server                 1       4                0                 +0.0
resolution-check             server                 2       1                0                 +0.0
network-segmentation         server                 2       8                0                 +2.4
internal-endpoint-identity   server                 1       6                0                 +0.0
plain-response               server                 2       2                1                 +2.8

4. shape of the cost curve
step        browser %     server %   browser endpoints   server endpoints
1.                  0            0                   0                  1
2.                  0            0                   0                  1
3.                  0           40                   3                  1
4.                  0           80                   2                  0
5.                100          100                   0                  1
in the browser direction, cost is realized only at the last step: breaking at the fourth step keeps the cost at zero
in the server direction, the third step produces 40% of the cost: breaking late does not zero out the cost
```

#### The Owner of the Precondition

The first output unpacks the two chains step by step and records **who holds** each step's
precondition. In the browser direction, two steps' owner is the victim, one is the browser, two
are the server. The number of steps the defender can decide directly on is two out of five; in
the remaining three, they have only an indirect effect.

In the server direction, the table flips: two steps' owner is the server itself, two are the
network, one is an internal service. **No step's precondition belongs to the victim.** This
direction does not depend on a user mistake at all; it plays out entirely within the
organization's own configuration. This is why the defense responsibility for two chains
gathered under the same class name falls to different teams.

#### Breaking Per Endpoint

The second output measures eleven endpoints. Of the six endpoints in the browser direction,
three break at the third step, two at the fourth step; one never breaks. Of the five endpoints
in the server direction, one breaks at the first step, one at the second, one at the third, one
at the fifth; one never breaks.

The `realized` column makes the difference concrete. In the browser direction, the total cost
is 267 transactions, and the realized amount is only **8** — because on the five endpoints that
break, the chain stops before reaching the fifth step, and in the browser direction, cost only
accrues at the last step. In the server direction, the total cost is 60 internal endpoints, and
the realized amount is **28.2**; the endpoint that cannot be broken produces all of its 17, and
the endpoint that breaks at the fifth step produces 11.2 of its 14.

The `uncontrolled` column gives a second reading. The endpoint with the most layers,
`branch-endpoint.connection-test`, breaks at the third step, and only two of its five steps are
uncontrolled; the two endpoints with the fewest layers have all five of their five steps
uncontrolled. On endpoints in between, the layer count and the uncontrolled-step count are not a
one-to-one complement, because two layers can break the same step:
`subscriber-portal.dispute-open` carries two layers, but both break the fourth step, so its
uncontrolled-step count is four. **Two layers landing on the same step does not win a second
link.**

The two totals can be written side by side but cannot be added together. One is daily
transactions, the other is the number of internal endpoints reached. When a defense report says
"two endpoints have no protection," it is talking about two different magnitudes: one affects 8
fictional transactions a day, the other opens access to 17 internal endpoints.

#### Removing a Single Layer

The third output is a sensitivity measurement: each layer is removed from every endpoint, and
the resulting difference in realized cost is recorded. Of nine layers, **six have a difference
of zero** — when removed, the breaking point shifts backward, but the cost does not change,
because another layer stands behind them.

Three layers do produce a difference. If `same-site-cookie` is removed, one endpoint becomes
unbreakable and the realized cost rises by **45 transactions**; its legitimate-work cost is only 2.
`plain-response` produces a difference of 2.8, `network-segmentation` of 2.4. By contrast, when
`re-authentication`, with a legitimate-work cost of 11, is removed, the difference is **zero**: the
most expensive layer adds nothing in this deployment.

This shows that layer selection has to be read from the **deployment**, not from a list. A
layer's value is not in its own strength, but in whether another layer that breaks before it
exists on the endpoints where it is deployed.

The reverse reading of the measurement comes from the same numbers. None of the six layers with
a zero difference are unnecessary; they are only **redundant in this deployment**. The moment
the layer in front of them is removed from an endpoint, they start producing a difference. The
question of whether a layer can be removed cannot be asked in isolation, then: the answer
depends on which steps the other layers standing on the same endpoint hold, and it has to be
recalculated every time the deployment changes.

#### The Shape of the Cost Curve

The fourth output gives the two directions' real difference. In the browser direction, the cost
curve is zero for four steps and jumps to 100% all at once at the fifth step. Breaking at the
fourth step and breaking at the first step give **the same result**: zero cost. In this
direction, there is almost no such thing as "breaking late"; the chain either completes or it
does not.

In the server direction, the curve is stepped: 40% of the cost is realized once the third step
completes, 80% once the fourth completes. Breaking at the fifth step — that is, blocking the
response from being reflected back out — prevents only 20% of the cost. The internal endpoints
have already been reached, and which services are alive has already been learned; the response
not coming back does not undo that.

**The gain from breaking early is not the same in the two chains gathered under the same class
name.** In the browser direction, every point before the last link has equal value; in the
server direction, every step is more expensive than the one before it, and the defense cannot
afford to fall behind the second step.

#### Summary

- Request forgery's two directions are separate chains: one uses the victim's identity, the
  other the server's network position. Both are five steps, but their precondition owners
  differ.
- In the browser direction, 2 of the five steps' preconditions are in the victim's hands; in
  the server direction, no step's precondition belongs to the victim, and the entire chain
  plays out within the organization's own configuration.
- In 2 of the eleven endpoints, no link breaks at all: one is 8 fictional transactions a day,
  the other 17 internal endpoints. The two numbers are in separate units and are not added
  together.
- Removing 6 of the nine layers does not change the realized cost; removing a layer with a
  legitimate-work cost of 2 produces a 45-transaction difference, while removing the most expensive
  layer (legitimate-work cost 11) produces 0.
- The cost curves differ: in the browser direction, cost is realized only at the fifth step;
  in the server direction, 40% is realized by the third step and 80% by the fourth. Breaking
  late leaves a real loss only in the second direction.

#### Next Step

In this lesson's server direction, the request went to an **address**, and the address coming
from outside was the chain's first link. The same structure turns into a different class when,
instead of an address, it is a **file path**: the endpoint takes an item name, appends it to a
root directory, and reads the result. The next lesson counts that chain — where the path
component comes from, which check remains after the concatenation, how the cost is determined
by the number of items the process identity can read, and at which step an indirect identifier
mapping breaks the chain.

### Path Traversal

Source: https://academia.sh/en/courses/attack-techniques/directory-traversal

Measuring path traversal as a five-step chain and a count of accessible items: in a thirty-item fictional inventory, the root check brings the item count down to twelve and the sensitivity down to seven; the narrow process identity removes no items within the jail; the identifier mapping breaks the chain at the second step and leaves ten items; and the jail, which breaks no step at all, brings the sensitivity down from forty-one to twenty.

In the previous lesson's server direction, the request went to an **address** that came from
outside. The same structure turns into a different class when, instead of an address, it is a
**file path**: the endpoint takes an item name, appends it to the root directory it means to
serve from, and reads the result. Path traversal is that chain.

This lesson's measure departs from the previous ones on one point. Here, the cost of breaking
late is not a transaction or session count, but the **number of readable items** — and that
number does not depend on where the chain breaks, but on the size of the inventory the process
sees when it does not. That is why a fictional file inventory sits at the center of this
measurement. No path string is ever produced; items are defined only by their location and
permission properties.

#### The Chain's Five Steps

1. **The name comes from the request.** Precondition: the endpoint takes the name of the item
   to serve from a request field.
2. **The name is concatenated into the path.** Precondition: the name is not mapped to an
   identifier; it is used directly as a path component.
3. **The concatenated path falls outside the root.** Precondition: there is no check, after
   normalization, on whether the path stays under the root.
4. **The process identity can read that item.** Precondition: the file permissions of the
   identity the process runs as cover that item.
5. **The content is written to the response.** Precondition: the response body carries the
   item's content as-is.

The third step is the one that carries this class's name, but it does not produce anything on
its own. A path that falls outside the root does not produce a read unless the fourth step also
holds. **What determines this class's cost is not the third step, but the inventory the fourth
step covers.**

The second step is also a decision in its own right, and it is skipped in most discussions. An
endpoint can determine the item it serves not by the name coming from the request, but by
selecting from a predetermined set of identifiers. In that case, the value coming from the
request is not a path component; it is a key, and if the key is outside the set, the request goes
unanswered. This decision is the earliest breakable point in the chain, and it defines the
inventory not from the root, but from the **list**.

**TA18:** the step sequence is a model. **TA19:** each layer breaks a single step; the jail
breaks no step, it only narrows the inventory. **TA20:** the inventory is 30 fictional items,
and each item is defined by location, permission, and a sensitivity weight. **TA21:** the
accessible set is the result of the deployed layers narrowing the inventory.

```js
// pt/inventory.mjs — a fictional file inventory and the path traversal chain (model).
// No real file system, no real path, and no traversal string at all; items are defined
// only by their location and permission properties.
export const STEP = ["name-from-request", "path-concatenation", "escape-root",
  "process-read", "response-write"];

export const LAYER = [
  { name: "identifier-mapping",      breaks: 2, legit: 6 },
  { name: "root-check",              breaks: 3, legit: 1 },
  { name: "narrow-process-identity", breaks: 4, legit: 4 },
  { name: "output-boundary",         breaks: 5, legit: 3 },
  { name: "jail",                    breaks: 0, legit: 9 },
];

// root = is it under the root the app means to serve from, jail = is it inside the process's jail,
// owner = the item's owner, shared = is it world-readable, sens = fictional sensitivity weight.
const I = (name, root, jail, owner, shared, sens) => ({ name, root, jail, owner, shared, sens });
export const ITEM = [
  I("invoice/2026-01.doc", 1, 1, "app", 0, 1), I("invoice/2026-02.doc", 1, 1, "app", 0, 1),
  I("invoice/2026-03.doc", 1, 1, "app", 0, 1), I("report/period-summary.doc", 1, 1, "app", 0, 1),
  I("report/meter-dump.doc", 1, 1, "app", 0, 1), I("template/notification.text", 1, 1, "app", 0, 0),
  I("template/invoice.text", 1, 1, "app", 0, 0), I("field/photo-1.image", 1, 1, "app", 0, 0),
  I("field/photo-2.image", 1, 1, "app", 0, 0), I("field/photo-3.image", 1, 1, "app", 0, 0),
  I("download/dump.archive", 1, 1, "app", 0, 1), I("download/history.archive", 1, 1, "app", 0, 1),
  I("app/setting.config", 0, 1, "app", 0, 3),
  I("app/database.connection", 0, 1, "app", 0, 3),
  I("app/signing.key", 0, 1, "app", 0, 3),
  I("app/log/access.record", 0, 1, "app", 0, 2),
  I("app/log/error.record", 0, 1, "app", 0, 2),
  I("app/version.text", 0, 1, "app", 1, 0),
  I("shared/timezone.table", 0, 1, "system", 1, 0),
  I("shared/codepage.table", 0, 1, "system", 1, 0),
  I("system/user-list.record", 0, 0, "system", 1, 2),
  I("system/weight-table.record", 0, 0, "system", 1, 1),
  I("system/network-setting.config", 0, 0, "system", 1, 2),
  I("system/service-definition.unit", 0, 0, "system", 1, 1),
  I("system/secret/identity.store", 0, 0, "system", 0, 3),
  I("system/secret/key.store", 0, 0, "system", 0, 3),
  I("neighbor/collector-setting.config", 0, 0, "neighbor", 0, 3),
  I("neighbor/validator-setting.config", 0, 0, "neighbor", 0, 3),
  I("neighbor/invoice-template.text", 0, 0, "neighbor", 0, 1),
  I("neighbor/log-archive.record", 0, 0, "neighbor", 0, 2),
];

// Identifier mapping: the endpoint selects from a fixed set of identifiers instead of the name.
export const MAPPING = ITEM.filter((k) => k.root && !k.name.startsWith("download/"));

export const ENDPOINT = [
  ["subscriber-portal.invoice-download", ["identifier-mapping", "root-check", "narrow-process-identity"]],
  ["subscriber-portal.report-view", ["root-check", "narrow-process-identity", "output-boundary"]],
  ["field-app.photo-fetch", ["root-check", "jail"]],
  ["billing-generation.template-read", ["identifier-mapping"]],
  ["report.dump-download", ["narrow-process-identity"]],
  ["admin.record-view", []],
];

export const write = (g, ...s) => console.log(s.map((v, i) =>
  (g[i] < 0 ? String(v).padEnd(-g[i]) : String(v).padStart(g[i]))).join(""));
```

```js
// pt/measure.mjs — accessible item count: for each layer set, how much of the inventory
// stays readable. No randomness; no path string is ever produced.
import { STEP, LAYER, ITEM, MAPPING, ENDPOINT, write } from "./inventory.mjs";

function accessible(d) {
  if (d.includes("identifier-mapping")) return MAPPING;              // step 2 broken
  let k = d.includes("jail") ? ITEM.filter((x) => x.jail) : ITEM;
  if (d.includes("root-check")) k = k.filter((x) => x.root);         // step 3 broken
  if (d.includes("narrow-process-identity"))                         // step 4 broken
    k = k.filter((x) => x.owner === "app" || x.shared);
  return k;
}
const sens = (k) => k.reduce((t, x) => t + x.sens, 0);
const firstBreak = (d) => Math.min(...LAYER
  .filter((x) => d.includes(x.name) && x.breaks > 0).map((x) => x.breaks), Infinity);

console.log(`${STEP.length}-step chain, ${LAYER.length} layers, ` +
  `${ITEM.length}-item fictional inventory (under root ${ITEM.filter((k) => k.root).length}, ` +
  `inside jail ${ITEM.filter((k) => k.jail).length}, total sensitivity ${sens(ITEM)})\n`);

const A = [-26, 9, 10, 12];
console.log("1. layer-to-step mapping");
write(A, "layer", "breaks", "legit", "deployment");
for (const k of LAYER)
  write(A, k.name, k.breaks ? `step ${k.breaks}` : "none", k.legit,
    ENDPOINT.filter(([, d]) => d.includes(k.name)).length);

const B = [-46, 8, 14, 13];
console.log("\n2. eight setups (excluding identifier mapping)");
write(B, "deployment", "items", "sensitivity", "first break");
const TRIO = ["root-check", "narrow-process-identity", "jail"];
for (let m = 0; m < 8; m++) {
  const d = TRIO.filter((_, i) => m & (1 << i));
  const k = accessible(d);
  const h = firstBreak(d);
  write(B, d.length ? d.join(" + ") : "none", k.length, sens(k),
    h === Infinity ? "not broken" : `step ${h}`);
}
const em = accessible(["identifier-mapping"]);
console.log(`identifier mapping alone: ${em.length} items, sensitivity ${sens(em)}, ` +
  `breaks at step 2; its legitimate-work cost is every new item having to be added to the mapping`);

const C = [-38, 8, 13, 9, 14, 15];
console.log("\n3. chain per endpoint");
write(C, "endpoint", "layers", "first break", "items", "sensitivity", "outside root");
for (const [name, d] of ENDPOINT) {
  const k = accessible(d), h = firstBreak(d);
  write(C, name, d.length, h === Infinity ? "not broken" : `step ${h}`, k.length, sens(k),
    k.filter((x) => !x.root).length);
}
const open = ENDPOINT.filter(([, d]) => firstBreak(d) === Infinity);
console.log(`endpoints with no link broken at all: ${open.length}; at these endpoints, readable ` +
  `items ${accessible([]).length}, sensitivity ${sens(accessible([]))}`);

const D = [-20, 12, 14, 14];
console.log("\n4. the jail's separate place");
write(D, "deployment", "step broken", "items", "sensitivity");
for (const [name, d] of [["none", []], ["jail only", ["jail"]],
  ["root-check only", ["root-check"]], ["jail + root-check", ["jail", "root-check"]]]) {
  const k = accessible(d), h = firstBreak(d);
  write(D, name, h === Infinity ? "none" : `step ${h}`, k.length, sens(k));
}
console.log(`the jail breaks no step at all, but it brings the inventory down from ${ITEM.length} ` +
  `items to ${ITEM.filter((k) => k.jail).length}, and the sensitivity down from ${sens(ITEM)} to ` +
  `${sens(ITEM.filter((k) => k.jail))}`);
```

```
5-step chain, 5 layers, 30-item fictional inventory (under root 12, inside jail 20, total sensitivity 41)

1. layer-to-step mapping
layer                        breaks     legit  deployment
identifier-mapping           step 2         6           2
root-check                   step 3         1           3
narrow-process-identity      step 4         4           3
output-boundary              step 5         3           1
jail                           none         9           1

2. eight setups (excluding identifier mapping)
deployment                                       items   sensitivity  first break
none                                                30            41   not broken
root-check                                          12             7       step 3
narrow-process-identity                             24            26       step 4
root-check + narrow-process-identity                12             7       step 3
jail                                                20            20   not broken
root-check + jail                                   12             7       step 3
narrow-process-identity + jail                      20            20       step 4
root-check + narrow-process-identity + jail         12             7       step 3
identifier mapping alone: 10 items, sensitivity 5, breaks at step 2; its legitimate-work cost is every new item having to be added to the mapping

3. chain per endpoint
endpoint                                layers  first break    items   sensitivity   outside root
subscriber-portal.invoice-download           3       step 2       10             5              0
subscriber-portal.report-view                3       step 3       12             7              0
field-app.photo-fetch                        2       step 3       12             7              0
billing-generation.template-read             1       step 2       10             5              0
report.dump-download                         1       step 4       24            26             12
admin.record-view                            0   not broken       30            41             18
endpoints with no link broken at all: 1; at these endpoints, readable items 30, sensitivity 41

4. the jail's separate place
deployment           step broken         items   sensitivity
none                        none            30            41
jail only                   none            20            20
root-check only           step 3            12             7
jail + root-check         step 3            12             7
the jail breaks no step at all, but it brings the inventory down from 30 items to 20, and the sensitivity down from 41 to 20
```

#### The Inventory's Narrowing

The second output tries every combination of the three layers. With no layer at all, all 30
items are readable and the total sensitivity is 41; the root check alone brings this down to
**12 items and 7 sensitivity**. The item count drops by 60% while the sensitivity drops by 83%:
the 18 items outside the root carry a disproportionately large share of the inventory's
sensitivity.

The narrow process identity alone leaves 24 items and 26 sensitivity. It is noticeably weaker
than the root check, because it only looks at **ownership**: the app's own items and
world-readable system items are not eliminated. The app's own configuration, connection, and key
items sit outside the root and stay readable even when the process identity is narrowed.

The sensitivity column dropping faster than the item column is the most useful thing about this
measurement. Counting items gives the inventory's **size**, while the sensitivity total gives
**what is lost**. If a defense decision is judged by item count alone, a layer that closes twenty
of thirty items looks good; the sensitivity column tells how much of the total weight those
twenty items carried, and the two columns do not always move in the same direction.

The combinations say a second thing: the root check pins the result to **12/7** in every row it
appears in. No layer added to it changes the number. When the earliest breakable link determines
the result, the layers that follow it contribute nothing to the inventory — this does not mean
the fourth and fifth layers are unnecessary; it shows they would take effect if the root check were
ever missing from an endpoint.

The identifier mapping breaks the chain earlier: **at the second step**, before the name is even
turned into a path. What is left is 10 items and 5 sensitivity — a narrower set than the root
check, because the two archive items under the root are not part of the mapping. Its cost is of a
different kind: every new item has to be added to the mapping by hand; its legitimate-work cost of 6 is six
times the root check's.

#### The Jail's Separate Place

The fourth output makes this lesson's distinction. The jail **breaks no step at all**: the name
still comes from the request, is still concatenated into the path, still falls outside the root,
is still read, and is still written to the response. All five steps of the chain still complete.

What it does sits on the other side of the measure: it brings the inventory down from 30 items
to 20, and the sensitivity down from 41 to 20. In a defense table, the "link broken" row stays
empty, while the "cost of breaking late" row is cut in half. **A layer that does not break the
chain is not outside the measurement; it is the layer that reduces the cost.**

This distinction shows why this course keeps three columns separate. The breakable-link column
and the cost-of-breaking-late column can move independently: one layer can cut the second in
half without improving the first at all, another can improve the first and never touch the
second. An assessment made with a single column always misses one of these two layers.

The row where the jail and the narrow process identity are used together gives a second finding:
the two together produce **the same result** as the jail alone. All 20 items inside the jail are
already either the app's own items or world-readable items; the narrow identity removes no item
there. When two layers narrow the same set, the second one's contribution does not show up in the
number.

#### Breaking Per Endpoint

The third output measures six endpoints. At four endpoints, the chain breaks at the second or
third step, and the count of outside-root items in the accessible set is **zero**. At one
endpoint, the chain breaks at the fourth step and 24 items remain; 12 of those are outside the
root. At one endpoint, no link breaks at all: 30 items, 41 sensitivity, 18 items outside the
root.

The difference between two endpoints is a single layer. `report.dump-download` carries only the
narrow process identity; if the root check had been added, readable items would have dropped
from 24 to 12, sensitivity from 26 to 7, and its legitimate-work cost would have been **1**. In the same
measurement, the jail, with a legitimate-work cost of 9, would have brought the sensitivity down from 26 to
only 20.

This comparison is this lesson's practical conclusion: the cheapest layer is the root check, and
it sits right behind the one that narrows the most — it leaves 12 items at a cost of 1, while the
identifier mapping, which breaks at the second step, costs 6 for 10. The jail and the narrow
identity do not replace it; they bound the remaining cost on an endpoint where it is forgotten.

Where the output boundary sits is also readable in the same table. This layer, which breaks the
fifth step, is deployed on only one of six endpoints, and at that endpoint the chain already
breaks at the third step; that is, it changes no number in this run. Its value shows up at an
endpoint where all three layers before it are missing at once — and there is such an endpoint in
the table, but the output boundary is not there either. **The layers' deployment was built to
repeat one another, not to complement one another**; the measurement does not show this at a
glance, but it becomes visible once the layer sets are compared endpoint by endpoint.

#### Summary

- Path traversal is a five-step chain, and the third step, which carries the class's name,
  does not produce a read on its own; what determines the cost is the inventory the fourth step
  covers.
- In the 30-item fictional inventory, the root check alone brings readable items down to 12 and
  sensitivity down to 7; the item count drops by 60%, the sensitivity by 83%.
- The narrow process identity leaves 24 items and 26 sensitivity; the app's own
  configuration and key items outside the root are not eliminated by the ownership criterion.
- The identifier mapping breaks the chain at the earliest point — the second step — and leaves
  10 items, but its legitimate-work cost of 6 is six times the root check's.
- The jail breaks no step at all; it brings the inventory down from 30 to 20 items and the
  sensitivity down from 41 to 20. Used together with the narrow process identity, the second
  layer removes no item.
- At 1 of six endpoints, no link breaks at all (30 items, 41 sensitivity); adding a single layer
  at one endpoint would have brought sensitivity down from 26 to 7, at a legitimate-work cost of 1.

#### Next Step

What the four classes so far have in common is that the value was handed to an **interpreter**:
a query parser, a page resolver, an address resolver, a path concatenator. The fifth has no
interpreter. A value overflows into the neighboring memory space because it is larger than the
space allocated for it, and the item in that neighboring space becomes something else. The next
lesson measures that class at the logic level: which neighboring space the overflow corrupts in
a bounded-array model, how many cases the boundary check catches, which language class removes
this step entirely, and which compiler and operating-system check breaks which step.

### Buffer Overflow

Source: https://academia.sh/en/courses/attack-techniques/buffer-overflow

Measuring buffer overflow in a bounded-array model: in a thirty-two-byte region, eleven copy lengths corrupt thirty-three neighboring fields in total; the guard value is corrupted in four of eleven cases; the corrupted return label stays valid in neither of the two cases it is corrupted in; the bounds check catches nine of eleven cases; and in one of six components, no link can be broken at all.

What the four classes so far have in common is that a value was handed to an **interpreter**.
The fifth has no interpreter. A value is written into the neighboring space because it is longer
than the space allocated for it, and the item in that neighboring space becomes something else.

This lesson handles the class at the **logic level**. What is measured is not how to produce an
overflow, but what result follows from fields sitting adjacent in a region, and which check cuts
which step. The region, the field layout, and the copy are a **data-structure model** written
for this lesson; no real memory layout, no real address, and no machine code appears at all.

#### The Result of Adjacency

The fields inside a region sit adjacent to one another: each field's start is the previous
one's end. If a value longer than its size is written into a field, the extra bytes are **the
next field's bytes**. The copy operation does not know this; all it is given is a start and a
length. A bounds check is the step that looks at whether the copy exceeds the target field's
size, and if so, does not perform the operation at all.

The region below has seven fields, and their order matters: the counter reading, the tariff
code, and the auth flag are data fields; the guard value is a known, fixed item; the return
label names which continuation point the function will return to. **This is a model**: not the
layout of a real call record, but an array written to measure the result of adjacency.

**TA22:** the region has a fixed size, and its fields are adjacent. **TA23:** a copy with no
bounds check writes the requested length, not the target field's length; bytes that overflow
past the end of the region are counted separately. **TA24:** the step sequence is five steps,
and each check breaks a single step. **TA25:** the copied value is a single byte value; its
content is not chosen.

```js
// bo/region.mjs — bounded-array model: a fixed-size region and its named fields inside it.
// No real memory layout, no real address, and no machine code at all; this is a
// data-structure model.
export const FIELD = [
  ["name", 0, 12], ["counter", 12, 4], ["tariff", 16, 4], ["auth-flag", 20, 1],
  ["padding", 21, 3], ["guard-value", 24, 4], ["return-label", 28, 4],
];
export const SIZE = 32;
export const VALID_LABEL = [7, 9, 11];   // fictional: only these three labels are callable
export const FILL_BYTE = 65;             // the single byte value of the copied value

export function freshRegion() {
  const b = new Uint8Array(SIZE);
  b.set([79, 76, 67, 85, 77, 45, 48, 49], 0);   // name field: fictional counter id
  b.set([0, 0, 4, 210], 12);                     // counter reading
  b.set([0, 0, 0, 3], 16);                       // tariff code
  b[20] = 0;                                     // auth flag off
  b.set([222, 173, 190, 239], 24);               // guard value: fixed and known
  b.set([0, 0, 0, 7], 28);                       // return label: valid
  return b;
}

export function copyInto(b, fieldName, length, boundsCheck) {
  const [, start, size] = FIELD.find((a) => a[0] === fieldName);
  if (boundsCheck && length > size) return { written: 0, overflow: 0, rejected: true };
  const toWrite = Math.min(length, SIZE - start);
  for (let i = 0; i < toWrite; i++) b[start + i] = FILL_BYTE;
  return { written: toWrite, overflow: Math.max(0, length - (SIZE - start)), rejected: false };
}

export const corrupted = (b) => {
  const t = freshRegion();
  return FIELD.filter(([, start, size]) => {
    for (let i = start; i < start + size; i++) if (b[i] !== t[i]) return true;
    return false;
  }).map(([name]) => name);
};

export const label = (b) => b[28] * 16777216 + b[29] * 65536 + b[30] * 256 + b[31];
export const guardCorrupted = (b) => corrupted(b).includes("guard-value");

export const write = (g, ...s) => console.log(s.map((v, i) =>
  (g[i] < 0 ? String(v).padEnd(-g[i]) : String(v).padStart(g[i]))).join(""));
```

```js
// bo/measure.mjs — the same set of lengths is run in two setups: without a bounds check and
// with one. Corrupted neighboring fields are counted. No randomness.
import { FIELD, SIZE, VALID_LABEL, freshRegion, copyInto, corrupted, label,
  guardCorrupted, write } from "./region.mjs";

const LENGTH = [8, 12, 13, 16, 20, 21, 24, 25, 28, 32, 36];
const nameSize = FIELD.find((a) => a[0] === "name")[2];

console.log(`region ${SIZE} bytes, ${FIELD.length} fields; target field "name" ${nameSize} bytes`);
console.log(`field layout: ${FIELD.map(([a, b, u]) => `${a}[${b}..${b + u - 1}]`).join(" ")}\n`);

const A = [-9, 9, 10, 11, 20, 12, 10];
console.log("1. without a bounds check");
write(A, "length", "written", "overflow", "corrupted", "farthest corrupted", "guard", "label");
let totalNeighbor = 0, guardHit = 0, labelCorrupted = 0, validCount = 0;
for (const u of LENGTH) {
  const b = freshRegion();
  const r = copyInto(b, "name", u, false);
  const neighbor = corrupted(b).filter((a) => a !== "name");
  totalNeighbor += neighbor.length;
  if (guardCorrupted(b)) guardHit++;
  const e = label(b);
  if (neighbor.includes("return-label")) {
    labelCorrupted++;
    if (VALID_LABEL.includes(e)) validCount++;
  }
  write(A, u, r.written, r.overflow, neighbor.length, neighbor.at(-1) ?? "-",
    guardCorrupted(b) ? "corrupted" : "intact",
    neighbor.includes("return-label") ? (VALID_LABEL.includes(e) ? "valid" : "invalid") : "intact");
}
console.log(`\nacross ${LENGTH.length} lengths, ${totalNeighbor} neighboring fields were corrupted ` +
  `in total; ${LENGTH.filter((u) => u > nameSize).length} lengths exceed the field's size`);
console.log(`cases where the guard value was corrupted: ${guardHit}/${LENGTH.length}; cases where ` +
  `the return label was corrupted: ${labelCorrupted}, and in ${validCount} of those the label ` +
  `stayed valid`);

const B = [-9, 12, 12, 16];
console.log("\n2. same set, with a bounds check");
write(B, "length", "result", "corrupted", "caught");
let caught = 0, passed = 0;
for (const u of LENGTH) {
  const b = freshRegion();
  const r = copyInto(b, "name", u, true);
  if (r.rejected) caught++; else passed++;
  write(B, u, r.rejected ? "rejected" : "written",
    corrupted(b).filter((a) => a !== "name").length, r.rejected ? "yes" : "no");
}
console.log(`the bounds check caught ${caught} of ${LENGTH.length} cases, ${passed} passed because ` +
  `they stayed inside the field; corrupted neighboring fields: 0`);

const STEP = [
  ["bounds-unchecked-copy", "the language or call does not check the length"],
  ["length-from-outside", "the length to copy comes from an input field"],
  ["neighbor-field-corruption", "there is a meaningful item next to the target"],
  ["decision-determining-field", "the corrupted field is a flag, a counter, or a continuation record"],
  ["jump-to-requested-branch", "the corrupted value is accepted as a valid branch name"],
];
const CONTROL = [
  ["bounds-checked-language", 1, "language class", 0],
  ["input-length-contract", 2, "app", 2],
  ["compiler-bounds-check", 3, "compiler", 5],
  ["guard-value-check", 4, "compiler", 3],
  ["non-executable-data-region", 5, "operating system", 1],
  ["layout-randomization", 5, "operating system", 1],
];
const C = [-29, 9, 20, 8];
console.log("\n3. step sequence and step-to-control mapping");
STEP.forEach(([a, o], i) => console.log(`  ${i + 1}. ${a.padEnd(29)}precondition: ${o}`));
write(C, "control", "breaks", "layer", "legit");
for (const [name, n, layer, m] of CONTROL) write(C, name, `step ${n}`, layer, m);

const COMPONENT = [
  ["collector.driver", "bounds-unchecked", ["compiler-bounds-check", "guard-value-check",
    "non-executable-data-region", "layout-randomization"]],
  ["collector.parser", "bounds-unchecked", ["guard-value-check", "layout-randomization"]],
  ["branch-endpoint.protocol-layer", "bounds-unchecked", ["input-length-contract",
    "non-executable-data-region"]],
  ["field-device.firmware", "bounds-unchecked", []],
  ["validator.service", "bounds-checked", []],
  ["billing-generation.service", "bounds-checked", []],
];
const D = [-33, -18, 11, 14, 14];
console.log("\n4. chain per component");
write(D, "component", "language class", "controls", "first break", "uncontrolled");
for (const [name, lang, d] of COMPONENT) {
  const set = lang === "bounds-checked" ? ["bounds-checked-language", ...d] : d;
  const breaks = CONTROL.filter(([a]) => set.includes(a)).map(([, n]) => n);
  const h = breaks.length ? Math.min(...breaks) : 0;
  write(D, name, lang, set.length, h ? `step ${h}` : "not broken",
    STEP.length - new Set(breaks).size);
}
const open = COMPONENT.filter(([, lang, d]) =>
  lang !== "bounds-checked" && d.length === 0).length;
console.log(`components with no link broken at all: ${open}; in the bounds-checked language ` +
  `class, the chain breaks at the first step, so the remaining four steps never even arise`);
```

```
region 32 bytes, 7 fields; target field "name" 12 bytes
field layout: name[0..11] counter[12..15] tariff[16..19] auth-flag[20..20] padding[21..23] guard-value[24..27] return-label[28..31]

1. without a bounds check
length     written  overflow  corrupted  farthest corrupted       guard     label
8                8         0          0                   -      intact    intact
12              12         0          0                   -      intact    intact
13              13         0          1             counter      intact    intact
16              16         0          1             counter      intact    intact
20              20         0          2              tariff      intact    intact
21              21         0          3           auth-flag      intact    intact
24              24         0          4             padding      intact    intact
25              25         0          5         guard-value   corrupted    intact
28              28         0          5         guard-value   corrupted    intact
32              32         0          6        return-label   corrupted   invalid
36              32         4          6        return-label   corrupted   invalid

across 11 lengths, 33 neighboring fields were corrupted in total; 9 lengths exceed the field's size
cases where the guard value was corrupted: 4/11; cases where the return label was corrupted: 2, and in 0 of those the label stayed valid

2. same set, with a bounds check
length         result   corrupted          caught
8             written           0              no
12            written           0              no
13           rejected           0             yes
16           rejected           0             yes
20           rejected           0             yes
21           rejected           0             yes
24           rejected           0             yes
25           rejected           0             yes
28           rejected           0             yes
32           rejected           0             yes
36           rejected           0             yes
the bounds check caught 9 of 11 cases, 2 passed because they stayed inside the field; corrupted neighboring fields: 0

3. step sequence and step-to-control mapping
  1. bounds-unchecked-copy        precondition: the language or call does not check the length
  2. length-from-outside          precondition: the length to copy comes from an input field
  3. neighbor-field-corruption    precondition: there is a meaningful item next to the target
  4. decision-determining-field   precondition: the corrupted field is a flag, a counter, or a continuation record
  5. jump-to-requested-branch     precondition: the corrupted value is accepted as a valid branch name
control                         breaks               layer   legit
bounds-checked-language         step 1      language class       0
input-length-contract           step 2                 app       2
compiler-bounds-check           step 3            compiler       5
guard-value-check               step 4            compiler       3
non-executable-data-region      step 5    operating system       1
layout-randomization            step 5    operating system       1

4. chain per component
component                        language class       controls   first break  uncontrolled
collector.driver                 bounds-unchecked            4        step 3             2
collector.parser                 bounds-unchecked            2        step 4             3
branch-endpoint.protocol-layer   bounds-unchecked            2        step 2             3
field-device.firmware            bounds-unchecked            0    not broken             5
validator.service                bounds-checked              1        step 1             4
billing-generation.service       bounds-checked              1        step 1             4
components with no link broken at all: 1; in the bounds-checked language class, the chain breaks at the first step, so the remaining four steps never even arise
```

#### The Order of Corruption

The first output runs through eleven lengths one by one and counts the corrupted neighboring
fields. The result is graduated: 13 bytes corrupts one field, 20 bytes two, 21 bytes three, 25
bytes five, 32 bytes six. There are 33 neighboring-field corruptions in total, and nine of the
eleven lengths exceed the target field's size.

The order is not random; it is **exactly the same as the fields' layout in the region**. A
one-byte overflow touches the nearest neighbor, a twelve-byte overflow touches four neighbors.
The only thing that determines which item an overflow corrupts is that item's distance from the
target. This explains why the defense has to look at the field layout: the same overflow
corrupts a different item in a different layout.

Three fields produce a separate result. `auth-flag` is a decision field: when it is corrupted, a
branch goes a different way. `guard-value` is a known, fixed item; the moment it is corrupted, the
process can see this before returning — it was corrupted in **4** of eleven cases.
`return-label` names where the function will return to, and it was corrupted in two cases.

The last line is this lesson's most important number: in **neither of the two cases** where the
return label was corrupted did the label stay valid. The copied value was a single byte value
and it was not chosen; the corruption happened, but the result did not produce a valid branch name.
**Corruption and steering control flow are not the same thing**; the step required for the second
is not covered by this lesson and will not be. That is why the chain's fifth step is written as a
separate step.

#### What the Bounds Check Catches

The second output runs the same eleven lengths with a bounds check. Nine cases are rejected, two
pass — and the two that pass are the lengths that do not exceed the target field's size. The
count of corrupted neighboring fields is **zero**.

What this table says is that the bounds check does not make a complicated inference. It makes a
single comparison: is the requested length greater than the target field's size. The cost of
this comparison is immeasurably small next to the checks needed to break the rest of the chain.

The result that follows from this is the third output's first row. If the bounds check is done
by **the language itself**, the chain breaks at the first step: there is no such thing as a copy
with no bounds check, the precondition for the next four steps never even arises, and its legitimate-work
cost is 0. The language class removing this class is not adding a check — it is **removing the
first step's precondition from the definition**.

#### Which Step Each Check Breaks

The third output maps five steps to six checks. The checks come from three separate layers, and
each lands on a different step.

The length contract at the app layer breaks **the second step**: the length to copy no longer
comes from outside. The bounds check the compiler adds breaks **the third step**: the copy can no
longer write outside the region. The guard-value check the compiler adds breaks **the fourth
step**: the corruption happens, but it is seen before the return, and the process halts. The
operating-system checks land on **the fifth step**.

Their legitimate-work costs differ: the language class is 0, the length contract 2, the guard value 3,
the operating-system checks 1 each, the compiler's bounds check 5. The most expensive check is not
one of the earliest-breaking ones — **earliness and cheapness do not go together in this class.**

The fourth output measures six components. Two components are in the bounds-checked language
class, and the chain breaks at the first step. Of the four bounds-unchecked components, one
breaks at the third step, one at the fourth, one at the second. One — the firmware — **breaks at
no step at all**: all five of the five steps are uncontrolled. In a runtime environment with no
compiler or operating-system checks, this class's only defense is the app's own length contract,
and it is absent there too.

#### Summary

- Fields inside a region sit adjacent to one another; a copy that exceeds the target field's
  size writes the extra bytes into the neighboring fields' bytes. The order of corrupted fields
  is exactly the same as the fields' layout.
- In the 32-byte region, 11 lengths corrupted 33 neighboring fields in total; 9 lengths exceeded
  the target field's size. The guard value was corrupted in 4 cases, the return label in 2.
- In neither of the 2 cases where the return label was corrupted did the label stay valid.
  Corruption and steering control flow are not the same thing, and that is why the chain's fifth
  step is kept separate.
- Over the same 11 lengths, the bounds check rejected 9 cases, 2 passed because they stayed
  inside the field, and the count of corrupted neighboring fields came out to 0. The check
  consists of a single comparison.
- The bounds-checked language class breaks the chain at the first step, at a legitimate-work cost of 0:
  the precondition for the remaining four steps never even arises. The length contract breaks
  the 2nd step, the compiler's bounds check the 3rd, the guard value the 4th, the
  operating-system checks the 5th.
- Of six components, the chain breaks at step 1 in 2, at step 2, 3, or 4 in 3; in 1, all five of
  the five steps are uncontrolled.

#### Next Step

In this lesson's chain, a field had no boundary, and the defense was about putting that boundary
in place. In the next class, the boundary already exists, and the attacker tries **not to cross
it, but to stay under it**: a few attempts per account, each attempt a valid request. The next
lesson separates three password-attack forms along cost and threshold: how many forms a lockout
stops, why a form that makes only a few attempts per account never triggers lockout at all, at
which attempt a rate limit kicks in, and how many legitimate users each threshold locks out.

### Password Attacks

Source: https://academia.sh/en/courses/attack-techniques/password-attacks

Separating three password-attack forms along the cost-and-threshold axis: across four hundred fictional accounts, brute force drops one hundred ninety, dictionary one hundred eighty, and spraying eighteen; account lockout stops two of the three forms and cuts the number of dropped accounts by eighty-seven percent; spraying, at three attempts per account, never triggers the lockout threshold at all; and the total legitimate-work cost of the two thresholds that see all three forms is four.

In the previous lesson's chain, a field had no boundary, and the defense was about putting
that boundary in place. In this lesson's class, the boundary already exists: an identity attempt
is either accepted or rejected, the number of attempts is counted, and it is tied to a threshold.
The attacker is not trying to cross the boundary, but to **stay under it**.

Three forms are separated. **Brute force** tries a large number of candidates against a single
account. **Dictionary** does the same thing with a shorter, more likely candidate sequence.
**Spraying** reverses this: a large number of accounts, only a few candidates against each. All
three use the same chain, but their behavior against the threshold differs, and this lesson
counts that difference. No password, candidate list, or attempt script gets written; each
account's password is represented only by the number giving **its rank in an ordered candidate
sequence**.

#### The Chain's Six Steps

1. **Valid username set.** Precondition: the account names are known or can be learned.
2. **Attempt channel open.** Precondition: the endpoint accepts identity attempts.
3. **Attempt budget stays under the threshold.** Precondition: no threshold counting at the
   scale used sees this rate.
4. **Correct match is found.** Precondition: the password is within the candidate range the
   budget covers.
5. **Second factor is not requested.** Precondition: no second verification factor is defined on
   the account.
6. **Session is established and used.** Precondition: there is no check on session behavior.

The third step is the one this lesson measures, and it does not depend on a single threshold but
on the threshold's **scale**. The same threshold value stops one form when counted per account,
and a different one when counted per source.

The fifth and sixth steps are kept outside this measurement, but they are part of the chain. If a
second verification factor is defined, the chain breaks at the fifth step even when the fourth
step completes: the correct password has been found, and it is useless. A check on session
behavior breaks the sixth step; there, the chain has completed and the measure has shifted to
detection. These two steps are the only defense left in cases where the third step can never be
broken — and every number in this lesson is calculated as if they did not exist, because what is
being measured is the threshold itself.

**TA26:** the account set is fictional; `rank` is the password's position in the ordered
candidate sequence. **TA27:** the three thresholds count at three separate scales. **TA28:** the
three forms are separated by only two numbers — how many accounts it touches and how many
attempts it makes per account. **TA29:** duration is calculated at the maximum rate the rate
limit allows.

```js
// pw/model.mjs — password-attack cost model. No password, dictionary, or attempt script at
// all; each account's password is represented only by the number giving "its rank in an
// ordered candidate sequence."
export const ACCOUNT_COUNT = 400;

// Self-written generator; seed is visible, gives the same result run to run.
function generator(seed) {
  let s = seed >>> 0;
  return () => (s = (s * 1664525 + 1013904223) >>> 0) / 4294967296;
}

export const BUCKET = [[24, 1, 5], [60, 6, 500], [96, 501, 50000], [40, 50001, 5000000]];

export function accounts(seed = 20260802) {
  const r = generator(seed);
  const h = [];
  for (const [count, lo, hi] of BUCKET)
    for (let i = 0; i < count; i++) h.push({ rank: lo + Math.floor(r() * (hi - lo + 1)) });
  while (h.length < ACCOUNT_COUNT) h.push({ rank: Infinity });
  return h;
}

// legit = fictional daily false-positive rate (blocking a legitimate user).
export const THRESHOLD = [
  { name: "account-lockout", scale: "account", limit: 5, legit: 30 },
  { name: "source-rate-limit", scale: "source", limit: 60, legit: 4 },
  { name: "global-rate-threshold", scale: "system", limit: 200, legit: 0 },
];

export const FORM = [
  { name: "brute-force", accountCount: 1, budget: 1000000 },
  { name: "dictionary", accountCount: 1, budget: 50000 },
  { name: "spraying", accountCount: ACCOUNT_COUNT, budget: 3 },
];

export const duration = (attempt) => {
  const min = attempt / THRESHOLD[1].limit;
  return min < 1 ? `${(min * 60).toFixed(0)} sec` : min < 60 ? `${min.toFixed(0)} min`
    : min < 1440 ? `${(min / 60).toFixed(1)} hr`
    : `${(min / 1440).toFixed(1)} day`;
};

export const write = (g, ...s) => console.log(s.map((v, i) =>
  (g[i] < 0 ? String(v).padEnd(-g[i]) : String(v).padStart(g[i]))).join(""));
```

```js
// pw/measure.mjs — three forms are run under three thresholds: how many attempts per account,
// at which attempt each threshold kicks in, how many accounts drop, and in how much time.
// Randomness comes from the generator, the seed is visible; the same result comes out run to run.
import { ACCOUNT_COUNT, accounts, THRESHOLD, FORM, duration, write } from "./model.mjs";

const H = accounts();
const bucket = (h) => h.rank <= 5 ? "first 5" : h.rank <= 500 ? "first 500"
  : h.rank <= 50000 ? "first 50,000" : h.rank <= 5000000 ? "first 5,000,000" : "out of budget";
const BUCKETS = ["first 5", "first 500", "first 50,000", "first 5,000,000", "out of budget"];

console.log(`${ACCOUNT_COUNT} fictional accounts, ${FORM.length} forms, ${THRESHOLD.length} thresholds\n`);

const A = [-18, 11, 9];
console.log("1. distribution of accounts across the candidate sequence");
write(A, "bucket", "accounts", "share");
for (const k of BUCKETS) {
  const n = H.filter((h) => bucket(h) === k).length;
  write(A, k, n, `${((n / ACCOUNT_COUNT) * 100).toFixed(1)}%`);
}

// Does a threshold see a form: account scale counts per account, source and system scale
// count over the total attempts.
const sees = (b, e) => (e.scale === "account" ? b.budget : b.accountCount * b.budget) > e.limit;

const B = [-13, 10, 15, 16, 16, 27];
console.log("\n2. threshold behavior per form");
write(B, "form", "accounts", "per account", "total attempts", "lockout", "first threshold seeing it");
for (const b of FORM) {
  const seeing = THRESHOLD.filter((e) => sees(b, e));
  write(B, b.name, b.accountCount, b.budget, b.accountCount * b.budget,
    sees(b, THRESHOLD[0]) ? `at attempt ${THRESHOLD[0].limit}` : "never triggers",
    seeing.length ? seeing[0].name : "none");
}

// How many accounts would drop if each form targeted every account in turn.
const drop = (b, lockoutOn) => {
  const budget = lockoutOn ? Math.min(b.budget, THRESHOLD[0].limit) : b.budget;
  return H.filter((h) => h.rank <= budget).length;
};

const C = [-13, 14, 15, 22, 8, 22];
console.log("\n3. how many of the targeted accounts drop");
write(C, "form", "no lockout", "with lockout", "lockout's difference", "diff", "duration per account");
for (const b of FORM) {
  const y = drop(b, false), v = drop(b, true);
  write(C, b.name, `${y}/${ACCOUNT_COUNT}`, `${v}/${ACCOUNT_COUNT}`, y - v,
    `${(((y - v) / Math.max(y, 1)) * 100).toFixed(0)}%`, duration(b.budget));
}
console.log(`forms where lockout reduces the number of dropped accounts: ` +
  `${FORM.filter((b) => drop(b, false) > drop(b, true)).length}/${FORM.length}`);

const p = FORM[2];
const D = [-24, 10, 10, 18];
console.log("\n4. which threshold sees spraying");
write(D, "threshold", "scale", "limit", "sees it?");
for (const e of THRESHOLD)
  write(D, e.name, e.scale, e.limit, sees(p, e) ? `at attempt ${e.limit}` : "does not see it");
console.log(`spraying makes ${p.budget} attempts per account; since the lockout threshold is ` +
  `${THRESHOLD[0].limit}, it never triggers on any account. Total ${p.accountCount * p.budget} attempts ` +
  `exceeds the rate threshold by ${(p.accountCount * p.budget / THRESHOLD[2].limit).toFixed(1)}x, taking ${duration(p.accountCount * p.budget)}`);

const E = [-24, 9, 18, 36];
console.log("\n5. the legitimate-work cost of the thresholds");
write(E, "threshold", "legit", "forms seeing it", "forms it sees");
for (const e of THRESHOLD) {
  const g = FORM.filter((b) => sees(b, e));
  write(E, e.name, e.legit, `${g.length}/${FORM.length}`, g.map((b) => b.name).join(",") || "-");
}
console.log(`total legitimate-work cost ${THRESHOLD.reduce((t, e) => t + e.legit, 0)} fictional items/day; ` +
  `the total cost of the two thresholds that see all three forms ${THRESHOLD[1].legit + THRESHOLD[2].legit}`);
```

```
400 fictional accounts, 3 forms, 3 thresholds

1. distribution of accounts across the candidate sequence
bucket               accounts    share
first 5                    24     6.0%
first 500                  60    15.0%
first 50,000               96    24.0%
first 5,000,000            40    10.0%
out of budget             180    45.0%

2. threshold behavior per form
form           accounts    per account  total attempts         lockout  first threshold seeing it
brute-force           1        1000000         1000000    at attempt 5            account-lockout
dictionary            1          50000           50000    at attempt 5            account-lockout
spraying            400              3            1200  never triggers          source-rate-limit

3. how many of the targeted accounts drop
form             no lockout   with lockout  lockout's difference    diff  duration per account
brute-force         190/400         24/400                   166     87%              11.6 day
dictionary          180/400         24/400                   156     87%               13.9 hr
spraying             18/400         18/400                     0      0%                 3 sec
forms where lockout reduces the number of dropped accounts: 2/3

4. which threshold sees spraying
threshold                    scale     limit          sees it?
account-lockout            account         5   does not see it
source-rate-limit           source        60     at attempt 60
global-rate-threshold       system       200    at attempt 200
spraying makes 3 attempts per account; since the lockout threshold is 5, it never triggers on any account. Total 1200 attempts exceeds the rate threshold by 6.0x, taking 20 min

5. the legitimate-work cost of the thresholds
threshold                   legit   forms seeing it                       forms it sees
account-lockout                30               2/3              brute-force,dictionary
source-rate-limit               4               3/3     brute-force,dictionary,spraying
global-rate-threshold           0               3/3     brute-force,dictionary,spraying
total legitimate-work cost 34 fictional items/day; the total cost of the two thresholds that see all three forms 4
```

#### Two Numbers, Three Forms

The second output separates the three forms with only two numbers: how many accounts it
touches, how many attempts it makes per account. Brute force is 1 account and 1,000,000
attempts; dictionary 1 account and 50,000; spraying 400 accounts and **3** attempts.

The total attempt count does not reverse the ranking, but it is misleading: spraying's total is
1,200, which is 1.2 thousandths of brute force's. Against that, brute force targets a single
account, spraying all four hundred. **Naming an attack form by its total attempt count does not
say which threshold will see it.**

The lockout column gives the difference directly. Brute force and dictionary both hit lockout on
the fifth attempt; spraying never hits it at all. The reason is a single comparison: lockout
counts **per account**, and spraying's per-account attempt count is under the threshold.
Lowering the threshold from 5 to 3 is not enough either; spraying reduces its budget to 2 and
still stays under. **A threshold that counts at the account scale, by definition, does not see a
form that stays small at the account scale.**

#### How Many Accounts Drop

The third output measures the case where each form targets every account in turn. With no
lockout, brute force drops 190 of 400 accounts, dictionary 180, spraying 18. With lockout, all
three forms' per-account budget drops to 5: brute force and dictionary drop to **24 accounts**,
spraying **stays at 18**.

Lockout's difference is 166 and 156 accounts in the first two forms — **87%** of the number of
dropped accounts. In spraying the difference is zero. These two numbers sitting side by side is
this lesson's result: the same threshold is the strongest item of the defense in two forms, and
nothing at all in the third.

It is also notable that all three forms converge on nearly the same number under lockout: 24, 24,
and 18. When the per-account budget drops to five, all the difference between the forms is
erased, because the difference was coming from the budget itself. Lockout does not target an
attack form, it targets the **budget**; that is why it has no effect at all on a form whose budget
was already small.

The duration column gives the other side of the cost. At the maximum rate the rate limit allows,
brute force takes **11.6 days** for a single account, dictionary **13.9 hours**, spraying **3
seconds**. Spraying does not just avoid lockout; it drops eighteen accounts in three-millionths of
the time brute force spends on one.

Duration itself is not a defense either — it is a **budget constraint**. Even if no threshold ever
kicks in over an eleven-day run, the attempt has to stay sustainable over that whole time: the
connection has to stay open, the source cannot get blocked, the account cannot get deleted. The
rate limit's real function is to grow this budget; it does not directly reduce the number of
dropped accounts, it stretches the time needed to drop one, and opens up room during that time
for detection to work.

The first output says where this difference comes from. 6% of accounts are within the first five
candidates, 45% cannot be found in any budget. Raising the budget from 50,000 to 1,000,000 only
moves the dropped-account count from 180 to 190 — **twenty times the budget, ten accounts**. The
first few items in the candidate sequence give more than everything after them combined; that is
the observation spraying rests on.

#### The Threshold's Scale and Cost

The fourth output puts spraying to each of the three thresholds separately. The threshold that
counts at the account scale does not see it. The threshold that counts at the source scale sees it
at attempt 60, the one that counts at the system scale at attempt 200. The total of 1,200
attempts exceeds the rate threshold sixfold, and takes 20 minutes at the maximum rate.

This shows where the measure shifts to: against spraying, the chain might not break at the third
step, but it **is seen**. In the link that cannot be broken, the measure shifts to detection, and
detection's unit is the attempt: how many attempts pass under the threshold, how many accounts
get tried before the alarm fires.

The fifth output writes the cost. Account lockout's legitimate-work cost is 30 fictional items a day,
and it sees two of the three forms. The source rate limit's cost is 4, and it sees all three. The
rate threshold's cost is **0**, and it sees all three too — because it rejects no request at all,
it only counts and reports.

The three thresholds' total cost is 34, and the total cost of the two thresholds that see all
three forms is **4**. The two items with the widest scope carry one-eighth of the cost. This
does not mean lockout is unnecessary: lockout is the single item that brings the dropped-account
count from 190 down to 24. What it means is that scope and cost **do not accumulate on the same
item** — and a defense set cannot be evaluated without writing the two down separately.

The threshold with zero cost has a separate place, and it should be named at this course's
scale of measurement: that threshold **breaks no link at all**. Because it rejects no request, it
does not remove the third step's precondition; it only counts. Writing it in a defense list as
"the threshold that sees all three forms" is correct; writing it as "the threshold that stops all
three forms" is not. The difference between these two sentences is exactly the distinction this
course draws between a broken link and detection: an item that only counts does not shorten the
chain, it only surfaces the point where the chain is seen.

#### Summary

- Three forms are separated by only two numbers: how many accounts it touches and how many
  attempts it makes per account. Brute force is 1 account/1,000,000 attempts, dictionary
  1/50,000, spraying 400/3.
- Account lockout stops brute force and dictionary at the fifth attempt; it never sees spraying
  at all, because it counts at the account scale and spraying's per-account budget is under the
  threshold.
- Lockout brings the dropped-account count down from 190 and 180 to 24 (an 87% reduction); in
  spraying the difference is 0.
- At the maximum rate, brute force takes 11.6 days for a single account, dictionary 13.9 hours,
  spraying 3 seconds for 18 accounts. Raising the budget from 50,000 to 1,000,000 raises the
  dropped-account count by only 10.
- The threshold that counts spraying at the source scale sees it at attempt 60, the one that
  counts at the system scale at attempt 200; the total of 1,200 attempts exceeds the rate
  threshold sixfold. In the link that cannot be broken, the measure shifts to detection.
- The three thresholds' total legitimate-work cost is 34 fictional items/day; the total of the two
  thresholds that see all three forms is 4. Scope and cost do not accumulate on the same item,
  and the zero-cost threshold breaks no link at all — it only counts.

#### Next Step

In all six classes so far, the chain always started with a request from the attacker: a field
was filled in, a link was sent, an attempt was made. In the next class, the first step is thrown
by the victim, and what they do is ordinary — they open a page. The next lesson counts this class
as a **precondition chain**: how many conditions have to hold at the same time, how many
combinations remain once one of the conditions falls away, and which browser and network checks
break how many conditions at once.

### Drive-By Downloads

Source: https://academia.sh/en/courses/attack-techniques/drive-by-downloads

Counting browser-based infection as a precondition chain: across six hundred fictional endpoints, all six of six conditions hold at once in only twenty-two of them; the item has already been written to disk at seventy-three endpoints that hold through the fourth condition; the single most effective defense on its own brings the remainder down to one; the defense that breaks two conditions at once ends up the weakest because its coverage is narrow; and two defenses reach, with a legitimate-work cost of nine, the same result that seven defenses reach with a cost of fifty-five.

In all seven classes so far, the chain always started with a request from the attacker: a field
was filled in, a link was sent, an attempt was made. In this class, the first step is thrown by
the **victim**, and what they do is ordinary — they open a page.

Drive-by download is not the name of a single step; it is the name of several independent
conditions holding at the same time. That is why this lesson counts the chain as an **AND
sequence**: if all six of the six conditions do not hold, nothing happens. The measurement runs
over a fictional fleet of endpoints; no infection step, no page, and no component name gets
written.

#### Six Conditions

1. **Page reached.** The user opens a page that carries content under the attacker's control.
   Precondition: there is a path to that page.
2. **Third-party content.** The page can load content from outside its own origin.
   Precondition: there is no source restriction.
3. **Unpatched component.** There is an unfixed item in the loading component. Precondition: the
   browser or plugin is not up to date.
4. **Unapproved write.** An item can be written to disk without user approval. Precondition: the
   download completes on its own.
5. **Executability.** The written item can be run. Precondition: there is no app allowlist.
6. **Protection miss.** Endpoint protection does not stop the item. Precondition: there is no
   signature or behavior match.

An AND chain favors the defense: **dropping a single condition is enough**. The difficulty is not
in choosing the condition; it is in how much of the fleet the defense reaches. This lesson
measures those two numbers — the broken condition and the coverage — together.

The conditions do not all belong to the same side either. The first and second condition belong
to the network and the page's own configuration, the third and fifth to the endpoint's software
state, the fourth to browser behavior, the sixth to protection software. This distribution shows
why, inside an organization, this class cannot be handed to a single team's responsibility: the
six conditions are spread across four separate management domains, and none of them can see the
others' numbers.

**TA30:** the chain is an AND sequence; `rate` is the share of endpoints in the fictional fleet
where that condition holds. **TA31:** each defense breaks one or two conditions; `coverage` is
the share of endpoints it is deployed to. **TA32:** after the fourth condition, the item is
considered written to disk; the boundary of early breaking is the third condition.

```js
// db/fleet.mjs — browser-based infection's precondition chain (model). No real page, no real
// component, and no infection step at all; each endpoint is only a bit vector carrying the
// state of the conditions and the defenses.
export const ENDPOINT_COUNT = 600;

export const CONDITION = [
  { name: "page-reached",         rate: 0.60 },
  { name: "third-party-content",  rate: 0.75 },
  { name: "unpatched-component",  rate: 0.40 },
  { name: "unapproved-write",     rate: 0.60 },
  { name: "executability",        rate: 0.80 },
  { name: "protection-miss",      rate: 0.35 },
];

export const DEFENSE = [
  { name: "name-resolution-filter", breaks: [0], coverage: 0.85, legit: 5 },
  { name: "content-policy",        breaks: [1], coverage: 0.60, legit: 7 },
  { name: "automatic-update",      breaks: [2], coverage: 0.75, legit: 3 },
  { name: "download-approval",     breaks: [3], coverage: 0.80, legit: 9 },
  { name: "app-allowlist",         breaks: [4], coverage: 0.45, legit: 14 },
  { name: "endpoint-protection",   breaks: [5], coverage: 0.90, legit: 6 },
  { name: "browser-isolation",     breaks: [2, 4], coverage: 0.30, legit: 11 },
];

function generator(seed) {
  let s = seed >>> 0;
  return () => (s = (s * 1664525 + 1013904223) >>> 0) / 4294967296;
}

// The fleet is generated once; the seed is visible, gives the same result run to run.
export function fleet(seed = 20260803) {
  const r = generator(seed);
  const f = [];
  for (let i = 0; i < ENDPOINT_COUNT; i++) {
    const k = CONDITION.map((x) => r() < x.rate);
    const s = DEFENSE.map((x) => r() < x.coverage);
    f.push({ k, s });
  }
  return f;
}

export const write = (g, ...s) => console.log(s.map((v, i) =>
  (g[i] < 0 ? String(v).padEnd(-g[i]) : String(v).padStart(g[i]))).join(""));
```

```js
// db/measure.mjs — how many endpoints hold all six conditions at once, and where each defense
// brings that number down to. Randomness comes from the generator, the seed is visible.
import { ENDPOINT_COUNT, CONDITION, DEFENSE, fleet, write } from "./fleet.mjs";

const F = fleet();
// At an endpoint, the nth condition holds if none of the deployed defenses break it.
const holds = (u, i, set) => u.k[i] &&
  !DEFENSE.some((s, j) => set.includes(s.name) && u.s[j] && s.breaks.includes(i));
const fullChain = (u, set) => CONDITION.every((_, i) => holds(u, i, set));
const nConditions = (u, set, n) => CONDITION.slice(0, n).every((_, i) => holds(u, i, set));
const count = (set, f) => F.filter((u) => f(u, set)).length;

console.log(`${ENDPOINT_COUNT} fictional endpoints, ${CONDITION.length} conditions, ${DEFENSE.length} defenses\n`);

const A = [-22, 13, 10, 14];
console.log("1. endpoints holding per condition");
write(A, "condition", "target rate", "holding", "up to here");
for (const [i, k] of CONDITION.entries())
  write(A, k.name, k.rate.toFixed(2), F.filter((u) => u.k[i]).length,
    count([], (u) => nConditions(u, [], i + 1)));
const baseline = count([], fullChain);
console.log(`all six of the six conditions hold at ${baseline} endpoints ` +
  `(${((baseline / ENDPOINT_COUNT) * 100).toFixed(1)}%); the item is already written to disk ` +
  `at ${count([], (u) => nConditions(u, [], 4))} endpoints that hold through the fourth condition`);

const B = [-24, 12, 12, 14, 10];
console.log("\n2. when a single defense is deployed");
write(B, "defense", "breaks", "coverage", "remaining", "legit");
for (const s of DEFENSE)
  write(B, s.name, s.breaks.map((i) => i + 1).join(","), s.coverage.toFixed(2),
    count([s.name], fullChain), s.legit);
console.log(`remaining with no defense at all: ${baseline}`);

const C = [-24, 12, 12, 18, 14];
console.log("\n3. cumulative, starting from the biggest reducer");
write(C, "added defense", "remaining", "reduction", "cumulative legit", "remaining %");
let set = [], legit = 0;
while (set.length < DEFENSE.length) {
  const candidate = DEFENSE.filter((s) => !set.includes(s.name))
    .map((s) => ({ s, remaining: count([...set, s.name], fullChain) }))
    .sort((a, b) => a.remaining - b.remaining || a.s.legit - b.s.legit)[0];
  const before = count(set, fullChain);
  set.push(candidate.s.name); legit += candidate.s.legit;
  write(C, candidate.s.name, candidate.remaining, before - candidate.remaining, legit,
    `${((candidate.remaining / baseline) * 100).toFixed(1)}%`);
}

const OUTCOME = ["no trace", "no trace", "no trace", "item written to disk",
  "item ran", "protection missed it"];
const D = [-11, 20, 27, -24];
console.log("\n4. the cost of late breaking (undefended fleet)");
write(D, "condition", "holding up to here", "dropped at this condition", "  stage outcome");
for (let n = 1; n <= CONDITION.length; n++) {
  const hold = count([], (u) => nConditions(u, [], n));
  write(D, `${n}.`, hold, count([], (u) => nConditions(u, [], n - 1)) - hold, `  ${OUTCOME[n - 1]}`);
}
console.log(`after the fourth condition the item has been written to disk: the boundary of ` +
  `early breaking is the third condition (${count([], (u) => nConditions(u, [], 3))} endpoints)`);
console.log(`remaining in full deployment: ${count(DEFENSE.map((s) => s.name), fullChain)}, ` +
  `holding through the fourth condition: ` +
  `${count(DEFENSE.map((s) => s.name), (u, q) => nConditions(u, q, 4))}`);
```

```
600 fictional endpoints, 6 conditions, 7 defenses

1. endpoints holding per condition
condition               target rate   holding    up to here
page-reached                   0.60       362           362
third-party-content            0.75       440           262
unpatched-component            0.40       241           118
unapproved-write               0.60       362            73
executability                  0.80       482            63
protection-miss                0.35       221            22
all six of the six conditions hold at 22 endpoints (3.7%); the item is already written to disk at 73 endpoints that hold through the fourth condition

2. when a single defense is deployed
defense                       breaks    coverage     remaining     legit
name-resolution-filter             1        0.85             5         5
content-policy                     2        0.60            10         7
automatic-update                   3        0.75             5         3
download-approval                  4        0.80             6         9
app-allowlist                      5        0.45            10        14
endpoint-protection                6        0.90             1         6
browser-isolation                3,5        0.30            16        11
remaining with no defense at all: 22

3. cumulative, starting from the biggest reducer
added defense              remaining   reduction  cumulative legit   remaining %
endpoint-protection                1          21                 6          4.5%
automatic-update                   0           1                 9          0.0%
name-resolution-filter             0           0                14          0.0%
content-policy                     0           0                21          0.0%
download-approval                  0           0                30          0.0%
browser-isolation                  0           0                41          0.0%
app-allowlist                      0           0                55          0.0%

4. the cost of late breaking (undefended fleet)
condition    holding up to here  dropped at this condition  stage outcome
1.                          362                        238  no trace
2.                          262                        100  no trace
3.                          118                        144  no trace
4.                           73                         45  item written to disk
5.                           63                         10  item ran
6.                           22                         41  protection missed it
after the fourth condition the item has been written to disk: the boundary of early breaking is the third condition (118 endpoints)
remaining in full deployment: 0, holding through the fourth condition: 0
```

#### The Intersection of Six Conditions

The first output counts the conditions one by one and cumulatively. Looked at one by one, no
condition is rare: the narrowest condition holds at 221 of 600 endpoints, the widest at 482. The
cumulative column, though, drops fast: 362, 262, 118, 73, 63, and finally **22**.

The ratio between the cumulative column's first row and its last row is sixteenfold. Every step
of this drop is a multiplication, and none of the factors is close to zero; what makes the
result small is the **number** of factors. Halving a condition's rate halves the result too, but
removing a condition entirely zeroes the result out — in an AND chain, the difference between
these two moves is not quantitative, it is qualitative.

At **3.7%** of the fleet, all six of the six conditions hold at the same time. This number
explains why the class looks both widespread and rare at once. Every condition on its own is
ordinary; what is out of the ordinary is the intersection. In a report, the sentence "40% of
endpoints have an out-of-date component" can be true and still say nothing about the size of
this class.

The biggest drop after the first condition is at the third: from 262 to 118. The unpatched-
component condition is the item that narrows the intersection the most in the middle of the
chain; the reason is that a relatively low rate is being applied to what is still a large
remainder. **In an AND chain, the narrowest condition determines the result** — and from a
defense standpoint, this means narrowing the narrowest condition further is the most efficient
move.

#### Broken Condition and Coverage

The second output deploys each defense on its own. The results range from 1 to 16, and the
ranking has nothing to do with the order of the broken condition; **it is about coverage.**

Endpoint protection brings the remainder from 22 down to **1**: its coverage is 0.90, and the
condition it breaks is already rare. The name-resolution filter brings it to 5, automatic update to 5,
download approval to 6. App allowlist stays at 10, because its coverage is 0.45.

The last row is this output's most notable result. Browser isolation **breaks two conditions at
once** — none of the other six defenses do this — and despite that, it only brings the remainder
down to 16; it is the weakest of the seven. The reason is that its coverage is 0.30. **A defense
that breaks two conditions and reaches a third of the fleet is weaker than a defense that breaks
one condition and reaches nine-tenths of it.** A layer's strength is measured not by the number
of conditions it breaks, but by the product of the broken condition and its coverage.

#### Cumulative Addition

The third output adds defenses in each round starting from the biggest reducer. In the first two
steps, the remaining endpoint count goes from 22 to 1, then to **0**, and the cumulative
legitimate-work cost is **9**. The remaining five defenses drop no further endpoints at all and raise the cost
to 55.

This does not mean the other five defenses are unnecessary, and this distinction matters. The
measurement is a **snapshot**: at this fleet, with these coverage values, with these condition
rates. When the third condition's rate rises from 0.40 to 0.70 — when a new unpatched item
appears — the ranking changes, and the zero-remainder defenses start producing numbers. In an
AND chain, spare layers' function is not to bring down today's number, but to **keep the number
from spiking when a condition's rate changes**.

Still, one thing is clear: two items bring this fleet to zero with a legitimate-work cost of nine units.
The seven items' total cost is 55. If the defense budget is limited, the ranking is read off
this table, not off a layer list.

The order itself is not a coincidence either. The greedy selection takes endpoint protection in
the first round, because the condition it breaks is the fleet's narrowest, and its coverage is
among the highest. It takes automatic update in the second round; the fact that the one
remaining endpoint falls outside protection's coverage requires dropping a second condition at
that endpoint. From the third round on, there is nothing left to choose, and the ranking is made
by legitimate-work cost alone.

#### The Boundary of Early Breaking

The fourth output gives the cost stage by stage. Through the first three conditions, **no trace
is left at all**: the user has opened a page, the page has loaded content, an item has been
found in the component. At the fourth condition, an item is written to disk — at **73 endpoints**
in the undefended fleet. At the fifth condition the item runs (63 endpoints), at the sixth
protection does not see it (22 endpoints).

The boundary of early breaking is in this table: if the chain stops by the third condition,
nothing is left behind. Stopping after the fourth condition means an item is present at the
endpoint and an investigation is required. **The 51 endpoints between 73 and 22 are exactly this
in-between zone**: the chain has not completed, but an item has been written to disk, and a work
item is born that needs to be found, classified, and cleaned up.

This distinction often gets lost in defense reports. The sentence "no infection occurred" looks
at the number 22; the real workload is in the number 73. A defense that breaks the first three
conditions never produces this workload at all; a defense that breaks the last two produces it,
and while doing so, looks "successful." This course's third column — the cost of late breaking —
exists exactly to write down this difference.

In full deployment, both numbers are zero: neither a disk write nor a completed chain remains.
This deployment's cost is 55 legitimate-work units, and it is also possible to reach the same result with
two items that cost 9 — the difference will show up once the condition rates change.

#### Summary

- Drive-by download is an AND chain: all six of the six conditions must hold. The individual
  conditions are not rare (between 221 and 482 endpoints), the intersection is: 22 of 600
  endpoints, or 3.7%.
- The cumulative count drops 362, 262, 118, 73, 63, 22; the biggest drop is at the narrowest
  condition. In an AND chain, the narrowest condition determines the result.
- Among defenses deployed on their own, coverage determines the ranking, not the broken
  condition: the item with 0.90 coverage brings 22 down to 1; the item with 0.30 coverage that
  breaks two conditions at once leaves it at 16.
- Two defenses bring the remainder to 0 for a legitimate-work cost of 9; the seven defenses' total cost is
  55, for zero additional endpoints dropped. Spare layers' function is not today's number, but
  absorbing the spike that occurs when a condition's rate changes.
- No trace is left through the first three conditions; after the fourth condition, the item has
  been written to disk. In the undefended fleet, the item is written at 73 endpoints, the chain
  completes at 22 — the 51 in between is the zone where the chain has not completed but still
  produces a cleanup work item.

#### Next Step

This lesson counted an item reaching an endpoint, but never asked what that item **is**. Most of
the time, all the defense has in hand is that item: no source, no documentation, no known
author. The next lesson measures that question — which indicators can be pulled out of a binary,
which ones can never be pulled out at all, and how many items countermeasures hide. That lesson
is also this course's closing lesson: twenty-two lessons' chain steps, breakable link, and cost
of late breaking are gathered into a single table.

### Reverse Engineering Fundamentals

Source: https://academia.sh/en/courses/attack-techniques/reverse-engineering-fundamentals

Measuring reverse engineering's place in defense: the total defense value of twenty fictional indicators being thirty-eight, twelve items coming out of the raw binary through a string scan, symbol stripping together with string hiding bringing those twelve down to zero, three items being impossible to extract in any deployment and visible only in the victim's own record, and the chain columns of the course's twenty-two lessons being gathered into a single table.

The previous lesson counted an item reaching the endpoint but never asked what that item was.
Most of the time, all the defense has in hand is that item: no source, no documentation, no
known author. Reverse engineering is the work of extracting from that item an **indicator the
defense can use**.

This lesson takes the class from the other end of the measurement. In the eight lessons so far,
the chain belonged to the attacker and the defense was the one breaking it. Here the chain
**belongs to the defense**: acquiring the sample, doing static extraction, doing dynamic
observation, converting the indicators into a rule, and deploying the rule to the fleet. The
breaking side is the countermeasures. Throughout the measurement, no real sample, real tool, or
real analysis step is written; the binary in this lesson is **generated by itself**, and the
scan is actually run.

#### Three Types of Indicator, One Type of Gap

The items that can be extracted from a binary are not all one type. Items of **string** and
**symbol** type exist as text inside the binary; a scan gives them up directly. Items of
**behavior** type are not written into the binary; they are only seen once the item is run. A
fourth type cannot be extracted by any means at all: the item does not carry that information, it
gets it from the **victim's own environment**.

This fourth type is this lesson's actual point. No matter how detailed an analysis report is, it
cannot say what the item's address on the target network is, what a derived session key is, or
which organization was targeted. Those items are only seen in the **victim's own record**.

**TA33:** the indicator inventory is fictional; `defense` is the item's value for a detection
rule. **TA34:** the binary is actually generated; the items carrying text are written into
regions, the rest of the region is filled with bytes from the generator. **TA35:** the string
scan collects printable byte sequences of four and longer. **TA36:** the defense's analysis
chain is five steps, and each countermeasure breaks one step.

```js
// tm/binary.mjs — a fictional binary item and the indicators inside it (model). No real
// malware, no real sample, and no real analysis tool; the binary is generated here.
export const INDICATOR = [
  { name: "config-key",        type: "string", text: "measurement.setting.threshold", defense: 2 },
  { name: "component-label",   type: "string", text: "counter-collector-1", defense: 1 },
  { name: "send-interval",     type: "string", text: "send-interval=900", defense: 2 },
  { name: "filename-pattern",  type: "string", text: "temp-measurement-data", defense: 3 },
  { name: "retry-limit",       type: "string", text: "retry-limit-threshold", defense: 1 },
  { name: "build-label",       type: "string", text: "build-2026-04-11", defense: 1 },
  { name: "version-label",     type: "string", text: "settings-signature-v3", defense: 1 },
  { name: "func-connect",      type: "symbol", text: "connect", defense: 2 },
  { name: "func-collectData",  type: "symbol", text: "collectData", defense: 2 },
  { name: "func-writeLog",     type: "symbol", text: "writeLog", defense: 1 },
  { name: "func-timer",        type: "symbol", text: "setTimer", defense: 2 },
  { name: "func-readConfig",   type: "symbol", text: "readConfig", defense: 1 },
  { name: "startup-order",           type: "behavior", defense: 3 },
  { name: "file-write-pattern",      type: "behavior", defense: 3 },
  { name: "timing-interval",         type: "behavior", defense: 2 },
  { name: "restart-response",        type: "behavior", defense: 2 },
  { name: "resource-usage-profile",  type: "behavior", defense: 1 },
  { name: "target-network-address",  type: "unextractable", defense: 3 },
  { name: "session-key",             type: "unextractable", defense: 3 },
  { name: "victim-identity",         type: "unextractable", defense: 2 },
];

function generator(seed) {
  let s = seed >>> 0;
  return () => (s = (s * 1664525 + 1013904223) >>> 0) & 255;
}

export function generateBinary({ stripSymbols = false, hideStrings = false, pack = false } = {}) {
  const r = generator(20260804);
  const byte = [];
  const place = (m) => { for (const c of m) byte.push(c.charCodeAt(0)); byte.push(0); };
  const fill = (n) => { for (let i = 0; i < n; i++) byte.push(r() | 128); };  // unprintable

  fill(24);                                                 // header region
  for (const g of INDICATOR.filter((x) => x.type === "symbol")) {
    if (stripSymbols || pack) fill(g.text.length + 1); else place(g.text);
  }
  fill(32);                                                 // code region
  for (const g of INDICATOR.filter((x) => x.type === "string")) {
    if (pack) fill(g.text.length + 1);
    else if (hideStrings) { for (const c of g.text) byte.push(c.charCodeAt(0) ^ 90); byte.push(0); }
    else place(g.text);
  }
  fill(24);
  return Uint8Array.from(byte);
}

export function scanStrings(b, minLen = 4) {
  const found = [];
  let s = "";
  for (const x of b) {
    if (x >= 32 && x <= 126) s += String.fromCharCode(x);
    else { if (s.length >= minLen) found.push(s); s = ""; }
  }
  if (s.length >= minLen) found.push(s);
  return found;
}

export const write = (g, ...s) => console.log(s.map((v, i) =>
  (g[i] < 0 ? String(v).padEnd(-g[i]) : String(v).padStart(g[i]))).join(""));
```

```js
// tm/measure.mjs — how many indicators come out of the binary, how many the countermeasures
// hide, which item cannot come out at any deployment. The string scan is actually run.
import { INDICATOR, generateBinary, scanStrings, write } from "./binary.mjs";

const TYPE = ["string", "symbol", "behavior", "unextractable"];
const value = (k) => k.reduce((t, g) => t + g.defense, 0);

console.log(`${INDICATOR.length} fictional indicators, total defense value ${value(INDICATOR)}\n`);

const A = [-14, 9, 12, 22];
console.log("1. indicator types");
write(A, "type", "count", "defense", "extraction path");
const PATH = { string: "string scan", symbol: "string scan",
  behavior: "dynamic observation", unextractable: "none" };
for (const t of TYPE) {
  const k = INDICATOR.filter((g) => g.type === t);
  write(A, t, k.length, value(k), PATH[t]);
}

const extracted = (option) => {
  const found = scanStrings(generateBinary(option));
  return INDICATOR.filter((g) => g.text && found.includes(g.text));
};

const DEPLOYMENT = [
  ["no countermeasure", {}],
  ["symbol stripping", { stripSymbols: true }],
  ["string hiding", { hideStrings: true }],
  ["symbol stripping + string hiding", { stripSymbols: true, hideStrings: true }],
  ["packing", { pack: true }],
];
const B = [-36, 9, 11, 11, 17];
console.log("\n2. what the string scan extracts (real run)");
write(B, "deployment", "count", "defense", "hidden", "remaining share");
const raw = extracted({});
for (const [name, s] of DEPLOYMENT) {
  const c = extracted(s);
  write(B, name, c.length, value(c), raw.length - c.length,
    `${((value(c) / value(INDICATOR)) * 100).toFixed(1)}%`);
}
console.log(`the binary is ${generateBinary({}).length} bytes (same across all deployments; only ` +
  `the content changes). Of the ${raw.length} items extracted from the raw binary, ` +
  `${raw.filter((g) => g.type === "string").length} are strings, ` +
  `${raw.filter((g) => g.type === "symbol").length} are symbols.`);

const STEP = ["sample-acquisition", "static-extraction", "dynamic-observation",
  "rule-conversion", "fleet-deployment"];
const COUNTERMEASURE = [
  { name: "symbol-stripping", breaks: 2, hides: ["symbol"] },
  { name: "string-hiding", breaks: 2, hides: ["string"] },
  { name: "packing", breaks: 2, hides: ["string", "symbol"] },
  { name: "debugger-detection", breaks: 3, hides: ["behavior"] },
  { name: "conditional-execution", breaks: 3, hides: ["behavior"] },
];
const C = [-24, 10, 20, 10, 12];
console.log("\n3. countermeasure-step mapping");
write(C, "countermeasure", "breaks", "hides type", "count", "defense");
for (const o of COUNTERMEASURE) {
  const k = INDICATOR.filter((g) => o.hides.includes(g.type));
  write(C, o.name, `step ${o.breaks}`, o.hides.join("+"), k.length, value(k));
}
console.log(`${STEP.length}-step analysis chain: ${STEP.join(" -> ")}`);

const D = [-34, 10, 12, 17];
console.log("\n4. remaining indicators (countermeasures cumulative)");
write(D, "deployment", "count", "defense", "remaining share");
const remaining = (types) => INDICATOR.filter((g) => !types.includes(g.type) && g.type !== "unextractable");
for (const [name, t] of [["none", []], ["static hiding", ["string", "symbol"]],
  ["dynamic hiding", ["behavior"]], ["both together", ["string", "symbol", "behavior"]]]) {
  const k = remaining(t);
  write(D, name, k.length, value(k), `${((value(k) / value(INDICATOR)) * 100).toFixed(1)}%`);
}
const cant = INDICATOR.filter((g) => g.type === "unextractable");
console.log(`items unextractable in any deployment: ${cant.length}, defense value ` +
  `${value(cant)} (${((value(cant) / value(INDICATOR)) * 100).toFixed(1)}%); ` +
  `these items are only seen in the victim's own record`);
```

```
20 fictional indicators, total defense value 38

1. indicator types
type              count     defense       extraction path
string                7          11           string scan
symbol                5           8           string scan
behavior              5          11   dynamic observation
unextractable         3           8                  none

2. what the string scan extracts (real run)
deployment                              count    defense     hidden  remaining share
no countermeasure                          12         19          0            50.0%
symbol stripping                            7         11          5            28.9%
string hiding                               5          8          7            21.1%
symbol stripping + string hiding            0          0         12             0.0%
packing                                     0          0         12             0.0%
the binary is 280 bytes (same across all deployments; only the content changes). Of the 12 items extracted from the raw binary, 7 are strings, 5 are symbols.

3. countermeasure-step mapping
countermeasure              breaks          hides type     count     defense
symbol-stripping            step 2              symbol         5           8
string-hiding               step 2              string         7          11
packing                     step 2       string+symbol        12          19
debugger-detection          step 3            behavior         5          11
conditional-execution       step 3            behavior         5          11
5-step analysis chain: sample-acquisition -> static-extraction -> dynamic-observation -> rule-conversion -> fleet-deployment

4. remaining indicators (countermeasures cumulative)
deployment                             count     defense  remaining share
none                                      17          30            78.9%
static hiding                              5          11            28.9%
dynamic hiding                            12          19            50.0%
both together                              0           0             0.0%
items unextractable in any deployment: 3, defense value 8 (21.1%); these items are only seen in the victim's own record
```

#### What the Scan Extracts

The second output is a real run: printable byte sequences are collected in the generated
280-byte binary and matched against the inventory. **12 items** come out of the raw binary — 7
strings, 5 symbols — and their defense value is 19, **half** of the inventory's total value.

Countermeasures bring this number down step by step. Symbol stripping brings 12 down to 7,
string hiding brings it to 5. When the two are applied together, or when a single packing step
is used, the scan gives **zero items**. When the number drops to zero, the binary is still 280
bytes and still does exactly the same job; the only thing that changes is whether a static scan
can read anything out of it.

This is the measured form of the most common misconception in analysis reports: **a scan coming
back empty does not show the item is harmless — it shows that scan did not work on that item.**
The same binary, the same behavior, a different presentation.

#### Which Step Each Countermeasure Breaks

The third output maps the five countermeasures to the defense's analysis chain. Three of them
break the **second step** — static extraction. Two of them break the **third step** — dynamic
observation.

None of them break the first step. Acquiring the sample is the link countermeasures cannot reach:
if the item has reached the endpoint, it is in the defense's hands. The fourth and fifth steps —
rule conversion and fleet deployment — are also outside the countermeasures' scope; their speed
depends on the defense's own operation.

A conclusion fitting this course's measure follows from this. The defense's analysis chain has
**two breakable links**, and both are in the middle. The start and end of the chain are links
the attacker cannot reach. The choice of countermeasure looks at which step the defense has less
backup for: static extraction's backup is dynamic observation; dynamic observation has no
backup.

#### The Unextractable Item

The fourth output counts the remaining indicators. With no countermeasure at all, 17 items and a
defense value of 30 are extractable. With static hiding applied, 5 items and a value of 11
remain; with dynamic hiding applied, 12 items and a value of 19. With both applied together,
**nothing at all** comes out of the binary itself.

The last line is this lesson's answer. Three items — target network address, session key, and
victim identity — cannot be extracted in any deployment, and their defense values total 8,
**21.1%** of the inventory. These items never exist in the binary at all; the item gets them
from the environment it runs in.

The conclusion is this: reverse engineering gives the defense a set of indicators, but **never
all of them**, and against countermeasures, the part it gives can drop all the way to zero. The
victim's own records are, for this reason, not an alternative to analysis but its complement:
the last three items exist only there, and two of the five highest-defense-value items are in
that set.

#### Summary

- The items that can be extracted from a binary are four types: string and symbol via static
  scan, behavior seen only at runtime, and a fourth type that cannot be extracted by any means.
- A string scan on the 280-byte fictional binary extracted 12 items (7 strings, 5 symbols),
  defense value 19 — half the inventory's total value.
- Symbol stripping brought the extracted item count down to 7, string hiding to 5; either
  applying both together or packing alone brought it to zero. The binary is the same size and
  the same function; only readability changes.
- Three of the five countermeasures break the analysis chain's second step, two break its third
  step. The first, fourth, and fifth steps are links countermeasures cannot reach.
- Three items cannot be extracted in any deployment; their defense value is 8, 21.1% of the
  inventory. These items are only seen in the victim's own record, and they are a complement to
  analysis, not an alternative to it.

#### Course Wrap-Up

Across twenty-two lessons, a single question was asked: at which link can this chain be broken,
and what does breaking it late cost. The table below gathers each lesson's three columns in
order; every number is read from its own lesson.

| Lesson | Chain step | Breakable link | Cost of late breaking |
|---|---|---|---|
| Threat Actors | 8 steps | 8 thresholds, 1 level passes | 64 items |
| Advanced Persistent Threat | 8 steps, 330 days | 5 monitored, 2 alert | 48 items |
| Attack Lifecycle | 8 steps | 5 controls, 3 empty steps | 20.14 items |
| Adversary Behavior Classifications | 8 steps, 10,800 chains | 72 chains unbreakable | 1.39 items average |
| Diamond Model | 8 steps, 6 links | infrastructure at step 8 | 41 items |
| Known and Unknown Threats | 8 controls | 7 in known, 5 in unknown | 19.69 items |
| Fundamentals of Social Engineering | 6 steps | 4 monitored, earliest at 3rd | 9 items |
| Phishing Types | 5 steps | earliest at step 2 | 11 items |
| Other Channels | 5 steps | 4 in email, 0 in voice call | 11 items |
| Physical Techniques | 3 chains | first step unmonitored | 7, 5, 7 items |
| Domain Spoofing | registration chain | step 1 if monitoring exists | 6 items gain |
| Watering Hole Attacks | 6 steps | 3rd in 3 sources, 6th in 2 pages | 12 items |
| Awareness Programs | 5 links | notification breaks the 3rd link | 5 items |
| Web Application Risks | 39 steps, 9 chains | control at 17 steps | 480 items |
| Injection Attacks | 6 steps | step 2, at 9/12 places | 480 items |
| Cross-Site Scripting | 5, 6, and 4 steps | 3 compositions do not break | 340 sessions |
| Request Forgery | 5 + 5 steps | 3rd in browser, 1st on server | 267 operations, 60 internal endpoints |
| Path Traversal | 5 steps | step 2, identifier mapping | 30 items, 41 sensitivity |
| Buffer Overflow | 5 steps | step 1, language class | 6 neighboring fields |
| Password Attacks | 6 steps | step 3, threshold's scale | 190 accounts |
| Drive-By Downloads | 6 conditions | 3rd condition, early boundary | 73 endpoints |
| Reverse Engineering Fundamentals | 5 steps, analysis | step 2, countermeasure | 8 defense value |

The table requires naming a single rule: **an attack is not a moment, it is a sequence of steps
with preconditions.** No row is a single event; every row has interlinked steps, and every step
has a condition that must hold. The defense's job, for this reason, is not to break the chain —
the chain can already be broken in many places. The defense's job is to find the **earliest
breakable link**, write down its legitimate-work cost, and count how many places that link is
actually deployed.

All twenty-two rows show this only on paper. Step sequences were modeled, controls were mapped
to steps, coverage was counted, and cost was written down — all on data structures we wrote
ourselves. Paper cannot show whether a defense actually holds; only **an authorized person
trying it** can show that. That trial's rules, how its scope is defined, where to stop, and in
what form findings get reported were never covered in this course at all. The next course —
**Security Tools and Penetration Testing** — measures exactly that.

