Merge pull request #389 from Shirofune-Security/fix/363-domain-ntlm-audit

Enable full domain NTLM auditing only on domain controllers
This commit is contained in:
Zach Mathis (田中ザック) authored and GitHub committed 2026-09-19 13:13:01 +09:00
commit 6fd86f21ce
9 files changed
+350 -5

No files matched your search

+25
View File
@@ -0,0 +1,25 @@
name: Domain NTLM regressions
on:
push:
branches: ['**']
pull_request:
workflow_dispatch:
permissions:
contents: read
jobs:
test:
runs-on: windows-latest
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Test domain NTLM policy in Windows PowerShell 5.1
shell: powershell
run: ./tests/DomainNtlm.Tests.ps1
- name: Test domain NTLM policy in PowerShell 7
shell: pwsh
run: ./tests/DomainNtlm.Tests.ps1
- name: Test domain NTLM audit output in Windows PowerShell 5.1
shell: powershell
run: ./tests/DomainNtlmAuditOutput.Tests.ps1
- name: Test domain NTLM audit output in PowerShell 7
shell: pwsh
run: ./tests/DomainNtlmAuditOutput.Tests.ps1
+2 -1
View File
@@ -10,6 +10,7 @@
**バグ修正:**
- `configure`で全てのホストに`AuditNTLMInDomain=2`を設定していた問題を修正し、ドメインコントローラと確認できたホストにのみ`7` (Enable all)を設定するようにした。その他のホストや役割を判定できないホストでは、この設定を変更しない。ドメインNTLM監査設定を明示的に表示し、設定後の値の確認とレジストリエラーの報告にも対応した。 (#389) (@Shirofune-Security)
- ルールのフィルタ条件が全て適用されず最後の条件のみが適用されていたため、ルール数が正確ではなかった。 (#358) (@fukusuket)
- 依存するログが無効になっているルールも使用可能として報告されていた。 (#358) (@fukusuket)
- 複数のカテゴリに属するルールが重複してカウントされ、CSVファイルにも重複して出力されていた。 (#358) (@fukusuket)
@@ -53,4 +54,4 @@
- `audit-settings`: Windows Event Log audit policy settingsをチェックする
- `audit-filesize`: Windows Event Logファイルサイズをチェックする
- `update-rules`: WELAのSigmaルール設定ファイルを更新する
- `update-rules`: WELAのSigmaルール設定ファイルを更新する
+2 -1
View File
@@ -12,6 +12,7 @@
**Bug Fixes:**
- Fixed domain NTLM auditing: `configure` now sets `AuditNTLMInDomain=7` (Enable all) only on confirmed domain controllers, instead of writing `2` on every host. This setting is left unchanged on other hosts and hosts whose role cannot be determined. Audit output reports the domain NTLM setting, and configuration verifies registry writes and reports failures. (#389) (@Shirofune-Security)
- Rule filtering applied only the last criterion instead of all of them, so rule counts were inaccurate. (#358) (@fukusuket)
- Rules were reported as usable even when the logs they depend on were disabled. (#358) (@fukusuket)
- Rules that belong to multiple categories were counted and written to the CSV files multiple times. (#358) (@fukusuket)
@@ -55,4 +56,4 @@
- `audit-settings`: Check Windows Event Log audit policy settings.
- `audit-filesize`: Check Windows Event Log file size.
- `update-rules`: Update WELA's Sigma rules config files.
- `update-rules`: Update WELA's Sigma rules config files.
+101 -3
View File
@@ -451,6 +451,12 @@ function AuditLogSetting {
}
}
$domainNtlm = Get-WelaDomainNtlmState
$auditResult += [WELA]::new(
"NTLM Authentication", "Domain NTLM auditing", $domainNtlm.Description, @(),
"Not configured", "Enable all (7) on domain controllers only", "",
"AuditNTLMInDomain; applicability is determined from Win32_OperatingSystem.ProductType."
)
$auditResult | ForEach-Object { $_.CountByLevel() }
$auditResult | ForEach-Object {
@@ -479,7 +485,14 @@ function AuditLogSetting {
$disabledCount = ($_.Group | Where-Object { $notEnabled -contains $_.CurrentSetting } | ForEach-Object { $_.Rules.Count } | Measure-Object -Sum).Sum
$out = ""
$color = ""
if (@($_.Group | Where-Object { $_.CurrentSetting -ne "Unknown" }).Count -eq 0) {
if (@($_.Group | Where-Object { $_.Rules.Count -gt 0 }).Count -eq 0) {
# Configuration-only rows have no rule coverage to aggregate.
# Preserve their observed state, including applicability and errors.
$out = ($_.Group | Select-Object -ExpandProperty CurrentSetting -Unique) -join '; '
if (-not $out) { $out = 'Unknown' }
$color = 'DarkYellow'
}
elseif (@($_.Group | Where-Object { $_.CurrentSetting -ne "Unknown" }).Count -eq 0) {
# 設定を確認できないカテゴリ。無効と断定はできない
$out = "Unknown"
$color = "DarkYellow"
@@ -978,6 +991,90 @@ function UpdateRules {
}
}
function Get-WelaDomainNtlmState {
# ProductType distinguishes an actual DC from a member server with AD DS tools installed.
$state = [pscustomobject]@{
Applicable = $false
Readable = $false
Value = $null
Description = 'Unknown (computer role could not be determined)'
}
try {
$os = Get-CimInstance -ClassName Win32_OperatingSystem -Property ProductType -ErrorAction Stop
switch ($os.ProductType) {
1 { $state.Description = 'Not applicable (Windows client)'; return $state }
2 { $state.Applicable = $true }
3 { $state.Description = 'Not applicable (member or standalone server, including non-DC AD CS)'; return $state }
default { return $state }
}
} catch {
$state.Description = "Unknown (computer role query failed: $($_.Exception.Message))"
return $state
}
try {
$path = 'HKLM:\SYSTEM\CurrentControlSet\Services\Netlogon\Parameters'
$state.Description = 'Not configured'
if (Test-Path -LiteralPath $path -ErrorAction Stop) {
$properties = Get-ItemProperty -LiteralPath $path -ErrorAction Stop
$property = $properties.PSObject.Properties['AuditNTLMInDomain']
if ($null -ne $property) {
$state.Value = $property.Value
$state.Description = switch ($state.Value) {
0 { 'Disabled (0)' }
7 { 'Enable all (7)' }
default { "Value $($state.Value) (not interpreted as Enable all)" }
}
}
}
$state.Readable = $true
} catch {
$state.Description = "Unknown (domain NTLM registry read failed: $($_.Exception.Message))"
}
return $state
}
function Set-WelaDomainNtlmAudit {
[CmdletBinding(SupportsShouldProcess = $true)]
param ([switch]$Auto)
$state = Get-WelaDomainNtlmState
Write-Host "Domain NTLM auditing: $($state.Description)"
if (-not $state.Applicable) {
Write-Host '[SKIPPED] Domain NTLM policy is only changed on a confirmed domain controller.' -ForegroundColor Yellow
return
}
if (-not $state.Readable) {
throw 'Domain NTLM policy was not changed because its current state could not be read.'
}
if ($state.Value -eq 7) {
Write-Host '[SKIPPED] Domain NTLM auditing is already Enable all (7).' -ForegroundColor Yellow
return
}
$path = 'HKLM:\SYSTEM\CurrentControlSet\Services\Netlogon\Parameters'
if (-not $PSCmdlet.ShouldProcess("$path\AuditNTLMInDomain", 'Set domain NTLM auditing to Enable all (7)')) { return }
if (-not $Auto) {
$response = Read-Host "Change domain NTLM auditing from '$($state.Description)' to 'Enable all (7)'? (Y/n)"
if ($response -ne '' -and $response -ne 'Y') {
Write-Host '[SKIPPED] Domain NTLM auditing.' -ForegroundColor Yellow
return
}
}
try {
if (-not (Test-Path -LiteralPath $path -ErrorAction Stop)) {
New-Item -Path $path -Force -ErrorAction Stop | Out-Null
}
Set-ItemProperty -LiteralPath $path -Name AuditNTLMInDomain -Value 7 -Type DWord -ErrorAction Stop
$after = Get-WelaDomainNtlmState
if (-not $after.Applicable -or -not $after.Readable -or $after.Value -ne 7) {
throw "Read-back did not confirm Enable all (7). Observed: $($after.Description)"
}
Write-Host '[OK] Domain NTLM auditing: Enable all (7), registry value verified.' -ForegroundColor Green
Write-Host 'Group Policy or MDM may reapply a different value; validate events on the domain controller.'
} catch {
throw "Domain NTLM configuration failed: $($_.Exception.Message)"
}
}
function Set-RegistryConfig {
# レジストリを変更するため -WhatIf / -Confirm に対応する
[CmdletBinding(SupportsShouldProcess = $true, ConfirmImpact = 'Medium')]
@@ -1294,11 +1391,12 @@ function ConfigureAuditSettings {
Write-Host ""
$regPaths = @(
@{Path = "HKLM:\SYSTEM\CurrentControlSet\Control\Lsa\MSV1_0"; Name = "RestrictSendingNTLMTraffic"; Value = 2},
@{Path = "HKLM:\SYSTEM\CurrentControlSet\Control\Lsa\MSV1_0"; Name = "AuditReceivingNTLMTraffic"; Value = 2},
@{Path = "HKLM:\SYSTEM\CurrentControlSet\Services\Netlogon\Parameters"; Name = "AuditNTLMInDomain"; Value = 2}
@{Path = "HKLM:\SYSTEM\CurrentControlSet\Control\Lsa\MSV1_0"; Name = "AuditReceivingNTLMTraffic"; Value = 2}
)
Set-RegistryConfig -RegPaths $regPaths -Auto:$Auto
Set-WelaDomainNtlmAudit -Auto:$Auto
# LDAP query logging (Directory Service EventID 1644) - domain controllers only.
# "15 Field Engineering" = 5 makes expensive / inefficient LDAP searches log as 1644, which surfaces
# BloodHound / SharpHound-style directory reconnaissance. Only applied where the NTDS role is present.
+122
View File
@@ -0,0 +1,122 @@
# Safe unit regressions: load function definitions without dispatching WELA or changing host policy.
$ErrorActionPreference = 'Stop'
$tokens = $null
$parseErrors = $null
$ast = [System.Management.Automation.Language.Parser]::ParseFile((Join-Path $PSScriptRoot '../WELA.ps1'), [ref]$tokens, [ref]$parseErrors)
if ($parseErrors.Count) { throw ($parseErrors | Out-String) }
foreach ($functionName in @('Get-WelaDomainNtlmState', 'Set-WelaDomainNtlmAudit')) {
$definition = $ast.Find({ param($node) $node -is [System.Management.Automation.Language.FunctionDefinitionAst] -and $node.Name -eq $functionName }, $true)
if (-not $definition) { throw "Missing function $functionName" }
. ([scriptblock]::Create($definition.Extent.Text))
}
function Assert-Equal($Actual, $Expected, [string]$Message) {
if ($Actual -cne $Expected) { throw "$Message. Expected '$Expected', got '$Actual'." }
$script:assertions++
}
function Assert-Throws([scriptblock]$Action, [string]$Message) {
$threw = $false
try { & $Action } catch { $threw = $true }
Assert-Equal $threw $true $Message
}
function Reset-Policy($Value, $ProductType = 2) {
$script:value = $Value
$script:productType = $ProductType
$script:writes = 0
$script:prompts = 0
$script:reads = 0
$script:keyExists = $true
$script:roleFails = $false
$script:readFails = $false
$script:writeFails = $false
$script:ignoreWrite = $false
$script:response = 'Y'
}
function Get-CimInstance {
param($ClassName, $Property, $ErrorAction)
if ($script:roleFails) { throw 'CIM unavailable' }
return [pscustomobject]@{ ProductType = $script:productType }
}
function Test-Path { param($LiteralPath, $ErrorAction) return $script:keyExists }
function Get-ItemProperty {
param($LiteralPath, $ErrorAction)
$script:reads++
if ($script:readFails) { throw 'Access denied' }
if ($null -eq $script:value) { return [pscustomobject]@{} }
return [pscustomobject]@{ AuditNTLMInDomain = $script:value }
}
function New-Item { param($Path, [switch]$Force, $ErrorAction) $script:keyExists = $true }
function Set-ItemProperty {
param($LiteralPath, $Name, $Value, $Type, $ErrorAction)
if ($script:writeFails) { throw 'Access denied' }
if ($Name -ne 'AuditNTLMInDomain') { throw "Unexpected write: $Name" }
$script:writes++
if (-not $script:ignoreWrite) { $script:value = $Value }
}
function Read-Host { param($Prompt) $script:prompts++; return $script:response }
$script:assertions = 0
foreach ($initial in @($null, 0, 2, 7)) {
Reset-Policy $initial
Set-WelaDomainNtlmAudit -Auto
Assert-Equal $script:value 7 "DC initial value '$initial' reaches Enable all"
$expectedWrites = if ($initial -eq 7) { 0 } else { 1 }
Assert-Equal $script:writes $expectedWrites 'Already configured DCs are idempotent'
}
Reset-Policy 2
Assert-Equal ((Get-WelaDomainNtlmState).Description) 'Value 2 (not interpreted as Enable all)' 'Migration reports old value without inventing semantics'
foreach ($role in @(1, 3)) {
Reset-Policy 2 $role
Set-WelaDomainNtlmAudit -Auto
Assert-Equal $script:writes 0 "No domain policy writes on non-DC ProductType $role"
Assert-Equal $script:reads 0 'Non-DC domain registry is not read'
Assert-Equal ((Get-WelaDomainNtlmState).Description -like 'Not applicable*') $true 'Non-DC is reported as not applicable'
Assert-Equal $script:value 2 'Existing non-DC value is preserved'
}
foreach ($role in @($null, 0, 42)) {
Reset-Policy 2 $role
Set-WelaDomainNtlmAudit -Auto
Assert-Equal $script:writes 0 'Unknown role never receives domain policy'
Assert-Equal ((Get-WelaDomainNtlmState).Description -like 'Unknown*') $true 'Unknown role is not labeled non-DC'
}
Reset-Policy 2
$script:roleFails = $true
Set-WelaDomainNtlmAudit -Auto
Assert-Equal $script:writes 0 'Failed role discovery never receives domain policy'
Assert-Equal ((Get-WelaDomainNtlmState).Description -like 'Unknown*query failed*') $true 'Role errors are visible'
Reset-Policy 2
$script:readFails = $true
Assert-Throws { Set-WelaDomainNtlmAudit -Auto } 'Unreadable DC policy fails visibly'
Assert-Equal $script:writes 0 'Unreadable current value is preserved'
Reset-Policy $null
$script:keyExists = $false
Set-WelaDomainNtlmAudit -Auto
Assert-Equal $script:keyExists $true 'Missing DC policy key is created'
Assert-Equal $script:value 7 'Missing DC key receives Enable all'
Reset-Policy 2
Set-WelaDomainNtlmAudit -WhatIf
Assert-Equal $script:writes 0 'WhatIf preserves policy'
Assert-Equal $script:prompts 0 'WhatIf does not prompt without Auto'
$script:response = 'n'
Set-WelaDomainNtlmAudit
Assert-Equal $script:writes 0 'Declining preserves policy'
$script:response = ''
Set-WelaDomainNtlmAudit
Assert-Equal $script:value 7 'Confirming applies policy'
foreach ($confirmation in @('y', 'Y')) {
Reset-Policy 2
$script:response = $confirmation
Set-WelaDomainNtlmAudit
Assert-Equal $script:value 7 "Confirmation '$confirmation' applies policy"
Assert-Equal $script:writes 1 "Confirmation '$confirmation' writes once"
}
Reset-Policy 2
$script:response = 'N'
Set-WelaDomainNtlmAudit
Assert-Equal $script:writes 0 'Uppercase refusal preserves policy'
Reset-Policy 2
$script:writeFails = $true
Assert-Throws { Set-WelaDomainNtlmAudit -Auto } 'Write failure propagates'
Reset-Policy 2
$script:ignoreWrite = $true
Assert-Throws { Set-WelaDomainNtlmAudit -Auto } 'Read-back mismatch propagates'
Write-Host "PASS: $script:assertions domain NTLM assertions (mocked; no host changes)."
+65
View File
@@ -0,0 +1,65 @@
# Exercise the real audit renderer/CSV exports with injected observations and rules.
# Only a temporary directory is written; no Windows policy is read or changed.
$ErrorActionPreference = 'Stop'
$tokens = $null; $parseErrors = $null
$ast = [System.Management.Automation.Language.Parser]::ParseFile((Join-Path $PSScriptRoot '../WELA.ps1'), [ref]$tokens, [ref]$parseErrors)
if ($parseErrors.Count) { throw ($parseErrors | Out-String) }
$class = $ast.Find({ param($node) $node -is [System.Management.Automation.Language.TypeDefinitionAst] -and $node.Name -eq 'WELA' }, $true)
. ([scriptblock]::Create($class.Extent.Text))
$definition = $ast.Find({ param($node) $node -is [System.Management.Automation.Language.FunctionDefinitionAst] -and $node.Name -eq 'AuditLogSetting' }, $true)
. ([scriptblock]::Create($definition.Extent.Text))
$script:assertions = 0
function Assert-Equal($Actual, $Expected, [string]$Message) {
if ($Actual -cne $Expected) { throw "$Message. Expected '$Expected', got '$Actual'." }
$script:assertions++
}
function TestAdministrator { return $true }
function CollectAuditpol { param([switch]$UseCached) return $true }
function GetAuditpol { return @{} }
function Get-WelaDomainNtlmState { return [pscustomobject]@{ Description = $script:description } }
function Export-MitreHeatmap { param($sigmaRules, $OutputPath, $UseIdealCount) }
function BuildAuditResult {
param($all_rules, $Baseline, $enabledguid)
$all_rules[0].applicable = $true
@(
[WELA]::new('Fixture rules', 'Available', 'Success', @($all_rules[0]))
[WELA]::new('Fixture rules', 'Unavailable', 'No Auditing', @($all_rules[1]))
)
}
$script:ScriptRoot = Join-Path ([IO.Path]::GetTempPath()) ('wela-domain-output-' + [guid]::NewGuid().ToString('N'))
$null = New-Item -ItemType Directory -Path $script:ScriptRoot
$script:SecurityRulesPath = Join-Path $script:ScriptRoot 'rules.json'
try {
@(
@{ id = 'available-rule'; title = 'Available fixture'; level = 'high'; subcategory_guids = @() }
@{ id = 'unavailable-rule'; title = 'Unavailable fixture'; level = 'medium'; subcategory_guids = @() }
) | ConvertTo-Json -Depth 4 | Set-Content -LiteralPath $script:SecurityRulesPath -Encoding UTF8
foreach ($observed in @(
'Enable all (7)',
'Disabled (0)',
'Not configured',
'Value 2 (not interpreted as Enable all)',
'Not applicable (Windows client)',
'Not applicable (member or standalone server, including non-DC AD CS)',
'Unknown (computer role could not be determined)',
'Unknown (domain NTLM registry read failed: Access denied)'
)) {
$script:description = $observed
$output = (AuditLogSetting -outType std -Baseline YamatoSecurity 6>&1 | Out-String)
$expectedHeading = 'NTLM Authentication: ' + $observed
Assert-Equal ($output -match ('(?m)^' + [regex]::Escape($expectedHeading) + '\r?$')) $true "Console heading retains '$observed'"
Assert-Equal ($output -match 'NTLM Authentication: Partially Enabled') $false 'An empty rule array does not imply partial enablement'
Assert-Equal ($output -match 'Fixture rules: Partially Enabled') $true 'Ordinary rule coverage aggregation is preserved'
$row = @(Import-Csv -LiteralPath (Join-Path $script:ScriptRoot 'WELA-Audit-Result.csv') | Where-Object SubCategory -eq 'Domain NTLM auditing')
Assert-Equal $row.Count 1 'CSV contains one domain NTLM setting row'
Assert-Equal $row[0].CurrentSetting $observed 'CSV retains the observed configuration state'
Assert-Equal $row[0].RuleCount '0' 'Configuration row claims no detection rules'
Assert-Equal @(Import-Csv -LiteralPath (Join-Path $script:ScriptRoot 'UsableRules.csv')).Count 1 'Configuration row does not change usable rule counts'
Assert-Equal @(Import-Csv -LiteralPath (Join-Path $script:ScriptRoot 'UnusableRules.csv')).Count 1 'Configuration row does not change unusable rule counts'
}
Write-Host "PASS: $script:assertions domain NTLM output assertions (mocked observations; temporary CSV files only)."
} finally {
Remove-Item -LiteralPath $script:ScriptRoot -Recurse -Force
}
+27
View File
@@ -36,6 +36,33 @@ Check the Windows event log file size with Yamato Security's recommendations and
## configure
The `configure` command sets the recommended Windows event log audit policy and file size.
Domain NTLM auditing (`AuditNTLMInDomain`) is set to `7` (**Enable all**) only on
confirmed domain controllers. Windows clients, member/standalone servers and
non-DC AD CS servers report **Not applicable** and retain any existing value.
An unavailable or unknown computer role is reported as **Unknown** and skipped.
Role detection uses `Win32_OperatingSystem.ProductType=2`, not the presence of
AD DS tools or a registry key. Before a change, WELA reports the previous numeric
value; legacy value `2` is not described as full auditing. Changes respect the
usual confirmation prompt or `-Auto` and are verified by a registry read-back.
`audit-settings` includes role applicability and the current value in its console
and CSV output. Incoming and outgoing NTLM controls are separate from this policy.
The [Microsoft NTLM auditing guidance](https://learn.microsoft.com/en-us/defender-for-identity/deploy/configure-windows-event-collection#configure-ntlm-auditing)
describes the policy and event collection prerequisites. A registry read-back
does not prove that events were generated, and GPO or MDM may overwrite a local
change. Validate benign domain NTLM activity and expected Operational events on
an isolated DC before deployment. `tests/DomainNtlm.Tests.ps1` uses mocked OS and
registry access; the associated Windows workflow runs Windows PowerShell 5.1 and
PowerShell 7 without changing host policy.
Live-DC validation for [issue #363](https://github.com/Yamato-Security/WELA/issues/363)
remains pending. Before closing that issue, record the Windows build and confirmed
DC role, the previous registry value/type, the verified `AuditNTLMInDomain=7`
DWORD, and representative NTLM event XML from benign test authentication. Also
check that a second run is idempotent and that clients, member servers and non-DC CAs leave
this domain-only setting unchanged. Mocked policy tests and console/CSV regression
tests do not provide this event-generation evidence.
#### `configure` command examples
Apply Yamato Security's recommended settings (with confirmation prompt before changing settings):
```
+2
View File
@@ -8,10 +8,12 @@
**改善:**
- ベースライン定義を`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)
**バグ修正:**
- `configure`で全てのホストに`AuditNTLMInDomain=2`を設定していた問題を修正し、ドメインコントローラと確認できたホストにのみ`7` (Enable all)を設定するようにした。その他のホストや役割を判定できないホストでは、この設定を変更しない。ドメインNTLM監査設定を明示的に表示し、設定後の値の確認とレジストリエラーの報告にも対応した。 (#389) (@Shirofune-Security)
- ルールのフィルタ条件が全て適用されず最後の条件のみが適用されていたため、ルール数が正確ではなかった。 (#358) (@fukusuket)
- 依存するログが無効になっているルールも使用可能として報告されていた。 (#358) (@fukusuket)
- 複数のカテゴリに属するルールが重複してカウントされ、CSVファイルにも重複して出力されていた。 (#358) (@fukusuket)
+4
View File
@@ -7,11 +7,15 @@
**Improvements:**
- Added a `configure-sacl` command that sets targeted audit SACLs on the autostart/persistence registry keys and sensitive files the detection rules watch, so File System (4663), Registry (4657) and Handle Manipulation (4656) auditing produce useful events without enabling global object auditing. It covers machine-wide objects plus per-user HKCU keys and profile AppData across all user profiles and the Default profile (so future users inherit the SACL). Targets live in `config/audit_sacl_targets.json`. (#361) (@YamatoSecurity)
- `configure` now also enables Detailed Tracking > Process Termination (4689), Object Access > Detailed File Share (5145), and (on domain controllers) LDAP query logging (Directory Service 1644 via NTDS `15 Field Engineering`), so a full detection baseline is applied without any manual `auditpol`/registry steps. (#361) (@YamatoSecurity)
- 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:**
- Fixed domain NTLM auditing: `configure` now sets `AuditNTLMInDomain=7` (Enable all) only on confirmed domain controllers, instead of writing `2` on every host. This setting is left unchanged on other hosts and hosts whose role cannot be determined. Audit output reports the domain NTLM setting, and configuration verifies registry writes and reports failures. (#389) (@Shirofune-Security)
- Rule filtering applied only the last criterion instead of all of them, so rule counts were inaccurate. (#358) (@fukusuket)
- Rules were reported as usable even when the logs they depend on were disabled. (#358) (@fukusuket)
- Rules that belong to multiple categories were counted and written to the CSV files multiple times. (#358) (@fukusuket)