# Diagrams — Module S04: CTF Harness Engineering

> All Mermaid validated in Mermaid Live Editor. n8n JSON is structurally valid and importable.

---

## Diagram 1 — The CTF Domain Router (n8n)

**Type**: n8n workflow (importable JSON)
**Purpose**: The primary visual — the meta-agent-plus-specialists architecture. Shows a challenge entering the router, being classified into one of six domains, and dispatched to the matching specialized agent with its tool subset.

```json
{
  "name": "S04 — CTF Domain Router",
  "nodes": [
    { "name": "Challenge In", "type": "n8n-nodes-base.manualTrigger", "position": [200, 360], "notes": "Challenge description + attached artifacts (binary, pcap, image, URL)" },
    { "name": "Meta-Agent / Classifier", "type": "n8n-nodes-base.set", "position": [440, 360], "notes": "Reads description + file-type signal. Emits domain label. Routes to specialist. Allowed to be uncertain." },
    { "name": "WebAgent", "type": "n8n-nodes-base.set", "position": [760, 80], "notes": "Burp headless, sqlmap, ffuf, curated XSS payloads. Request-send-inspect loop." },
    { "name": "PwnAgent", "type": "n8n-nodes-base.set", "position": [760, 200], "notes": "pwntools, GDB+pwndbg, checksec, ROPgadget, one_gadget. checksec-before-reasoning loop." },
    { "name": "CryptoAgent", "type": "n8n-nodes-base.set", "position": [760, 320], "notes": "CyberChef recipes, SageMath, frequency analyzers, padding-oracle scripts." },
    { "name": "ForensicsAgent", "type": "n8n-nodes-base.set", "position": [760, 440], "notes": "binwalk, strings, exiftool, tshark, Volatility. Artifact-triage loop." },
    { "name": "ReversingAgent", "type": "n8n-nodes-base.set", "position": [760, 560], "notes": "Ghidra headless, radare2/rizin, ltrace, strace, gdb. Decompile-triage loop. Heaviest context consumer." },
    { "name": "MiscAgent", "type": "n8n-nodes-base.set", "position": [760, 680], "notes": "zsteg, stegsolve, CyberChef, custom Python. Variable reasoning loop." },
    { "name": "TriageAgent (uncertain)", "type": "n8n-nodes-base.set", "position": [440, 540], "notes": "When meta-agent is uncertain: runs cheap probes (file, checksec, strings freq, HTTP code) then re-routes." },
    { "name": "Flag Extractor (middleware)", "type": "n8n-nodes-base.set", "position": [1020, 360], "notes": "Regex sweep over every tool output. Surfaces flag candidates to verifier." },
    { "name": "Scoreboard (CTFd)", "type": "n8n-nodes-base.httpRequest", "position": [1240, 360], "notes": "POST /api/v1/challenges/attempt with token. Rate-limited. correct/incorrect/already_solved handling." }
  ],
  "connections": {
    "Challenge In": { "main": [[{ "node": "Meta-Agent / Classifier", "type": "main", "index": 0 }]] },
    "Meta-Agent / Classifier": { "main": [
      [{ "node": "WebAgent", "type": "main", "index": 0 }],
      [{ "node": "PwnAgent", "type": "main", "index": 0 }],
      [{ "node": "CryptoAgent", "type": "main", "index": 0 }],
      [{ "node": "ForensicsAgent", "type": "main", "index": 0 }],
      [{ "node": "ReversingAgent", "type": "main", "index": 0 }],
      [{ "node": "MiscAgent", "type": "main", "index": 0 }],
      [{ "node": "TriageAgent (uncertain)", "type": "main", "index": 0 }]
    ] },
    "TriageAgent (uncertain)": { "main": [[{ "node": "Meta-Agent / Classifier", "type": "main", "index": 0 }]] },
    "WebAgent": { "main": [[{ "node": "Flag Extractor (middleware)", "type": "main", "index": 0 }]] },
    "PwnAgent": { "main": [[{ "node": "Flag Extractor (middleware)", "type": "main", "index": 0 }]] },
    "CryptoAgent": { "main": [[{ "node": "Flag Extractor (middleware)", "type": "main", "index": 0 }]] },
    "ForensicsAgent": { "main": [[{ "node": "Flag Extractor (middleware)", "type": "main", "index": 0 }]] },
    "ReversingAgent": { "main": [[{ "node": "Flag Extractor (middleware)", "type": "main", "index": 0 }]] },
    "MiscAgent": { "main": [[{ "node": "Flag Extractor (middleware)", "type": "main", "index": 0 }]] },
    "Flag Extractor (middleware)": { "main": [[{ "node": "Scoreboard (CTFd)", "type": "main", "index": 0 }]] }
  }
}
```

