Gap
data/frameworks/mitre-attack.json declares the 14 ATT&CK tactics (scoring.tactics: TA0001 Initial Access, TA0002 Execution, …). But ATT&CK controlIds are technique IDs (T1078, T1078.001) which do not encode the tactic — a technique→tactic lookup is required to group findings by tactic.
Today this map is missing from CheckID. M365-Assess maintains a partial copy at src/M365-Assess/controls/mitre-technique-map.json (~100 entries), but data/registry.json references 152 unique parent techniques and 477 unique technique IDs. So even the downstream copy is incomplete, and any other consumer (Az-Assess, future projects) has to either reinvent the map or live without tactic grouping.
This is a schema-independent fix — it doesn't require the spike (#317) to land. The technique→tactic map is data, not taxonomy structure.
Deliverables
1. Data file: data/mitre-technique-map.json
{
"$schema": "../mitre-technique-map.schema.json",
"version": "ATT&CK v15.1",
"generatedAt": "2026-04-27",
"source": "https://github.com/mitre/cti enterprise-attack.json",
"map": {
"T1078": ["TA0001", "TA0003", "TA0004", "TA0005"],
"T1078.001": ["TA0001", "TA0003", "TA0004", "TA0005"],
"T1190": ["TA0001"],
...
}
}
Notes on shape:
- Many-to-many: techniques can map to multiple tactics (T1078 "Valid Accounts" is in 4 tactics). Values must be arrays.
- Coverage: at minimum all 152 parent techniques + 477 sub-techniques referenced in
data/registry.json. Better: the full ATT&CK Enterprise matrix (~600+ techniques) so registry growth doesn't hit gaps.
- Versioned: capture the ATT&CK release the map was generated from, so consumers can detect drift.
2. Schema: data/mitre-technique-map.schema.json
JSON-Schema mirroring the frameworks.schema.json pattern, validating shape and tactic-code enum (TA0001–TA0043).
3. Generator: scripts/Build-MitreTechniqueMap.py
Pulls the latest ATT&CK STIX bundle from https://github.com/mitre/cti (or its CDN), extracts attack-pattern objects, builds the technique_id → [tactic_id] map. Wired into the existing scripts/ pipeline alongside Build-CisM365Crosswalk.py etc. Output is deterministic so diffs are reviewable.
4. Registry validation
Pester test confirms every mitre-attack controlId in data/registry.json resolves to at least one tactic via the map. Fails CI if registry references a technique not in the map.
Migration of M365-Assess's downstream copy
Once shipped here:
- M365-Assess imports/syncs
data/mitre-technique-map.json (mirroring how it already syncs data/frameworks/*.json).
- M365-Assess deletes
src/M365-Assess/controls/mitre-technique-map.json (the partial downstream copy) in a follow-up PR.
- The M365-Assess
Export-FrameworkCatalog.ps1 reads from the synced location.
- M365-Assess's React
FrameworkQuilt (PR #843) gains a attack-tactic-lookup extractor that uses this map to render the tactic breakdown row.
Acceptance criteria
Notes
displayOrder of tactics in mitre-attack.json already reflects kill-chain order (Reconnaissance → Resource Development → … → Impact). The map should preserve that ordering when consumers iterate.
- ATT&CK ICS and Mobile matrices are out of scope — Enterprise only.
Gap
data/frameworks/mitre-attack.jsondeclares the 14 ATT&CK tactics (scoring.tactics: TA0001 Initial Access, TA0002 Execution, …). But ATT&CK controlIds are technique IDs (T1078,T1078.001) which do not encode the tactic — a technique→tactic lookup is required to group findings by tactic.Today this map is missing from CheckID. M365-Assess maintains a partial copy at
src/M365-Assess/controls/mitre-technique-map.json(~100 entries), butdata/registry.jsonreferences 152 unique parent techniques and 477 unique technique IDs. So even the downstream copy is incomplete, and any other consumer (Az-Assess, future projects) has to either reinvent the map or live without tactic grouping.This is a schema-independent fix — it doesn't require the spike (#317) to land. The technique→tactic map is data, not taxonomy structure.
Deliverables
1. Data file:
data/mitre-technique-map.json{ "$schema": "../mitre-technique-map.schema.json", "version": "ATT&CK v15.1", "generatedAt": "2026-04-27", "source": "https://github.com/mitre/cti enterprise-attack.json", "map": { "T1078": ["TA0001", "TA0003", "TA0004", "TA0005"], "T1078.001": ["TA0001", "TA0003", "TA0004", "TA0005"], "T1190": ["TA0001"], ... } }Notes on shape:
data/registry.json. Better: the full ATT&CK Enterprise matrix (~600+ techniques) so registry growth doesn't hit gaps.2. Schema:
data/mitre-technique-map.schema.jsonJSON-Schema mirroring the
frameworks.schema.jsonpattern, validating shape and tactic-code enum (TA0001–TA0043).3. Generator:
scripts/Build-MitreTechniqueMap.pyPulls the latest ATT&CK STIX bundle from
https://github.com/mitre/cti(or its CDN), extractsattack-patternobjects, builds thetechnique_id → [tactic_id]map. Wired into the existingscripts/pipeline alongsideBuild-CisM365Crosswalk.pyetc. Output is deterministic so diffs are reviewable.4. Registry validation
Pester test confirms every
mitre-attackcontrolId indata/registry.jsonresolves to at least one tactic via the map. Fails CI if registry references a technique not in the map.Migration of M365-Assess's downstream copy
Once shipped here:
data/mitre-technique-map.json(mirroring how it already syncsdata/frameworks/*.json).src/M365-Assess/controls/mitre-technique-map.json(the partial downstream copy) in a follow-up PR.Export-FrameworkCatalog.ps1reads from the synced location.FrameworkQuilt(PR #843) gains aattack-tactic-lookupextractor that uses this map to render the tactic breakdown row.Acceptance criteria
data/mitre-technique-map.jsoncovers all 152 parent + 477 sub-technique IDs in currentdata/registry.jsondata/mitre-technique-map.schema.jsonvalidates shape; CI gate fails on schema violationscripts/Build-MitreTechniqueMap.pyregenerates deterministically from upstream STIXdata/frameworks/mitre-attack.jsonreferences the map file (TBD by spike spike: multi-axis taxonomy schema for frameworks #317 whether viaaxes[].extract.lookupor a siblinglookupTablefield)docs/architecture.mdupdated to mention the map in the data inventoryNotes
displayOrderof tactics inmitre-attack.jsonalready reflects kill-chain order (Reconnaissance → Resource Development → … → Impact). The map should preserve that ordering when consumers iterate.