category: fleet ยท status: shipped (v0.1, telemetry-sim mode) ยท updated: 2026-11-02

Purple Loop (fleet/purple-loop/)

The red-trains-blue loop nobody has shipped: a red agent generates randomized ATT&CK attacks, the attacks are rendered into Sysmon-style telemetry (or run in a real Windows lab), and a blue agent hunts them โ€” both trajectories are kept as verified training data. This is the training-loop closure our [[landscape-2026]] dossier identified as unclaimed white space (Simbian benchmarked blue but trains nothing; CyberGym-E2E is eval-only).

The loop


                 โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
                 โ”‚                    PURPLE LOOP (per run)                 โ”‚
                 โ”‚                                                          โ”‚
  seed + โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ–บโ”‚  attack_gen.py          RED                              โ”‚
  weekly refresh โ”‚  โ”€โ”€ pick N atomics (T1003, T1059, T1105, T1560, ...)     โ”‚
                 โ”‚  โ”€โ”€ randomize: file names, paths, payload hashes,        โ”‚
                 โ”‚     hosts/users/domains, GUIDs, ports, timing            โ”‚
                 โ”‚  โ”€โ”€ emit attack_plan.json (kill-chain ordered)           โ”‚
                 โ”‚        โ”‚                                                 โ”‚
                 โ”‚        โ–ผ                                                 โ”‚
                 โ”‚  telemetry_sim.py      BRIDGE                            โ”‚
                 โ”‚  โ”€โ”€ Sysmon-style JSON: EID 1/3/5/7/10/11/13/22           โ”‚
                 โ”‚     (process trees, image loads, file writes, registry,  โ”‚
                 โ”‚      network, DNS โ€” real, decodable command lines)       โ”‚
                 โ”‚  โ”€โ”€ benign noise: synthetic fleet, or bootstrap from an  โ”‚
                 โ”‚     exported real stream (e.g. BOTSv3, CC0)              โ”‚
                 โ”‚  โ”€โ”€ randomized injection point + per-step dwell jitter   โ”‚
                 โ”‚  โ”€โ”€ ground_truth.json  โ—„โ”€โ”€ PRIVATE, never shown to blue  โ”‚
                 โ”‚        โ”‚                      (labels: technique, guid,  โ”‚
                 โ”‚        โ”‚                       timestamps, narrative steps)โ”‚
                 โ”‚        โ–ผ                                                 โ”‚
                 โ”‚  โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”     โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”  โ”‚
                 โ”‚  โ”‚ hunt_env.py     โ”‚ BLUEโ”‚ sigma_gen.py   DETECTOR   โ”‚  โ”‚
                 โ”‚  โ”‚ DuckDB(SQLite)  โ”‚     โ”‚ Sigma YAML in             โ”‚  โ”‚
                 โ”‚  โ”‚ 50-query budget โ”‚     โ”‚  โ†’ SQL over events        โ”‚  โ”‚
                 โ”‚  โ”‚ 10-row results  โ”‚     โ”‚  โ†’ EQL equivalent         โ”‚  โ”‚
                 โ”‚  โ”‚ verdicts = tech โ”‚     โ”‚  โ†’ P/R/F1 vs labels       โ”‚  โ”‚
                 โ”‚  โ”‚  + entity + win โ”‚     โ”‚   (per technique)         โ”‚  โ”‚
                 โ”‚  โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜     โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜  โ”‚
                 โ”‚           โ”‚ grader: coverage (Simbian-style narrative   โ”‚
                 โ”‚           โ”‚ steps), precision, per-technique/tactic     โ”‚
                 โ”‚           โ”‚ recall, attribution gap                    โ”‚
                 โ”‚           โ–ผ                                            โ”‚
                 โ”‚  trajectories/: red plan + blue transcript + detector  โ”‚
                 โ”‚  scores โ€” BOTH SIDES' trajectories kept for SFT/RLVR    โ”‚
                 โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

  improvement direction: red variants that survive the blue grader raise the
  bar; blue hunts/detectors that catch more variants raise theirs. Filtered
  both ways โ†’ verified trajectories for post-training (see ../../RESULTS.md).

Files

