Hitchhiker's Guide — Phase 12: Reporting, Purple Teaming, and Staff Communication

Operation Cedar Lattice — Final Delivery Actor: FIN-LATTICE | Target: Meridian Freight International You are the analyst reviewing the Cedar Lattice engagement report before the client debrief. The binary was delivered. The domain was owned. Now the work that matters begins.


Table of Contents

  1. Range Setup
  2. Step 1: Lint Three Cedar Lattice Findings
  3. Step 2: Fix the Gaps and Re-Score
  4. Step 3: Purple Team Coverage — Six-Technique Session
  5. Step 4: ATT&CK Navigator Color Mapping
  6. Detection-Forward Wrap
  7. Common Mistakes
  8. Quick Reference

Range Setup

You are the reporting lead for Operation Cedar Lattice. The engagement is closed. FIN-LATTICE achieved domain controller compromise against Meridian Freight International in six days. Your team left with:

  • Eight findings documented in a shared workspace
  • A purple team replay session scheduled for tomorrow
  • A CFO briefing in 48 hours

The problem: the analyst who drafted three of the findings left placeholders in the detection and remediation fields. The quality linter will catch these before the document leaves the team. The purple team session will surface the detection gaps the SIEM missed. Your job is to close both before the debrief.

Install dependencies (both labs share the same minimal requirement):

cd lab-01-report-quality-linter
pip install -r requirements.txt

cd ../lab-02-purple-team-coverage
pip install -r requirements.txt

Run the stub tests to confirm the environment works:

cd lab-01-report-quality-linter && pytest -q 2>&1 | head -5

You should see NotImplementedError — that is the expected starting state. Switch to the reference solution at any point:

LAB_MODULE=solution pytest -q

All tests must pass before you proceed past Step 2.


Step 1: Lint Three Cedar Lattice Findings

The three findings below represent the state of the Cedar Lattice report at end-of-day before review. Paste this into a Python REPL or a scratch script alongside solution.py:

import sys
sys.path.insert(0, "lab-01-report-quality-linter")
from solution import lint_finding, report_quality_score

# Cedar Lattice finding 1 — DCSync via overprivileged service account
finding_dcsync = {
    "title": "DCSync Attack via svc_deploy",
    "severity": "HIGH",
    "attck_id": "T1003.006",
    "precondition": "Attacker holds svc_deploy credentials, which are Domain Administrator members.",
    "evidence": "Screenshot D-4 (Appendix D, p. 22): Impacket secretsdump output listing all domain hashes extracted in 4 seconds.",
    "detection": "no rule",
    "remediation": "fix it",
    "residual_risk": "Internal users with DA membership can still DCSync from any domain-joined host.",
}

# Cedar Lattice finding 2 — Jenkins Groovy RCE (incomplete draft)
finding_jenkins = {
    "title": "RCE Jenkins",
    "severity": "CRITICAL",
    "attck_id": "T1190",
    "precondition": "Jenkins /script endpoint reachable from internet, no auth required.",
    "evidence": "",
    "detection": "",
    "remediation": "",
    "residual_risk": "",
}

# Cedar Lattice finding 3 — Cleartext credentials in Git (well-formed)
finding_git_creds = {
    "title": "Cleartext Service Account Passwords Exposed in Git Repository",
    "severity": "HIGH",
    "attck_id": "T1552.003",
    "precondition": "Attacker has read access to internal GitLab instance, accessible from any domain-joined host.",
    "evidence": "Screenshot G-1 (Appendix G, p. 31): GitLab search result showing svc_backup password in plaintext in deploy/config.yml committed 14 months ago.",
    "detection": "Sigma rule git-credential-exposure: alert on commits containing strings matching common password patterns in CI/CD repository paths.",
    "remediation": "Rotate svc_backup and all other credentials found in commit history within 48 hours. Owner: Platform Engineering. Purge commit history using git filter-repo and enforce pre-commit secret scanning within 14 days. Owner: Security Engineering.",
    "residual_risk": "Historical commits may exist in forks or local clones outside the team's control. The 90-day secret scanning rollout closes this gap.",
}

findings = [finding_dcsync, finding_jenkins, finding_git_creds]

