Lesson map
Lesson 38 published. Map, example, EX112.
flowchart TD
N0["Learning outcomes and connection"]
N1["38.1 Workflows as state machines"]
N2["38.2 Access gates"]
N3["38.3 Evidence store"]
N4["38.4 Tests and rollback"]
N5["Chapter synthesis"]
N0 --> N1 --> N2 --> N3 --> N4 --> N5
N5 --> EX["Worked example"]
EX --> Q["Exercise EX112"]
Logical drawing
flowchart LR
N0["Learning outcomes and connection"]
N1["38.1 Workflows as state machines"]
N2["38.2 Access gates"]
N3["38.3 Evidence store"]
N4["38.4 Tests and rollback"]
N5["Chapter synthesis"]
N0 --> N1 --> N2 --> N3 --> N4 --> N5 --> Q["EX112"]
[Learning outcomes and connection]
|
v
[38.1 Workflows as state machines]
|
v
[38.2 Access gates]
|
v
[38.3 Evidence store]
|
v
[38.4 Tests and rollback]
|
v
[Chapter synthesis]
|
v
[Worked example] --> [Exercise EX112]
Learning outcomes and connection
You will be able to (1) implement a small case workflow as explicit states and transitions; (2) enforce an access gate on sensitive transitions; (3) store evidence with integrity records; (4) run normal and failure test cases; and (5) roll back safely. This chapter implements the capstone proposed in Chapter 35 in a local, simulated environment; Chapter 39 evaluates it.
38.1 Workflows as state machines
A case moves through states — new, triaged, contained, recovered, closed — by transitions with rules: who may perform them and what must exist first. Modelling the workflow as a state machine (Chapter 6) makes rules testable and violations visible.
In Lab 38 the states and allowed transitions are written as a dictionary, so the rules are data that a reviewer can read in one place. A transition not in that table, such as moving straight from new to closed, is denied with an explicit reason rather than silently accepted.
38.2 Access gates
Some transitions need specific authority: containment of production systems requires the incident lead; closure requires the service owner's acceptance. A gate checks the actor's role and records the decision either way.
In Lab 38 the gate denies an analyst's attempt to contain HC-0301 and an engineer's attempt to close it, and both denials appear in the log. Recording refusals matters as much as recording approvals: they show the gates were tested in use, and a pattern of denials may reveal a role definition that does not match how people actually work. Note the lab's limit: the caller supplies its own role, so a real system must look up the role from an authenticated identity.
38.3 Evidence store
Attach evidence by reference with a hash (Chapter 17) and record who attached it and when. The store is append-only: corrections add new entries rather than editing old ones.
In Lab 38 the sign-in export is stored with the first twelve characters of its SHA-256 digest and the name of the analyst who attached it. Containment was denied until that item existed. A full system would keep the whole digest, record the time and store the log somewhere the workflow's own users cannot edit, so that the evidence record survives a dispute.
38.4 Tests and rollback
Test normal paths, forbidden transitions, unauthorized actors and missing evidence. Rollback means reverting a state change by a new, recorded transition (for example contained → triaged when containment is lifted), never by deleting history.
Lab 38 exercises a rollback from contained to triaged and then contains again; all three steps stay in the log. Its tests include a wrong role, missing evidence and a forbidden closure, and the final line, ten log entries with three denials, gives a quick check that every attempt was recorded. Run the tests after each change to the transition table, because a rule added for one case can silently block another.
38.5 Worked example and lab (synthetic): minimal workflow
Save as lab38.py.
# Lab 38 - a minimal case workflow with role gates, evidence hashes and an append-only log (stdlib only)
import hashlib
TRANSITIONS = { # (from, to): roles allowed
("new", "triaged"): {"analyst", "lead"},
("triaged", "contained"): {"lead"},
("contained", "triaged"): {"lead"}, # recorded rollback
("contained", "recovered"): {"lead", "engineer"},
("recovered", "closed"): {"owner"},
}
REQUIRES_EVIDENCE = {"contained", "closed"}
class Case:
def __init__(self, cid):
self.cid, self.state, self.log, self.evidence = cid, "new", [], []
def attach(self, actor, name, data):
h = hashlib.sha256(data).hexdigest()[:12]
self.evidence.append((name, h, actor))
self.log.append(("attach", actor, name, h))
def move(self, actor, role, to):
allowed = TRANSITIONS.get((self.state, to))
if allowed is None:
result = "DENIED: no such transition"
elif role not in allowed:
result = f"DENIED: role {role} not allowed"
elif to in REQUIRES_EVIDENCE and not self.evidence:
result = "DENIED: evidence required"
else:
result = "OK"
self.state = to
self.log.append(("move", actor, f"{to}", result))
return result
c = Case("HC-0301")
tests = [
("ana", "analyst", "triaged"),
("ana", "analyst", "contained"), # wrong role
("lee", "lead", "contained"), # no evidence yet
]
for t in tests:
print(t, "->", c.move(*t))
c.attach("ana", "idp-signins.json", b"synthetic sign-in export")
for t in [("lee", "lead", "contained"), ("lee", "lead", "triaged"),
("lee", "lead", "contained"), ("eng", "engineer", "recovered"),
("eng", "engineer", "closed"), ("own", "owner", "closed")]:
print(t, "->", c.move(*t))
print("final state:", c.state, "| log entries:", len(c.log),
"| denied:", sum(1 for e in c.log if e[3].startswith("DENIED")))
Expected output:
('ana', 'analyst', 'triaged') -> OK
('ana', 'analyst', 'contained') -> DENIED: role analyst not allowed
('lee', 'lead', 'contained') -> DENIED: evidence required
('lee', 'lead', 'contained') -> OK
('lee', 'lead', 'triaged') -> OK
('lee', 'lead', 'contained') -> OK
('eng', 'engineer', 'recovered') -> OK
('eng', 'engineer', 'closed') -> DENIED: role engineer not allowed
('own', 'owner', 'closed') -> OK
final state: closed | log entries: 10 | denied: 3
Interpretation: the gates work for the tested paths: an analyst cannot contain, containment needs evidence, an engineer cannot close. The rollback (contained → triaged) is a recorded transition. The log keeps denied attempts — they are evidence about the process. Limits: roles are passed in by the caller and not authenticated (a real system must verify identity); "any evidence" satisfies the evidence rule (a stricter rule would require specific evidence types); and the log is in memory, not tamper-evident storage.
38.6 Common errors and limits
- Testing only the happy path.
- Deleting history to "undo" a mistake.
- Trusting the caller's claimed role.
-
Treating a lab prototype as production-ready.
-
Accepting "any evidence" as enough for containment, as the lab does, when a real gate should require specific evidence types.
- Keeping the log in memory or in a file that ordinary users can edit.
- Adding transitions to make a failing test pass without asking whether the test or the rule was wrong.
- Letting a closed case be reopened by editing its state directly rather than through a recorded transition with a named role.
Exercises — chapter 38
Attempt each exercise before opening the worked results. Every case, organization, person, address and number in an exercise is synthetic unless a cited public source is named.
EX112 — Add a gate. Steps: require that closure is denied unless an evidence item named starting with acceptance exists; add a test showing denial and success. Rubric: 3 = rule implemented, both tests shown with output; 2 = rule without tests; 1 = not run; 0 = none.
EX113 — Failure cases. Steps: list five additional failure tests the capstone should include. Rubric: 3 = unknown state, repeated transition, closed case reopened without rule, evidence attached after closure, unauthenticated actor; 2 = three; 1 = one; 0 = none.
EX114 — Hardening plan. Steps: describe three changes needed to move from the lab to a pilot with real users. Rubric: 3 = authentication and role lookup from directory, tamper-evident persistent log, specific evidence requirements and retention, plus access controls on evidence; 2 = two; 1 = one; 0 = none.
Chapter synthesis
Implement workflows as explicit states and gated transitions, keep evidence by hash in an append-only record, test failures as carefully as successes, and roll back by recorded transitions — while stating the prototype's limits.