filerole
attack_gen.pyred: pick atomics, emit randomized per-run variants (plan.json)
telemetry_sim.pybridge: synthesize Sysmon-style events.jsonl + private ground_truth.json
hunt_env.pyblue: SQL threat-hunt environment + grader + reference agent
sigma_gen.pydetector: Sigma YAML โ†’ SQL/EQL โ†’ precision/recall vs labeled stream
rules/sample Sigma rules (fire on the embedded catalog)
smoke.shend-to-end: attack โ†’ telemetry โ†’ hunt โ†’ sigma

Quick start


cd fleet/purple-loop
./smoke.sh                       # full loop, stdlib-only (SQLite path)
python3 attack_gen.py --seed 20261102 --num-atomics 6 --out runs/demo/plan.json
python3 telemetry_sim.py --plan runs/demo/plan.json --noise-events 6000 --out-dir runs/demo
python3 hunt_env.py  --events runs/demo/events.jsonl --ground-truth runs/demo/ground_truth.json \
                     --agent demo --out runs/demo/hunt_results.json
python3 sigma_gen.py --events runs/demo/events.jsonl --ground-truth runs/demo/ground_truth.json \
                     --rules rules --out runs/demo/sigma_results.json

Optional deps (all documented, all degrade gracefully):

rules (built-in mini-YAML covers the documented subset otherwise).

(conversion is ours either way, so output is identical without it).

Agent protocol (hunt_env)

Episode = one campaign. reset() โ†’ briefing + schema_help + actions.


{"action": "run_sql", "sql": "SELECT ..."}                  // costs 1 of 50 queries
{"action": "submit_verdicts", "verdicts": [                 // free (Simbian-style)
  {"technique_id": "T1003", "entity": "rundll32.exe",
   "time_window": {"start": "2026-11-02 08:00:00", "end": "2026-11-02 08:05:00"},
   "note": "comsvcs minidump"}]}
{"action": "give_up"}

Observation for run_sql: {"query_result": {"ok", "columns", "rows" (max 10), "row_count" (always full), ...}}. A verdict is a true positive iff its technique matches an as-yet-uncovered narrative step, its time window overlaps the step's events, and its entity equals (case-insensitive) a step entity โ€” full path, basename, host, user, or process GUID.

Grader output: coverage_score (fraction of narrative steps โ€” the primary metric), precision, recall per technique and per ATT&CK tactic, evidence_seen_malicious / attribution_gap (evidence seen in query results minus verdicts landed โ€” Simbian's observed failure mode).

Plug an LLM in with --agent module:/abs/path/agent.py:hunt where hunt(env, info) loops env.step(...), or replay a fixed policy with --agent script:actions.jsonl.

Sigma subset and SQL/EQL mapping (sigma_gen)

Field names are Sysmon EventData names (identical to the events columns). logsource category โ†’ EventID filter: process_creationโ†’1, network_connectionโ†’3, image_loadโ†’7, process_accessโ†’10, file_createโ†’11, registry_set/registry_addโ†’13, dns_queryโ†’22.

Sigma constructSQL emitted (both engines)EQL emitted
plain value xLOWER(col) LIKE LOWER('x') ESCAPE '/'field == 'x'
wildcards */?LIKE with %/_ (metachars escaped)field like~ 'x'
`field\contains`LIKE '%x%'field like~ 'x'
`field\startswith`LIKE 'x%'field like~ 'x*'
`field\endswith`LIKE '%x'field like~ '*x'
list for one fieldOR (AND with `\all`)or (and with all)
several fields in one selectionANDand
value: nullcol IS NULLfield == null
`field\re`Python-side regex post-filter (portable)field regex~ '...'
sel1 and not filter1(...) AND NOT (...)... and not ...
1 of sel*, N of them, all of themSUM(CASE) >= N / chained ANDor/and + comment
condition list [a, b]disjunction of conditionsdisjunction
keyword-only selection, `\base64offset`, flow mappings, โ€ฆrejected loudly (UnsupportedSigma)โ€”

Scoring: per rule โ€” matched events, TP/FP/FN, precision, recall, F1. Recall denominator = malicious events of the rule's attack.tXXXX tag(s) if tagged, else all malicious events (so a rule is judged on what it claims to cover).

Verified upstream facts (2026-11-02)

atomic_tests[] with name, description, supported_platforms, input_arguments{name:{description,type,default}}, executor{name, command, elevation_required, cleanup_command}, dependencies โ€” mirrored by our embedded catalog; --art-dir imports the real thing from a local checkout.

5/7/8/10/11/12/13/14/15/17/18/19-21/22/23/25/26 field names as emitted here (ProcessGuid, Hashes SHA1=,MD5=,SHA256=,IMPHASH=, TargetObject with HKLM/ HKU abbreviations, โ€ฆ).

tags) and pySigma (pip install pysigma, LGPL-2.1, SigmaRule object model, backends/pipelines) โ€” used as documented above.