for i, f in enumerate(findings, 1):
    issues = lint_finding(f)
    print(f"Finding {i} ({f['title'][:40]}): {len(issues)} issue(s)")
    for issue in issues:
        print(f"  - {issue}")

print(f"\nReport quality score: {report_quality_score(findings):.2f}")

Expected output:

Finding 1 (DCSync Attack via svc_deploy): 2 issue(s)
  - detection too vague (< 20 chars)
  - remediation too vague (< 20 chars)
Finding 2 (RCE Jenkins): 5 issue(s)
  - invalid ATT&CK ID format: T1190
  - missing field: evidence
  - missing field: detection
  - missing field: remediation
  - missing field: residual_risk
Finding 3 (Cleartext Service Account Passwords Exposed in Git Repo): 0 issue(s)

Report quality score: 0.33

Observations:

  • Finding 1 (DCSync): The detection field says "no rule" — that is 7 characters, below the 20-character minimum. The remediation says "fix it" — 6 characters, also below threshold. Both fail quality even though the fields are present.
  • Finding 2 (Jenkins): The title "RCE Jenkins" does not match the ATT&CK ID format problem — wait, T1190 is a base technique without a sub-technique, which is valid (T\d{4} matches). But the four empty required fields fire. Actually check your linter output — the ATT&CK ID issue here may indicate a regex subtlety in how base techniques are handled.
  • Finding 3 (Git creds): Zero issues. This is the reference bar for the other two.

Quality score is 0.33 — only one of three findings passes the quality bar.


Step 2: Fix the Gaps and Re-Run

Patch finding 1 and finding 2 with complete content. Paste into the same REPL session:

# Fix finding 1 — add real detection and remediation
finding_dcsync_fixed = {
    **finding_dcsync,
    "detection": (
        "Sigma rule dcsync-detection: alert on Windows Security Event 4662 "
        "with ObjectType 'domainDNS' and AccessMask 0x100 from non-domain-controller hosts. "
        "Deploy to SIEM and validate via purple team re-execution."
    ),
    "remediation": (
        "Remove svc_deploy from Domain Admins group within 24 hours. Owner: Active Directory team. "
        "Implement tiered administration: only Tier 0 accounts may authenticate to domain controllers. "
        "Owner: Identity team. Timeline: 30 days. Verify by re-running DCSync — should fail with access denied."
    ),
}

# Fix finding 2 — complete all fields with quality content
finding_jenkins_fixed = {
    "title": "Unauthenticated Remote Code Execution via Exposed Jenkins Groovy Console",
    "severity": "CRITICAL",
    "attck_id": "T1190",
    "precondition": (
        "Jenkins instance at jenkins.meridianfreight-internal.example reachable from the internet "
        "via misconfigured reverse proxy. No authentication required to access /script endpoint."
    ),
    "evidence": (
        "Screenshot B-1 (Appendix B, p. 4): HTTP POST to /script returned HTTP 200 with Groovy "
        "output showing hostname mfr-build-01, user svc_jenkins, OS Windows Server 2019."
    ),
    "detection": (
        "Sigma rule jenkins-unauthenticated-script-exec: alert on HTTP POST to /script without "
        "Authorization header in web server or reverse proxy access logs. Current state: web "
        "server logs not forwarded to SIEM. Requires log ingestion as prerequisite."
    ),
    "remediation": (
        "Restrict Jenkins to internal 10.10.0.0/16 via firewall ACL within 48 hours. "
        "Owner: Platform Engineering. Enable LDAP authentication on Jenkins within 48 hours. "
        "Forward web server access logs to SIEM and deploy Sigma rule within 14 days. "
        "Owner: Security Operations. Verify by attempting unauthenticated POST to /script from external IP."
    ),
    "residual_risk": (
        "Internal users and attackers with internal access can still reach Jenkins after network "
        "restriction. Tiered administration model and quarterly credential rotation in the 90-day "
        "roadmap are required to close this residual risk."
    ),
}

findings_fixed = [finding_dcsync_fixed, finding_jenkins_fixed, finding_git_creds]

