Merge pull request #360 from Yamato-Security/update-mitre

feat: support MITRE ATT&CK v19 in Navigator heatmaps
This commit is contained in:
Zach Mathis (田中ザック) authored and GitHub committed 2026-09-14 19:28:08 +09:00
commit 4fdb6b09fc
7 files changed
+377 -27

No files matched your search

+67
View File
@@ -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.
+1
View File
@@ -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)
**バグ修正:**
+1
View File
@@ -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:**
+192 -25
View File
@@ -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"
+112
View File
@@ -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()
+2 -1
View File
@@ -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)
**バグ修正:**
+2 -1
View File
@@ -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:**