Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
```
Expand Down
47 changes: 47 additions & 0 deletions docs/exercises/README.md
Original file line number Diff line number Diff line change
@@ -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 |
| --- | --- | --- | --- |
50 changes: 50 additions & 0 deletions docs/exercises/TEMPLATE.md
Original file line number Diff line number Diff line change
@@ -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-domain-its-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.
Loading