diff --git a/.github/workflows/update-attack-remap.yml b/.github/workflows/update-attack-remap.yml new file mode 100644 index 00000000..2a14d0c6 --- /dev/null +++ b/.github/workflows/update-attack-remap.yml @@ -0,0 +1,67 @@ +name: update MITRE ATT&CK remap table + +on: + schedule: + # ATT&CK ships twice a year plus occasional Agile releases, so check monthly. + - cron: '0 21 2 * *' + workflow_dispatch: + +permissions: + contents: write + pull-requests: write + +jobs: + build: + runs-on: ubuntu-latest + steps: + - name: Checkout self repository + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + + - name: Set up Python + uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6.3.0 + with: + python-version: '3.12' + + - name: Regenerate the remap block in WELA.ps1 + run: python3 tools/update_attack_remap.py WELA.ps1 + + - name: Verify WELA.ps1 still parses + shell: pwsh + run: | + $errors = $null + $tokens = $null + [void][System.Management.Automation.Language.Parser]::ParseFile( + (Resolve-Path ./WELA.ps1), [ref]$tokens, [ref]$errors) + if ($errors) { + $errors | ForEach-Object { Write-Host $_.Message } + exit 1 + } + Write-Host 'WELA.ps1 parsed successfully' + + - name: Check for changes + id: check + run: | + if [ -n "$(git status --porcelain WELA.ps1)" ]; then + echo "change_exist=true" >> $GITHUB_ENV + echo "action_date=$(date '+%Y-%m-%d %H:%M:%S')" >> $GITHUB_ENV + else + echo "change_exist=false" >> $GITHUB_ENV + fi + + - name: Create Pull Request + if: env.change_exist == 'true' + id: cpr + uses: peter-evans/create-pull-request@5f6978faf089d4d20b00c7766989d076bb2fc7f1 # v8.1.1 + with: + token: ${{ secrets.GITHUB_TOKEN }} + commit-message: Update MITRE ATT&CK remap table (${{ env.action_date }}) + branch: attack/auto-remap-update + delete-branch: true + title: '[Auto] MITRE ATT&CK remap table update (${{ env.action_date }})' + branch-suffix: timestamp + body: | + Regenerated `$script:AttackTechniqueRemap` / `$script:AttackVersion` in `WELA.ps1` + from the latest Enterprise ATT&CK release. + + If `$script:AttackVersion` changed, the ATT&CK major version has moved. Re-check the + heatmap in ATT&CK Navigator and refresh `screenshots/mitre.png` before merging. diff --git a/CHANGELOG-Japanese.md b/CHANGELOG-Japanese.md index 121c1e20..d4b501a2 100644 --- a/CHANGELOG-Japanese.md +++ b/CHANGELOG-Japanese.md @@ -6,6 +6,7 @@ - ベースライン定義を`WELA.ps1`から`config/baselines.json`に外部化し、ベースラインの追加・変更をJSONの編集のみで行えるようにした。 (#358) (@fukusuket) - `Microsoft-Windows-DFSN-Server/Admin`チャネルを`audit-settings`と`audit-filesize`の確認対象に追加した。 (#358) (@fukusuket) +- MITRE ATT&CK Navigatorのヒートマップを ATT&CK v19 に対応させ、ATT&CK側でrevokedとなった技術IDを置換先に書き換えるようにした(例: v19で`T1685`に統合された`T1562`と`T1562.001`)。Navigatorはrevokedのエントリを黙って破棄するため、従来はその分のカバレッジがヒートマップから欠落していた。 (@fukusuket) **バグ修正:** diff --git a/CHANGELOG.md b/CHANGELOG.md index f5f70642..678598e3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,7 @@ - Baseline definitions were moved out of `WELA.ps1` into a `config/baselines.json` config file, so adding or changing a baseline is now a JSON-only edit. (#358) (@fukusuket) - The `Microsoft-Windows-DFSN-Server/Admin` channel is now checked by `audit-settings` and `audit-filesize`. (#358) (@fukusuket) +- MITRE ATT&CK Navigator heatmaps are now generated for ATT&CK v19, and technique IDs that ATT&CK has revoked are rewritten to their replacements (for example `T1562` and `T1562.001`, which v19 folded into `T1685`). Navigator silently discards revoked entries, so that coverage used to disappear from the heatmap. (@fukusuket) **Bug Fixes:** diff --git a/WELA.ps1 b/WELA.ps1 index 1690b119..ac6007b5 100644 --- a/WELA.ps1 +++ b/WELA.ps1 @@ -559,6 +559,162 @@ function AuditLogSetting { } +# BEGIN ATTACK-REMAP (auto-generated by tools/update_attack_remap.py - do not edit by hand) +# Source: MITRE ATT&CK Enterprise v19.2 (revoked-by relationships, chains collapsed) +$script:AttackVersion = "19" +$script:AttackTechniqueRemap = @{ + "T1002" = "T1560" + "T1004" = "T1547.004" + "T1009" = "T1027.001" + "T1013" = "T1547.010" + "T1015" = "T1546.008" + "T1017" = "T1072" + "T1019" = "T1542.001" + "T1022" = "T1560" + "T1023" = "T1547.009" + "T1024" = "T1573" + "T1028" = "T1021.006" + "T1031" = "T1543.003" + "T1032" = "T1573" + "T1035" = "T1569.002" + "T1038" = "T1574.001" + "T1042" = "T1546.001" + "T1044" = "T1574.010" + "T1045" = "T1027.002" + "T1050" = "T1543.003" + "T1053.001" = "T1053.002" + "T1054" = "T1685" + "T1058" = "T1574.011" + "T1060" = "T1547.001" + "T1063" = "T1518.001" + "T1065" = "T1571" + "T1066" = "T1027.005" + "T1067" = "T1542.003" + "T1070.001" = "T1685.005" + "T1070.002" = "T1685.006" + "T1073" = "T1574.001" + "T1075" = "T1550.002" + "T1076" = "T1021.001" + "T1077" = "T1021.002" + "T1079" = "T1573" + "T1081" = "T1552.001" + "T1084" = "T1546.003" + "T1085" = "T1218.011" + "T1086" = "T1059.001" + "T1088" = "T1548.002" + "T1089" = "T1685" + "T1093" = "T1055.012" + "T1094" = "T1095" + "T1096" = "T1564.004" + "T1097" = "T1550.003" + "T1099" = "T1070.006" + "T1100" = "T1505.003" + "T1101" = "T1547.005" + "T1103" = "T1546.010" + "T1107" = "T1070.004" + "T1109" = "T1542.002" + "T1116" = "T1553.002" + "T1117" = "T1218.010" + "T1118" = "T1218.004" + "T1121" = "T1218.009" + "T1122" = "T1546.015" + "T1126" = "T1070.005" + "T1128" = "T1546.007" + "T1130" = "T1553.004" + "T1131" = "T1547.002" + "T1138" = "T1546.011" + "T1139" = "T1552.003" + "T1141" = "T1056.002" + "T1142" = "T1555.001" + "T1143" = "T1564.003" + "T1144" = "T1553.001" + "T1145" = "T1552.004" + "T1146" = "T1070.003" + "T1147" = "T1564.002" + "T1148" = "T1690" + "T1150" = "T1647" + "T1151" = "T1036.006" + "T1152" = "T1569.001" + "T1154" = "T1546.005" + "T1155" = "T1059.002" + "T1156" = "T1546.004" + "T1157" = "T1574.004" + "T1158" = "T1564.001" + "T1159" = "T1543.001" + "T1160" = "T1543.004" + "T1161" = "T1546.006" + "T1162" = "T1647" + "T1163" = "T1037.004" + "T1164" = "T1547.007" + "T1165" = "T1037.005" + "T1166" = "T1548.001" + "T1167" = "T1555.002" + "T1168" = "T1053" + "T1169" = "T1548.003" + "T1170" = "T1218.005" + "T1171" = "T1557.001" + "T1172" = "T1090.004" + "T1173" = "T1559.002" + "T1174" = "T1556.002" + "T1177" = "T1547.008" + "T1178" = "T1134.005" + "T1179" = "T1056.004" + "T1180" = "T1546.002" + "T1181" = "T1055.011" + "T1182" = "T1546.009" + "T1183" = "T1546.012" + "T1184" = "T1563.001" + "T1186" = "T1055.013" + "T1188" = "T1090.003" + "T1191" = "T1218.003" + "T1192" = "T1566.002" + "T1193" = "T1566.001" + "T1194" = "T1566.003" + "T1196" = "T1218.002" + "T1198" = "T1553.003" + "T1206" = "T1548.003" + "T1208" = "T1558.003" + "T1209" = "T1547.003" + "T1214" = "T1552.002" + "T1215" = "T1547.006" + "T1223" = "T1218.001" + "T1483" = "T1568.002" + "T1487" = "T1561.002" + "T1488" = "T1561.001" + "T1492" = "T1565.001" + "T1493" = "T1565.002" + "T1494" = "T1565.003" + "T1500" = "T1027.004" + "T1501" = "T1543.002" + "T1502" = "T1134.004" + "T1503" = "T1555.003" + "T1504" = "T1546.013" + "T1506" = "T1550.004" + "T1514" = "T1548.004" + "T1519" = "T1546.014" + "T1522" = "T1552.005" + "T1527" = "T1550.001" + "T1536" = "T1578.004" + "T1547.011" = "T1647" + "T1562" = "T1685" + "T1562.001" = "T1685" + "T1562.002" = "T1685.001" + "T1562.003" = "T1690" + "T1562.004" = "T1686" + "T1562.006" = "T1685" + "T1562.007" = "T1686.001" + "T1562.008" = "T1685.002" + "T1562.009" = "T1688" + "T1562.010" = "T1689" + "T1562.011" = "T1685.003" + "T1562.012" = "T1685.004" + "T1562.013" = "T1686.002" + "T1574.002" = "T1574.001" + "T1656" = "T1684.001" + "T1672" = "T1684.002" +} +# END ATTACK-REMAP + function Export-MitreHeatmap { param ( [Parameter(Mandatory = $true)] @@ -570,32 +726,43 @@ function Export-MitreHeatmap { [Parameter(Mandatory=$false)] [bool]$UseIdealCount = $false ) + # ATT&CK Navigator のレイヤに載るのはテクニックIDのみ。 + # tactic(TA....)や cve./car./attack.g.... といったタグは対象外。 + # さらに ATT&CK が revoked にしたIDは置換先に書き換える。Navigator は revoked の + # エントリを黙って捨てるため、書き換えないとカバレッジが欠落する。 $tagMapping = @{} - $sigmaRules | ForEach-Object { - $rule = $_ - if ($rule.tags) { - $rule.tags | ForEach-Object { - $tag = $_ - # ATT&CK Navigator のレイヤに載るのはテクニックIDのみ。 - # tactic(TA....)や cve./car./attack.g.... といったタグは対象外。 - if ($tag -notmatch '^T\d{4}(\.\d{3})?$') { - return - } - if (-not $tagMapping.ContainsKey($tag)) { - $tagMapping[$tag] = @{ - titles = @() - idealCount = 0 - applicableCount = 0 - } - } - $tagMapping[$tag].titles += $rule.title - if ($rule.applicable -eq $true) { - $tagMapping[$tag].applicableCount++ - } - if ($rule.ideal -eq $true) { - $tagMapping[$tag].idealCount++ + foreach ($rule in $sigmaRules) { + if (-not $rule.tags) { + continue + } + # Two tags on one rule can collapse onto the same technique (T1562 and T1562.001 + # both become T1685), so de-duplicate per rule before counting. + $techniqueIds = [System.Collections.Generic.HashSet[string]]::new() + foreach ($tag in $rule.tags) { + if ($tag -cnotmatch '^T\d{4}(\.\d{3})?$') { + continue + } + $techniqueId = $tag + if ($script:AttackTechniqueRemap.ContainsKey($tag)) { + $techniqueId = $script:AttackTechniqueRemap[$tag] + } + [void]$techniqueIds.Add($techniqueId) + } + foreach ($techniqueId in $techniqueIds) { + if (-not $tagMapping.ContainsKey($techniqueId)) { + $tagMapping[$techniqueId] = @{ + titles = @() + idealCount = 0 + applicableCount = 0 } } + $tagMapping[$techniqueId].titles += $rule.title + if ($rule.applicable -eq $true) { + $tagMapping[$techniqueId].applicableCount++ + } + if ($rule.ideal -eq $true) { + $tagMapping[$techniqueId].idealCount++ + } } } @@ -630,8 +797,8 @@ function Export-MitreHeatmap { $heatmap = @{ "name" = "WELA detection heatmap" "versions" = @{ - "attack" = "18" - "navigator" = "5.2.0" + "attack" = $script:AttackVersion + "navigator" = "5.3.2" "layer" = "4.5" } "domain" = "enterprise-attack" diff --git a/tools/update_attack_remap.py b/tools/update_attack_remap.py new file mode 100644 index 00000000..b865128e --- /dev/null +++ b/tools/update_attack_remap.py @@ -0,0 +1,112 @@ +#!/usr/bin/env python3 +"""Regenerate the ATT&CK revoked-technique remap block embedded in WELA.ps1. + +Sigma/hayabusa rule tags keep referring to technique IDs that MITRE has since +revoked (e.g. T1562.001 -> T1685 in ATT&CK v19). ATT&CK Navigator silently +drops layer entries whose techniqueID is revoked, so WELA rewrites those IDs +before emitting the heatmap. This script keeps that lookup table in sync with +the latest Enterprise ATT&CK release. + +Usage: + python3 tools/update_attack_remap.py WELA.ps1 # fetch latest, rewrite in place + python3 tools/update_attack_remap.py # print the block only + python3 tools/update_attack_remap.py --bundle x.json --attack-version 19.2 WELA.ps1 +""" +import argparse +import json +import re +import sys +import urllib.request + +INDEX_URL = "https://raw.githubusercontent.com/mitre-attack/attack-stix-data/master/index.json" +BEGIN = "# BEGIN ATTACK-REMAP (auto-generated by tools/update_attack_remap.py - do not edit by hand)" +END = "# END ATTACK-REMAP" + + +def fetch_json(url): + with urllib.request.urlopen(url) as response: + return json.load(response) + + +def latest_enterprise(): + index = fetch_json(INDEX_URL) + collection = next(c for c in index["collections"] if c["name"] == "Enterprise ATT&CK") + newest = max(collection["versions"], key=lambda v: [int(x) for x in v["version"].split(".")]) + return newest["version"], fetch_json(newest["url"]) + + +def build_remap(bundle): + attack_ids = {} + revoked_by = {} + for obj in bundle["objects"]: + if obj.get("type") == "attack-pattern": + for ref in obj.get("external_references", []): + if ref.get("source_name") == "mitre-attack": + attack_ids[obj["id"]] = ref["external_id"] + elif obj.get("type") == "relationship" and obj.get("relationship_type") == "revoked-by": + revoked_by[obj["source_ref"]] = obj["target_ref"] + + direct = { + attack_ids[src]: attack_ids[dst] + for src, dst in revoked_by.items() + if src in attack_ids and dst in attack_ids + } + + # Collapse revocation chains (e.g. T1150 -> T1547.011) so WELA needs a single lookup. + def resolve(technique_id): + seen = set() + while technique_id in direct and technique_id not in seen: + seen.add(technique_id) + technique_id = direct[technique_id] + return technique_id + + return {k: resolve(k) for k in direct if resolve(k) != k} + + +def render(attack_version, remap): + width = max(len(k) for k in remap) + 2 + lines = [ + BEGIN, + "# Source: MITRE ATT&CK Enterprise v%s (revoked-by relationships, chains collapsed)" % attack_version, + '$script:AttackVersion = "%s"' % attack_version.split(".")[0], + "$script:AttackTechniqueRemap = @{", + ] + for old in sorted(remap): + lines.append(' %s = "%s"' % (('"%s"' % old).ljust(width), remap[old])) + lines += ["}", END] + return "\n".join(lines) + + +def main(): + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("target", nargs="?", help="path to WELA.ps1 (omit to print the block)") + parser.add_argument("--bundle", help="local enterprise-attack STIX bundle instead of downloading") + parser.add_argument("--attack-version", help="version label to use with --bundle") + args = parser.parse_args() + + if args.bundle: + if not args.attack_version: + parser.error("--attack-version is required with --bundle") + version = args.attack_version + with open(args.bundle, encoding="utf-8") as handle: + bundle = json.load(handle) + else: + version, bundle = latest_enterprise() + + remap = build_remap(bundle) + block = render(version, remap) + + if not args.target: + print(block) + return + + source = open(args.target, encoding="utf-8-sig").read() + pattern = re.compile(re.escape(BEGIN) + ".*?" + re.escape(END), re.S) + if not pattern.search(source): + sys.exit("marker block not found in %s" % args.target) + open(args.target, "w", encoding="utf-8-sig").write(pattern.sub(lambda _: block, source)) + print("updated %s: ATT&CK v%s, %d mappings" % (args.target, version, len(remap))) + + +if __name__ == "__main__": + main() diff --git a/website/docs/resources/changelog.ja.md b/website/docs/resources/changelog.ja.md index 824bdc5b..161dfb31 100644 --- a/website/docs/resources/changelog.ja.md +++ b/website/docs/resources/changelog.ja.md @@ -3,11 +3,12 @@ !!! info "情報" このページはプロジェクトの [`CHANGELOG.md`](https://github.com/Yamato-Security/WELA/blob/main/CHANGELOG-Japanese.md) を反映したものです。ダウンロードは [リリースページ](https://github.com/Yamato-Security/WELA/releases) をご覧ください。 -## 2.2.0 [2026/08/31] - Dev Release +## 2.2.0 [2026/xx/xx] - Dev Release **改善:** - ベースライン定義を`WELA.ps1`から`config/baselines.json`に外部化し、ベースラインの追加・変更をJSONの編集のみで行えるようにした。 (#358) (@fukusuket) +- MITRE ATT&CK Navigatorのヒートマップを ATT&CK v19 に対応させ、ATT&CK側でrevokedとなった技術IDを置換先に書き換えるようにした(例: v19で`T1685`に統合された`T1562`と`T1562.001`)。Navigatorはrevokedのエントリを黙って破棄するため、従来はその分のカバレッジがヒートマップから欠落していた。 (@fukusuket) **バグ修正:** diff --git a/website/docs/resources/changelog.md b/website/docs/resources/changelog.md index 87ae1eeb..f6da9eca 100644 --- a/website/docs/resources/changelog.md +++ b/website/docs/resources/changelog.md @@ -3,11 +3,12 @@ !!! info This page mirrors the project [`CHANGELOG.md`](https://github.com/Yamato-Security/WELA/blob/main/CHANGELOG.md). See the [Releases page](https://github.com/Yamato-Security/WELA/releases) for downloads. -## 2.2.0 [2026/08/31] - Dev Release +## 2.2.0 [2026/xx/xx] - Dev Release **Improvements:** - Baseline definitions were moved out of `WELA.ps1` into a `config/baselines.json` config file, so adding or changing a baseline is now a JSON-only edit. (#358) (@fukusuket) +- MITRE ATT&CK Navigator heatmaps are now generated for ATT&CK v19, and technique IDs that ATT&CK has revoked are rewritten to their replacements (for example `T1562` and `T1562.001`, which v19 folded into `T1685`). Navigator silently discards revoked entries, so that coverage used to disappear from the heatmap. (@fukusuket) **Bug Fixes:**