mirror of
https://github.com/Yamato-Security/WELA.git
synced 2026-10-08 15:25:24 +02:00
feat: update MITRE ATT&CK Navigator heatmaps for ATT&CK v19 and handle revoked technique IDs
This commit is contained in:
7 files changed
+377
-27
No files matched your search
@@ -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.
|
||||
@@ -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)
|
||||
|
||||
**バグ修正:**
|
||||
|
||||
|
||||
@@ -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:**
|
||||
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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()
|
||||
@@ -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)
|
||||
|
||||
**バグ修正:**
|
||||
|
||||
|
||||
@@ -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:**
|
||||
|
||||
|
||||
Reference in new issue
Block a user