Lesson 22 / 22
Reporting
Measuring technical findings as a claim-to-evidence mapping: in a fictional 20-claim report draft built on 22 items collected by a three-person shift, 10 claims come out directly supported, 9 are inference, and 1 is unsupported. Seven of the 9 claims counted as inference drop there because of chain-of-custody gaps, and 2 are already inference. Perfect collection yields 11 direct claims, while host-by-host collection with a one-person team drops that to 7 and raises unsupported claims to 3.
Published Updated
In this lesson
- Maps report claims to the evidence items they rest on and sorts them into direct, inference, and unsupported
- Counts how many claims a chain-of-custody gap drops from direct support
- Measures the report's counterpart to collection order and team size as a claim count
Contents
This topic’s five lessons each produced a separate number: how many items the chain broke at, how many items scope brought in, how many items order cost, how many events the drift reversed, on how many clean machines a single trace was also found. None of these is a report sentence. A report consists of claims, and each claim is an opinion, not a number, until the item it rests on is written down.
This lesson measures that mapping. A fictional twenty-claim draft is built on top of the item set from the first three lessons — the same 28 items, the same chain gaps, the same collection model. Three columns are counted separately: how many claims are directly supported by an item, how many rest on inference, and how many have no evidence at all.
- FE31. Report draft is the list of claims planned to be written at the end of the examination. The twenty claims are fictional.
- FE32. A claim is directly supported only if at least one of its basis items was collected, that item is defensible (its chain is intact, its hash matches), and it directly carries the claim.
- FE33. If a claim’s basis was collected but the item’s chain is broken, or the claim reaches the item only through inference, the claim is inference.
- FE34. If none of a claim’s basis items were collected, the claim is unsupported.
- FE35. The item set, chain gaps, and collection model are the same as the previous lessons’; since the generator seed did not change, the numbers come from exactly the same set.
Report Draft and the Mapping Rule
// forensics/report.mjs — the MODEL for mapping report claims to evidence items. // The item set, chain failures, and collection order are the same as the model in the first // three lessons. const generator = (t) => { let s = t >>> 0; return () => (s = (s * 1664525 + 1013904223) >>> 0) / 4294967296; }; export const CLASS = { "network-state": [120, 45], "process-list": [240, 60], "open-handle": [300, 55], "session-state": [600, 70], "memory-content": [900, 420], "temp-file": [3600, 180], "flow-record": [86400, 240], "remote-log": [2592000, 300], "disk-image": [604800, 5400], "configuration": [604800, 90], }; const D5 = ["collection", "packaging", "transit", "lab", "analysis"]; const D3 = ["collection", "transit", "analysis"], D2 = ["intake", "analysis"]; export const HOST = { "branch-endpoint-3": [D5, ["network-state", "process-list", "open-handle", "session-state", "memory-content", "temp-file", "disk-image", "configuration"]], "field-laptop-14": [D5, ["network-state", "process-list", "session-state", "memory-content", "disk-image"]], "collector-2": [D3, ["network-state", "process-list", "open-handle", "session-state", "memory-content", "disk-image", "configuration"]], "portal-frontend": [D3, ["network-state", "process-list", "memory-content", "configuration"]], "central-log": [D2, ["remote-log", "configuration"]], "flow-collector": [D2, ["flow-record", "configuration"]], }; const UNPROTECTED = ["branch-endpoint-3/temp-file", "collector-2/disk-image", "portal-frontend/configuration"]; // The same generator sequence as the first lesson: one draw per item, one (or two) draws per // transfer. export function items() { const r = generator(20260731), list = []; for (const [host, [nodes, classes]] of Object.entries(HOST)) for (const c of classes) { const name = `${host}/${c}`; r(); let missing = 0; for (let i = 1; i < nodes.length; i += 1) if (r() < 0.11) { r(); missing += 1; } list.push({ name, host, class: c, lifetime: CLASS[c][0], duration: CLASS[c][1], defensible: missing === 0 && !UNPROTECTED.includes(name) }); } return list; } // The collection from the third lesson: n people, the given order, arrival delay to remote hosts. const REMOTE = ["branch-endpoint-3", "field-laptop-14"]; export function collect(order, n, delay) { const free = Array(n).fill(0), taken = new Set(); for (const k of order) { const i = free.indexOf(Math.min(...free)); const start = Math.max(free[i], REMOTE.includes(k.host) ? delay : 0); if (start <= k.lifetime) { taken.add(k.name); free[i] = start + k.duration; } } return taken; } export const ORDER = { "by volatility": (K) => [...K].sort((a, b) => a.lifetime - b.lifetime || a.duration - b.duration), "host by host": (K) => [...K], }; // Report draft: claim -> [basis items, does the basis directly carry the claim] export const CLAIM = { "an unauthorized session was opened at the branch endpoint": [["central-log/remote-log", "branch-endpoint-3/session-state"], true], "an outbound connection was established from the branch endpoint": [["flow-collector/flow-record", "branch-endpoint-3/network-state"], true], "a new process ran at the branch endpoint": [["branch-endpoint-3/process-list", "branch-endpoint-3/memory-content"], true], "the process wrote a file": [["branch-endpoint-3/open-handle", "branch-endpoint-3/disk-image"], true], "the written file was later deleted": [["branch-endpoint-3/disk-image"], true], "the item was set to run at startup": [["branch-endpoint-3/configuration"], true], "the temp files at the branch endpoint were cleared": [["branch-endpoint-3/temp-file"], true], "a connection was established to the collector": [["flow-collector/flow-record", "collector-2/network-state"], true], "a session was opened at the collector": [["collector-2/session-state", "central-log/remote-log"], true], "a process was started at the collector": [["collector-2/process-list", "collector-2/memory-content"], true], "subscriber data was read from the collector": [["collector-2/disk-image", "collector-2/open-handle"], true], "the collector's configuration did not change": [["collector-2/configuration"], true], "the same session was used on the field laptop": [["field-laptop-14/session-state", "field-laptop-14/memory-content"], true], "the field laptop accessed the collector": [["field-laptop-14/network-state", "flow-collector/flow-record"], true], "a file was modified on the field laptop": [["field-laptop-14/disk-image"], true], "a token was used at the portal frontend": [["portal-frontend/memory-content", "central-log/remote-log"], true], "the portal frontend's setting was changed": [["portal-frontend/configuration"], true], "all the accesses are from the same person": [["central-log/remote-log"], false], "data was transmitted to a third party": [[], false], "this same access never happened before the incident": [["central-log/remote-log", "flow-collector/flow-record"], false], }; export function classify(claim, taken, items) { const [basis, direct] = CLAIM[claim]; const held = basis.filter((d) => taken.has(d)); if (!held.length) return "unsupported"; if (!direct) return "inference"; return held.some((d) => items.find((k) => k.name === d).defensible) ? "direct" : "inference"; }
The mapping rule has three inputs: whether the item was collected, whether the item’s chain is intact, and whether the item directly carries the claim. The first two were measured in previous lessons; the third is the examiner’s judgment, and it is filled in while the claim is written — if it is filled in afterward, the strength of the basis gets chosen to fit the claim.
The Class of the Twenty Claims
// forensics/mapping.mjs — the claim-to-evidence mapping and collection order's counterpart in the report. import { items, collect, ORDER, CLAIM, classify } from "./report.mjs"; const y = (g, ...s) => console.log(s.map((v, i) => (g[i] < 0 ? String(v).padEnd(-g[i]) : String(v).padStart(g[i]))).join("").trimEnd()); const K = items(), claims = Object.keys(CLAIM); const main = collect(ORDER["by volatility"](K), 3, 600); // three-person shift, 600 s travel console.log(`model: ${K.length} evidence items, ${K.filter((k) => k.defensible).length} ` + `defensible; ${claims.length} claims in the report draft`); console.log(`three-person shift, order of volatility, 600-second arrival: ${main.size} items taken`); const A = [-65, 9, 8, 13, 11]; console.log(""); y(A, "claim", "basis", "held", "class", "reason"); for (const i of claims) { const [d, direct] = CLAIM[i]; const held = d.filter((x) => main.has(x)); const s = classify(i, main, K); y(A, i, d.length, held.length, s, s === "direct" ? "-" : s === "unsupported" ? "no item" : direct ? "chain" : "claim"); } const count = (taken) => ["direct", "inference", "unsupported"].map((s) => claims.filter((i) => classify(i, taken, K) === s).length); const [d0, c0, k0] = count(main); const chain = claims.filter((i) => CLAIM[i][1] && classify(i, main, K) === "inference").length; console.log(`\ndirectly supported ${d0}, inference ${c0}, unsupported ${k0} (total ${claims.length})`); console.log(`of the ${c0} claims counted as inference, ${chain} dropped because of chain gaps, ` + `${c0 - chain} were already inference`); const B = [-22, 7, 9, 10, 10, 13]; console.log(""); y(B, "collection", "people", "taken", "direct", "inference", "unsupported"); for (const [name, people, delay] of [["by volatility", 3, 0], ["by volatility", 3, 600], ["by volatility", 1, 0], ["host by host", 3, 0], ["host by host", 1, 0]]) { const a = collect(ORDER[name](K), people, delay); y(B, `${name}${delay ? " +600 s" : ""}`, people, a.size, ...count(a)); }
model: 28 evidence items, 16 defensible; 20 claims in the report draft three-person shift, order of volatility, 600-second arrival: 22 items taken claim basis held class reason an unauthorized session was opened at the branch endpoint 2 2 inference chain an outbound connection was established from the branch endpoint 2 1 direct - a new process ran at the branch endpoint 2 1 inference chain the process wrote a file 2 1 direct - the written file was later deleted 1 1 direct - the item was set to run at startup 1 1 inference chain the temp files at the branch endpoint were cleared 1 1 inference chain a connection was established to the collector 2 2 direct - a session was opened at the collector 2 2 direct - a process was started at the collector 2 2 direct - subscriber data was read from the collector 2 2 inference chain the collector's configuration did not change 1 1 direct - the same session was used on the field laptop 2 2 direct - the field laptop accessed the collector 2 1 direct - a file was modified on the field laptop 1 1 direct - a token was used at the portal frontend 2 1 inference chain the portal frontend's setting was changed 1 1 inference chain all the accesses are from the same person 1 1 inference claim data was transmitted to a third party 0 0 unsupported no item this same access never happened before the incident 2 2 inference claim directly supported 10, inference 9, unsupported 1 (total 20) of the 9 claims counted as inference, 7 dropped because of chain gaps, 2 were already inference collection people taken direct inference unsupported by volatility 3 28 11 8 1 by volatility +600 s 3 22 10 9 1 by volatility 1 21 8 10 2 host by host 3 19 9 10 1 host by host 1 16 7 10 3
The result sits in two lines: of the twenty claims, ten are directly supported, nine are inference, and one is unsupported. The real information is in the line below. Seven of the nine claims counted as inference land there not because they are inherently inference, but because the chain of custody of the item they rest on is broken. The items are collected, they are on hand, their contents are read — they are not defensible. This is the report’s counterpart to the 2.6 percent blank-record-field rate measured in the first lesson: seven sentences that could have been directly supported drop one class down.
The reason column separates three defects, each with a different remedy. Rows marked “chain” are a record problem, and some fraction of them can be closed after the fact. The two rows marked “claim” are a logic problem: the sentence “all the accesses are from the same person” does not rise by adding any item, because an item shows a session, not an identity. The single row marked “no item” is a scope problem.
The table below gives collection decisions their report counterpart. Under the perfect condition — three people, the correct order, no delay — all 28 items are taken and the number of directly supported claims is 11. If the same team spends six hundred seconds traveling, that drops to 10; if the team shrinks to one person, to 8; if collection goes host by host and the team is down to one person, it drops to 7, and the number of unsupported claims rises from one to three. A single ordering decision costs four report sentences and two unanswered questions.
Claim, Inference, and What Goes Unwritten
How the three columns are written into the report matters as much as measuring them. If the class is not written down, the reader gives every sentence the same weight; an inference sentence sits next to a directly supported one and cannot be told apart from it. That distinction is the writer’s job, not the reader’s.
An inference sentence can go into the report; it cannot go in without stating that it is inference. An unsupported claim is not deleted — it moves under an “asked and unanswered” heading. That the question was asked, and why it could not be answered, carries decision weight: if it is not known whether the data left the network, writing down that it is not known is not the same thing as not writing it at all.
The items collected and the items that go into the report also differ: 28 items were collected, 22 remained on hand, 23 items were cited as a basis, and 18 of those were on hand. An unused item is not a defect, but the report’s length is measured not by the items collected but by the items mapped.
The methodology section is itself a list of items: hash values, chain-of-custody records, collection order, arrival delay, the schedule’s remaining drift. They are written so that the same result can be reproduced.
The last boundary concerns the legal context, and it is drawn in a single sentence: the examination states what the item shows, not how the act should be characterized. Characterization is another profession’s job; the moment a technical report takes its place, it opens even its correct technical part to dispute. This is also why the report’s scope, its written authorization, and the list of systems examined go on the first page.
Summary
- A report consists of claims, and each claim is mapped to the item it rests on; the mapping rule’s three inputs are whether the item was collected, is defensible, and directly carries the claim.
- In the twenty-claim draft, with 22 items, 10 claims are directly supported, 9 are inference, and 1 is unsupported.
- Seven of the 9 claims counted as inference dropped there because of chain-of-custody gaps; the items are on hand, they cannot be defended.
- The three defects have separate remedies: a chain gap is a record problem, remaining inference is a logic problem, and the absence of an item is a scope problem.
- Perfect collection yields 11 direct claims; host-by-host collection with a one-person team drops that to 7 and raises unsupported claims to 3.
Course Wrap-Up
| Lesson | Initial record / source | Steps to decision | Cost of delay |
|---|---|---|---|
| Security Operations Center | 273 alerts, 3-person shift | 26 cases, 1 handover point | 64 alerts unseen, median 987 minutes |
| Log Aggregation and Normalization | 3 sources, 1-8 minute delay | 8-field schema, 2 intermediate tables | 9 redundant fields, 404 records actorless |
| Security Information and Event Management | 3,083 raw events | 12 candidates, 10 alerts, 43 minutes | 2 of 5 campaigns never appear |
| Network Flow Data | 2,979 flows, 7 fields | 4 metrics, 43 flags | 4 of 10 questions unanswered, 8.8 GB outage |
| False Positives and False Negatives | 900 candidates, 20 items | 4 attributes, 1 threshold | at threshold 72, 7 missed, 42 extra items |
| Detection Engineering | 26 techniques, 8 tactics | 11 rules, 2 versions | 2 techniques uncovered, 513 minutes review |
| Threat Hunting | 12,910 records, 6 hypotheses | 2-3 elimination steps | 2 items in 1,691 minutes, 846 minutes/item |
| Threat Intelligence | 900 indicators, 12 definitions | 90-day window | 828 dead indicators, 55 contacts missed |
| Response Automation | 8 procedures, 158 items/day | 3 modes, 1,215 minutes manual work | 2.65 wrong actions/day, 457 minutes |
| Preparation | 12 preparation items | 6-step chain | unprepared: 470 minutes, 13 items |
| Identification | 30 assets, 20 logged | 7 criteria, 3-73 minutes | legitimate work interrupted at 13 assets |
| Containment | 8 short, 4 long actions | 7 containment patterns | late decision: 5 extra items, 372 minutes |
| Eradication | 22 points, 9 assets | 5 discovery methods, 506 minutes | 5 points missed, 8 reinfections in 60 days |
| Recovery | 7 rollback points | 4-stage verification | dirty backup: 1,347 minutes, 9.5 days of data |
| Lessons Learned | 16 questions, 9 record sources | 12 improvement items | 5 questions unanswered, 136 person-hours |
| Communication and Notification Obligations | 12 stakeholders, 5 external | 4,320-minute window | late notification: 64,540 minutes over |
| Forensic Examination Principles | 28 items, 6 hosts | 4 transfers, 5 record fields | 12 items contestable |
| Disk Imaging | 15 items, 7 zones | 4 copy scopes | 6 items unverifiable on a live copy |
| Memory Dump Analysis | 28 items, 18 volatile | 10-class order of volatility | 9 items in 1,200 seconds |
| Timeline Creation | 3 sources, 54 events | 19 anchors, 2 coefficients | 33 events misplaced |
| Structure and Record Examination | 12 structures, base 4-187 | 7 claims, 3 classes | 5 claims without basis at 90 days |
| Reporting | 20 claims, 22 items | 3-class mapping | 7 claims drop at the chain |
The fourth column has no empty cell. Each of the twenty-two lessons wrote the cost of delay as a duration or an item count, and in none did it come out to zero. The course’s rule is read from this column: the output of a security operation is not the number of alerts, but the time between detection and containment; if that time is not measured, the pipeline is not considered built. The alert count measures workload; it does not prove a built pipeline.
The second and third columns are the rule’s two halves. A record that was never dropped in the first place cannot be recovered by any step; every extra step, in turn, is both an elimination and a delay. In none of the twenty-two lessons did both improve at once.
This pipeline rested on a single silent assumption, which the last six lessons used most plainly: that logs, images, and memory dumps sit on machines you can reach. The collector’s disk could be imaged; the branch endpoint could be visited. But part of the systems now run on hardware that is no longer yours; there, no image can be taken, and the first link in the chain of custody is in someone else’s hands. M24/K09 Cloud and Virtualization Security begins with exactly this question.
To keep your progress and take notes, Log in
My notes
Log in to take notes.