Lesson 09 — Correlation, quiet queries and reviewer handoff
Week 2, day 4. Work through the teaching and the worked checkpoint before attempting the assigned exercise(s). Compare your artifact with the final worked result only afterward.
2.9 Contained lab: correlate records without inventing an actor
The earlier labs examined when a row became visible and whether a copied file changed. A responder also needs to ask whether records from two systems refer to the same activity. An account label, a session identifier and a human identity are different things. If an application audit records desk-a but omits its session identifier, several identity sessions for that account could fit the same time. A nearby timestamp cannot choose one by itself. An exact identifier can narrow a candidate relationship, but it still does not identify the person who used the session or establish that the edit was authorized. NIST SP 800-92 provides high-level log-management background, and NIST SP 800-86 discusses investigation across source types. Neither older publication prescribes this book's matching code or claims that an account/time join proves attribution. The following method is an original beginner-level teaching fixture.
This is a separate optional Q1 replay, not a new R record or a correction to R13. In the canonical Round B evidence, R13 reports one session within its available range and lacks device details. Q1 deliberately supplies two overlapping sessions for a different synthetic comparison so a learner can see an ambiguous join. Never import Q1 into the 09:20 canonical case decision, cite it as a discovered Harbor Library production fact, or reinterpret R13 as false. Both tables below are invented, contain no patron data and use explicit UTC instants on the same fictional date. The session intervals are half-open: start <= event < end, so an event at the printed end time is outside that row.
Card Q1-A — isolated identity-session table. The snapshot is an exercise table, not an authenticated export. session_id values are synthetic labels; no password, token or device identifier is supplied.
| Session | Account | Start UTC | End UTC |
|---|---|---|---|
S-A |
desk-a |
09:00 | 09:15 |
S-B |
desk-a |
09:02 | 09:12 |
S-C |
front-b |
09:00 | 09:20 |
Card Q1-B — isolated application-edit table. A blank application session_id means the field is missing from this row; it is not a statement that no session existed. The booking labels X1–X4 are additional exercise-only identifiers and have no relation to B17–B20.
| Edit | Account label | Event UTC | Application session_id |
Booking |
|---|---|---|---|---|
E-A |
desk-a |
09:05 | blank | X1 |
E-B |
desk-a |
09:07 | S-B |
X2 |
E-C |
desk-a |
09:18 | blank | X3 |
E-D |
desk-a |
09:06 | S-X |
X4 |
The first pass checks account label and interval. E-A falls inside both S-A and S-B, not S-C because its account differs. E-B falls inside both by time, but its explicit S-B key narrows the supplied candidate set to one. E-C has no account-and-interval match: the latest supplied desk-a session ends at 09:15. E-D falls inside both desk-a intervals by time, yet its explicit S-X key matches neither supplied ID. A responder should not silently discard the explicit key and report either S-A or S-B as the source. That would change the meaning of the evidence to make a join succeed.
The second pass asks whether the apparent matches deserve trust. The two tables might cover different collection windows, use clocks with different error, or have been filtered on different account identifiers. Q1 does not provide a query manifest, source-clock check, application schema showing that both session_id fields share one namespace, or a record of session rotation. E-C's zero matches therefore means no row matched in this supplied table under this rule, not that no session existed at 09:18. E-D's key mismatch may point to a missing identity row, a malformed application field or a wrong assumption about key namespaces. Its cause is unknown. Even E-B's one exact-key match does not prove who controlled S-B or why the edit occurred.
Reproduce the four classifications locally
The Python 3 standard-library program below keeps the invented rows in memory. It opens no file, makes no network request and alters no source. It validates that supplied session IDs are unique and that every interval has a start before its end; it then prints the candidates under the classroom rule. Use a scratch interpreter or reason through the rows on paper. If you change a fixture, label it a variant rather than rewriting Q1's original result.
from datetime import datetime, timedelta
DAY = "2026-10-11T"
SESSIONS = [
("S-A", "desk-a", "09:00", "09:15"),
("S-B", "desk-a", "09:02", "09:12"),
("S-C", "front-b", "09:00", "09:20"),
]
EDITS = [
("E-A", "desk-a", "09:05", ""),
("E-B", "desk-a", "09:07", "S-B"),
("E-C", "desk-a", "09:18", ""),
("E-D", "desk-a", "09:06", "S-X"),
]
def utc(clock):
value = datetime.fromisoformat(DAY + clock + "+00:00")
if value.utcoffset() != timedelta(0):
raise ValueError("expected an explicit UTC instant")
return value
seen = set()
parsed = []
for sid, account, start, end in SESSIONS:
if sid in seen or utc(start) >= utc(end):
raise ValueError("duplicate session ID or invalid interval")
seen.add(sid)
parsed.append((sid, account, utc(start), utc(end)))
for eid, account, event, explicit_sid in EDITS:
moment = utc(event)
window = [sid for sid, owner, start, end in parsed
if owner == account and start <= moment < end]
if explicit_sid:
matches = [sid for sid in window if sid == explicit_sid]
status = "KEY_AND_WINDOW_MATCH" if matches else "EXPLICIT_KEY_UNMATCHED"
elif len(window) == 0:
matches, status = [], "NO_WINDOW_MATCH"
elif len(window) == 1:
matches, status = window, "ONE_WINDOW_CANDIDATE"
else:
matches, status = window, "AMBIGUOUS_WINDOW_CANDIDATES"
print(f"{eid}: {status} candidates={','.join(matches) or '-'}")
The method has an important asymmetry. An explicit key that fails to match is not treated like an absent key. The former is evidence of a mismatch under the assumed schema; the latter simply leaves account/time as a weaker possible relationship. ONE_WINDOW_CANDIDATE, if a variant ever produces it, means one row survived this table filter, not a measured probability that the row is correct. The program never emits a person's name or a compromise verdict because neither table supplies that evidence. It also does not check that the tables are complete or that the account labels were populated consistently; those limits belong in the learner's result note.
Practice task. Submit a four-row result table with event ID, status, candidate session IDs and one sentence stating what the result cannot establish. Then write a bounded custodian request for the missing query range, source-clock quality and shared session-key semantics. Identify the fictional incident lead as the person who must authorize any additional collection under the classroom role boundary; no learner may query a real identity or booking system. Finally, create a variant Q1-V: remove only E-B's explicit S-B key and predict which classification changes. Do not treat a changed result as a new canonical event.
Expected evidence: four classifications matching the printed program output, a limitation for each, a request that names the smallest useful source and fields, and the Q1-V prediction. Rubric (six points): one point for correctly identifying both E-A candidates; one for E-B's key-and-window match without human attribution; one for E-C's zero-match limit; one for preserving E-D's explicit-key mismatch; one for a proportionate request including clock, coverage and schema; and one for predicting that E-B becomes ambiguous when its key is removed. A 6/6 score verifies reasoning on this fixture, not the trustworthiness of a real log join. The exact output and model request appear in the final answer appendix after the Module 2 evidence-package audit result.
Q1-S — qualify identifiers and test sensitivity to clock uncertainty
Learning goal: Explain why equal key text is not enough to join two sources, calculate which session intervals could contain an uncertain event time, and keep input defects separate from an absence of matching evidence. Complete the four original Q1 cases first. This extension changes neither their inputs nor their four expected output lines.
Classification and safety: Q1-S is a separate, entirely invented fixture dated 2040-03-16. Its realms, account and correlation keys are harmless labels, not authentication material, Harbor Library observations or additions to R1–R16. The laboratory uses only Python's standard library and in-memory data. It performs no collection, login, session access, network request, source modification or cleanup operation.
Equal text versus an equal identifier
A namespace is the context in which an identifier has its meaning. A qualified key retains that context with the value. In this exercise, the qualified key is the pair (realm, key). Thus (realm-a, K7) and (realm-b, K7) are different identifiers even though their last component is identical. A real source contract might instead need an issuer, tenant, application, generation or another documented qualifier. Do not assume that this two-field classroom key is sufficient for every platform.
The following logical drawing represents filtering a supplied candidate table, not a network path or a successful authentication:
Application row: realm-a | K7 | desk-x
|
v
Select the declared realm
/ \
realm-a rows realm-b rows
K6, desk-x K7, desk-x
K7, desk-x Excluded: wrong realm
|
v
Select K7 and desk-x
|
v
Test the time interval
|
v
Candidate relationship only
No person, permission or intent established
Text equivalent: Start with the application's declared realm, retain only rows in that realm, then compare the explicit key, account and time. The other realm's K7 does not become evidence about this row. When the realm is unknown, stop with an unresolved namespace rather than search every realm and select a convenient result. A supplied but nonmatching key remains nonmatching; it must not be replaced by a weaker account/time lookup.
Three concepts must also remain separate. A session correlation label relates records to a declared session. An interaction identifier relates records to one interaction, such as an individual request. A human attribution is a further evidential claim about who acted. The OWASP Logging Cheat Sheet discusses interaction identifiers as a way to preserve application-known relationships and distinguishes event time from logging time. It does not make this exercise's realm/key pair an authenticated identity.
Do not make a correlation exercise an excuse to copy live session secrets. OWASP's session logging guidance warns against recording raw session IDs and describes a protected correlation approach. For a real review, request only the approved non-secret correlation representation, its scope and its documented mapping. Q1-S already supplies safe invented labels; it teaches no token acquisition or session replay.
Time uncertainty changes a set of candidates, not the event's history
The original Q1 tests one stated event time against exact supplied session endpoints. Q1-S asks a different question: what changes when the event's comparable time is known only to lie in a closed range [low, high]? The range is an explicit assumption, not an ingestion delay, a measured probability interval or permission to use a later-released record at an earlier checkpoint.
For a session whose supplied interval is [start, end), define two tests:
| Test | Meaning under the stated model | Comparison |
|---|---|---|
| Possible candidate | At least one time in the event range lies inside the session interval | low < end and high >= start |
| Covers all times | Every time in the event range lies inside that session interval | start <= low and high < end |
The endpoint asymmetry is intentional. A session includes its start but excludes its end. An event range ending exactly at the session start still overlaps at that included instant. A range starting exactly at the session end does not overlap. A range that includes the exclusive end cannot pass the all-times test. These comparisons are derived from the declared intervals; they are not a NIST severity or confidence scale.
Work the mechanism: Suppose realm-a/K6 lasts from 09:00 to 09:15 exclusive and realm-a/K7 from 09:02 to 09:12 exclusive. An event with no key could have occurred between 09:11 and 09:13 inclusive. K6 contains the entire range. K7 contains only the part before 09:12. Both therefore remain possible, while only K6 covers all times.
UTC minute 00 01 02 03 04 05 06 07 08 09 10 11 12 13 14 15
realm-a/K6 [--------------------------------------------)
realm-a/K7 [-----------------------------)
event range [-----]
Figure reading: Every tick is one minute after 09:00 UTC. [ and ] include an endpoint; ) excludes it. The event range intersects both session intervals but is contained only by K6. These are invented intervals, not collected timestamps. The table below gives the exact numeric endpoints independently of the drawing.
One all-times candidate is not a unique attribution: K7 remains possible if the event was in the earlier part of the range. The all-times test is robustness to this particular time assumption, not proof of source truth or a higher probability. Likewise, widening an assumed range until a desired session matches does not validate the range. A custodian would need to substantiate the clock relationship and bound. If no usable bound is supplied, Q1-S reports UNRESOLVED_TIME_BOUND instead of silently assuming zero error.
This model treats session endpoints as exact and varies only the event time. Uncertain session clocks, changes in clock offset, session-key reuse and incomplete exports require additional evidence or a different model. Do not import the example's two-minute event range into the canonical 09:20 decision.
Complete independent input and execution contract
The program uses integer minutes after midnight UTC on its artificial date: 09:00 is 540, 09:02 is 542, 09:07 is 547, 09:11 is 551, 09:12 is 552, 09:13 is 553 and 09:15 is 555. Integer endpoints make the arithmetic inspectable; the program is not a parser for production timestamps or sub-minute evidence.
| Realm | Key | Account | Start inclusive | End exclusive |
|---|---|---|---|---|
| realm-a | K6 | desk-x | 540 | 555 |
| realm-a | K7 | desk-x | 542 | 552 |
| realm-b | K7 | desk-x | 542 | 552 |
A repeated bare K7 across realms is permitted; a repeated qualified key is rejected. Repeated exported rows might represent duplicate delivery rather than two sessions, but this small program does not decide that question: preserve the packet and ask about row identity and export semantics instead of silently deduplicating conflicting records. The account and key comparisons are exact and case-sensitive; the model does not invent alias mappings or trim identifiers into equality.
key="" is the explicit missing-key marker for this fixture. A numeric zero is a type error, not a missing key. realm=None and event_range=None mean the required namespace or comparable time bound has not been supplied. all_times=() means no tested row covers every time, except when an UNRESOLVED status says the test was not performed; always read the status with the sets. An empty packet is unresolved. A nonempty packet with no matches still establishes only the result of filtering those supplied rows.
Run the complete listing below in a fresh Q1-S session, independent of the original Q1 program. It prints eleven lines. Predict them before consulting the Q1-S worked result.
from dataclasses import dataclass
@dataclass(frozen=True)
class Session:
realm: str
key: str
account: str
start: int
end: int
# Minutes after midnight UTC on the invented date 2040-03-16.
# Keys are harmless classroom labels, not authentication tokens.
SESSIONS = (
Session("realm-a", "K6", "desk-x", 540, 555),
Session("realm-a", "K7", "desk-x", 542, 552),
Session("realm-b", "K7", "desk-x", 542, 552),
)
def nonblank(value):
return isinstance(value, str) and bool(value.strip())
def correlate(rows, realm, account, key, event_range):
"""Filter declared toy rows; do not authenticate sources or identify people."""
if not isinstance(rows, (tuple, list)):
raise ValueError("INVALID_PACKET")
if not rows:
return "UNRESOLVED_EMPTY_PACKET", (), ()
seen = set()
for row in rows:
if not isinstance(row, Session):
raise ValueError("INVALID_SESSION_ROW")
if not all(nonblank(x) for x in (row.realm, row.key, row.account)):
raise ValueError("INVALID_SESSION_IDENTIFIER")
if (type(row.start) is not int or type(row.end) is not int
or not 0 <= row.start < row.end <= 1440):
raise ValueError("INVALID_SESSION_INTERVAL")
identity = (row.realm, row.key)
if identity in seen:
raise ValueError("DUPLICATE_QUALIFIED_KEY")
seen.add(identity)
if not nonblank(account):
raise ValueError("INVALID_ACCOUNT")
if not isinstance(key, str) or (key and not nonblank(key)):
raise ValueError("INVALID_EDIT_KEY")
if realm is None:
return "UNRESOLVED_NAMESPACE", (), ()
if not nonblank(realm):
raise ValueError("INVALID_NAMESPACE")
if event_range is None:
return "UNRESOLVED_TIME_BOUND", (), ()
if (not isinstance(event_range, tuple) or len(event_range) != 2
or any(type(x) is not int for x in event_range)
or not 0 <= event_range[0] <= event_range[1] < 1440):
raise ValueError("INVALID_EVENT_RANGE")
low, high = event_range
possible, all_times = [], []
for row in rows:
if row.realm != realm or row.account != account:
continue
if key and key != row.key:
continue # Never discard a supplied key to force a match.
label = row.realm + "/" + row.key
if low < row.end and high >= row.start:
possible.append(label)
if row.start <= low and high < row.end:
all_times.append(label)
status = "CANDIDATES_IN_SUPPLIED_ROWS" if possible else "NO_MATCH_IN_SUPPLIED_ROWS"
return status, tuple(sorted(possible)), tuple(sorted(all_times))
CASES = (
("QUALIFIED_KEY", SESSIONS, "realm-a", "K7", (547, 547)),
("KEY_ABSENT", SESSIONS, "realm-a", "", (547, 547)),
("NAMESPACE_UNKNOWN", SESSIONS, None, "K7", (547, 547)),
("AT_EXCLUSIVE_END", SESSIONS, "realm-a", "K7", (552, 552)),
("BOUNDARY_UNCERTAIN", SESSIONS, "realm-a", "", (551, 553)),
("BOUND_UNKNOWN", SESSIONS, "realm-a", "K7", None),
("OMITTED_K7", (SESSIONS[0],), "realm-a", "K7", (547, 547)),
("EMPTY_PACKET", (), "realm-a", "K7", (547, 547)),
("DUPLICATE_KEY", SESSIONS + (SESSIONS[1],), "realm-a", "K7", (547, 547)),
("FALSE_KEY", SESSIONS, "realm-a", 0, (547, 547)),
("REVERSED_RANGE", SESSIONS, "realm-a", "K7", (553, 551)),
)
for name, rows, realm, key, bounds in CASES:
try:
status, possible, all_times = correlate(rows, realm, "desk-x", key, bounds)
except ValueError as exc:
print(f"{name}: {exc}")
else:
print(f"{name}: {status} possible={','.join(possible) or '-'} "
f"all_times={','.join(all_times) or '-'}")
The central decision model can be read without Python; an empty packet also stops with an unresolved result:
Validate the supplied rows and field types
| invalid | valid
v v
Report input defect Namespace and time bound supplied?
| no | yes
v v
Unresolved Filter realm and account
|
Keep any explicit key
|
Compare the two time tests
|
Report sets AND remaining limits
Text equivalent: Malformed input cannot support a match. Missing namespace or time information produces an unresolved result. Otherwise preserve the stated realm, account and any explicit key, compute possible and all-times sets, and explain their limits. No branch approves collection, selects a human actor or triggers containment. The implementation reports the first applicable problem rather than proving every untested field valid.
Practice, evidence and assessment
Keep the original Q1 six-point task unchanged. For this separate Q1-S practice, submit the eleven-line transcript and an eleven-row ledger with case | what changed | result | blocked conclusion | smallest repair or follow-up. Then solve four variations without changing the original packet: (A) set the key to K7 in the boundary-uncertain case; (B) use K7 with the exact range (542, 542); (C) use K7 with (552, 553); (D) reverse the input row order for the qualified-key case. For each, predict both candidate sets or the reason no test is possible.
Finally, write one bounded request identifying the source/realm mapping, permitted non-secret correlation fields, event and collection ranges, clock-bound evidence, relevant export omissions and approval owner. Do not request every account or supply a real token. Record which parts of the request are proposals rather than received evidence.
Supplemental rubric, six points: one point for the eleven reproduced lines; one for distinguishing qualified keys and refusing an unknown realm; one for correctly deriving both time tests and the four variations; one for distinguishing invalid input, unresolved prerequisites and no match in a nonempty packet; one for explaining why a unique all-times row does not eliminate a second possible row; and one for the minimal authorized request with explicit source and attribution limits. These points assess Q1-S only. They do not add a numbered EX identifier, change the course's existing 0–2 exercise rubrics or certify an operational investigation.
Synthesis: A useful correlation result states its identity namespace, selection, time assumptions and remaining alternatives. Type checks keep malformed inputs out of the calculation; they do not authenticate evidence. A possible set is not a probability distribution, and an all-times set is not a list of proven actors. Continue with the existing quiet-query laboratory to examine the separate question of source coverage.
2.10 Offline laboratory: when is a quiet-query claim defensible?
Learning purpose: Check evidence release, query scope, ingestion-coverage metadata and an unknown retention anchor without replaying the existing T1/T2 search or E1 hash laboratory.
Classification: Entirely synthetic classroom variant VQ2. This is not a new R-series observation, an amendment to R14, a real ingestion watermark or a production collection.
Environment: Python 3 standard library, in memory. No files, network operations, credentials or service changes.
Input interpretation and acceptance contract
The VQ2 scenarios ask a hypothetical custodian whether a claim of no additional matching edit is even conditionally defensible for the application-audit / desk-a / 09:00 ≤ event < 09:12 UTC scope.
The fixture includes an artificial returned teaching identifier, VQ-01. This is not R9, is not authenticated to R9, and must not be merged with canonical Harbor Library events. A hypothetical ingestion watermark claims coverage through a specified event-time boundary. Even a passing result remains conditional on the custodian's attestation that the source, metadata, clock, collection health and query actually mean what the manifest claims.
A watermark is a claim about ingest progression, not a custody receipt or producer-completeness proof. Treat a quiet-query decision as a sequence of separate checks:
Expected producer events and source health
→ transport / collector continuity
→ ingestion and indexing progress at the declared cutoff
→ query source + account + closed-open UTC time interval
→ actual returned identifiers and query execution receipt
→ narrowly worded inference, with a recheck trigger
Text alternative: The arrow chain shows six necessary inquiries, not six verified controls in the classroom fixture. The code below checks only declared syntax, time ordering, query scope and returned markers. It does not verify producer events, gaps during transport, a collector's health, trustworthy ingestion metadata, the execution receipt or an independently attested query. A passing toy result therefore remains CONDITIONAL_IF_ATTESTED, never proof that no change occurred. A duplicated identifier is not a second independently collected event.
Primary-source context, not certification of VQ2: NIST SP 800-61 Rev. 3 (final, April 2025) treats incident response in the context of cybersecurity risk management; NIST SP 800-92 (September 2006) concerns enterprise log-management practices; the multi-agency Best practices for event logging and threat detection (22 August 2024) covers collection, storage and analysis. Their broad guidance supports scrutinizing evidence coverage; the exact fixture fields, status labels, clock values and expected outputs below are original synthetic teaching conventions, not prescribed NIST/CISA algorithms.
Pre-run predictions:
- The original R14 record has no complete query manifest, so the program cannot certify its scope.
- An early watermark ending at 09:07 cannot establish ingestion completeness for the whole interval ending at 09:12.
- A manifest for another account is irrelevant to a
desk-a-only claim. - A watermark later than its own query-run time is internally inconsistent in this simplified fixture.
- A 24-hour retention period without an anchor never yields a valid deletion deadline.
Reproducible Python 3 code
RELEASE = {"R9": "09:20", "R14": "09:20", "R15": "09:20"}
def minute(clock):
if (
not isinstance(clock, str)
or len(clock) != 5
or clock[2] != ":"
or not (clock[:2] + clock[3:]).isascii()
or not (clock[:2] + clock[3:]).isdigit()
):
raise ValueError("expected an HH:MM clock")
hour, minute_value = int(clock[:2]), int(clock[3:])
if not (0 <= hour < 24 and 0 <= minute_value < 60):
raise ValueError("clock out of range")
return 60 * hour + minute_value
def can_use(record, decision):
return minute(RELEASE[record]) <= minute(decision)
print(
"release:",
"R9@09:05=" + ("YES" if can_use("R9", "09:05") else "NO"),
"R9@09:20=" + ("YES" if can_use("R9", "09:20") else "NO"),
)
base = {
"source": "application-audit",
"account": "desk-a",
"start": "09:00",
"end": "09:12",
"run": "09:20",
"watermark": "09:12",
"returned": ("VQ-01",),
}
variants = [
("CANONICAL_R14", None),
("EARLY_INGESTION", {**base, "run": "09:12", "watermark": "09:07"}),
("HYPOTHETICAL_MANIFEST", base),
("OTHER_ACCOUNT", {**base, "account": "helper-b"}),
("IMPOSSIBLE_WATERMARK", {**base, "run": "09:12", "watermark": "09:16"}),
]
def assess(manifest):
if manifest is None:
return "UNKNOWN_MANIFEST"
if not isinstance(manifest, dict):
return "INVALID_MANIFEST"
required = ("source", "account", "start", "end", "run", "watermark", "returned")
if any(key not in manifest for key in required):
return "INCOMPLETE_MANIFEST"
if not isinstance(manifest["source"], str) or not isinstance(manifest["account"], str):
return "INVALID_SCOPE_FIELDS"
returned = manifest["returned"]
if not isinstance(returned, tuple):
return "INVALID_RESULT_TYPE"
if any(not isinstance(value, str) or not value for value in returned):
return "INVALID_RESULT_VALUE"
if len(returned) != len(set(returned)):
return "DUPLICATE_RESULTS"
try:
run = minute(manifest["run"])
watermark = minute(manifest["watermark"])
start = minute(manifest["start"])
end = minute(manifest["end"])
except (KeyError, ValueError, TypeError):
return "INVALID_CLOCK"
if start >= end:
return "INVALID_QUERY_INTERVAL"
if watermark > run:
return "INCONSISTENT_METADATA"
if (
manifest["source"] != "application-audit"
or manifest["account"] != "desk-a"
or manifest["start"] != "09:00"
or manifest["end"] != "09:12"
):
return "SCOPE_MISMATCH"
if watermark < end:
return "INGESTION_NOT_COVERED"
if manifest["returned"] != ("VQ-01",):
return "RESULT_NOT_AS_ASSUMED"
return "CONDITIONAL_IF_ATTESTED"
for name, manifest in variants:
print(f"{name}: {assess(manifest)}")
policy = {"period_hours": 24, "anchor": None}
print("R15_DEADLINE:", "UNKNOWN" if policy["anchor"] is None else "REQUIRES_VALIDATION")
assert assess(variants[2][1]) == "CONDITIONAL_IF_ATTESTED"
assert policy["anchor"] is None
print("BAD_CLOCK:", assess({**base, "run": "25:99"}))
assert assess({**base, "run": "25:99"}) == "INVALID_CLOCK"
# Additional adversarial teaching manifests. None changes canonical R1–R16.
negative_controls = [
("MISSING_SOURCE", {key: value for key, value in base.items() if key != "source"}, "INCOMPLETE_MANIFEST"),
("MISSING_RESULTS", {key: value for key, value in base.items() if key != "returned"}, "INCOMPLETE_MANIFEST"),
("NOT_A_MAPPING", "application-audit", "INVALID_MANIFEST"),
("BAD_RESULT_TYPE", {**base, "returned": ["VQ-01"]}, "INVALID_RESULT_TYPE"),
("BAD_RESULT_VALUE", {**base, "returned": ("VQ-01", None)}, "INVALID_RESULT_VALUE"),
("DUPLICATE_RESULT", {**base, "returned": ("VQ-01", "VQ-01")}, "DUPLICATE_RESULTS"),
("EXTRA_RESULT", {**base, "returned": ("VQ-01", "VQ-02")}, "RESULT_NOT_AS_ASSUMED"),
]
for name, manifest, expected in negative_controls:
actual = assess(manifest)
print(f"{name}: {actual}")
assert actual == expected, (name, expected, actual)
Exact expected output — executed against the displayed fixture
release: R9@09:05=NO R9@09:20=YES
CANONICAL_R14: UNKNOWN_MANIFEST
EARLY_INGESTION: INGESTION_NOT_COVERED
HYPOTHETICAL_MANIFEST: CONDITIONAL_IF_ATTESTED
OTHER_ACCOUNT: SCOPE_MISMATCH
IMPOSSIBLE_WATERMARK: INCONSISTENT_METADATA
R15_DEADLINE: UNKNOWN
BAD_CLOCK: INVALID_CLOCK
MISSING_SOURCE: INCOMPLETE_MANIFEST
MISSING_RESULTS: INCOMPLETE_MANIFEST
NOT_A_MAPPING: INVALID_MANIFEST
BAD_RESULT_TYPE: INVALID_RESULT_TYPE
BAD_RESULT_VALUE: INVALID_RESULT_VALUE
DUPLICATE_RESULT: DUPLICATE_RESULTS
EXTRA_RESULT: RESULT_NOT_AS_ASSUMED
The first eight output lines are unchanged from the original VQ2 transcript; the following seven are the new adversarial results. The complete displayed Python listing was validated as one program on a synthetic, in-memory fixture with Python 3.13.5. This is a local execution check, not independent expert review, an operational ingestion test or verification of the whole manuscript.
Annotated results and negative controls
releaseestablishes only learner availability of R9: absent at the 09:05 Round A decision and available by the Round B review. The program has no authentic source-ingestion timestamps.CANONICAL_R14lacks sufficient metadata. The correct conclusion is unknown query envelope, not "no edits happened."EARLY_INGESTIONmodels an under-covered interval: the declared watermark ends before the queried interval does. A result at that point cannot exclude events that had not yet arrived at the store.HYPOTHETICAL_MANIFESTis conditional, not confirmed. It accepts a fully specified classroom envelope and declared ingestion boundary but has no external verification that the declaration is truthful or complete.OTHER_ACCOUNTis a negative scope control: evidence forhelper-bcannot establish adesk-anegative finding even if other metadata looks complete.IMPOSSIBLE_WATERMARKrejects contradictory classroom metadata rather than silently treating a future watermark as a real guarantee.R15_DEADLINEremains UNKNOWN. The program intentionally cannot turn a period into a deletion instant while the policy anchor is absent.
Adversarial input diagnosis (same artificial query contract):
| Control | Reason for refusal | Appropriate analyst response |
|---|---|---|
MISSING_SOURCE, MISSING_RESULTS |
A mandatory query scope or result field is absent; the former listing raised KeyError instead of giving the learner a bounded outcome. |
Request the complete original manifest rather than assuming a zero-hit search. |
NOT_A_MAPPING |
Free text is not a structured manifest. | Identify the missing document and ask for its custodian. |
BAD_RESULT_TYPE |
A list was supplied where this fixture requires a tuple; real log APIs can legitimately use other formats. | Validate the source's actual schema; do not generalize the toy tuple rule to production. |
BAD_RESULT_VALUE |
The returned marker tuple contains a non-string; this fixture cannot interpret it as a record identifier. | Request a typed, documented result and execution receipt. |
DUPLICATE_RESULT |
A repeated marker is not independent corroboration. | Preserve the original rows and investigate duplication in the collection/query path. |
EXTRA_RESULT |
VQ-02 violates this fixture's assumption that only VQ-01 is returned. |
Reopen the previously bounded claim and validate the newly reported row; do not call it a real incident event. |
Counterexample and decision boundary: Two scenarios can print the same single marker even though one collector silently missed a producer event and the other retained every producer event. The toy program cannot distinguish them because the manifest does not carry independently verified production, transport and indexing receipts. Formally, let S mean "query scope matches" and W mean "watermark covers the end". S ∧ W is necessary under this lab's assumption but insufficient to prove event completeness. A custodian must separately attest source health, transport continuity, indexing and execution, and disclose remaining gaps. Even such an attestation supports only a scoped observation, not universal absence or a named actor. This is an original illustrative inference, not a numerical probability estimate.
Lab limitations: The minute helper handles only single-day HH:MM timestamps; it is not a timezone or multi-day event parser. It does not authenticate query metadata or establish clock correctness. VQ-01 is a synthetic result marker, not an application audit of R9. A conditional pass tests the logic of the stated envelope; it cannot prove producer completeness, an authorized export or a real incident.
Required submission: Provide the eight original baseline lines and the seven adversarial-control lines, a manifest-assessment table naming one uncertainty for each scenario, a two-sentence explanation of why the 09:05 decision cannot cite R9, and a preservation request that leaves R15's exact deletion deadline unknown. Add a proposed falsifier: identify the metadata or new qualifying row that would invalidate your current quiet-query conclusion. A syntactically plausible but out-of-range clock is not evidence of ingestion coverage.
Pass criteria: Exact reproducibility of all 15 output lines, including missing-field refusals without exceptions; no release-boundary violation; no unsupported retention arithmetic; clear distinction between a declared watermark and authenticated completeness; no merger of VQ2 into R1–R16.
2.11 Contrastive 09:20 decision example
Incorrect handoff:
"R9 proves the employee attacked B19 at 09:03. R14 proves no further edits happened, and R15 means we have exactly until 09:03 tomorrow to preserve the logs. The records are hashed, so we can declare the account compromised and close collection."
The sentence makes several unsupported jumps. R9 is an application-related booking record under an account label, not verified human attribution. R14 has unspecified source/query bounds and does not prove a universal quiet period. R15 does not define its retention anchor. No completed export or digest check for the underlying canonical records is supplied. Even a genuine matching digest would not settle authorization or root cause. The analyst also confuses account-compromise confidence with preservation urgency.
Correct bounded assessment:
"At the simulated 09:20 review, R9 reports a B19 change at 09:03 UTC; R10's supplied training plan names B17/B18 but not B19, and R12 records that the staff member does not recall B19. These observations warrant an authorized training-scope and service-impact check without identifying the human operator. R14 describes a quiet result only within its unspecified query range; R15 reports a 24-hour deletion policy whose anchor remains unknown. The incident lead should seek a bounded availability and preservation decision with the application custodian and review the matter by 09:35 UTC, or sooner on a verified new unexplained edit. No production copy, restriction or completed identity investigation is established."
The better version does not avoid making a decision. It makes a specific, reviewable next decision without asserting evidence that is not present.
2.12 Three-lane handoff for independent review
A handoff is complete when another analyst can trace important claims to sources, identify why an apparently stronger conclusion was rejected, and continue through an authorized next step. Include an exact checkpoint, the original record IDs, owner of the next request, decision status and revisit condition.
A compact handoff has three lanes:
Evidence lane: "R9 reports B19 edited; R10 omits B19 from the supplied plan; R11 records a second patron report; R13 lacks device/location detail." These are statements about the supplied records.
Assessment lane: "Training remains plausible for B17/B18; B19 authorization and human attribution remain unresolved; overall patron impact is not measured." These are interpretations with limits.
Action lane: "Propose P-A; request custodian confirmation of the retention anchor and actual source coverage; ask the service owner to authorize a bounded impact comparison; review at 09:35 or on a verified fresh edit." These are plans, not completed operations.
A receiver should be able to challenge the assessment without questioning whether an unseen action occurred. The handoff should say "requested" when requested, "approved" only when approval is documented, and "collected" only when a collection receipt exists. This discipline also prevents an exercise-only query manifest from being misreported as an operational source observation.
The next chapter may consider containment and recovery decisions, but Module 2 cannot pre-authorize those changes or introduce Round C evidence. Its immediate result is a bounded, preservation-aware decision ready for review.
2.13 Handoff and common errors
A useful handoff lets a second analyst reconstruct the decision without guessing: case ID; current time and timezone; affected service; observed facts with record IDs; live hypotheses; confidence and limits; known service impact; actions completed under which authority; evidence locations; next question; decision owner; review deadline. It should also separate the source's event time from the time the analyst learned of the event.
Common errors are copying only an alert title, silently converting timezones, treating a staff recollection as proof, relying on a log summary that omits key fields or query boundaries, hashing a file without recording which bytes were hashed, or using a high severity label without naming harm and urgency. A careful analyst can be concise: cite the observation, state the limit and identify the next decision. T1 shows why “I discovered R9 at 09:20” does not mean “R9 occurred at 09:20”; conversely, the 09:03 event time does not mean R9 was available for a 09:05 decision. T2 adds a second trap: repeating an event-time query after delayed ingestion can reveal another row without changing the event's occurrence time. The E1 package audit keeps byte integrity, source provenance and authority separate. Q1 adds a cross-source trap: an account/time overlap is a candidate relationship, while a missing or unmatched session key cannot be repaired by confidently naming a person.
Worked checkpoint
Two records may share a booking key and fall inside a declared time window while still leaving the human actor unknown. A quiet query is bounded by source, filter, ingestion cutoff and coverage; without those fields it cannot establish absence.
Practice — Lesson 09
EX022 — Handoff
Objective: allow a second analyst to continue without guessing. Inputs: the Round B snapshot R1–R16 and the fictional approval path. Output: a five-sentence handoff at 09:20 UTC that includes case ID and service, at least two evidence IDs, a live hypothesis and alternative, one uncertainty, an authorized next action, the custodian or owner, and the 09:35 review or an observable earlier trigger. A score of 2 distinguishes a request or proposal from a completed action.
Extension: Preserve the existing five-sentence maximum. Require three distinct verbal states in those sentences: a direct record observation, an assessment with its limitation, and a next action stated as proposed unless actual approval/execution is supplied. Do not imply that an approved classroom exercise is a completed production collection.
For full credit: The handoff names R9, R10 or R11 as appropriate, identifies unresolved B19 authorization, keeps R15's deadline uncertain and states the 09:35 review or earlier verified trigger.
HOLD2 supplementary task (after the required artifact): Preserve the five-sentence maximum at 09:20, with case ID, two evidence IDs, competing hypotheses, uncertainty, a proposed authorized next action, owner and 09:35 or earlier review. Within those sentences, say whether P-A/P-B are merely proposed and whether there is any confirmed source-hold effect. Do not insert HOLD2's simulated positive result or later R17–R24 evidence.
For full credit: The handoff gives a second analyst a clear request to the custodian about the R15 policy anchor and source-hold feasibility, without claiming a completed hold or an exact deletion instant.
Worked answer: EX022 in the final appendix.
EX024 — Escalation threshold
Objective: define a testable escalation rule. Inputs: the 09:20 case state and R15's retention notice. Output: two hypothetical observation/response pairs, each naming who verifies the new signal, why it raises harm or urgency, who decides on the appropriate response and when they reconvene. Containment and preservation require separate decisions. Do not claim either event happened. A score of 2 uses observable triggers rather than “if things get worse.”
HOLD2/TRI2 refinement: Keep exactly two hypothetical observation/response pairs. One concerns a future source-verified unexplained edit or owner-confirmed material booking mismatch, followed by human review of proportionate, separately authorized containment. The other concerns a future custodian-confirmed source-preservation failure, inability to retrieve the selected oldest item, or inability to clarify a material retention risk through the approved escalation route. Name the source, verifier, harm or loss rationale, decision owner and earlier review trigger for each. Neither trigger is observed in R1–R16.
For full credit: A source-hold failure trigger is distinct from confirmed malicious activity or proven deletion. The incident lead coordinates an authorized preservation alternative, not an automatic disruptive action.
Worked answer: EX024 in the final appendix.
Continue studying — lesson 09 of at least 20
← Lesson 08 · Course lesson index · Lesson 10 →
Continue to the next authored daily lesson.