From b724d9e9162f6f24331576418f08fd51bbbc2b7f Mon Sep 17 00:00:00 2001 From: Daren9m <46686703+Daren9m@users.noreply.github.com> Date: Sat, 13 Jun 2026 22:17:28 -0600 Subject: [PATCH] feat(data): adopt mitre-technique-map.json (refs #324) Adopt M365-Assess's ATT&CK technique->tactic map as canonical CheckID data so mitre-attack findings can be grouped by tactic (technique IDs don't encode it). Adopted verbatim (100 entries) + CheckID schema + test. - data/mitre-technique-map.json ($schema added; map data unchanged) - data/mitre-technique-map.schema.json (technique-ID pattern + 14-tactic enum; values may be a single code or an array for future multi-tactic completeness) - tests/MitreTechniqueMap.Tests.ps1 (key format; tactic codes cross-checked against frameworks/mitre-attack.json scoring.tactics) - REFERENCES.md: canonical reference data row Coverage is partial by design of the source: the adopted map covers ~20% (96/477) of the technique IDs referenced by registry.json. Full ATT&CK coverage + a generator (scripts/Build-MitreTechniqueMap.py) remain to fully close #324. Refs #324 --- REFERENCES.md | 12 ++- data/mitre-technique-map.json | 106 +++++++++++++++++++++++++++ data/mitre-technique-map.schema.json | 61 +++++++++++++++ tests/MitreTechniqueMap.Tests.ps1 | 50 +++++++++++++ 4 files changed, 228 insertions(+), 1 deletion(-) create mode 100644 data/mitre-technique-map.json create mode 100644 data/mitre-technique-map.schema.json create mode 100644 tests/MitreTechniqueMap.Tests.ps1 diff --git a/REFERENCES.md b/REFERENCES.md index b32d06d..c8a90e9 100644 --- a/REFERENCES.md +++ b/REFERENCES.md @@ -40,6 +40,16 @@ Manual workflow: edit `data/scf-check-mapping.json` → `python scripts/Build-Re | **Stitch-M365** | Private | Submodule (`Engine/lib/CheckID/`) | Manual submodule update | | **Darn** | [Galvnyz/Darn](https://github.com/Galvnyz/Darn) | Planned | — | +### Canonical Reference Data + +Beyond `registry.json` and `frameworks/*.json`, CheckID is the single source of truth for cross-consumer reference data. Consumers fetch these from the tagged release instead of maintaining per-repo copies (which drift): + +| File | Purpose | Consumed by | +|------|---------|-------------| +| `data/mitre-technique-map.json` | ATT&CK technique-to-tactic lookup (technique IDs do not encode the tactic) | `mitre-attack` framework grouping by tactic | + +Each canonical data file has a sibling `*.schema.json` and Pester coverage under `tests/`. + ### CI Cascade Flow ``` @@ -57,7 +67,7 @@ CheckID notify-downstream.yml → repository_dispatch to: **CI cache sync** (recommended for PowerShell tools like M365-Assess): - Add `sync-checkid.yml` workflow that receives `checkid-released` dispatch -- Fetch `data/registry.json` and `data/frameworks/*.json` from the tagged version +- Fetch `data/registry.json`, `data/frameworks/*.json`, and the canonical reference data files (see [Canonical Reference Data](#canonical-reference-data)) from the tagged version - Store in a local `controls/` directory **Git submodule** (recommended for .NET apps like M365-Remediate): diff --git a/data/mitre-technique-map.json b/data/mitre-technique-map.json new file mode 100644 index 0000000..03c87ec --- /dev/null +++ b/data/mitre-technique-map.json @@ -0,0 +1,106 @@ +{ + "$schema": "./mitre-technique-map.schema.json", + "description": "Maps MITRE ATT&CK technique IDs to tactic codes. Derived from ATT&CK STIX data.", + "map": { + "T1001": "TA0011", + "T1001.001": "TA0011", + "T1001.002": "TA0011", + "T1001.003": "TA0011", + "T1003": "TA0006", + "T1003.001": "TA0006", + "T1003.002": "TA0006", + "T1003.003": "TA0006", + "T1003.004": "TA0006", + "T1003.005": "TA0006", + "T1003.006": "TA0006", + "T1003.007": "TA0006", + "T1003.008": "TA0006", + "T1005": "TA0009", + "T1008": "TA0011", + "T1011": "TA0010", + "T1011.001": "TA0010", + "T1020.001": "TA0010", + "T1021": "TA0008", + "T1021.001": "TA0008", + "T1021.002": "TA0008", + "T1021.003": "TA0008", + "T1021.004": "TA0008", + "T1021.005": "TA0008", + "T1021.006": "TA0008", + "T1040": "TA0006", + "T1046": "TA0007", + "T1048": "TA0010", + "T1052": "TA0010", + "T1052.001": "TA0010", + "T1059": "TA0002", + "T1059.006": "TA0002", + "T1071": "TA0011", + "T1072": "TA0002", + "T1078": "TA0001", + "T1078.002": "TA0001", + "T1078.004": "TA0001", + "T1090": "TA0011", + "T1090.004": "TA0011", + "T1098": "TA0003", + "T1110": "TA0006", + "T1110.001": "TA0006", + "T1110.002": "TA0006", + "T1110.003": "TA0006", + "T1110.004": "TA0006", + "T1114": "TA0009", + "T1133": "TA0001", + "T1134": "TA0004", + "T1136": "TA0003", + "T1137": "TA0003", + "T1137.001": "TA0003", + "T1137.002": "TA0003", + "T1137.003": "TA0003", + "T1137.004": "TA0003", + "T1137.005": "TA0003", + "T1137.006": "TA0003", + "T1176": "TA0003", + "T1185": "TA0009", + "T1190": "TA0001", + "T1195": "TA0001", + "T1195.001": "TA0001", + "T1195.002": "TA0001", + "T1199": "TA0001", + "T1204": "TA0002", + "T1204.001": "TA0002", + "T1204.002": "TA0002", + "T1213": "TA0009", + "T1218": "TA0005", + "T1218.001": "TA0005", + "T1218.002": "TA0005", + "T1485": "TA0040", + "T1505": "TA0003", + "T1530": "TA0009", + "T1534": "TA0043", + "T1537": "TA0010", + "T1539": "TA0006", + "T1550": "TA0008", + "T1550.001": "TA0008", + "T1550.004": "TA0008", + "T1552": "TA0006", + "T1552.005": "TA0006", + "T1552.007": "TA0006", + "T1556": "TA0003", + "T1557": "TA0006", + "T1557.001": "TA0006", + "T1557.002": "TA0006", + "T1558": "TA0006", + "T1561": "TA0040", + "T1563.002": "TA0008", + "T1566": "TA0001", + "T1567": "TA0010", + "T1583": "TA0042", + "T1584": "TA0042", + "T1586": "TA0042", + "T1589": "TA0043", + "T1598": "TA0043", + "T1602": "TA0009", + "T1602.001": "TA0009", + "T1602.002": "TA0009", + "T1621": "TA0006" + } +} diff --git a/data/mitre-technique-map.schema.json b/data/mitre-technique-map.schema.json new file mode 100644 index 0000000..be7c5aa --- /dev/null +++ b/data/mitre-technique-map.schema.json @@ -0,0 +1,61 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://github.com/Galvnyz/CheckID/data/mitre-technique-map.schema.json", + "title": "MITRE ATT&CK Technique-to-Tactic Map", + "description": "Schema for mitre-technique-map.json - maps ATT&CK technique IDs to their tactic code(s) so consumers can group mitre-attack findings by tactic (technique IDs do not encode the tactic). Adopted from M365-Assess; coverage is currently partial vs the full ATT&CK matrix (see issue #324). Values may be a single tactic code or an array (techniques can belong to multiple tactics).", + "type": "object", + "required": ["description", "map"], + "additionalProperties": false, + "properties": { + "$schema": { + "type": "string", + "description": "Reference to this JSON Schema file for editor validation." + }, + "description": { + "type": "string", + "minLength": 1 + }, + "version": { + "type": "string", + "description": "Optional ATT&CK release the map was generated from (e.g. 'ATT&CK v15.1')." + }, + "source": { + "type": "string", + "description": "Optional provenance of the mapping data." + }, + "generatedAt": { + "type": "string", + "pattern": "^\\d{4}-\\d{2}-\\d{2}$", + "description": "Optional ISO date the map was generated." + }, + "map": { + "type": "object", + "minProperties": 1, + "description": "ATT&CK technique ID (T#### or T####.###) to tactic code(s).", + "propertyNames": { + "pattern": "^T\\d{4}(\\.\\d{3})?$" + }, + "additionalProperties": { "$ref": "#/$defs/tacticRef" } + } + }, + "$defs": { + "tacticCode": { + "type": "string", + "enum": [ + "TA0043", "TA0042", "TA0001", "TA0002", "TA0003", "TA0004", "TA0005", + "TA0006", "TA0007", "TA0008", "TA0009", "TA0011", "TA0010", "TA0040" + ] + }, + "tacticRef": { + "oneOf": [ + { "$ref": "#/$defs/tacticCode" }, + { + "type": "array", + "minItems": 1, + "uniqueItems": true, + "items": { "$ref": "#/$defs/tacticCode" } + } + ] + } + } +} diff --git a/tests/MitreTechniqueMap.Tests.ps1 b/tests/MitreTechniqueMap.Tests.ps1 new file mode 100644 index 0000000..41659fe --- /dev/null +++ b/tests/MitreTechniqueMap.Tests.ps1 @@ -0,0 +1,50 @@ +Describe 'MITRE ATT&CK Technique Map Integrity' { + BeforeAll { + $projectRoot = Split-Path -Parent $PSScriptRoot + $mapPath = "$projectRoot/data/mitre-technique-map.json" + $frameworkPath = "$projectRoot/data/frameworks/mitre-attack.json" + + $data = Get-Content -Path $mapPath -Raw | ConvertFrom-Json + $framework = Get-Content -Path $frameworkPath -Raw | ConvertFrom-Json + + # The tactic codes the mitre-attack framework declares (source of truth for valid tactics). + $declaredTactics = [System.Collections.Generic.HashSet[string]]::new() + foreach ($p in $framework.scoring.tactics.PSObject.Properties) { [void]$declaredTactics.Add($p.Name) } + + $techniquePattern = '^T\d{4}(\.\d{3})?$' + $entries = @($data.map.PSObject.Properties) + } + + Context 'Top-level structure' { + It 'Has description and map' { + $data.PSObject.Properties.Name | Should -Contain 'description' + $data.PSObject.Properties.Name | Should -Contain 'map' + } + It 'Points at the local JSON schema' { + $data.'$schema' | Should -Be './mitre-technique-map.schema.json' + } + It 'Map is non-empty' { + $entries.Count | Should -BeGreaterThan 0 + } + } + + Context 'Technique keys' { + It 'Every map key is a well-formed ATT&CK technique ID' { + $bad = $entries | Where-Object { $_.Name -notmatch $techniquePattern } | ForEach-Object { $_.Name } + ($bad -join ', ') | Should -BeNullOrEmpty -Because 'keys must match T#### or T####.###' + } + } + + Context 'Tactic values' { + It 'Every mapped tactic code is declared by the mitre-attack framework' { + $unknown = [System.Collections.Generic.HashSet[string]]::new() + foreach ($e in $entries) { + foreach ($code in @($e.Value)) { + if (-not $declaredTactics.Contains($code)) { [void]$unknown.Add($code) } + } + } + ($unknown | Sort-Object) -join ', ' | Should -BeNullOrEmpty ` + -Because 'every tactic code must exist in frameworks/mitre-attack.json scoring.tactics' + } + } +}