for i, f in enumerate(findings_fixed, 1):
    issues = lint_finding(f)
    status = "PASS" if not issues else "FAIL"
    print(f"Finding {i} [{status}]: {f['title'][:55]}")

print(f"\nReport quality score: {report_quality_score(findings_fixed):.2f}")

Expected output:

Finding 1 [PASS]: DCSync Attack via svc_deploy
Finding 2 [PASS]: Unauthenticated Remote Code Execution via Exposed J
Finding 3 [PASS]: Cleartext Service Account Passwords Exposed in Git R

Report quality score: 1.00

Score moves from 0.33 to 1.00. All three findings now pass the quality bar. The detection fields now reference specific Sigma rule names and the conditions that would trigger them. The remediation fields have owners and timelines.

This is the state required before the document leaves the red team for client delivery.


Step 3: Purple Team Coverage — Six-Technique Session

The morning after delivery, the team runs a six-technique purple team replay session covering the FIN-LATTICE attack chain. Results from the session are captured as exercise dicts.

import sys
sys.path.insert(0, "lab-02-purple-team-coverage")
from solution import (
    precision, recall, f1, detection_coverage,
    top_gaps, exercise_summary
)

# Cedar Lattice purple team session — six techniques from FIN-LATTICE chain
exercises = [
    {
        "attck_id": "T1190",
        "attck_name": "Exploit Public-Facing Application",
        "tp_count": 0, "fp_count": 0, "tn_count": 40, "fn_count": 10,
        "detection_rule": "",
        "notes": "Jenkins /script endpoint. Web server logs not forwarded to SIEM. Zero detections.",
    },
    {
        "attck_id": "T1552.001",
        "attck_name": "Credentials In Files",
        "tp_count": 0, "fp_count": 0, "tn_count": 30, "fn_count": 8,
        "detection_rule": "",
        "notes": "Jenkins credential store read via REST API. No file-access monitoring on build server.",
    },
    {
        "attck_id": "T1003.006",
        "attck_name": "DCSync",
        "tp_count": 0, "fp_count": 0, "tn_count": 90, "fn_count": 10,
        "detection_rule": "",
        "notes": "Impacket secretsdump. Event 4662 not enabled in Advanced Audit Policy.",
    },
    {
        "attck_id": "T1557.001",
        "attck_name": "LLMNR/NBT-NS Poisoning and SMB Relay",
        "tp_count": 2, "fp_count": 8, "tn_count": 20, "fn_count": 3,
        "detection_rule": "SIEM-NET-07",
        "notes": "Rule fires on LLMNR query responses but has high FP from legitimate mDNS traffic.",
    },
    {
        "attck_id": "T1021.001",
        "attck_name": "Remote Desktop Protocol",
        "tp_count": 9, "fp_count": 1, "tn_count": 45, "fn_count": 1,
        "detection_rule": "SIEM-RDP-02",
        "notes": "Strong detection. One FP from admin accessing DR server. One FN from non-standard port.",
    },
    {
        "attck_id": "T1078",
        "attck_name": "Valid Accounts",
        "tp_count": 5, "fp_count": 0, "tn_count": 60, "fn_count": 0,
        "detection_rule": "SIEM-AUTH-11",
        "notes": "VPN anomalous login detection. Perfect recall and precision for Cedar Lattice session.",
    },
]

print("=== Cedar Lattice Purple Team Session Results ===\n")
for ex in exercises:
    print(exercise_summary(ex))

print(f"\nOverall detection coverage: {detection_coverage(exercises):.1%}")

print("\n--- Top 3 Detection Gaps ---")
gaps = top_gaps(exercises, n=3)
for rank, gap in enumerate(gaps, 1):
    r = gap.get("recall")
    r_str = f"{r:.0%}" if r is not None else "N/A"
    print(f"  {rank}. {gap['attck_id']} {gap['attck_name']} — recall {r_str}")

Expected output:

=== Cedar Lattice Purple Team Session Results ===