(wineventlog, Sysmon, stream:*, CloudTrail, โ€ฆ). We consume it only as an exported JSONL noise bootstrap โ€” export recipe below.

50-query budget, 10-row results with full counts reported, free flag submission, coverage over narrative steps, attribution-gap finding, time-shift + entity-obfuscation anti-memorization. Their engine is SQLite (505-column flat logs table); ours is DuckDB-with-SQLite-fallback โ€” an intentional divergence, contract identical.

Exporting BOTSv3 as noise bootstrap

BOTSv3 ships pre-indexed for Splunk, so JSONL must be exported first: install the dataset under $SPLUNK_HOME/etc/apps, then e.g.


index=botsv3 sourcetype="xmlwineventlog:microsoft-windows-sysmon/operational"
| table _time, EventID, Computer, UtcTime, ProcessGuid, Image, CommandLine, ...
| outputtext delim=comma

(or splunk search ... -output json). Feed the result to telemetry_sim.py --bootstrap export.jsonl. Without it the synthetic fleet is used; both paths are labeled in ground_truth.json:bootstrap.

Smoke test

./smoke.sh runs the whole loop on three seeds and asserts: events were generated; malicious events exist and were labeled; the reference hunter lands coverage > 0 with precision = 1.0 on at least one seed (and < 1.0 coverage on at least one other โ€” variants that survive hunting are the point); every sample Sigma rule compiles; at least one rule scores F1 > 0. Exit 0 = green.

Honest limitations

  1. Telemetry is synthesized, not executed. Command lines are realistic and

decodable (e.g. -enc payloads are real UTF-16LE base64), but no Windows host ran them. Event-field fidelity โŠ‚ real Sysmon (8 event IDs, no CallTrace detail, no HKCR renames). A real-lab path (Invoke-AtomicRedTeam โ†’ WEF/Winlogbeat โ†’ this schema) is the planned upgrade; the grader contract does not change.

  1. Catalog breadth. 12 procedures across 8 techniques today. --art-dir

imports real atomics, but imported tests get generic process-creation-only traces until per-technique telemetry templates are written.

  1. Benign noise is synthetic by default (โ‰ˆ8 app families, 12 hosts, no

Run-key/LSASS lookalikes by construction โ€” asserted). BOTSv3 bootstrap helps realism but its Sysmon share is a 2018 CTF's, not an enterprise's.

  1. Grader strictness is tunable but unvalidated against humans. Entity +

window matching rewards exactly-attributed hunts; no partial credit for coarse windows (deliberate, cf. Simbian's microsecond-precision stance).

  1. Single-host campaigns, single telemetry source (Sysmon; no Security/

PowerShell/4688, no network sensors). Lateral movement renders as one host.

  1. Sigma subset โ‰  full Sigma. Keyword detections, base64offset,

correlation rules and flow YAML are rejected, not approximated. Rules from the SigmaHQ corpus (DRL 1.1) are NOT vendored here for license reasons.

  1. DuckDB/SQLite differences. LIKE case-sensitivity differs (documented in

schema_help); our reference queries use LOWER() everywhere. The SQL guard is a referee, not a security boundary.

  1. No LLM wired in-repo. The loop is agent-agnostic (module:/script:

runners); model calls belong to the training harness (streams F/D), keeping this component offline-reproducible.

Lineage (copy / avoid)

counts; free verdict submission + give_up; coverage scored over narrative steps; per-tactic recall table; attribution-gap diagnostic; seeded time-shift/entity-substitution/GUID re-randomization; determinism given a seed.

(ours: open, reproducible from a seed, ground truth stays with the episode); timestamp-only grading (ours: technique + entity + window = hunting, not copying); zero training on their own benchmark (ours: both trajectories kept as training data โ€” that gap is the reason this family exists).

survives a reseed; evaluate on fresh instances, not static fixtures.

(ART YAML schema, Sigma schema, CC0 noise data) โ€” with licenses respected (no SigmaHQ rule content vendored).

See also: [[held-out-eval]] (this family feeds the private eval) ยท [[landscape-2026]] ยท [[cyberfactory-openaegis]]