An empty intersection that shouldn’t have been empty#
While wiring up validation, I found a failure that was not in my code. I was mapping each deployed rule to its matching Atomic Red Team test by ATT&CK technique ID, and the intersection kept coming back empty for techniques that exist in both projects. Rules tagged for defense-evasion techniques matched nothing in Atomic Red Team, even when the techniques were present in both repositories.
The cause was a taxonomy change. It is exactly the kind of change that silently poisons a detection-coverage tool with hardcoded technique IDs.
What ATT&CK v18 changed#
ATT&CK v18 (2025) restructured the defense-evasion tactic. Both SigmaHQ and Atomic Red Team have migrated to it. The old IDs I had in my head, and that plenty of tooling still has baked in, no longer exist in the current corpus:
| Old | New |
|---|---|
T1562.001 Impair Defenses: Disable or Modify Tools | T1685 Disable or Modify Tools |
T1070.001 Indicator Removal: Clear Windows Event Logs | T1685.001 Disable or Modify Windows Event Log |
tactic defense-evasion | split into stealth and defense-impairment |
This is more than a relabel. It is a renumbering and a tactic split. The old defense-evasion tactic became two tactics, and specific techniques received new base IDs.
The number that makes it real#
I checked the assumption against the current SigmaHQ corpus, because “I think this changed” isn’t evidence:
attack.defense-evasionappears zero times.attack.stealthappears 944 times.attack.defense-impairmentappears 378 times.
The old tactic tag is gone. There are 13 techniques numbered T1650 or higher in SigmaHQ and 11 in Atomic Red Team - the whole upper range that didn’t exist a version ago.
The trap is straightforward: any coverage tool with pre-v18 technique IDs hardcoded will report gaps that are not real. It looks for T1562.001, finds nothing in a migrated corpus and paints a red cell for a technique you already detect, just under its new number, T1685. The result is damaging because it tells you that you are blind where you are not, encouraging work on a gap that is already covered.
Handling both vocabularies#
The fix is to accept both the legacy and the current names. src/attack.py keeps the old tactic shortname alongside the new ones, so an older or vendor-authored ruleset still parses, while current SigmaHQ tags resolve cleanly:
KNOWN_TACTICS = {
"reconnaissance", "resource-development", "initial-access", "execution",
"persistence", "privilege-escalation",
"defense-evasion", # legacy, pre-ATT&CK v18
"stealth", # v18
"defense-impairment", # v18
"credential-access", "discovery", "lateral-movement", "collection",
"command-and-control", "exfiltration", "impact",
}Technique parsing accepts both base and sub-technique forms via a single regex, and normalises case so attack.t1685.001 and attack.T1685.001 land in the same place:
TECHNIQUE_RE = re.compile(r"^attack\.(t\d{4}(?:\.\d{3})?)$", re.IGNORECASE)And the rule-selection tool targets the v18 IDs directly, so the curated set is future-shaped rather than carrying dead numbers:
# IDs follow ATT&CK v18 (T1562.001 -> T1685, T1070.001 -> T1685.001).
TARGET_TECHNIQUES = [
"T1003.001", "T1547.001", "T1053.005", "T1059.001", "T1218.011",
...
"T1087.001", "T1082", "T1057", "T1016", "T1685",
"T1685.001", "T1543.003", "T1197", "T1105", "T1548.002",
...
]The Navigator layer output is likewise stamped "attack": "18", so the coverage map renders against the current matrix rather than an older snapshot where those cells would be misplaced.
The mapping that resolves sub-techniques to their parent#
There’s a second, quieter place this matters. Atomic Red Team organises tests by technique folder, and not every sub-technique has its own atomics. When validation looks for a test for T1685.001 and finds none, it falls back to the parent technique T1685 before giving up:
def parent_technique(technique):
"""T1003.001 -> T1003; a base technique is returned unchanged."""
return technique.split(".", 1)[0].upper()That tiny helper keeps the renumbering from causing a second kind of false gap - a rule marked untestable purely because the atomics live one level up in the tree.
The honest gap I can’t close#
Handling the renumbering fixes the fake gaps. It doesn’t manufacture coverage that isn’t there, and one real gap remains: 125 of the 390 techniques tagged in SigmaHQ have no Windows-capable Atomic Red Team test at all. Those rules can be deployed, but they can never be proven by simulation, because there’s no atomic to run.
The correct way to show that is to leave the gap visible. On the coverage map, those techniques appear as deployed (orange, live but unproven), never validated (green, proven end to end). Orange is not a failure. It means “we shipped a detection but have not fired a test to confirm it”, which is the ethic behind this project and the subject of the coverage-map post later in the series.
The general lesson#
Taxonomies move. MITRE ships a new ATT&CK version roughly once a year, and each one can renumber techniques, split tactics, deprecate IDs. If your detection tooling treats technique IDs as eternal constants, it will drift out of sync with your rule sources and your simulation library - and the failure mode is the worst one: silent, confident, wrong. A coverage tool that reports gaps that don’t exist wastes exactly the effort you built it to save.
Build for the taxonomy to change. Accept both vocabularies, verify your assumptions against the current corpus rather than your memory of it, and stamp your outputs with the version they were built against.
Next: deploying converted rules into QRadar without filling the SIEM with duplicates.