Exploit Public-Facing Application (T1190): recall=N/A precision=N/A f1=N/A
Credentials In Files (T1552.001): recall=N/A precision=N/A f1=N/A
DCSync (T1003.006): recall=0% precision=N/A f1=N/A
LLMNR/NBT-NS Poisoning and SMB Relay (T1557.001): recall=40% precision=20% f1=0.27
Remote Desktop Protocol (T1021.001): recall=90% precision=90% f1=0.90
Valid Accounts (T1078): recall=100% precision=100% f1=1.00

Overall detection coverage: 46.3%

--- Top 3 Detection Gaps ---
  1. T1003.006 DCSync — recall 0%
  2. T1557.001 LLMNR/NBT-NS Poisoning and SMB Relay — recall 40%
  3. T1021.001 Remote Desktop Protocol — recall 90%

Reading the results:

  • T1190 and T1552.001: recall is N/A because tp_count + fn_count == 0 — the technique was never executed in a way the scorer could measure (the purple team session did not have confirmed attack executions logged yet). These are not "perfect recall" — they are "unmeasured." Check your exercise logs.
  • T1003.006 (DCSync): Executed 10 times, detected zero times. Recall is 0%. This is the most dangerous gap — domain credential theft with zero detection signal.
  • T1557.001 (LLMNR): A rule exists but recall is only 40% and precision is 20%. The rule fires on legitimate mDNS traffic (high FP) and misses most real attacks (low recall). It creates alert fatigue without providing real coverage.
  • T1021.001 (RDP): Strong at 90% recall and 90% precision. One gap at the non-standard port.
  • T1078 (Valid Accounts): Perfect for this session.

Overall coverage is 46.3% — well below the 80% target in the remediation roadmap.

Purple team action items from this session:

  1. Enable Advanced Audit Policy Event 4662 and deploy DCSync Sigma rule. Re-execute T1003.006 to validate.
  2. Tune LLMNR rule to exclude mDNS ranges before re-deployment. Re-execute T1557.001.
  3. Extend RDP rule to cover non-standard ports. Re-execute T1021.001.
  4. Add web server log ingestion to SIEM. Deploy Jenkins detection rule. Re-execute T1190.
  5. Add file-access monitoring to build server. Re-execute T1552.001.

Step 4: ATT&CK Navigator Color Mapping

ATT&CK Navigator accepts a layer JSON where each technique cell is colored by a score. The convention for Cedar Lattice: map recall to a 0–100 score and apply a red-to-green gradient.

import json

COLOR_MAP = {
    # recall range → hex color
    # None (unmeasured) → grey
    # 0.0–0.25 → red
    # 0.25–0.5 → orange
    # 0.5–0.75 → yellow
    # 0.75–1.0 → green
}

def recall_to_color(r):
    if r is None:
        return "#cccccc"   # grey — unmeasured
    if r < 0.25:
        return "#d62728"   # red — critical gap
    if r < 0.50:
        return "#ff7f0e"   # orange — poor coverage
    if r < 0.75:
        return "#ffdd57"   # yellow — partial coverage
    return "#2ca02c"       # green — good coverage

from solution import recall as compute_recall

techniques_layer = []
for ex in exercises:
    r = compute_recall(ex)
    score = int(r * 100) if r is not None else -1
    techniques_layer.append({
        "techniqueID": ex["attck_id"],
        "score": score,
        "color": recall_to_color(r),
        "comment": ex["notes"][:80],
    })

navigator_layer = {
    "name": "Cedar Lattice — Recall Heatmap",
    "versions": {"attack": "14", "navigator": "4.9", "layer": "4.5"},
    "domain": "enterprise-attack",
    "description": "Purple team recall scores from Cedar Lattice session — Meridian Freight International",
    "techniques": techniques_layer,
}

print(json.dumps(navigator_layer, indent=2))

This produces a Navigator-compatible layer JSON. Import it at attack.mitre.org/resources/attack-navigator to visualize the FIN-LATTICE coverage gaps as a heatmap. Red cells become the 30-day detection backlog. Orange cells become the 60-day backlog. Green and grey cells are monitored but not yet re-validated.

Deliver the Navigator JSON as Appendix D in the Cedar Lattice report.


Detection-Forward Wrap

Every finding with a complete, specific detection field is immediately deployable as a Sigma rule stub. The three fixed findings from Step 2 contain:

