Lesson map
Lesson 04 published. Map, example, EX007.
flowchart TD
N0["Learning outcomes and connection"]
N1["4.1 Programs as evidence handlers"]
N2["4.2 Variables, functions and validation"]
N3["4.4 Tests that matter"]
N4["Chapter synthesis"]
N0 --> N1 --> N2 --> N3 --> N4
N4 --> EX["Worked example"]
EX --> Q["Exercise EX007"]
Logical drawing
flowchart LR
N0["Learning outcomes and connection"]
N1["4.1 Programs as evidence handlers"]
N2["4.2 Variables, functions and validation"]
N3["4.4 Tests that matter"]
N4["Chapter synthesis"]
N0 --> N1 --> N2 --> N3 --> N4 --> Q["EX007"]
[Learning outcomes and connection]
|
v
[4.1 Programs as evidence handlers]
|
v
[4.2 Variables, functions and validation]
|
v
[4.4 Tests that matter]
|
v
[Chapter synthesis]
|
v
[Worked example] --> [Exercise EX007]
Learning outcomes and connection
After this chapter you can (1) write a small, deterministic program that reads synthetic case records; (2) validate inputs with explicit rules; (3) preserve every rejected record with its reasons instead of discarding it; and (4) test conservation — nothing enters without leaving through a known door. It prepares Chapter 9 (robust modules), Chapter 24 (evidence pipelines) and Chapter 30 (invariants).
4.1 Programs as evidence handlers
In incident response, a script is not just a convenience; it touches material that may later support a decision or a legal claim. Three habits follow. Determinism: the same input yields the same output, so others can reproduce it. Transparency: the program records what it did, including what it refused. Preservation: the original input is never modified; its hash is recorded before processing so later readers can verify they hold the same bytes (FIPS 180-4 specifies SHA-256).
4.2 Variables, functions and validation
Keep parsing, validation and output in separate functions. A validation function takes one record and returns a list of named errors; an empty list means valid. Names matter: "severity" is a better error than "bad row". Validate type (is it a timestamp?), range (severity 1–4), presence (reporter required) and uniqueness (no duplicate key). Use a standard parser for CSV — hand-splitting on commas breaks on quoted fields, which is exactly how many real intake tools corrupt data. In Lab 4 the validate function returns the names timestamp, severity and reporter, which a reviewer can count and sort later.
4.3 Worked example (synthetic): HVSS case intake
Lab 4 parses six records exported from a synthetic intake form. Four are flawed in different ways.
"""Lab 4 (synthetic): validating intake parser that quarantines, never discards."""
import csv, io, hashlib, json
RAW = """case_id,reported_at,severity,reporter,summary
C-1001,2026-03-02T08:14:00Z,3,ops@hvss.example,Login failures spike
C-1002,2026-03-02T08:20:00Z,7,ops@hvss.example,Severity out of range
C-1003,not-a-date,2,desk@hvss.example,Bad timestamp
C-1001,2026-03-02T08:14:00Z,3,ops@hvss.example,Login failures spike
C-1004,2026-03-02T09:02:00Z,2,,Missing reporter
C-1005,2026-03-02T09:30:00Z,4,soc@hvss.example,"Phish, with comma"
"""
from datetime import datetime
def validate(r):
errs=[]
try: datetime.strptime(r["reported_at"],"%Y-%m-%dT%H:%M:%SZ")
except ValueError: errs.append("timestamp")
if r["severity"] not in {"1","2","3","4"}: errs.append("severity")
if not r["reporter"]: errs.append("reporter")
return errs
digest = hashlib.sha256(RAW.encode()).hexdigest()
accepted, quarantine, seen = [], [], set()
for n, r in enumerate(csv.DictReader(io.StringIO(RAW)), start=2):
key = (r["case_id"], r["reported_at"])
e = validate(r) + (["duplicate"] if key in seen else [])
seen.add(key)
(quarantine if e else accepted).append({"line": n, "errors": e, **r})
print("raw sha256", digest[:16])
print("accepted", [a["case_id"] for a in accepted])
for q in quarantine: print("quarantined line", q["line"], q["case_id"], q["errors"])
print("conservation:", len(accepted)+len(quarantine), "==", RAW.count("\n")-1)
Expected output:
raw sha256 954554b24a46d4a7
accepted ['C-1001', 'C-1005']
quarantined line 3 C-1002 ['severity']
quarantined line 4 C-1003 ['timestamp']
quarantined line 5 C-1001 ['duplicate']
quarantined line 6 C-1004 ['reporter']
conservation: 6 == 6
Observe: the duplicate is detected by a composite key (case ID plus timestamp), the comma inside a quoted summary survives, every quarantined record keeps its line number and reasons, and a conservation check proves that accepted plus quarantined equals input lines. A reviewer can open the quarantine and decide; the program never silently decided for them. Note that line numbers start at 2 because line 1 of the export is the header row.
4.4 Tests that matter
Write tests for each rule and for the conservation property, plus one for idempotence — running the program twice on the same input gives the same output. Test the boundary values (severity 0, 1, 4, 5) and the empty file. A test that only checks the happy path tells you nothing about the failures that occur during incidents, when inputs are at their worst.
For Lab 4 the boundary tests are concrete: severities 0 and 5 must be quarantined with the reason severity, while 1 and 4 must be accepted. An empty export with only the header row should produce no accepted records and a conservation line of 0 == 0. Running the parser twice must print the same digest prefix 954554b24a46d4a7 and the same quarantine list.
4.5 Errors and limits
except: passor equivalent silent swallowing of errors.- Dropping invalid rows rather than quarantining them.
- Editing the original file in place.
- Validation rules hard-coded without a version, so later readers cannot know which rules applied.
Each of these errors would break something visible in Lab 4. Swallowing the ValueError from strptime would let C-1003 through with the text not-a-date as its report time. Dropping instead of quarantining would make the conservation line print 2 == 6. Editing the export in place would change the SHA-256 digest that later readers rely on to confirm they hold the original bytes.
Exercises — chapter 04
EX007 — Extend validation. Objective: add a rule without breaking conservation. Inputs: Lab 4. Steps: add a rule that reporter must end in .example (synthetic domain); add a seventh record that violates it; rerun and show conservation. Expected evidence: code diff and output. Rubric: 3 = rule works, conservation holds, new reason named; 2 = rule works without evidence; 1 = attempt; 0 = none.
EX008 — Duplicate semantics. Objective: reason about keys. Inputs: Lab 4. Steps: explain what happens if two different reporters submit the same case ID at the same second with different summaries; propose and justify a better duplicate rule. Expected evidence: explanation and rule. Rubric: 3 = identifies that the second is quarantined as a duplicate though it carries new information, and proposes content-aware handling (hash of the full row, or same-key-different-content flagged as conflict); 2 = partial; 1 = vague; 0 = none.
Chapter synthesis
Write response programs that are deterministic, transparent and preserving: hash the input, validate with named rules, quarantine instead of discard and test conservation. These small habits scale into the evidence pipelines of later chapters.