---
title: Reporting
source: 'https://academia.sh/en/courses/security-operations/reporting'
course: 'Security Operations and Incident Response'
language: en
updated: '2026-09-26T03:02:57+00:00'
license: 'CC BY-SA 4.0'
objectives:
    - '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"
---

# 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.

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

```js
// 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

```js
// 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.
