From 3c58a1d263ecb912e033897978d58e0443bff713 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 14 Oct 2026 10:41:30 +0000 Subject: [PATCH 1/2] docs(exercises): a record and template for purple-team exercises docs/exercises/ holds one dated file per exercise against the lab domain: techniques with ATT&CK IDs, the detections expected before the run, what Wazuh, Velociraptor, Zeek, Suricata and Loki actually saw, and the gaps that open issues. Files are records like the changelog, describe activity rather than payloads, and stay inside the lab. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01A4769sNhNE79DH3SBN4YXx --- CLAUDE.md | 3 ++- README.md | 1 + docs/exercises/README.md | 47 +++++++++++++++++++++++++++++++++++ docs/exercises/TEMPLATE.md | 50 ++++++++++++++++++++++++++++++++++++++ 4 files changed, 100 insertions(+), 1 deletion(-) create mode 100644 docs/exercises/README.md create mode 100644 docs/exercises/TEMPLATE.md diff --git a/CLAUDE.md b/CLAUDE.md index ab435098..53fcc056 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -32,7 +32,8 @@ Two kinds of document, edited differently: first, and a correction is a new entry that names the one it corrects. An accepted ADR in `docs/adr/` is immutable: a changed decision is a new ADR that supersedes it. An old ADR that says something has not happened yet - stays as written. + stays as written. An exercise in `docs/exercises/` is a record too: a re-run + is a new file. - **Current-state documents** must stay true: `README.md`, `docs/hardware.md`, `docs/network.md`, `docs/architecture.md`, `docs/security.md`, `docs/roadmap.md`, `docs/runbooks/` and diff --git a/README.md b/README.md index 01cf6cef..1dbd0d37 100644 --- a/README.md +++ b/README.md @@ -224,6 +224,7 @@ inbound passes may still reach it. Data flow and the maintained Mermaid topology │ ├── observability.md security.md roadmap.md changelog.md │ ├── diagrams/ # the network diagram (SVG) and its predecessors │ ├── adr/ # 94 ADRs — architecture decision records +│ ├── exercises/ # purple-team exercise records — see its README │ └── runbooks/ # 49 runbooks; successor-handover.md is the front door └── Makefile # make help ``` diff --git a/docs/exercises/README.md b/docs/exercises/README.md new file mode 100644 index 00000000..80dc6d7c --- /dev/null +++ b/docs/exercises/README.md @@ -0,0 +1,47 @@ +# Exercises + +One file per purple-team exercise against the lab domain: what was run, +what the estate was expected to see, what it actually saw, and what changed +because of it. The point is the gap between *expected* and *observed*. An +exercise that finds no gap is still worth a file, because it proves a +detection works on a date. + +## The rules + +- **One exercise, one file**, named `YYYY-MM-DD-short-name.md`, copied from + [`TEMPLATE.md`](TEMPLATE.md). +- **An exercise file is a record.** Like [`changelog.md`](../changelog.md), it + is never rewritten after the day. A re-run is a new file that links the + earlier one, so the history shows a detection being fixed, not just the end + state. +- **Every technique carries its MITRE ATT&CK ID**, so exercises can be searched + by technique and coverage can be counted. +- **Only inside the lab.** Every target is on the lab domain or the range + ([ADR-0007](../adr/0007-defensive-estate-and-offensive-range.md), + [ADR-0014](../adr/0014-put-ifrit-on-imaginationlan-and-give-the-targets-no-route.md)). + Nothing here is run against the household tiers or anything outside the + house. +- **Describe the activity, not the payload.** Name the technique and the tool + and say what was done. No working exploit code, no captured credentials or + hashes, no tokens. This repository is public, and + [`security.md`](../security.md#what-this-repository-deliberately-does-not-publish) + applies here as everywhere. +- **A missed or weak detection opens an issue.** The issue is the follow-up; + the exercise file links it. + +## Where the evidence comes from + +| Source | Lives on | What it sees | +| --- | --- | --- | +| Wazuh | `odin` ([`stacks/soc`](../../stacks/soc)) | Agent alerts from the domain's hosts, Sysmon included | +| Velociraptor | `odin` ([`stacks/soc`](../../stacks/soc)) | Endpoint state, collected on demand | +| Zeek | `fenrir` ([`stacks/sensor`](../../stacks/sensor)) | Network metadata from the mirrored lab bridge | +| Suricata | `morpheus` | Alerts at the firewall, the chokepoint ([ADR-0006](../adr/0006-detect-at-the-chokepoint.md)) | +| Loki and Grafana | `alexander` ([`stacks/lab`](../../stacks/lab)) | The lab's logs, searchable by time window | + +## Index + +Newest first. Add a row with each new file. + +| Date | Exercise | Techniques | Result | +| --- | --- | --- | --- | diff --git a/docs/exercises/TEMPLATE.md b/docs/exercises/TEMPLATE.md new file mode 100644 index 00000000..6385f94d --- /dev/null +++ b/docs/exercises/TEMPLATE.md @@ -0,0 +1,50 @@ +# YYYY-MM-DD: Short name of the exercise + +**Date:** YYYY-MM-DD · **Operator:** you · **Issue:** #N, if one exists · +**Re-run of:** link to an earlier exercise, or *none* + +## Objective + +One or two sentences: what this exercise tests, and which detection it is +meant to prove or disprove. + +## Scope + +- **Attacker:** the host or VM the activity came from. +- **Targets:** the lab hosts it touched. Lab domain or range only. +- **Window:** start and end time, with the time zone, so the logs can be + pulled for exactly this period. +- **Lab state:** anything switched on for this run, such as which deliberate + weakness tags were enabled + ([ADR-0083](../adr/0083-give-the-lab-domains-deliberate-weaknesses-as-switchable-tags.md)). + +## Techniques + +One row per step, in the order run. Describe the activity, not the payload. + +| # | Time | ATT&CK ID | Technique | Tool | What was done | +| --- | --- | --- | --- | --- | --- | +| 1 | hh:mm | T0000 | Name | Tool | One line | + +## Expected detections + +Written before the run, so the result cannot shape the expectation. + +| # | Source | Expected rule or signal | +| --- | --- | --- | +| 1 | Wazuh / Zeek / Suricata / Velociraptor / Loki | Rule ID or query, and what it should show | + +## Observed + +| # | Detected? | Where, and how long after the step | Notes | +| --- | --- | --- | --- | +| 1 | Yes / Partly / No | Source and delay | Alert text summarised, not pasted with secrets | + +## Gaps and fixes + +For every *No* or *Partly* above: why it was missed, and what changes. +Each fix links the issue or PR that makes it. + +## Lessons + +What this exercise taught, in a few lines. From 187c9741c2c5058b26dfe4feebc18ac9f104f678 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 14 Oct 2026 10:46:18 +0000 Subject: [PATCH 2/2] docs(exercises): fix the ADR-0083 link in the template The template pointed at 0083-give-the-lab-domains-... ; the accepted file is 0083-give-the-lab-domain-its-... . Every exercise copied from the template would have carried the broken link. Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_01A4769sNhNE79DH3SBN4YXx --- docs/exercises/TEMPLATE.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/exercises/TEMPLATE.md b/docs/exercises/TEMPLATE.md index 6385f94d..9be98a24 100644 --- a/docs/exercises/TEMPLATE.md +++ b/docs/exercises/TEMPLATE.md @@ -16,7 +16,7 @@ meant to prove or disprove. pulled for exactly this period. - **Lab state:** anything switched on for this run, such as which deliberate weakness tags were enabled - ([ADR-0083](../adr/0083-give-the-lab-domains-deliberate-weaknesses-as-switchable-tags.md)). + ([ADR-0083](../adr/0083-give-the-lab-domain-its-deliberate-weaknesses-as-switchable-tags.md)). ## Techniques