OpenCyber Academy
Engineering · Chapter 4 · English
Course code: OCA-131Chapter 04: OCA-131-C004

Chapter 04 — Programming I

Open, free learning for anyone who wants to protect people and technology. No account is required to read this text or its worked answers.

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

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.

Worked answers

EX007. Add to validate: if r["reporter"] and not r["reporter"].endswith(".example"): errs.append("reporter_domain"). Add a record such as C-1006,2026-03-02T10:00:00Z,2,ops@hvss.test,Wrong domain. Output: C-1006 quarantined with ['reporter_domain']; conservation 7 == 7. The empty-reporter case should keep only the reporter error (guarding the new rule with r["reporter"] and avoids double-reporting).

EX008. The second record is quarantined as duplicate although it carries a different summary and reporter — new information is pushed to review with a misleading reason. Better: compute a hash of the full normalised row; identical hash = true duplicate (quarantine as duplicate, linked to the first); same key with different hash = key_conflict sent to review with both versions shown. Never overwrite the first record.