Skip to main content

03 - MITRE ATT&CK Integration: Mapping Coverage Gaps Programmatically

MITRE ATT&CK Integration: Mappare Coverage Gap Programmaticamente Il framework MITRE ATT&CK (Adversarial Tactics, Techniques and Common Knowledge) e...

What do you think about this article?

At a Glance

MITRE ATT&CK Integration: Mappare Coverage Gap Programmaticamente Il framework MITRE ATT&CK (Adversarial Tactics, Techniques and Common Knowledge) e...

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.

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.

Sources and further reading

Try These Tools

Free developer tools related to this topic.

Commenti

Caricamento commenti...

Sign in to comment

Discuss this article in the community

Join the Telegram group to discuss with other developers, ask questions and share your experiences.

Did you like this article?

Explore other content on the blog or discover my projects