From 8c4298ca2a9185b2f6f1034411d0959312432518 Mon Sep 17 00:00:00 2001 From: marc Date: Fri, 22 May 2026 19:41:48 +0200 Subject: [PATCH] Health Score: cap MITRE Coverage at 100% by canonicalising tactics STAR rules sometimes label tactics with non-canonical names (observed: "Stealth", "Defense Impairment") which were counted as distinct tactics on top of the 14 canonical ATT&CK Enterprise ones, producing percentages > 100% (e.g. 15/14 = 107.1% on a busy tenant). Fix in get_health_score(): - Restrict covered_tactics to the 14 canonical ATT&CK Enterprise tactics. - Map known STAR aliases ("Stealth", "Defense Impairment") -> "Defense Evasion". - Derive TOTAL_TACTICS from the canonical set (single source of truth). Result: tactics_covered = 14, mitre_pct = 100.0 (was 15 / 107.1). --- backend/routers/coverage.py | 21 +++++++++++++++++++-- 1 file changed, 19 insertions(+), 2 deletions(-) diff --git a/backend/routers/coverage.py b/backend/routers/coverage.py index 29766bd..64d1e5d 100644 --- a/backend/routers/coverage.py +++ b/backend/routers/coverage.py @@ -1098,7 +1098,21 @@ def _compute_health(db) -> dict: parser_pct = round((covered_sources / total_sources * 100) if total_sources else 0.0, 1) # --- MITRE coverage --- - TOTAL_TACTICS = 14 # standard ATT&CK Enterprise tactic count + # Standard ATT&CK Enterprise tactics (14). + CANONICAL_TACTICS = frozenset({ + "Reconnaissance", "Resource Development", "Initial Access", "Execution", + "Persistence", "Privilege Escalation", "Defense Evasion", "Credential Access", + "Discovery", "Lateral Movement", "Collection", "Command and Control", + "Exfiltration", "Impact", + }) + # SentinelOne STAR rules sometimes label tactics with non-canonical names. + # Map them to canonical ATT&CK so we don't over-count and exceed 100%. + TACTIC_ALIASES = { + "Stealth": "Defense Evasion", + "Defense Impairment": "Defense Evasion", + } + TOTAL_TACTICS = len(CANONICAL_TACTICS) + rules = db.query(ParsedRule).filter_by(rule_type="library").all() total_rules = len(rules) covered_tactics: set = set() @@ -1114,7 +1128,10 @@ def _compute_health(db) -> dict: if tactics or techniques: rules_with_mitre += 1 for t in tactics: - if t and t != "Uncategorized": + if not t or t == "Uncategorized": + continue + t = TACTIC_ALIASES.get(t, t) + if t in CANONICAL_TACTICS: covered_tactics.add(t) for tech in techniques: k = tech.get("id") or tech.get("name")