Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 11 additions & 1 deletion REFERENCES.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

```
Expand All @@ -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):
Expand Down
106 changes: 106 additions & 0 deletions data/mitre-technique-map.json
Original file line number Diff line number Diff line change
@@ -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"
}
}
61 changes: 61 additions & 0 deletions data/mitre-technique-map.schema.json
Original file line number Diff line number Diff line change
@@ -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" }
}
]
}
}
}
50 changes: 50 additions & 0 deletions tests/MitreTechniqueMap.Tests.ps1
Original file line number Diff line number Diff line change
@@ -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'
}
}
}
Loading