MITRE ATT&CK Integration: Mapping Coverage Gaps Programmatically
The MITRE ATT&CK (Adversarial Tactics, Techniques and Common Knowledge) framework has become the common language of defensive cybersecurity. With over 200 documented techniques for enterprise, organized across 14 tactics, ATT&CK represents the most comprehensive map of real attacker behavior. But having the map is not enough: you need to know where your organization stands on that map - which techniques you can detect and which represent critical gaps.
In this article we will see how to programmatically integrate MITRE ATT&CK into the Detection Engineering workflow: how to download and parse the STIX data, how to map existing rules to techniques, how to identify and prioritize coverage gaps, and how to generate heatmaps and automated reports to communicate detection posture to management.
What You Will Learn
- ATT&CK framework structure: tactics, techniques, sub-techniques, procedures
- ATT&CK STIX data: download, parsing and querying via Python
- Automatic mapping of Sigma rules to ATT&CK techniques
- Coverage analysis: quantitative coverage metrics
- Gap identification and prioritization based on threat intelligence
- Generating ATT&CK Navigator heatmaps in JSON format
- Automated reports for CISO and management
- Integration into DaC pipeline for continuous coverage tracking
ATT&CK Framework Structure
MITRE ATT&CK organizes knowledge about attack techniques in a three-level hierarchy:
- Tactics (14): the attacker's objective at a given moment. Example: "Credential Access" (stealing credentials), "Lateral Movement" (expanding within the network). Tactics are the "why" of a malicious action.
- Techniques (200+): the specific method used to achieve the tactic's objective. Example: T1003 "OS Credential Dumping". Techniques are the "how".
- Sub-techniques (400+): specific variants of a technique. Example: T1003.001 "LSASS Memory", T1003.002 "Security Account Manager".
- Procedures: specific documented instances of how an APT group used a technique. They provide real-world context.
Downloading and Parsing ATT&CK STIX Data
pip install mitreattack-python requests pandas
#!/usr/bin/env python3
"""ATT&CK Framework Data Client."""
import json
import requests
from pathlib import Path
from dataclasses import dataclass, field
from mitreattack.stix20 import MitreAttackData
@dataclass
class AttackTechnique:
id: str
name: str
tactic: str
description: str
is_subtechnique: bool
parent_id: str = ""
platforms: list[str] = field(default_factory=list)
data_sources: list[str] = field(default_factory=list)
class AttackFrameworkClient:
ENTERPRISE_URL = (
"https://raw.githubusercontent.com/mitre/cti/master/"
"enterprise-attack/enterprise-attack.json"
)
def __init__(self, cache_path: str = "attack_data.json"):
self.cache_path = Path(cache_path)
self._ensure_data()
self.attack_data = MitreAttackData(str(self.cache_path))
def _ensure_data(self) -> None:
if not self.cache_path.exists():
print("Downloading MITRE ATT&CK data...")
resp = requests.get(self.ENTERPRISE_URL, timeout=60)
resp.raise_for_status()
with open(self.cache_path, 'w') as f:
json.dump(resp.json(), f)
def get_all_techniques(self) -> list[AttackTechnique]:
techniques = []
stix_techniques = self.attack_data.get_techniques(
remove_revoked_deprecated=True
)
for t in stix_techniques:
attack_id = ""
for ref in t.get("external_references", []):
if ref.get("source_name") == "mitre-attack":
attack_id = ref.get("external_id", "")
break
if not attack_id:
continue
kill_chain_phases = t.get("kill_chain_phases", [])
tactic = (kill_chain_phases[0].get("phase_name", "unknown")
if kill_chain_phases else "unknown")
is_subtechnique = "." in attack_id
parent_id = attack_id.split(".")[0] if is_subtechnique else ""
techniques.append(AttackTechnique(
id=attack_id,
name=t.get("name", ""),
tactic=tactic,
description=t.get("description", "")[:500],
is_subtechnique=is_subtechnique,
parent_id=parent_id,
platforms=t.get("x_mitre_platforms", []),
data_sources=t.get("x_mitre_data_sources", [])
))
return techniques
client = AttackFrameworkClient()
techniques = client.get_all_techniques()
print(f"Techniques loaded: {len(techniques)}")
Mapping Sigma Rules to ATT&CK Techniques
#!/usr/bin/env python3
"""Sigma to ATT&CK Coverage Mapper."""
import yaml
from pathlib import Path
from dataclasses import dataclass, field
@dataclass
class RuleInfo:
path: str
title: str
status: str
level: str
@dataclass
class TechniqueCoverage:
technique_id: str
technique_name: str
tactic: str
rules: list[RuleInfo] = field(default_factory=list)
@property
def coverage_score(self) -> float:
if not self.rules:
return 0.0
score = 0.0
for rule in self.rules:
status_weight = {
"stable": 1.0, "test": 0.5, "experimental": 0.25
}.get(rule.status, 0.1)
level_weight = {
"critical": 1.0, "high": 0.8, "medium": 0.5,
"low": 0.3, "informational": 0.1
}.get(rule.level, 0.1)
score += status_weight * level_weight
return min(1.0, score)
def extract_attack_tags(rule: dict) -> list[str]:
technique_ids = []
for tag in rule.get("tags", []):
tag_lower = tag.lower()
if tag_lower.startswith("attack.t"):
tech_id = tag.replace("attack.", "").upper()
if len(tech_id) >= 5:
technique_ids.append(tech_id)
return technique_ids
def build_coverage_map(
rules_dir: str,
attack_client: AttackFrameworkClient
) -> dict[str, TechniqueCoverage]:
coverage_map: dict[str, TechniqueCoverage] = {}
rules_path = Path(rules_dir)
for rule_file in rules_path.rglob("*.yml"):
try:
with open(rule_file, encoding='utf-8') as f:
rule = yaml.safe_load(f)
except Exception:
continue
if not isinstance(rule, dict):
continue
technique_ids = extract_attack_tags(rule)
rule_info = RuleInfo(
path=str(rule_file.relative_to(rules_path)),
title=rule.get("title", "Unknown"),
status=rule.get("status", "experimental"),
level=rule.get("level", "medium")
)
for tech_id in technique_ids:
if tech_id not in coverage_map:
technique = attack_client.get_technique_by_id(tech_id)
coverage_map[tech_id] = TechniqueCoverage(
technique_id=tech_id,
technique_name=technique.name if technique else "Unknown",
tactic=technique.tactic if technique else "unknown"
)
coverage_map[tech_id].rules.append(rule_info)
return coverage_map
Gap Analysis and Prioritization
@dataclass
class CoverageGap:
technique_id: str
technique_name: str
tactic: str
priority_score: float
reasons: list[str] = field(default_factory=list)
def calculate_gap_priority(
technique: AttackTechnique,
threat_intel: dict
) -> float:
score = 0.0
# Factor 1: Frequency in recent campaigns (0-40 points)
freq = threat_intel.get(technique.id, 0)
score += min(40.0, freq * 10)
# Factor 2: High-impact tactics (0-30 points)
high_impact_tactics = [
"credential-access", "lateral-movement",
"impact", "exfiltration", "command-and-control"
]
if technique.tactic in high_impact_tactics:
score += 30.0
# Factor 3: Available data sources (0-20 points)
if len(technique.data_sources) >= 3:
score += 20.0
elif len(technique.data_sources) >= 1:
score += 10.0
# Factor 4: Common platform support (0-10 points)
if "Windows" in technique.platforms:
score += 5.0
if "Linux" in technique.platforms:
score += 5.0
return score
# Threat intel frequency from public reports
THREAT_INTEL_FREQUENCY = {
"T1059.001": 8, # PowerShell - very common
"T1003.001": 7, # LSASS Memory - very common
"T1566.001": 9, # Spearphishing Attachment
"T1190": 6, # Exploit Public-Facing Application
"T1021.001": 5, # Remote Desktop Protocol
"T1053.005": 4, # Scheduled Task
"T1078": 6, # Valid Accounts
}
Generating ATT&CK Navigator Heatmaps
def generate_navigator_layer(
coverage_map: dict[str, TechniqueCoverage],
gaps: list[CoverageGap],
layer_name: str = "Detection Coverage"
) -> dict:
techniques_layer = []
for tech_id, coverage in coverage_map.items():
score = coverage.coverage_score
if score >= 0.8:
color = "#4caf50" # Green: excellent coverage
elif score >= 0.5:
color = "#ff9800" # Orange: partial coverage
else:
color = "#f44336" # Red: insufficient coverage
techniques_layer.append({
"techniqueID": tech_id,
"color": color,
"score": round(score * 100),
"comment": f"{len(coverage.rules)} rules, score {score:.2f}"
})
# Mark uncovered high-priority gaps
covered_ids = set(coverage_map.keys())
for gap in gaps[:20]:
if gap.technique_id not in covered_ids:
techniques_layer.append({
"techniqueID": gap.technique_id,
"color": "#b71c1c", # Dark red: critical gap
"score": 0,
"comment": f"PRIORITY GAP (score: {gap.priority_score:.0f})"
})
return {
"name": layer_name,
"versions": {"attack": "14", "navigator": "4.9", "layer": "4.5"},
"domain": "enterprise-attack",
"techniques": techniques_layer,
"gradient": {
"colors": ["#ff6666", "#ffe766", "#8ec843"],
"minValue": 0,
"maxValue": 100
}
}
# Save layer for ATT&CK Navigator
import json
layer = generate_navigator_layer(coverage_map, gaps)
with open("navigator_layer.json", "w") as f:
json.dump(layer, f, indent=2)
print("Layer saved. Import at https://mitre-attack.github.io/attack-navigator/")
Best Practices for Coverage Analysis
| Practice | Description | Impact |
|---|---|---|
| Focus on most-used techniques | Use M-Trends, DBIR reports to prioritize based on real campaigns | High |
| Consider data sources first | You cannot detect what you do not log. Audit data sources before rules | Critical |
| Filter by relevant platforms | If you have no macOS, do not prioritize macOS-only techniques | Medium |
| Track coverage over time | Measure the delta week by week to show program progress | High |
| Validate rules periodically | A rule untested for 6+ months may no longer work correctly | High |
Common Mistakes in Coverage Analysis
- Counting rules, not quality: 10 experimental rules are worth less than 2 tested stable rules. Use the weighted coverage_score
- Ignoring data sources: without Sysmon deployed, many Windows techniques cannot be detected regardless of the rules
- 100% coverage is impossible: some techniques are very hard to detect (living off the land). Focus on high-risk, high-detectability gaps
- Reports without action: every gap report must generate a detection backlog with owners and deadlines
Conclusions
Programmatically integrating MITRE ATT&CK into the Detection Engineering workflow transforms coverage analysis from a manual, subjective activity into an automated, objective, continuous process. The result is a measurable detection program, prioritized on real threats, and communicable to management with concrete data.
Next Article in the Series
In the next article we will automate alert response with SOAR Playbooks in Python, orchestrating containment, enrichment, and notification actions automatically.
Related deep dive: 04 - SOAR Playbook in Python: Automazione Incident Response.
Related deep dive: 05 - Threat Intelligence Ingestion: Processore Feed STIX/TAXII.
Frequently asked questions
What is the MITRE ATT CK framework used for?
The MITRE ATT CK framework represents a comprehensive map of real attacker behavior, organizing over 200 documented techniques across 14 tactics.
How does programmatically integrating MITRE ATT CK into the Detection Engineering workflow benefit an organization?
It transforms coverage analysis from a manual, subjective activity into an automated, objective, continuous process.
What is the three-level hierarchy used by the MITRE ATT CK framework to organize knowledge about attack techniques?
The three-level hierarchy consists of tactics, techniques, and common knowledge.
What type of reports can be generated using the integrated MITRE ATT CK data?
Automated reports that communicate detection posture to management with concrete data.
Commenti
Caricamento commenti...
Sign in to comment