**Reading the diagram**: Left to right. A challenge enters the meta-agent with its description and attached artifacts. The meta-agent emits a domain label and dispatches to one of six specialized agents, each with its own prompt and tool subset. If the meta-agent cannot route with confidence, the challenge goes to the TriageAgent, which runs cheap probes (file type, checksec, strings frequency, HTTP response code) and re-routes with the added signal. Every specialized agent's tool output flows through the Flag Extractor middleware, which scans for flag patterns and submits verified candidates to the CTFd scoreboard. The router is allowed to be uncertain; the agents are not.

---

## Diagram 2 — Per-Domain Tool Stacks

**Type**: Mermaid (flowchart)
**Purpose**: The six domains side by side, with their canonical tool stacks and the reasoning loop that distinguishes each. The diagram a student pins above the desk.

```mermaid
flowchart LR
    subgraph WEB["Web"]
        W1["Burp headless"]
        W2["sqlmap"]
        W3["ffuf"]
        W4["curated XSS set"]
        WL["loop:<br/>send - inspect - escalate"]
        W1 --> WL
        W2 --> WL
        W3 --> WL
        W4 --> WL
    end
    subgraph PWN["Pwn"]
        P1["checksec"]
        P2["pwntools"]
        P3["GDB + pwndbg"]
        P4["ROPgadget / one_gadget"]
        PL["loop:<br/>protections - offset - chain"]
        P1 --> PL
        P2 --> PL
        P3 --> PL
        P4 --> PL
    end
    subgraph CRY["Crypto"]
        C1["CyberChef recipes"]
        C2["SageMath"]
        C3["frequency analyzers"]
        C4["padding-oracle scripts"]
        CL["loop:<br/>weakness - math exploit"]
        C1 --> CL
        C2 --> CL
        C3 --> CL
        C4 --> CL
    end
    subgraph FOR["Forensics"]
        F1["file / binwalk"]
        F2["strings"]
        F3["exiftool"]
        F4["tshark / Volatility"]
        FL["loop:<br/>triage - extract - search"]
        F1 --> FL
        F2 --> FL
        F3 --> FL
        F4 --> FL
    end
    subgraph REV["Reversing"]
        R1["Ghidra headless"]
        R2["radare2 / rizin"]
        R3["ltrace / strace"]
        R4["gdb"]
        RL["loop:<br/>decompile - locate - invert"]
        R1 --> RL
        R2 --> RL
        R3 --> RL
        R4 --> RL
    end
    subgraph MIS["Misc"]
        M1["zsteg"]
        M2["stegsolve"]
        M3["CyberChef"]
        M4["custom Python"]
        ML["loop:<br/>medium - canonical transforms"]
        M1 --> ML
        M2 --> ML
        M3 --> ML
        M4 --> ML
    end

    style WEB fill:#0d1b2a,stroke:#1b4f72,color:#e4e4e8
    style PWN fill:#0d1b2a,stroke:#1b4f72,color:#e4e4e8
    style CRY fill:#0d1b2a,stroke:#1b4f72,color:#e4e4e8
    style FOR fill:#0d1b2a,stroke:#1b4f72,color:#e4e4e8
    style REV fill:#0d1b2a,stroke:#1b4f72,color:#e4e4e8
    style MIS fill:#0d1b2a,stroke:#1b4f72,color:#e4e4e8
    style WL fill:#14141f,stroke:#5eead4,color:#5eead4
    style PL fill:#14141f,stroke:#5eead4,color:#5eead4
    style CL fill:#14141f,stroke:#5eead4,color:#5eead4
    style FL fill:#14141f,stroke:#5eead4,color:#5eead4
    style RL fill:#14141f,stroke:#5eead4,color:#5eead4
    style ML fill:#14141f,stroke:#5eead4,color:#5eead4
```

**Reading the diagram**: Six domains, each a self-contained subgraph. The tools inside each subgraph are loaded only when the router dispatches to that domain — they are not present in other agents' manifests. The teal box at the bottom of each is the domain's reasoning loop, encoded into the specialized prompt: Web iterates send-inspect-escalate, Pwn runs checksec before anything else, Crypto hunts a mathematical weakness, Forensics triages artifacts before extracting, Reversing decompiles and locates the comparison to invert, Misc tries canonical transforms against the encoding medium. Reversing is the heaviest context consumer (decompiler output saturates fast); Forensics rarely saturates.

