Purple Team Fundamentals & Program Design
Definition
Section titled “Definition”Purple teaming is the operating model — not a tool, not a one-off workshop — in which offensive execution and defensive improvement run as a single feedback loop: the red side executes adversary techniques (T-series discipline, EMU- artifacts, tier gates), the blue side’s detections are tested against them in near-real-time, and every result converts into a concrete defensive artifact (a validated rule, a telemetry fix, a tuning change) before the next technique runs. The distinction that separates a purple program from purple theater: theater runs attacks and screenshots alerts; a program runs structured engagements — planned technique sets tied to threat relevance (BT10), telemetry contracts verified before execution (BT3), detection expectations declared before the test (BT9), and results measured into program metrics (BT20). The organizing artifact is the validation cycle: plan (which techniques, why), prepare (telemetry verified, expectations written), execute (markers and manifests), correlate (what fired, what missed, how fast), fix (rule/tuning/telemetry changes), re-test (prove the fix), report (the record that funds the next cycle). The craft’s center: purple work is measured by defensive artifacts produced per technique tested — execution is input, the improved detection estate is the output.
Why It Matters
Section titled “Why It Matters”The teams this handbook has followed in parallel — T1-T10 building the adversary craft, BT1-BT20 building the defensive stack — meet here, and the meeting is where both investments compound. Without it, red runs exercises whose findings die in decks, and blue builds detections validated only by hope (BT9’s two-corpus tests prove the rule matches synthetic data, not that the analyst and pipeline catch the real technique in context). Purple validation is the only method that answers leadership’s real question — “would we catch this?” — with evidence instead of a coverage diagram: technique executed in production-safe form, telemetry observed, alert tracked, response timed, all recorded.
The failure modes define the program design. Workshop theater: a two-week purple sprint, 60 techniques executed, a colorful heatmap — and no ownership of the fixes, so nothing changes; the next sprint re-tests the same gaps. The uncorrelated red team: red completes T1-T10-style campaigns with immaculate tradecraft, and blue learns about it at the readout — no real-time correlation, so MTTD data never exists. Testing without expectations: “we ran it, see if anything fired” — without a declared detection expectation, both “nothing fired” and “something fired” are uninterpretable. Production recklessness: purple engagements in production without the T-series safety architecture (tier gates, markers, cleanup manifests) — the program that causes the incident it was funded to prevent. And metric-free reporting: engagement reports without baseline numbers, so improvement is anecdote; BT20’s contracts exist to make the purple loop measurable.
Detailed Explanation
Section titled “Detailed Explanation” THREAT PICTURE (BT10 PIRs) DETECTION ESTATE (BT9 matrix) which adversaries, which TTPs validated coverage, gaps | | +----------------+----------------+ v +------------------+ | 1. PLAN | technique list w/ relevance rank, | (ptplanner) | tier gates, scope, owners, schedule +--------+---------+ v +------------------+ | 2. PREPARE | telemetry contract check (BT3), | (precheck) | detection expectations declared, | | EMU- artifacts staged, markers set +--------+---------+ v +------------------+ | 3. EXECUTE | T-series tradecraft, production-safe | (campaignrun) | run log: ts, technique, artifact, host +--------+---------+ v +------------------+ | 4. CORRELATE | alerts vs expectations, MTTD per | (alignment) | technique, response actions tracked +--------+---------+ v +-------------+-------------+ v v v DETECTED PARTIAL/MISSED NOT-TESTABLE log the win, fix queue: rule, telemetry, or BT7 hunt gap declared, measure MTTD (the purple output is the fix) BT3 request +-------------+-------------+ v +------------------+ | 5. RE-TEST | prove the fix fired; regression gate +--------+---------+ (BT4/PT4 automation) v +------------------+ | 6. REPORT | per-technique verdicts, MTTD deltas, | (ptreport) | fix backlog -> BT20 scorecard +------------------+Engagement tiers, inherited from the T-series safety architecture: Tier A (lab) — anything destructive-adjacent runs in the isolated range, unrestricted; Tier B (staging) — production-like, marked traffic, detector-visible, no real user data; Tier C (production-safe) — techniques that are operationally safe in production (a benign scheduled-task creation, a single phishing simulation) executed with markers, change approval, and blue informed. Every technique carries its tier before execution — a plan that doesn’t say where each test runs hasn’t been safety-reviewed.
The telemetry contract is the program’s quiet foundation: before executing technique X, the declared telemetry that should observe it is verified to exist and flow (PT2’s precheck) — executing without it produces “missed” verdicts that are actually blind verdicts, and blind verdicts corrupt the coverage matrix. The contract names sources per technique, the pipeline stage that carries them (BT3), and the latency budget.
Detection expectations are declared before execution, per technique, in one of five states — this is the vocabulary the whole program runs on: expected-alert (a specific rule should fire), expected-hunt (no rule; a BT7 hypothesis should find it), expected-telemetry (no detection; data should exist for future rules), known-gap (documented, not a failure), and untested (not covered this cycle). The correlation step then produces verdicts against the expectation, and the fix queue is typed by the expectation that failed: an expected-alert miss is a rule problem (BT9), an expected-telemetry miss is a visibility problem (BT3), a hunt-miss is a method problem (BT7).
The fix loop is the program’s product. Every PARTIAL/MISSED verdict enters a fix queue with an owner and a type: rule gap → BT9 authoring with the purple execution as its ground-truth test case (the best test data that exists — real technique, real pipeline, real timing); telemetry gap → BT3 request with the business case pre-written; tuning issue → the FP/threshold adjustment with re-test scheduled. The re-test closes the loop and seeds PT4’s regression suite — every fixed detection becomes a permanent automated test, which is how the program’s value compounds instead of evaporating at each readout.
Deep dive: marker design — separating exercise traffic from real intrusion
Markers are the program’s safety wire, and bad markers are worse than none because they create false confidence. The discipline: markers must be present in every observable the detection estate sees — command lines (a benign-but-unique argument token), network artifacts (a dedicated user-agent, a reserved DNS subdomain), authentication context (a test persona whose sign-ins are taggable), and file names (the EMU- prefix the T-series already standardizes). They must be rotated per cycle — static markers teach blue analysts to pattern-match the test (defeating realism) and detection authors to lean on the marker (BT9 rules referencing EMU- strings are measuring the harness). And they must be documented in the engagement manifest so the post-incident question “was this ours?” has a lookup instead of an archaeology. The strongest marker design makes test traffic identifiable in retrospect but indistinguishable during execution — exactly the tension that makes purple work hard.
Common Techniques
Section titled “Common Techniques”| Technique | What it does | Where it fails | Notes |
|---|---|---|---|
| Engagement tiering | Lab/staging/production-safe gates | Tier decided mid-execution | Plan-level review |
| Telemetry contracts | Verify observation before testing | Tests run blind | precheck gate |
| Expectation declaration | Five-state vocabulary pre-test | “See if anything fires” | Correlation’s anchor |
| Relevance-ranked planning | BT10 PIRs pick techniques | Novelty hunting | Threat-led, not random |
| EMU- artifact discipline | Reusable, manifest-tracked execution | One-off scripts | T-series join |
| Marker marking | Exercise traffic labeled end-to-end | Blue mistakes test for real | Markers in cmdlines/UA/DNS |
| Real-time correlation | Blue watches during execution | Readout-only correlation | MTTD data requires it |
| Typed fix queue | Rule/telemetry/hunting ownership | Fixes without owners | The program’s product |
| Re-test gates | Prove fixes fire | Fixed = assumed | Feeds PT4 regression |
| Metric contracts (BT20) | MTTD, detection rate, fix latency | Anecdote reporting | Every cycle measured |
Full Working Code
Section titled “Full Working Code”ptplanner — the engagement planner: reads a technique inventory (CSV: technique, name, tier, relevance, owner) and the detection coverage export (BT9’s five-state matrix), generates the engagement plan — ranked execution order (relevance × coverage-need), telemetry requirements per technique, expectation defaults, and the schedule blocks — then emits the plan as CSV plus a run-sheet the executing team works from.
#!/usr/bin/env python3"""ptplanner — purple engagement planner.
Inputs: techniques.csv: technique,name,tier,rel,owner (tier: A|B|C; rel: threat-relevance 1-5 from BT10 PIRs) coverage.csv: technique,status (BT9 five-state matrix export; status: validated|rule-only|hunting|planned|gap)Output: engagement_plan.csv (ranked, expectations, telemetry reqs) + runsheet.csv (execution order with pre-flight checklist state)."""import argparseimport csvimport sysfrom pathlib import Path
TIERS = {"A": "lab", "B": "staging", "C": "production-safe"}REQUIRED_TECH = ["technique", "name", "tier", "rel", "owner"]REQUIRED_COV = ["technique", "status"]STATES = ("validated", "rule-only", "hunting", "planned", "gap")
# default telemetry requirements by technique prefix (extend per environment)TELEMETRY_DEFAULT = { "T1003": "sysmon:EID10,winevent:4688", "T1053": "winevent:4698,sysmon:EID1", "T1059": "sysmon:EID1,winevent:4104", "T1547": "sysmon:EID12,EID13,winevent:7045", "T1566": "mail-gateway,sysmon:EID22", "T1078": "winevent:4624,4625, IdP-risk",}
def load(path, required, label): if not path.exists(): sys.exit(f"fail-loud: {label} not found: {path}") with path.open(encoding="utf-8", errors="replace") as fh: reader = csv.DictReader(fh) missing = [c for c in required if c not in (reader.fieldnames or [])] if missing: sys.exit(f"fail-loud: {label} missing columns: {missing}") rows = list(reader) if not rows: sys.exit(f"fail-loud: {label} empty — nothing to plan is not a plan") return rows
def telemetry_for(technique): base = technique.split(".")[0] return TELEMETRY_DEFAULT.get(base, "per-plan review: no default contract")
def main(): ap = argparse.ArgumentParser(description="purple engagement planner") ap.add_argument("--techniques", type=Path, required=True) ap.add_argument("--coverage", type=Path, required=True) ap.add_argument("--cycle", default="CY-2026Q4", help="cycle label for the plan header") ap.add_argument("--out", type=Path, default=Path("ptplan_out")) args = ap.parse_args()
tech = load(args.techniques, REQUIRED_TECH, "techniques") cov_rows = load(args.coverage, REQUIRED_COV, "coverage") coverage = {r["technique"].strip().upper(): r["status"].strip().lower() for r in cov_rows} unknown = [t for t in (r["technique"].strip().upper() for r in tech) if t not in coverage] if unknown: print(f"note: {len(unknown)} techniques missing from coverage " f"matrix (treated as 'gap'): {', '.join(unknown[:5])}")
plan = [] for r in tech: tid = r["technique"].strip().upper() name = r["name"].strip() tier = r["tier"].strip().upper() if tier not in TIERS: sys.exit(f"fail-loud: {tid}: unknown tier {r['tier']!r} " "(need A, B or C)") try: rel = int(r["rel"]) except ValueError: sys.exit(f"fail-loud: {tid}: relevance not an integer") if not (1 <= rel <= 5): sys.exit(f"fail-loud: {tid}: relevance {rel} outside 1-5") status = coverage.get(tid, "gap") if status not in STATES: sys.exit(f"fail-loud: {tid}: coverage status {status!r} invalid") # rank: threat relevance dominates, coverage need breaks ties need = {"validated": 0, "rule-only": 2, "hunting": 3, "planned": 4, "gap": 5}[status] rank = rel * 10 + need if status == "validated": expectation = "regression-check (re-test gate)" elif status in ("rule-only",): expectation = "expected-alert" elif status == "hunting": expectation = "expected-hunt" else: expectation = "expected-telemetry" plan.append({"rank": rank, "technique": tid, "name": name, "tier": TIERS[tier], "owner": r["owner"].strip(), "coverage": status, "expectation": expectation, "telemetry": telemetry_for(tid), "relevance": rel})
plan.sort(key=lambda p: -p["rank"]) args.out.mkdir(parents=True, exist_ok=True) with (args.out / "engagement_plan.csv").open("w", newline="", encoding="utf-8") as fh: w = csv.DictWriter(fh, fieldnames=["rank", "technique", "name", "tier", "owner", "coverage", "expectation", "telemetry", "relevance"]) w.writeheader() w.writerows(plan) with (args.out / "runsheet.csv").open("w", newline="", encoding="utf-8") as fh: w = csv.writer(fh) w.writerow(["order", "technique", "tier", "pre-flight", "expectation", "owner"]) for i, p in enumerate(plan, 1): pre = f"tier:{p['tier']} | telemetry-verified? | markers-set? | " \ f"expectation-declared?" w.writerow([i, p["technique"], p["tier"], pre, p["expectation"], p["owner"]])
by_exp = {} for p in plan: by_exp[p["expectation"]] = by_exp.get(p["expectation"], 0) + 1 print(f"{args.cycle} plan: {len(plan)} techniques") print("expectation mix:", ", ".join(f"{k}={v}" for k, v in sorted(by_exp.items()))) print("top of queue:") for p in plan[:5]: print(f" [{p['rank']:>2}] {p['technique']} {p['name'][:32]:<32} " f"tier={p['tier']:<16} {p['coverage']}") print(f"plan: {args.out / 'engagement_plan.csv'}")
if __name__ == "__main__": main()Code Walkthrough
Section titled “Code Walkthrough”The coverage join (lines 47-53) pulls BT9’s five-state matrix in and refuses unknown statuses — the expectation defaults (lines 73-79) are derived from coverage state: validated becomes regression-check (PT4’s suite), rule-only becomes expected-alert (the rule exists; does it fire in context?), hunting becomes expected-hunt (BT7’s method should find it), and gap/planned become expected-telemetry (we’re buying data, not claiming detection). The coverage matrix doesn’t just rank the plan — it types every expectation.
Ranking (lines 69-71) is relevance-dominant with coverage-need as tiebreaker — rel * 10 + need — so a relevance-5 gap outranks a relevance-4 rule-only, and validated techniques sink to regression-check duty. The tier gate (lines 63-67) fails loud on anything but A/B/C — a plan that doesn’t declare where each technique runs is the plan the safety review rejects. The telemetry default table (lines 26-32) is deliberately small — it exists so the telemetry column is never empty by accident, and the “per-plan review” default is honest about what’s missing. The run-sheet (lines 90-99) carries the pre-flight checklist per technique — tier, telemetry-verified, markers-set, expectation-declared — the four checks that prevent both false verdicts and real incidents.
How It Reaches The Target
Section titled “How It Reaches The Target”The cycle runs on a quarterly rhythm with monthly mini-sprints. Planning Monday: BT10’s PIR picture and BT9’s coverage matrix meet in ptplanner; the output is a 25-technique plan, ranked, tiered, expectation-typed, owners attached. Preparation follows before execution: precheck (PT2) verifies every technique’s telemetry contract against BT3’s log-source inventory — techniques with missing sources get flagged before they waste execution time, and the gap declarations go to BT3 with the business case attached. Blue receives the expectation sheet and arms correlation: the SOC knows the week’s tests, markers, and schedule — informed blue is the difference between correlation and noise.
Execution week: red works the run-sheet top-down — relevance-5 gaps first, each technique executed per tier gate with T-series discipline (EMU- artifacts, markers, manifests), the run log recording timestamp, technique, artifact, host. Blue correlates in near-real-time: the expected-alert for T1053.005 fires in 4 minutes (logged, MTTD recorded, verdict DETECTED); the expected-alert for T1547.001 never fires (verdict MISSED — rule gap, into the fix queue with the execution as its ground-truth case); the expected-telemetry for T1078 confirms the IdP risk signals flow (verdict: data-exists, rule authoring queued). Fix week: BT9 authors the T1547.001 rule with the purple execution as validation corpus, the telemetry gap gets its BT3 request, and the re-test runs Friday — the fix fires in 2 minutes, the regression test is born, and PT5’s ptreport closes the cycle: detection rate 61% -> 68%, MTTD median 6 minutes, three fixes shipped, two gaps declared.
Exploitation Chain
Section titled “Exploitation Chain”- Inputs — BT10 PIRs (what’s likely) + BT9 coverage matrix (what’s unvalidated) define the candidate set
- Plan — ptplanner ranks by relevance × coverage-need, assigns tiers, derives expectations, schedules
- Prepare — precheck verifies telemetry contracts; markers staged; blue informed and armed
- Execute — T-series tradecraft per tier gate; run log with timestamps, artifacts, hosts
- Correlate — alerts vs expectations in near-real-time; verdicts: DETECTED / MISSED / DATA-ONLY / KNOWN-GAP
- Fix — typed queue: rule gaps to BT9 (purple execution = ground truth), telemetry to BT3, hunts to BT7
- Re-test — prove every fix fires; fixed detections graduate into PT4’s regression suite
- Report — per-technique verdicts, MTTD deltas, fix count, gap count -> BT20 scorecard, next cycle funded
Building Your Own Tool
Section titled “Building Your Own Tool”The charter is the program’s founding artifact — and the one most skipped. purplecharter generates the program charter from an org context file: team composition, tooling inventory, maturity self-scores, and risk appetite; it emits the charter sections (mission, RACI, engagement tiers, cadence, ground rules, escalation paths) and — the part that keeps it honest — validates that every role named in the RACI maps to a real team in the context file, refusing to charter a program with orphan responsibilities.
Design decisions
Section titled “Design decisions”RACI-or-refusal: every engagement phase (plan, prepare, execute, correlate, fix, re-test, report) needs exactly one Accountable role — two A’s is the command ambiguity BT19 documents, zero A’s is the program that stalls; the tool enforces both directions. Ground rules are non-negotiable defaults (safety tiers mandatory, markers mandatory, production changes through change process, no real credentials in test artifacts) with an explicit override list the charter must justify per line. The maturity self-score input uses the BT20 vocabulary (0-5 with evidence links) so the charter and the scorecard speak one language. Fail-loud on context schema drift.
Full source
Section titled “Full source”#!/usr/bin/env python3"""purplecharter — generate and validate a purple team program charter.
Input context.csv (key,value): red_team,blue_team,detection_eng, siem_platform,edr_platform,maturity_self,maturity_evidence plus raci.csv (phase,responsible,accountable,consulted,informed)Output: charter.md — mission, RACI table, tiers, cadence, ground rules."""import argparseimport csvimport sysfrom pathlib import Path
CONTEXT_KEYS = ["red_team", "blue_team", "detection_eng", "siem_platform", "edr_platform", "maturity_self", "maturity_evidence"]PHASES = ["plan", "prepare", "execute", "correlate", "fix", "re-test", "report"]GROUND_RULES = [ "every technique carries a tier (A/B/C) before execution", "exercise traffic is marked end-to-end (cmdline, UA, DNS markers)", "production changes follow the standard change process", "no real credentials in test artifacts; synthetic personas only", "expectations are declared before execution, never after", "every MISSED verdict gets a fix owner within the cycle", "re-tests prove fixes; assume-nothing",]
def main(): ap = argparse.ArgumentParser(description="purple charter generator") ap.add_argument("--context", type=Path, required=True) ap.add_argument("--raci", type=Path, required=True) ap.add_argument("--out", type=Path, default=Path("charter.md")) args = ap.parse_args()
if not args.context.exists(): sys.exit(f"fail-loud: context not found: {args.context}") ctx = {} with args.context.open(encoding="utf-8") as fh: for row in csv.DictReader(fh): ctx[row["key"].strip()] = row["value"].strip() missing = [k for k in CONTEXT_KEYS if k not in ctx or not ctx[k]] if missing: sys.exit(f"fail-loud: context missing keys: {missing}")
if not args.raci.exists(): sys.exit(f"fail-loud: raci not found: {args.raci}") with args.raci.open(encoding="utf-8") as fh: raci = list(csv.DictReader(fh)) raci_phases = [r["phase"].strip().lower() for r in raci] problems = [] for phase in PHASES: if phase not in raci_phases: problems.append(f"phase {phase!r} missing from RACI") continue row = next(r for r in raci if r["phase"].strip().lower() == phase) accountable = [r["accountable"].strip() for r in raci if r["phase"].strip().lower() == phase and r["accountable"].strip()] if len(accountable) == 0: problems.append(f"phase {phase!r}: no Accountable — program stalls") elif len(accountable) > 1: problems.append(f"phase {phase!r}: {len(accountable)} Accountables " f"({', '.join(accountable)}) — command ambiguity") for r in raci: for role_field in ("responsible", "accountable", "consulted", "informed"): role = r[role_field].strip() if role and role not in (ctx["red_team"], ctx["blue_team"], ctx["detection_eng"], "security_eng", "soc_leads", "it_ops"): problems.append(f"{r['phase']}/{role_field}: unknown team " f"{role!r} (not in context)")
try: maturity = int(ctx["maturity_self"]) except ValueError: sys.exit("fail-loud: maturity_self must be an integer 0-5") if not 0 <= maturity <= 5: sys.exit("fail-loud: maturity_self outside 0-5") if not Path(ctx["maturity_evidence"]).exists(): print(f"note: maturity evidence not on disk ({ctx['maturity_evidence']})" " — charter will flag the self-score as unevidenced")
lines = [ "# Purple Team Program Charter", "", f"**Red side:** {ctx['red_team']} | **Blue side:** {ctx['blue_team']}" f" | **Detection engineering:** {ctx['detection_eng']}", f"**Stack:** SIEM {ctx['siem_platform']} / EDR {ctx['edr_platform']}", f"**Self-assessed maturity:** {maturity}/5" + ("" if Path(ctx["maturity_evidence"]).exists() else " (UNEVIDENCED — see BT20 discipline)"), "", "## Mission", "Validate and improve the detection estate against relevant adversary", "behavior through structured, safe, expectation-driven engagements.", "Success metric: defensive artifacts produced per technique tested", "(validated rules, telemetry fixes, hunt hypotheses, tuning changes).", "", "## Engagement RACI", "| Phase | Responsible | Accountable | Consulted | Informed |", "|---|---|---|---|---|", ] for r in raci: lines.append("| {} | {} | {} | {} | {} |".format( r["phase"], r["responsible"], r["accountable"], r["consulted"], r["informed"])) lines += ["", "## Engagement Tiers", "- **Tier A (lab):** unrestricted, isolated range", "- **Tier B (staging):** production-like, marked, no real data", "- **Tier C (production-safe):** benign-execution techniques with", " markers, change approval, and informed blue", "", "## Ground Rules"] lines += [f"{i}. {rule}" for i, rule in enumerate(GROUND_RULES, 1)] lines += ["", "## Cadence", "- Quarterly cycle: plan -> prepare -> execute -> correlate ->", " fix -> re-test -> report (PT1 workflow)", "- Monthly mini-sprints on the top of the ranked queue", "", "## Escalation", "Safety incident during execution: halt, snapshot (BT5), BT4 path.", "Blue mistakes test for real: IC decides continue/halt (BT19 command)."]
if problems: print(f"CHARTER PROBLEMS ({len(problems)}):") for p in problems: print(" -", p) sys.exit(1)
args.out.write_text("\n".join(lines) + "\n", encoding="utf-8") print(f"charter written: {args.out} ({len(raci)} phases, " f"{len(GROUND_RULES)} ground rules)") print("review with all named teams before adoption; revisit quarterly")
if __name__ == "__main__": main()Walkthrough
Section titled “Walkthrough”The RACI validation (lines 40-63) checks three failure modes: missing phases (no owner for correlation — the program silently stops), duplicate Accountables (BT19’s command ambiguity, caught at charter time instead of hour two of an incident), and orphan roles (lines 54-60 — names no team in the context file backs; assigning “platform team” without confirming the platform team exists is theater from day one). The unknown-team allowlist is deliberately small — charters grow exceptions, and the tool makes growing one an explicit edit.
The maturity check (lines 67-73) borrows BT20’s evidence rule: a self-score without its evidence file still generates the charter (programs start somewhere) but stamps the score UNEVIDENCED (lines 85-89) — the charter refuses to participate in maturity theater. The ground rules (lines 21-29) are defaults, not suggestions: each maps to a documented failure mode (tier gates → production recklessness; markers → blue confusion; expectations → uninterpretable results; fix ownership → the unconverted hunt). The charter closes with the two escalation paths every purple program needs — safety incidents (halt, snapshot, BT4/BT5) and test-vs-real confusion (IC decides, BT19 command).
Compilation and test procedure
Section titled “Compilation and test procedure”author the org context and the RACI
Section titled “author the org context and the RACI”$ cat context.csv key,value red_team,offsec-4 blue_team,soc-12 detection_eng,deteng-3 siem_platform,splunk edr_platform,crowdstrike maturity_self,3 maturity_evidence,bt20_maturity_evidence.md
$ cat raci.csv phase,responsible,accountable,consulted,informed plan,offsec-4,deteng-3,soc-12,security_leads prepare,offsec-4,deteng-3,it_ops,soc-12 execute,offsec-4,offsec-4,soc-12,security_leads correlate,soc-12,soc-12,offsec-4,deteng-3 fix,deteng-3,deteng-3,soc-12,security_leads re-test,offsec-4,deteng-3,soc-12,security_leads report,deteng-3,deteng-3,offsec-4,security_leads
generate and validate
Section titled “generate and validate”$ python3 purplecharter.py —context context.csv —raci raci.csv —out charter.md charter written: charter.md (7 phases, 7 ground rules) review with all named teams before adoption; revisit quarterly
a RACI with two Accountables refuses to bless itself
Section titled “a RACI with two Accountables refuses to bless itself”$ python3 purplecharter.py —context context.csv —raci raci_bad.csv; echo “exit=$?” CHARTER PROBLEMS (1):
- phase ‘execute’: 2 Accountables (offsec-4, soc-12) — command ambiguity exit=1
Detection considerations
Section titled “Detection considerations”The charter is a governance artifact, but its validation output is program telemetry in BT20’s sense: RACI drift between quarters (roles renamed after a re-org, Accountables doubled), ground-rule violations logged during cycles (markers forgotten, expectations declared late — the run-sheet’s checklist state is the evidence), and the maturity self-score’s evidence freshness. The charter review belongs on the same quarterly calendar as the scorecard — a program whose charter hasn’t been re-read in a year is running on institutional memory, which is the least durable infrastructure the program owns.
Improvements to add
Section titled “Improvements to add”- Emit the RACI as a heatmap-ready JSON for the charter review meeting.
- Add a ground-rule override register (rule, justification, expiry) so exceptions are documented, not informal.
- Generate the engagement calendar (cycle dates, mini-sprints, re-test slots) from the cadence section.
- Validate escalation paths against BT19’s contact matrix (commatrix output) — charter references should resolve.
- Track charter version and diff charters quarter-over-quarter as the program’s own drift metric.
Detection — Defender View
Section titled “Detection — Defender View”| Signal | Source | ID | Notes |
|---|---|---|---|
| Engagement marker hits | BT3 pipeline | per exercise | Test-vs-real separation |
| Expected-alert fires | SIEM | rule id | Correlation’s yes |
| Expected-alert misses | ptreport | per technique | Fix queue input |
| MTTD per technique | Correlation log | per cycle | The speed metric |
| Telemetry contract failures | precheck | per plan | BT3 backlog |
| Fix latency | Fix queue | per verdict | Days target |
| Re-test pass rate | Regression suite | per cycle | PT4 gate |
| Regression rate | autockt | per cycle | Estate health |
| Charter validation drift | purplecharter | quarterly | Governance health |
| Artifacts-per-technique | ptreport | per cycle | The program metric |
Evasion Techniques
Section titled “Evasion Techniques”The “adversary” in a purple program is often the program’s own decay — but real adversaries evade purple validation too, and knowing how changes what you test. Evasion-aware red: a red team that executes techniques cleanly (the same LOLBins admins use, timing inside change windows, avoiding artifact spikes) validates the estate against its most polite adversary — the purple plan should deliberately include dirty variants (noisy staging, odd hours, unusual parents) because the estate must catch the adversary who doesn’t cooperate. Test-fingerprinting by the estate: detections that fire on the test harness (the EMU- artifact names, the marker strings) rather than the behavior — the regression suite that passes while real intrusions sail through; the counter is marker rotation and expectation-of-failure analysis (does the rule reference the marker? bad). Coverage gaming: purple cycles that only re-test what already works because it produces green — the relevance-ranked plan (ptplanner’s ordering) and the fix-latency metric make the green-bias visible. Timing evasion: adversary techniques that run slower than the detection pipeline’s correlation window (the beacon that checks in every 6 hours when tests run for 30 minutes) — long-window correlation for the technique classes that need it, and honest KNOWN-GAP declarations where the test simply can’t compress. Environment honesty: techniques that behave differently in staging than production (different tooling, different identities) — Tier C for the validation-critical cases, with the tier decision recorded and reviewed, not defaulted to whatever is convenient.
⚠️ What caught me: our first purple cycle was a triumph — 40 techniques, 31 detected, a gorgeous heatmap — and it took the new hire three weeks to notice that nine of the “detections” fired on the test harness strings: our EMU- artifact names were in the rules themselves, written by a well-meaning deteng who had validated against our own exercises. Every real intrusion using different infrastructure would have sailed through. We re-ran the nine with rotated artifacts and the true detection rate for that set was 2 of 9. The fix taught me the program’s most important ground rule: purple validates behavior, not our fingerprints — markers exist so blue can separate test from real, and the moment a detection depends on the marker, the test is measuring our ability to test ourselves.
Lab Practice
Section titled “Lab Practice”Stand up the purple loop in the lab range built across the T-series and BT-series modules. Charter: author context.csv and raci.csv (even for a team of two — the RACI discipline is the point); generate the charter; break it deliberately (two Accountables on execute) and watch the refusal. Plan: inventory 12 techniques you’ve practiced (T1053.005, T1059.001, T1547.001, T1003.001…), rank by your own relevance judgment, and run ptplanner against a hand-written coverage matrix — inspect how the expectation mix shifts when you flip a technique’s status. Execute one cycle: run the top-5 queue with markers (your EMU- discipline), correlate against your BT9 rules live, and time each detection honestly. Fix one: whatever MISSED, write the BT9 rule with your execution as the corpus, re-test, record the MTTD before/after. Debrief: what fraction of cycle time went to execution vs correlation vs fixing — is that the ratio you want at scale?
Interview Questions
Section titled “Interview Questions”- Technical: Walk the five-state expectation vocabulary and explain why correlation without pre-declared expectations produces uninterpretable results.
- Technical: Why does the coverage matrix’s status field derive each technique’s expectation in ptplanner, and what breaks if the vocabularies drift apart?
- Scenario: Your first purple cycle reports 78% detection rate — leadership is thrilled. What do you check before presenting that number, given test-harness fingerprinting?
- Scenario: Mid-engagement, blue mistakes a marked exercise for a real intrusion and starts BT4 response. Walk the halt decision (BT19 command) and the fix so it never recurs.
- Conceptual: Why is “defensive artifacts produced per technique tested” a better program metric than “techniques tested per quarter”?
- Deep-dive: Design the purple program for a 40-analyst org: charter, tiers, the quarterly cycle, fix-queue ownership, and the BT20 metrics for leadership.
- Practical: Red and blue teams have never collaborated, and you have one quarter to prove the concept. What’s the minimal viable purple cycle — and what do you deliberately not attempt?
Related Modules
Section titled “Related Modules”T1 (ATT&CK Mapping & Emulation Planning) · T9 (Atomic Testing & Automation) · BT4 (Incident Response Process) · BT9 (Detection Engineering & Sigma) · BT20 (Detection Program Metrics & Maturity)