L08 — Graph engineering: multi-target campaigns¶
L07's loop still runs one target at a time, in sequence. Real security assessments rarely look like that — a single engagement touches multiple hosts, some findings on one target unlock access to another, and the right next step depends on what was just found, not on a fixed schedule. This lecture is about representing that — a campaign, not a single task — as a graph the harness can reason over, instead of a list the harness marches through.
Why a list breaks down¶
A task list assumes each task is independent: run task 3 whether or not task 2 succeeded. A real campaign has dependencies — "task 3 (access the internal admin panel) only makes sense after task 2 (find valid credentials) succeeds," and "if task 2 fails three different ways, that's itself worth recording as a finding, not just a blocked task." A list has no way to express either relationship.
flowchart TD
T1[Recon: enumerate<br/>open services] --> T2[Find exposed<br/>admin endpoint]
T2 --> T3{Credentials<br/>required?}
T3 -->|yes| T4[Attempt credential<br/>exploitation — IDOR, default creds]
T3 -->|no| T5[Direct access —<br/>record as critical finding]
T4 -->|success| T6[Access internal panel]
T4 -->|fail: 3 methods tried| T7[Record as verified-hardened,<br/>mine as EvoBench task]
T6 --> T8[New scope: grant internal<br/>network as new Capability]
T8 --> T9[Recon on internal network —<br/>graph extends itself]
Node T8 is the important one: succeeding at T6 changes what scope
(L02) the next node is allowed —
the graph isn't fixed upfront, it grows based on verified results, with
each new grant still going through the same deny-by-default ScopeEnforcer
as every other capability in this course.
What this changes about the four subsystems¶
State carries more structure. A flat StateStore key per finding
(L04) still works, but now the harness also
needs to record the edges — which finding unlocked which subsequent task
— so a later session (or a human reviewing the campaign) can reconstruct
the actual attack path, not just a bag of unordered findings.
Scope grows monotonically, never shrinks mid-campaign. T8 above
adds a capability; nothing in a sound campaign graph should remove one
mid-run — if a target needs to be excluded, that's a new campaign with a
smaller starting scope, not a scope that contracts while an agent is mid-
task and might be relying on access it's about to lose.
Verification happens per-node, not just at the end. A campaign that
only verifies its final claimed outcome can't tell you where in a nine-
node chain things actually went wrong. Each node needs its own
Verifier call — the same discipline
as a single task, just applied at every edge instead of once.
Lifecycle becomes per-node, with the campaign itself as the outer
session. SessionLifecycle.init/checkpoint/teardown from
L05 still applies to the whole
campaign, but each node benefits from its own inner checkpoint — losing
node 7 of 9 shouldn't force a restart from node 1.
The honest caveat¶
Nothing in this course's current tools implements graph state directly —
ReceiptLedger stores
flat key-value receipts, not a graph structure. Representing the campaign
graph today means layering it on top: store each node as a receipt keyed by
a campaign-scoped id, and store edges as a separate receipt listing
(from_node, to_node, unlocked_by):
ledger.append(f"campaign-{cid}:node:T6", {"result": "access internal panel", "verified": True}, by="harness")
ledger.append(f"campaign-{cid}:edge:T4-T6", {"from": "T4", "to": "T6", "unlocked_by": "credential-exploit"}, by="harness")
Reconstructing the graph from this means rebuilding adjacency from every
edge receipt — but Ledger only supports exact-key lookup (head,
history), not enumerating keys by pattern, so nothing in ReceiptLedger
itself can answer "give me every edge in campaign cid." That's the same
gap EvoBench's Suite works
around with its own manifest file listing known task ids
(L06) — a campaign graph would need the
same kind of manifest, tracking known node and edge ids separately from the
ledger that stores their content. A purpose-built graph layer that does
this automatically, the way evobench.Suite does for flat regression
tasks, is the natural next tool here — not yet built. This is the multi-target
analogue of Open Problem #4 (cross-harness session
handoff): the interfaces this course defines are sound per-node, and
composing them into a graph is still manual.
This is the last lecture in the course for now. From here: the two Experiments apply everything to real data, and Open Problems lists what's left to build — including the graph-state gap this lecture just described.