---

## Diagram 3 — HITL vs Autonomous: The Speed Comparison

**Type**: Mermaid (flowchart, comparison)
**Purpose**: Side-by-side timing of the same challenge solved with approval gates versus without. Shows where the time goes and why HITL removal matters in CTF contexts — and the single gate that stays even in autonomous mode.

```mermaid
flowchart TD
    START["Challenge dispatched<br/>to PwnAgent"]

    START --> A1["Agent: I will run checksec"]
    A1 --> GATE1{"HITL gate?<br/>approve"}
    GATE1 -->|"wait 25s for human"| A2["checksec executes (2s)"]
    GATE1 -.->|"autonomous: skip gate"| A2B["checksec executes (2s)"]
    A2 --> A3["Agent: I will run ROPgadget"]
    A2B --> A3
    A3 --> GATE2{"HITL gate?<br/>approve"}
    GATE2 -->|"wait 30s for human"| A4["ROPgadget executes (5s)"]
    GATE2 -.->|"autonomous: skip gate"| A4B["ROPgadget executes (5s)"]
    A4 --> A5["Agent: send exploit payload"]
    A4B --> A5
    A5 --> GATE3{"HITL gate?<br/>approve"}
    GATE3 -->|"wait 28s for human"| A6["payload executes (1s)"]
    GATE3 -.->|"autonomous: skip gate"| A6B["payload executes (1s)"]
    A6 --> FLAG["Flag extracted"]
    A6B --> FLAG

    KEEP["Credential reuse gate<br/>STAYS in autonomous mode"]
    KEEP -.->|"even with HITL removed"| CRED["Discovered creds are NOT<br/>auto-reused against other services"]

    style GATE1 fill:#2a1810,stroke:#a04000,color:#f0a868
    style GATE2 fill:#2a1810,stroke:#a04000,color:#f0a868
    style GATE3 fill:#2a1810,stroke:#a04000,color:#f0a868
    style A2B fill:#0d2818,stroke:#1e8449,color:#82e0aa
    style A4B fill:#0d2818,stroke:#1e8449,color:#82e0aa
    style A6B fill:#0d2818,stroke:#1e8449,color:#82e0aa
    style KEEP fill:#2a0d0d,stroke:#a00000,color:#f08080
    style FLAG fill:#14141f,stroke:#5eead4,color:#5eead4
```

**Reading the diagram**: Left column is HITL-enabled, right column (dotted) is autonomous. The same three tool calls — checksec, ROPgadget, exploit payload — take roughly 85 seconds of wall-clock time waiting for human approval in HITL mode, versus 8 seconds of pure execution in autonomous mode. That is the budget that decides whether the harness is competitive. The bottom node is the exception that proves the rule: the credential-reuse gate stays even when every other gate is removed, because credential reuse against other services is the one unbounded-risk action in a CTF context. Remove the speed-killing gates; keep the blast-radius gate.

---

## Diagram 4 — The Flag Extraction Pipeline

**Type**: Mermaid (flowchart)
**Purpose**: What happens between "tool produced output" and "flag submitted to scoreboard". Shows the regex sweep, the decoy-filtering verifier, the rate-limited submission client, and the three CTFd response states.

```mermaid
flowchart TD
    TOOL["Tool output<br/>(any domain agent)"]
    SWEEP["Regex sweep<br/>over structured output"]
    PATTERNS["Flag patterns:<br/>flag{...}, CTF{...},<br/>competition prefix"]
    CANDIDATES["Flag candidates<br/>(0, 1, or many)"]

    TOOL --> SWEEP
    PATTERNS --> SWEEP
    SWEEP --> CANDIDATES

    CANDIDATES -->|"0 found"| CONTINUE["Agent continues solving"]
    CANDIDATES -->|"1+ found"| VERIFIER{"Verifier:<br/>plausibly the answer<br/>to THIS challenge?"}

    VERIFIER -->|"decoy / example / description text"| DROP["Drop<br/>log, do not submit"]
    VERIFIER -->|"plausible true flag"| SUBMIT["Submission client<br/>POST to CTFd"]

    SUBMIT --> RATE["Rate limiter<br/>2-3s between submissions<br/>per-challenge lockout after N wrong"]
    RATE --> CTFD{"CTFd response"}

    CTFD -->|"correct"| SOLVED["Mark solved<br/>kill parallel attempts<br/>free slot"]
    CTFD -->|"incorrect"| LOG["Log attempt<br/>agent continues"]
    CTFD -->|"already_solved"| TREAT["Treat as correct<br/>do not re-submit"]

    style SWEEP fill:#14141f,stroke:#5eead4,color:#5eead4
    style VERIFIER fill:#0d1b2a,stroke:#1b4f72,color:#e4e4e8
    style RATE fill:#2a1810,stroke:#a04000,color:#f0a868
    style SOLVED fill:#0d2818,stroke:#1e8449,color:#82e0aa
    style DROP fill:#2a0d0d,stroke:#a00000,color:#f08080
    style CTFD fill:#0d1b2a,stroke:#1b4f72,color:#e4e4e8
```