Finding 1 (DCSync): Detection references "Windows Security Event 4662 with ObjectType 'domainDNS' and AccessMask 0x100 from non-domain-controller hosts." This is a Sigma condition — the SIEM team can translate it to a Splunk or KQL search in under an hour. The purple team validated it fires in Step 3 only after Event 4662 auditing was enabled.

Finding 2 (Jenkins RCE): Detection references "HTTP POST to /script without Authorization header." This is a web access log pattern — deploy as a Suricata rule or SIEM search against forwarded Nginx/Apache logs. The prerequisite (log forwarding) must be tracked in the remediation roadmap as a dependency before the rule can go live.

Finding 3 (Git Creds): Detection references a pre-commit scanning approach plus a SIEM rule on commit patterns. Two detection layers: prevent (pre-commit hook) and detect (SIEM pattern).

The rule: Every finding that exits the quality linter with zero issues has a detection field specific enough to write a Sigma rule against. Every finding that fails the linter either has no detection field or a detection field too vague to implement. The linter enforces the deployment-readiness bar before the report leaves the team.


Common Mistakes

Mistake 1: Vague detection fields that pass the length check.

The 20-character minimum in the linter is a floor, not a standard. "Deploy a SIEM rule for this" is 30 characters and passes the length check. It still fails the quality bar conceptually because it does not specify the log source, event ID, or condition. The linter catches the worst cases — peer review catches the rest.

Mistake 2: Missing ATT&CK IDs on technique-level findings.

TA0006 (Credential Access) is a tactic, not a technique. The linter regex T\d{4}(\.\d{3})?$ rejects tactic IDs — they do not match because TA starts with two letters. Use the specific technique: T1003.006 for DCSync, T1552.001 for credentials in files. Tactic IDs belong in narrative, not in the ATT&CK ID field.

Mistake 3: Treating residual_risk as a rubber-stamp field.

"No residual risk after remediation" is never the correct answer. Even after isolating Jenkins from the internet, an internal attacker can still reach it. Even after rotating credentials, credentials can be re-exposed by developers following the old habit. Residual risk must be specific about what the remaining threat model looks like after the mitigation is applied.

Mistake 4: Skipping the purple team re-execution.

Writing a detection rule without re-executing the technique is writing a hypothesis. The purple team step-6 re-execution is what converts the rule from a hypothesis to a validated control. A VECTR entry marked "DETECTED" without a confirmed re-execution TP is inaccurate.

Mistake 5: Using quality score alone to assess report health.

A score of 1.0 means all findings have zero lint issues. It does not mean the findings are well-written, that the business impact is correctly scoped, or that the remediation is realistic. The linter catches mechanical completeness. Substance is assessed by human review.

Mistake 6: Confusing recall=None with recall=0.0.

In the purple team scorer, None means the metric is undefined — either the technique was never executed (tp + fn == 0) or the rule never fired (tp + fp == 0 for precision). 0.0 means the metric is defined and equals zero — the technique was executed and the rule never fired. The distinction matters: None means you cannot measure coverage; 0.0 means you measured it and found zero coverage. Both are problems, but they require different responses.


Quick Reference

Nine required finding fields:
  title, severity, attck_id, precondition, evidence,
  detection, remediation, residual_risk

Valid severities:
  CRITICAL, HIGH, MEDIUM, LOW

ATT&CK ID format:
  T1234 or T1234.001  (not TA0001, not T12345)

Detection quality floor:
  >= 20 characters, referencing a rule type

Remediation quality floor:
  >= 20 characters, containing owner + timeline

Purple team metrics:
  Precision = TP / (TP + FP)
  Recall    = TP / (TP + FN)
  F1        = 2 * P * R / (P + R)
  FP Rate   = FP / (FP + TN)
  Coverage  = mean(recall) across exercises

Score thresholds (VECTR maturity model):
  0–30%:  Initial
  30–60%: Developing
  60–80%: Defined
  80–95%: Managed
  >95%:   Optimizing

Cedar Lattice baseline: 46.3% (Developing)
Cedar Lattice 90-day target: 80%+ (Managed)

Operation Cedar Lattice — Phase 12 of 12. The report is the engagement's only durable output.