Map detections to MITRE ATT&CK and expose coverage gaps fast
For developers and detection engineers who need a defensible ATT&CK coverage view without buying another platform. You will export your current detections, normalize them into ATT&CK technique IDs, build a coverage matrix, and produce a gap list you can act on immediately.
TL;DR — Export your current detections into a CSV, normalize each rule to one or more ATT&CK technique IDs, then compare that list against the ATT&CK techniques relevant to your environment. The most common blocker is inconsistent tagging: fix it by enforcing literal
Txxxx/Txxxx.xxxtechnique IDs in your rule metadata before you calculate coverage. Reading time: ~5 min
Goal
When you finish, you will have a repeatable ATT&CK coverage report: a CSV of your detections mapped to ATT&CK techniques, a second CSV listing uncovered techniques for your chosen platforms, and a simple command you can rerun after every detection release.
Prerequisites
- Access to your detection rules repository or export from your SIEM/EDR as JSON, YAML, TOML, or CSV
- Python 3.11+ — check with:
python3 --version
jq1.6+ for JSON extraction — check with:
jq --version
gitif your detections live in a repo — check with:
git --version
- A local working directory with write access
- The ATT&CK STIX dataset URL or a local ATT&CK Enterprise JSON export
- A defined scope before you start: Enterprise only, and the platforms you actually run, for example
Windows,Linux,macOS,Azure AD,Office 365 - Detection metadata that includes ATT&CK references, or enough rule title/description context to add them manually
Steps
Step 1: Create a working folder and download ATT&CK data
Run:
mkdir -p ~/attack-gap-analysis && cd ~/attack-gap-analysis
curl -L -o enterprise-attack.json https://raw.githubusercontent.com/mitre-attack/attack-stix-data/master/enterprise-attack/enterprise-attack.json
ls -lh enterprise-attack.json
You should see enterprise-attack.json in the directory with a size typically in the tens of MB.
Step 2: Export your current detections into one machine-readable folder
If your detections are already in git, clone or pull them:
git clone https://git.example.com/security/detections.git rules
If you already have the repo:
cd rules && git pull --ff-only && cd ..
If your SIEM exports JSON, place the files under rules/. Then confirm what you have:
find rules -type f | sed 's#^#- #' | head -20
You should see rule files such as .json, .yml, .yaml, or .toml.
Step 3: Extract ATT&CK technique IDs from rule metadata
If your rules are JSON and contain ATT&CK IDs anywhere in the object, this command pulls them out:
find rules -type f -name '*.json' -print0 | xargs -0 -I{} sh -c 'jq -r --arg file "{}" '
'[$file, (.. | strings | select(test("^T[0-9]{4}(\\.[0-9]{3})?$")))] | @csv' '{}' ' \
| awk -F',' 'NF>1' > detections_to_attack.csv
If your rules are YAML/TOML and you do not have a parser already, extract candidate IDs with grep:
find rules -type f \( -name '*.yml' -o -name '*.yaml' -o -name '*.toml' -o -name '*.json' \) -print0 \
| xargs -0 grep -H -oE 'T[0-9]{4}(\.[0-9]{3})?' \
| awk -F: '{print $1","$2}' > detections_to_attack.csv
You should get a CSV where column 1 is the rule file and column 2 is a technique ID, for example:
rules/windows/powershell_suspicious.json,T1059.001
rules/linux/curl_to_tmp.yml,T1105
rules/identity/impossible_travel.json,T1078
Step 4: Build the in-scope ATT&CK technique list for your platforms
Create a Python script that reads ATT&CK STIX and emits Enterprise attack-patterns for your platforms:
cat > build_scope.py <<'python'
import json, csv
platforms = {"Windows","Linux","macOS","Azure AD","Office 365"}
with open("enterprise-attack.json","r",encoding="utf-8") as f:
data = json.load(f)
objects = data["objects"]
attack_patterns = [o for o in objects if o.get("type") == "attack-pattern" and not o.get("revoked", False) and not o.get("x_mitre_deprecated", False)]
rows = []
for o in attack_patterns:
ext = next((r for r in o.get("external_references", []) if r.get("source_name") == "mitre-attack"), None)
if not ext or "external_id" not in ext:
continue
tech_id = ext["external_id"]
plats = set(o.get("x_mitre_platforms", []))
if plats & platforms:
rows.append([tech_id, o.get("name",""), ";".join(sorted(plats))])
rows.sort()
with open("scope_techniques.csv","w",newline="",encoding="utf-8") as f:
w = csv.writer(f)
w.writerow(["technique_id","name","platforms"])
w.writerows(rows)
print(f"wrote {len(rows)} techniques")
python
python3 build_scope.py
head scope_techniques.csv
You should see wrote <number> techniques and a CSV header followed by ATT&CK IDs like T1059 or T1059.001.
Step 5: Compare scoped techniques to your mapped detections
Create a comparison script:
cat > compare_coverage.py <<'python'
import csv
from collections import defaultdict
mapped = defaultdict(set)
with open("detections_to_attack.csv","r",encoding="utf-8") as f:
for row in csv.reader(f):
if len(row) < 2:
continue
rule, tech = row[0].strip(), row[1].strip()
if tech.startswith("T"):
mapped[tech].add(rule)
covered = set(mapped.keys())
gaps = []
coverage = []
with open("scope_techniques.csv","r",encoding="utf-8") as f:
r = csv.DictReader(f)
for row in r:
tech = row["technique_id"].strip()
rules = sorted(mapped.get(tech, set()))
coverage.append([tech, row["name"], row["platforms"], len(rules), ";".join(rules)])
if not rules:
gaps.append([tech, row["name"], row["platforms"]])
with open("coverage_report.csv","w",newline="",encoding="utf-8") as f:
w = csv.writer(f)
w.writerow(["technique_id","name","platforms","detection_count","rules"])
w.writerows(coverage)
with open("coverage_gaps.csv","w",newline="",encoding="utf-8") as f:
w = csv.writer(f)
w.writerow(["technique_id","name","platforms"])
w.writerows(gaps)
print(f"covered={sum(1 for r in coverage if r[3] > 0)} total={len(coverage)} gaps={len(gaps)}")
python
python3 compare_coverage.py
column -s, -t < coverage_gaps.csv | head -20
You should see a summary like:
covered=143 total=412 gaps=269
and the first rows of coverage_gaps.csv.
Step 6: Review low-quality mappings before you trust the gap list
A rule tagged only to a parent technique can hide sub-technique gaps. Find parent-only mappings:
awk -F, '$2 ~ /^T[0-9]{4}$/ {print}' detections_to_attack.csv | head -20
Then find ATT&CK sub-techniques under a parent, example T1059:
jq -r '.objects[] | select(.type=="attack-pattern") | . as $o | ($o.external_references[]? | select(.source_name=="mitre-attack") | .external_id) as $id | select($id|startswith("T1059.")) | [$id,$o.name] | @tsv' enterprise-attack.json
You should see sub-techniques such as T1059.001 PowerShell. If your rule only detects PowerShell, retag it to T1059.001, not just T1059.
Step 7: Save the result as an artifact your team can reuse
Commit the scripts and generated reports, or publish the CSVs into your internal docs/build artifacts:
mkdir -p output
mv coverage_report.csv coverage_gaps.csv scope_techniques.csv output/
ls -1 output/
You should see exactly these files in output/ and be able to rerun the process after the next rule change.
Verify it works
Run the full pipeline from a clean shell:
cd ~/attack-gap-analysis
python3 build_scope.py
python3 compare_coverage.py
wc -l output/coverage_gaps.csv 2>/dev/null || wc -l coverage_gaps.csv
grep -E '^T[0-9]{4}(\.[0-9]{3})?,' coverage_gaps.csv | head
Expected results:
build_scope.pyprintswrote <number> techniquescompare_coverage.pyprintscovered=<n> total=<n> gaps=<n>withtotal > 0coverage_gaps.csvexists and starts with a header row followed by ATT&CK IDs- Spot-checking one covered technique in
coverage_report.csvshows the associated rule path(s)
Common pitfalls
Parent technique tags only
Mistake: tagging rules with T1059 when the rule is specific to PowerShell, AppleScript, or Command Shell.
Symptom: your coverage looks better than reality because one parent tag appears to cover all sub-techniques.
Fix: replace parent-only tags with the literal sub-technique IDs, for example T1059.001 for PowerShell.
Mixing ATT&CK versions in one report
Mistake: using a current ATT&CK dataset but old rule metadata copied from a previous ATT&CK release.
Symptom: some IDs in detections_to_attack.csv never appear in scope_techniques.csv, or names no longer match.
Fix: regenerate mappings against one ATT&CK dataset version and retag stale IDs before comparing coverage.
Counting vendor analytics as detections without evidence source checks
Mistake: mapping a detection to ATT&CK even though your telemetry source for that rule is disabled in production. Symptom: the matrix says "covered" but the alert never fires in real incidents. Fix: remove or flag mappings for rules whose required log source is not enabled, and keep a separate "implemented but inactive" state if needed.
Scoping every ATT&CK platform instead of your actual estate
Mistake: including ICS, containers, SaaS, and endpoint platforms you do not run.
Symptom: gaps is huge and not actionable.
Fix: edit platforms = {...} in build_scope.py to only your real platforms, then rerun the scripts.
Duplicate rule-to-technique rows inflating counts
Mistake: exporting the same rule multiple times from different branches, directories, or rule revisions.
Symptom: detection_count is inflated for some techniques.
Fix: deduplicate by rule path and technique ID before comparison; the provided script already stores rules in a set().
Treating ATT&CK mapping as proof of detection quality
Mistake: assuming a mapped technique is adequately covered without testing. Symptom: auditors are satisfied by the spreadsheet, but purple-team exercises still miss the behavior. Fix: keep the ATT&CK map for coverage accounting, then validate high-risk techniques with adversary simulation or replayed telemetry.
This article was written by an AI system and published pending human review. Verify anything you intend to act on.
Have a project in mind?
Get an instant AI price estimate for it, or talk directly to our team.
One email a month on what we learn building with AI