**Reading the diagram**: Every tool output — every one, from every agent — passes through the regex sweep. The sweep uses configurable patterns (the competition publishes its flag format). Zero matches means the agent keeps solving. One or more matches go to the verifier, which exists because challenge descriptions, examples, and decoy output all contain flag-shaped strings that would burn submission attempts. The verifier confirms plausibility before the submission client touches the scoreboard. The submission client is rate-limited (2–3 seconds between submissions, per-challenge lockout after N wrong attempts) to avoid self-inflicted throttling. CTFd returns one of three states: correct (mark solved, kill parallel attempts, free the slot), incorrect (log and continue), already_solved (treat as correct — a parallel session or teammate got there first; do not re-submit).

---

## Diagram 5 — Parallel Solve Architecture (multi-challenge)

**Type**: Mermaid (flowchart)
**Purpose**: The full multi-challenge parallel harness. Shows the challenge pool, the concurrency-limited dispatcher, isolated sessions running different domains concurrently, the intra-challenge parallelism pattern (two hypotheses, first flag wins), and the single scorecard that gives the operator visibility into all of it.

```mermaid
flowchart TD
    POOL["Challenge pool<br/>fetched from CTFd"]
    DISP["Dispatcher<br/>concurrency limit N<br/>(typically 3-5 slots)"]

    POOL --> DISP

    DISP --> SA["Session A"]
    DISP --> SB["Session B"]
    DISP --> SC["Session C"]
    DISP --> SD["Session D"]

    SA --> RA["Router -> WebAgent<br/>Challenge 1"]
    SB --> RB["Router -> PwnAgent<br/>Challenge 2"]
    SC --> RC1["Router -> PwnAgent<br/>Challenge 3: stack overflow hypothesis"]
    SD --> RC2["Router -> PwnAgent<br/>Challenge 3: format string hypothesis"]

    RA --> FA["FlagExtractor"]
    SB --> FB["FlagExtractor"]
    SC --> FC1["FlagExtractor"]
    SD --> FC2["FlagExtractor"]

    FA --> SCORE["Scorecard<br/>(operator view)"]
    FB --> SCORE
    FC1 --> SCORE
    FC2 --> SCORE

    WIN{"First flag<br/>from C1 or C2"}
    RC1 -.->|"wins"| WIN
    RC2 -.->|"loses -> KILL"| KILL["Session killed<br/>slot freed"]
    WIN --> SCORE

    SCORE --> NEXT["Pull next challenge<br/>when slot frees"]
    NEXT --> DISP

    style DISP fill:#14141f,stroke:#5eead4,color:#5eead4
    style SCORE fill:#0d1b2a,stroke:#1b4f72,color:#e4e4e8
    style WIN fill:#0d2818,stroke:#1e8449,color:#82e0aa
    style KILL fill:#2a0d0d,stroke:#a00000,color:#f08080
```

**Reading the diagram**: The challenge pool feeds a concurrency-limited dispatcher (3–5 slots, bounded by compute and per-team rate limits). Each slot runs an isolated session with its own router dispatch, specialized agent, and flag extractor. Sessions C and D show intra-challenge parallelism: the same challenge, two PwnAgents pursuing two hypotheses (stack overflow vs format string), first flag wins, the losing session is killed and its slot freed. Sessions do not share memory — sharing would reintroduce the context pollution that domain routing was designed to eliminate. Every session reports to a single scorecard, which is the operator's only sane view into five concurrent solve attempts. When a slot frees (challenge solved or hypothesis lost), the dispatcher pulls the next challenge from the pool. The scoreboard moves in parallel; the scorecard is how you keep track of it.
