Lesson 02 / 22
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.
Published Updated
In this lesson
- Models a long-running campaign through step durations and daily trace production
- Counts, separately, the number of days a control sees and the number of days no control sees
- Calculates dwell time from the detection day and derives the item cost of the delay
Contents
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.
// 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; }
// 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.
To keep your progress and take notes, Log in
My notes
Log in to take notes.