diff --git a/.github/workflows/audit-profiles.yml b/.github/workflows/audit-profiles.yml index 189ad52e..1530eac5 100644 --- a/.github/workflows/audit-profiles.yml +++ b/.github/workflows/audit-profiles.yml @@ -17,9 +17,21 @@ jobs: - name: Test shared profiles in PowerShell 7 shell: pwsh run: ./tests/audit-profiles.Tests.ps1 + - name: Test profile audit output in Windows PowerShell 5.1 + shell: powershell + run: ./tests/AuditProfileOutput.Tests.ps1 + - name: Test profile audit output in PowerShell 7 + shell: pwsh + run: ./tests/AuditProfileOutput.Tests.ps1 - name: Read all effective policies using native API in Windows PowerShell 5.1 shell: powershell run: ./tests/audit-profiles.Windows.Tests.ps1 - name: Read all effective policies using native API in PowerShell 7 shell: pwsh run: ./tests/audit-profiles.Windows.Tests.ps1 + - name: Test native writer failure handling in Windows PowerShell 5.1 + shell: powershell + run: ./tests/AuditProfileNativeWriter.Tests.ps1 + - name: Test native writer failure handling in PowerShell 7 + shell: pwsh + run: ./tests/AuditProfileNativeWriter.Tests.ps1 diff --git a/CHANGELOG-Japanese.md b/CHANGELOG-Japanese.md index ddab07e4..b57926fb 100644 --- a/CHANGELOG-Japanese.md +++ b/CHANGELOG-Japanese.md @@ -12,6 +12,7 @@ **バグ修正:** +- `audit-settings`でホストの役割に適用されない監査ポリシーを`Not applicable`と表示し、カテゴリの有効・無効の集計から除外するようにした。NTLMポリシーの値は、DWORD型で保存されている場合にのみ有効な設定値として解釈・検証する。 (#392) (@Shirofune-Security) - 設定時に外部コマンドの終了コードと変更後の設定値を確認し、処理の終了前にも再確認するようにした。書き込み失敗、設定の未反映、CAサービスの再起動失敗、最終確認時の設定の不一致を明示的に報告し、一律に成功とせず、0以外の終了コードを返すようにした。 (#392) (@Shirofune-Security) - `configure`で全てのホストに`AuditNTLMInDomain=2`を設定していた問題を修正し、ドメインコントローラと確認できたホストにのみ`7` (Enable all)を設定するようにした。その他のホストや役割を判定できないホストでは、この設定を変更しない。ドメインNTLM監査設定を明示的に表示し、設定後の値の確認とレジストリエラーの報告にも対応した。 (#389) (@Shirofune-Security) - ルールのフィルタ条件が全て適用されず最後の条件のみが適用されていたため、ルール数が正確ではなかった。 (#358) (@fukusuket) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5e4fc030..2cf6d090 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,7 @@ **Bug Fixes:** +- `audit-settings` now reports role-inapplicable audit policies as `Not applicable` and excludes them from category enablement totals. NTLM policy values are interpreted and verified only when stored as DWORDs. (#392) (@Shirofune-Security) - Configuration now checks native command exit codes, verifies settings after applying changes, and checks them again before finishing. Failed writes, ineffective changes, CA restart failures and settings that no longer match at the final check produce explicit results and a nonzero exit code instead of unconditional success. (#392) (@Shirofune-Security) - 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) diff --git a/WELA.ps1 b/WELA.ps1 index c8084139..2b78f3df 100644 --- a/WELA.ps1 +++ b/WELA.ps1 @@ -41,6 +41,7 @@ class WELA { [string] $Category [string] $SubCategory [string] $CurrentSetting = "" + [string] $AuditPolicyGuid = "" [array] $Rules [hashtable] $RulesCount [string] $DefaultSetting = "" @@ -456,7 +457,9 @@ function BuildAuditResult { if ($sharedPlan) { foreach ($policy in $sharedPlan.policies) { $rules = ApplyRules -rules $all_rules -guid $policy.guid - $current = if ($auditpol.ContainsKey($policy.guid)) { $auditpol[$policy.guid] } else { 'Unknown' } + $current = if ($policy.mode -eq 'not-applicable') { 'Not applicable' } + elseif ($auditpol.ContainsKey($policy.guid)) { $auditpol[$policy.guid] } + else { 'Unknown' } if ($policy.mode -ne 'not-applicable' -and $enabledguid -contains $policy.guid) { $rules | ForEach-Object { $_.applicable = $true } } @@ -468,8 +471,10 @@ function BuildAuditResult { $defaultSetting = if ($legacy) { $legacy.defaultSetting } else { '' } $volume = if ($legacy) { $legacy.volume } else { '' } $note = (@($policy.prerequisites, $policy.note) | Where-Object { $_ }) -join ' ' - $auditResult += [WELA]::New("Security Advanced ($($policy.category))", $policy.id, $current, [array]$rules, + $entry = [WELA]::New("Security Advanced ($($policy.category))", $policy.id, $current, [array]$rules, $defaultSetting, $policy.recommendation, $volume, $note) + $entry.AuditPolicyGuid = $policy.guid + $auditResult += $entry } } @@ -540,10 +545,14 @@ function AuditLogSetting { # ベースラインが扱っていないサブカテゴリでも、そのサブカテゴリが有効ならルールは動く。 # ルール自身が持つ subcategory_guids を見て救済する。 + # A live audit mask cannot make a role-inapplicable policy produce its events. + $notApplicableGuids = @($auditResult | Where-Object { + $_.CurrentSetting -eq 'Not applicable' -and $_.AuditPolicyGuid + } | Select-Object -ExpandProperty AuditPolicyGuid) $all_rules | ForEach-Object { if (-not $_.applicable) { foreach ($guid in $_.subcategory_guids) { - if ($enabledguid -contains $guid) { + if ($enabledguid -contains $guid -and $notApplicableGuids -notcontains $guid) { $_.applicable = $true break } @@ -581,18 +590,23 @@ function AuditLogSetting { if ($outType -eq "std") { $auditResult | Group-Object -Property Category | ForEach-Object { $notEnabled = @("No Auditing", "Disabled", "Unknown") - $enabledCount = ($_.Group | Where-Object { $notEnabled -notcontains $_.CurrentSetting } | ForEach-Object { $_.Rules.Count } | Measure-Object -Sum).Sum - $disabledCount = ($_.Group | Where-Object { $notEnabled -contains $_.CurrentSetting } | ForEach-Object { $_.Rules.Count } | Measure-Object -Sum).Sum + $summaryRows = @($_.Group | Where-Object { $_.CurrentSetting -ne 'Not applicable' }) + $enabledCount = ($summaryRows | Where-Object { $notEnabled -notcontains $_.CurrentSetting } | ForEach-Object { $_.Rules.Count } | Measure-Object -Sum).Sum + $disabledCount = ($summaryRows | Where-Object { $notEnabled -contains $_.CurrentSetting } | ForEach-Object { $_.Rules.Count } | Measure-Object -Sum).Sum $out = "" $color = "" - if (@($_.Group | Where-Object { $_.Rules.Count -gt 0 }).Count -eq 0) { + if ($summaryRows.Count -eq 0) { + $out = 'Not applicable' + $color = 'DarkYellow' + } + elseif (@($summaryRows | 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 '; ' + $out = ($summaryRows | Select-Object -ExpandProperty CurrentSetting -Unique) -join '; ' if (-not $out) { $out = 'Unknown' } $color = 'DarkYellow' } - elseif (@($_.Group | Where-Object { $_.CurrentSetting -ne "Unknown" }).Count -eq 0) { + elseif (@($summaryRows | Where-Object { $_.CurrentSetting -ne "Unknown" }).Count -eq 0) { # 設定を確認できないカテゴリ。無効と断定はできない $out = "Unknown" $color = "DarkYellow" @@ -611,7 +625,7 @@ function AuditLogSetting { $out = "Partially Enabled" $color = "DarkYellow" } - $enabledPercentage = "0.00%" + $enabledPercentage = "" if ($enabledCount + $disabledCount -ne 0) { $enabledPercentage = "({0:N2}%)" -f (($enabledCount / ($enabledCount + $disabledCount)) * 100) } @@ -1097,6 +1111,7 @@ function Get-WelaDomainNtlmState { Applicable = $false Readable = $false Value = $null + Type = $null Description = 'Unknown (computer role could not be determined)' } try { @@ -1119,10 +1134,15 @@ function Get-WelaDomainNtlmState { $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.Type = (Get-Item -LiteralPath $path -ErrorAction Stop).GetValueKind('AuditNTLMInDomain').ToString() + if ($state.Type -ne 'DWord') { + $state.Description = "Unknown registry type ($($state.Type)): value $($state.Value) (expected DWord)" + } else { + $state.Description = switch ($state.Value) { + 0 { 'Disabled (0)' } + 7 { 'Enable all (7)' } + default { "Value $($state.Value) (not interpreted as Enable all)" } + } } } } @@ -1149,7 +1169,7 @@ function Set-WelaDomainNtlmAudit { if (-not $state.Readable) { throw 'Domain NTLM policy was not changed because its current state could not be read.' } - if ($state.Value -eq 7) { + if ($state.Type -eq 'DWord' -and $state.Value -eq 7) { Write-Host '[SKIPPED] Domain NTLM auditing is already Enable all (7).' -ForegroundColor Yellow return } @@ -1168,7 +1188,7 @@ function Set-WelaDomainNtlmAudit { } 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) { + if (-not $after.Applicable -or -not $after.Readable -or $after.Type -ne 'DWord' -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 @@ -1272,6 +1292,7 @@ function Get-WelaOutgoingNtlmState { $path = 'HKLM:\SYSTEM\CurrentControlSet\Control\Lsa\MSV1_0' $name = 'RestrictSendingNTLMTraffic' $value = $null + $type = $null $readable = $true $description = 'Not configured (Allow all)' try { @@ -1281,11 +1302,16 @@ function Get-WelaOutgoingNtlmState { $property = $properties.PSObject.Properties[$name] if ($null -ne $property) { $value = $property.Value - $description = switch ($value) { - 0 { 'Allow all (0)' } - 1 { 'Audit all (1)' } - 2 { 'Deny all (2): authentication restriction, with block events' } - default { "Unknown registry value ($value)" } + $type = (Get-Item -LiteralPath $path -ErrorAction Stop).GetValueKind($name).ToString() + if ($type -ne 'DWord') { + $description = "Unknown registry type ($type): value $value (expected DWord)" + } else { + $description = switch ($value) { + 0 { 'Allow all (0)' } + 1 { 'Audit all (1)' } + 2 { 'Deny all (2): authentication restriction, with block events' } + default { "Unknown registry value ($value)" } + } } } } @@ -1295,6 +1321,7 @@ function Get-WelaOutgoingNtlmState { } [pscustomobject]@{ Value = $value + Type = $type Readable = $readable Description = $description PolicySource = Get-WelaOutgoingNtlmPolicySource @@ -1321,17 +1348,17 @@ function Set-WelaOutgoingNtlmPolicy { if (-not $state.Readable) { throw 'Outgoing NTLM was not changed because its current state could not be read.' } - if ($Mode -eq 'PreserveOrAudit' -and $state.Value -eq 2) { + if ($Mode -eq 'PreserveOrAudit' -and $state.Type -eq 'DWord' -and $state.Value -eq 2) { Write-Host '[PRESERVED] Existing Deny all enforcement. Use -OutgoingNtlmMode Audit to explicitly replace it.' -ForegroundColor Yellow return } - if ($Mode -eq 'PreserveOrAudit' -and $null -ne $state.Value -and $state.Value -notin @(0, 1, 2)) { - Write-Warning 'Unknown outgoing NTLM value was preserved. Select an explicit -OutgoingNtlmMode after reviewing policy.' + if ($Mode -eq 'PreserveOrAudit' -and $null -ne $state.Type -and ($state.Type -ne 'DWord' -or $state.Value -notin @(0, 1, 2))) { + Write-Warning 'Unknown outgoing NTLM value/type was preserved. Select an explicit -OutgoingNtlmMode after reviewing policy.' return } $desired = if ($Mode -eq 'Deny') { 2 } else { 1 } $description = if ($desired -eq 2) { 'Deny all (2): restrict outgoing NTLM authentication' } else { 'Audit all (1): log outgoing NTLM without denying it' } - if ($state.Value -eq $desired) { + if ($state.Type -eq 'DWord' -and $state.Value -eq $desired) { Write-Host "[SKIPPED] Outgoing NTLM is already $description." -ForegroundColor Yellow return } @@ -1353,15 +1380,15 @@ function Set-WelaOutgoingNtlmPolicy { if (-not $freshState.Readable) { throw 'Outgoing NTLM was not changed because its current state became unreadable.' } - if ($Mode -eq 'PreserveOrAudit' -and $freshState.Value -eq 2) { + if ($Mode -eq 'PreserveOrAudit' -and $freshState.Type -eq 'DWord' -and $freshState.Value -eq 2) { Write-Host '[PRESERVED] Deny all enforcement appeared before the write. Select explicit Audit mode to replace it.' -ForegroundColor Yellow return } - if ($Mode -eq 'PreserveOrAudit' -and $null -ne $freshState.Value -and $freshState.Value -notin @(0, 1, 2)) { - Write-Warning "Outgoing NTLM changed to an unknown value ($($freshState.Value)); it was preserved." + if ($Mode -eq 'PreserveOrAudit' -and $null -ne $freshState.Type -and ($freshState.Type -ne 'DWord' -or $freshState.Value -notin @(0, 1, 2))) { + Write-Warning "Outgoing NTLM changed to an unknown value/type ($($freshState.Value)/$($freshState.Type)); it was preserved." return } - if ($freshState.Value -eq $desired) { + if ($freshState.Type -eq 'DWord' -and $freshState.Value -eq $desired) { Write-Host "[SKIPPED] Outgoing NTLM is now already $description." -ForegroundColor Yellow return } @@ -1370,7 +1397,7 @@ function Set-WelaOutgoingNtlmPolicy { } Set-ItemProperty -LiteralPath $path -Name $name -Value $desired -Type DWord -ErrorAction Stop $after = Get-WelaOutgoingNtlmState - if (-not $after.Readable -or $after.Value -ne $desired) { + if (-not $after.Readable -or $after.Type -ne 'DWord' -or $after.Value -ne $desired) { throw "Read-back did not match requested value $desired. Observed: $($after.Description)" } Write-Host "[OK] Outgoing NTLM: $($after.Description)" -ForegroundColor Green diff --git a/modules/AuditProfiles.psm1 b/modules/AuditProfiles.psm1 index 445e497a..4dad5fc8 100644 --- a/modules/AuditProfiles.psm1 +++ b/modules/AuditProfiles.psm1 @@ -203,8 +203,16 @@ function Set-WelaEffectiveAuditPolicy { ) if ($Mode -eq 'minimum' -and $Mask -eq 0) { return } $arguments = @(Get-WelaAuditSetArguments -Guid $Guid -Mask $Mask -Mode $Mode) - $output = & auditpol.exe @arguments 2>&1 - if ($LASTEXITCODE -ne 0) { throw "auditpol /set failed ($LASTEXITCODE): $($output -join ' ')" } + $command = Get-Command -Name 'auditpol.exe' -CommandType Application -ErrorAction Stop + # Native stderr alone is not failure, including under Windows PowerShell 5.1. + $ErrorActionPreference = 'Continue' + $PSNativeCommandUseErrorActionPreference = $false + $global:LASTEXITCODE = $null + $output = @(& $command.Source @arguments 2>&1) + $exitCode = $global:LASTEXITCODE # Snapshot before formatting diagnostics or running another command. + if ($null -eq $exitCode -or $exitCode -ne 0) { + throw "auditpol /set failed ($exitCode): $($output -join ' ')" + } } function Get-WelaHostContext { diff --git a/scripts/Configuration.ps1 b/scripts/Configuration.ps1 index eb49793f..d8baddde 100644 --- a/scripts/Configuration.ps1 +++ b/scripts/Configuration.ps1 @@ -370,10 +370,10 @@ function Set-WelaNtlmConfigurationControl { $skipReason = $state.Description } elseif (-not $state.Readable) { throw "$Scope NTLM current state could not be read: $($state.Description)" - } elseif ($Scope -eq 'Outgoing' -and $Mode -eq 'PreserveOrAudit' -and $state.Value -eq 2) { + } elseif ($Scope -eq 'Outgoing' -and $Mode -eq 'PreserveOrAudit' -and $state.Type -eq 'DWord' -and $state.Value -eq 2) { $skipReason = 'Preserved existing Deny all enforcement (2); use -OutgoingNtlmMode Audit to explicitly replace it.' - } elseif ($Scope -eq 'Outgoing' -and $Mode -eq 'PreserveOrAudit' -and $null -ne $state.Value -and $state.Value -notin @(0, 1, 2)) { - $skipReason = "Preserved unknown outgoing NTLM value ($($state.Value)); select an explicit mode after policy review." + } elseif ($Scope -eq 'Outgoing' -and $Mode -eq 'PreserveOrAudit' -and $null -ne $state.Type -and ($state.Type -ne 'DWord' -or $state.Value -notin @(0, 1, 2))) { + $skipReason = "Preserved unknown outgoing NTLM value/type ($($state.Value)/$($state.Type)); select an explicit mode after policy review." } if (-not $skipReason) { if ($Scope -eq 'Outgoing' -and $Mode -eq 'Deny') { diff --git a/tests/AuditProfileNativeWriter.Tests.ps1 b/tests/AuditProfileNativeWriter.Tests.ps1 new file mode 100644 index 00000000..ea96f559 --- /dev/null +++ b/tests/AuditProfileNativeWriter.Tests.ps1 @@ -0,0 +1,79 @@ +# Executes only the exported writer's function definition with safe resolver and +# argument-builder fixtures. Native children emit diagnostics and exit; no auditpol +# command or Windows policy mutation is ever invoked. +$ErrorActionPreference = 'Stop' +$tokens = $null; $errors = $null +$path = Join-Path $PSScriptRoot '../modules/AuditProfiles.psm1' +$ast = [Management.Automation.Language.Parser]::ParseFile($path, [ref]$tokens, [ref]$errors) +if ($errors.Count) { throw ($errors | Out-String) } +$definition = $ast.Find({ param($node) $node -is [Management.Automation.Language.FunctionDefinitionAst] -and $node.Name -eq 'Set-WelaEffectiveAuditPolicy' }, $true) +if (-not $definition) { throw 'Native audit writer definition was not found.' } +. ([scriptblock]::Create($definition.Extent.Text)) +Set-StrictMode -Version 2.0 +$engine = (Get-Command -Name (Get-Process -Id $PID).Path -CommandType Application -ErrorAction Stop).Source +$guid = '0CCE922B-69AE-11D9-BED3-505054503030' +$script:assertions = 0; $script:lookups = 0 +function Assert($Condition, [string]$Message) { + if (-not $Condition) { throw "FAIL: $Message" } + $script:assertions++ +} +function Invoke-ExpectFailure([scriptblock]$Action, [string]$Pattern) { + $caught = '' + try { & $Action } catch { $caught = $_.ToString() } + Assert ($caught -match $Pattern) "Expected '$Pattern'; observed '$caught'" + return $caught +} +function Get-Command { + param($Name, $CommandType, $ErrorAction) + $script:lookups++ + if ($Name -ne 'auditpol.exe' -or $CommandType -ne 'Application' -or $ErrorAction -ne 'Stop') { + throw 'Writer must resolve the auditpol application with a terminating lookup.' + } + if ($script:lookupFails) { throw 'Injected auditpol lookup failure' } + [pscustomobject]@{ Source = $script:resolvedSource } +} +function Get-WelaAuditSetArguments { + param($Guid, $Mask, $Mode) + return $script:nativeArguments +} +$script:lookupFails = $true +$script:resolvedSource = $engine +$script:nativeArguments = @('-NoProfile', '-Command', 'exit 0') +$global:LASTEXITCODE = 0 +$null = Invoke-ExpectFailure { Set-WelaEffectiveAuditPolicy -Guid $guid -Mask 1 } 'Injected auditpol lookup failure' +Assert ($script:lookups -eq 1) 'A stale native zero cannot bypass a failed executable lookup' + +$script:lookupFails = $false +$script:resolvedSource = Join-Path ([IO.Path]::GetTempPath()) ('wela-missing-native-' + [guid]::NewGuid().ToString('N') + '.exe') +$global:LASTEXITCODE = 0 +$null = Invoke-ExpectFailure { Set-WelaEffectiveAuditPolicy -Guid $guid -Mask 1 } 'not recognized|failed' +Assert ($null -eq $global:LASTEXITCODE) 'An executable disappearing after lookup cannot retain an earlier success code' + +$script:resolvedSource = $engine +$script:nativeArguments = @('-NoProfile', '-Command', "[Console]::Error.WriteLine('writer native failure diagnostic'); exit 7") +$global:LASTEXITCODE = 0 +$caught = Invoke-ExpectFailure { Set-WelaEffectiveAuditPolicy -Guid $guid -Mask 1 } 'failed \(7\)' +Assert ($caught -match 'writer native failure diagnostic') 'Native exit failure retains stderr diagnostics' +Assert ($global:LASTEXITCODE -eq 7) 'The newly executed process exit code is observed' + +$script:nativeArguments = @('-NoProfile', '-Command', "[Console]::Error.WriteLine('non-fatal native diagnostic'); exit 0") +$global:LASTEXITCODE = 7 +$PSNativeCommandUseErrorActionPreference = $true +Set-WelaEffectiveAuditPolicy -Guid $guid -Mask 3 +Assert ($global:LASTEXITCODE -eq 0) 'A fresh zero succeeds even with stderr and a previous failure' +Assert ($ErrorActionPreference -eq 'Stop' -and $PSNativeCommandUseErrorActionPreference) 'Native preferences remain local to the writer' + +# A malformed/inert executable fixture returns without updating a process exit code. +# This proves absence of a new code cannot be mistaken for the previous zero. +$script:resolvedSource = { 'Fixture produced no native exit status' } +$script:nativeArguments = @() +$global:LASTEXITCODE = 0 +$null = Invoke-ExpectFailure { Set-WelaEffectiveAuditPolicy -Guid $guid -Mask 1 } 'failed \(\)' +Assert ($null -eq $global:LASTEXITCODE) 'Missing new native exit status is rejected' + +$script:lookupFails = $true +$before = $script:lookups +Set-WelaEffectiveAuditPolicy -Guid $guid -Mask 0 -Mode minimum +Assert ($script:lookups -eq $before) 'An empty minimum policy does not resolve or execute a writer' +$global:LASTEXITCODE = 0 # Expected fixture failures must not fail the CI shell wrapper. +Write-Host "PASS: $script:assertions native audit writer assertions (safe native children; no policy changes)." diff --git a/tests/AuditProfileOutput.Tests.ps1 b/tests/AuditProfileOutput.Tests.ps1 new file mode 100644 index 00000000..029cdb02 --- /dev/null +++ b/tests/AuditProfileOutput.Tests.ps1 @@ -0,0 +1,113 @@ +# Exercise the real profile, audit renderer, rule coverage and CSV output with +# injected audit observations. Only temporary files are written; no Windows policy changes. +$ErrorActionPreference = 'Stop' +Import-Module (Join-Path $PSScriptRoot '../modules/AuditProfiles.psm1') -Force +$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)) +foreach ($name in @('ApplyRules', 'BuildAuditResult', 'AuditLogSetting')) { + $definition = $ast.Find({ param($node) $node -is [System.Management.Automation.Language.FunctionDefinitionAst] -and $node.Name -eq $name }, $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 $script:observedAudit } +function Get-WelaSelectedContext { return [pscustomobject]@{ Role = $script:observedRole; Build = 26100 } } +function GetBaselineConfig { + # Advanced audit policies still come from the actual versioned profile. + return [pscustomobject]@{ baselines = [pscustomobject]@{ YamatoSecurity = [pscustomobject]@{} }; catalog = @() } +} +function Get-WelaOutgoingNtlmState { return [pscustomobject]@{ Description = 'Audit all (1)'; PolicySource = 'Test observation' } } +function Get-WelaDomainNtlmState { return [pscustomobject]@{ Description = 'Test observation' } } +function Export-MitreHeatmap { + param($sigmaRules, $OutputPath, $UseIdealCount) + $script:heatmapRules = @($sigmaRules) +} + +$catalog = (Import-WelaAuditProfiles).catalog +$guids = @{} +foreach ($policy in $catalog) { $guids[$policy.id] = $policy.guid } +$fallbackGuid = '00000000-0000-0000-0000-000000000001' +$script:ScriptRoot = Join-Path ([IO.Path]::GetTempPath()) ('wela-profile-output-' + [guid]::NewGuid().ToString('N')) +$null = New-Item -ItemType Directory -Path $script:ScriptRoot +$script:SecurityRulesPath = Join-Path $script:ScriptRoot 'rules.json' +try { + @( + @{ id = 'directory'; title = 'DC-only rule'; level = 'high'; subcategory_guids = @($guids['Directory Service Changes']) } + @{ id = 'kerberos'; title = 'DC-only rule in a mixed category'; level = 'high'; subcategory_guids = @($guids['Kerberos Authentication Service']) } + @{ id = 'credential'; title = 'Disabled rule in a mixed category'; level = 'medium'; subcategory_guids = @($guids['Credential Validation']) } + @{ id = 'ca'; title = 'CA-only rule'; level = 'medium'; subcategory_guids = @($guids['Certification Services']) } + @{ id = 'kernel'; title = 'Enabled applicable rule'; level = 'medium'; subcategory_guids = @($guids['Kernel Object']) } + @{ id = 'alternative'; title = 'Applicable alternative log source'; level = 'medium'; subcategory_guids = @($guids['Directory Service Changes'], $guids['Kernel Object']) } + @{ id = 'fallback'; title = 'Enabled policy outside the catalog'; level = 'low'; subcategory_guids = @($fallbackGuid) } + @{ id = 'unknown'; title = 'Uncategorized rule'; level = 'low'; subcategory_guids = @() } + ) | ConvertTo-Json -Depth 4 | Set-Content -LiteralPath $script:SecurityRulesPath -Encoding UTF8 + + foreach ($role in @('Client', 'MemberServer', 'DomainController', 'ADCS')) { + foreach ($observed in @('Success', 'No Auditing', 'Missing')) { + $script:observedRole = $role + $script:observedAudit = @{} + foreach ($policy in $catalog) { $script:observedAudit[$policy.guid] = 'No Auditing' } + foreach ($name in @('Directory Service Changes', 'Kerberos Authentication Service', 'Certification Services')) { + if ($observed -eq 'Missing') { $script:observedAudit.Remove($guids[$name]) } + else { $script:observedAudit[$guids[$name]] = $observed } + } + $script:observedAudit[$guids['Kernel Object']] = 'Success' + $script:observedAudit[$fallbackGuid] = 'Success' + + $output = AuditLogSetting -outType std -Baseline YamatoSecurity 6>&1 | Out-String + $rows = @(Import-Csv -LiteralPath (Join-Path $script:ScriptRoot 'WELA-Audit-Result.csv')) + foreach ($name in @('Directory Service Changes', 'Kerberos Authentication Service', 'Certification Services')) { + $policy = $catalog | Where-Object id -eq $name + $row = @($rows | Where-Object SubCategory -eq $name) + $expectedState = if ($role -notin $policy.roles) { 'Not applicable' } + elseif ($observed -eq 'Missing') { 'Unknown' } + else { $observed } + Assert-Equal $row.Count 1 "$role/$observed contains exactly one $name CSV row" + Assert-Equal $row[0].CurrentSetting $expectedState "$role/$observed $name uses role applicability before the live state" + $expectedRuleCount = if ($name -eq 'Directory Service Changes') { '2' } else { '1' } + Assert-Equal $row[0].RuleCount $expectedRuleCount "$role/$observed retains mapped rules for $name without dropping them from the corpus" + } + + if ($role -ne 'DomainController') { + Assert-Equal ($output -match '(?m)^Security Advanced \(DS Access\): Not applicable\r?$') $true "$role/$observed all-inapplicable category has no enabled percentage" + Assert-Equal ($output -match '(?m)^Security Advanced \(Account Logon\): Disabled\(0[.,]00%\)\r?$') $true "$role/$observed excludes DC-only rows from mixed category totals" + } + if ($role -ne 'ADCS') { + Assert-Equal ($output -match '(?m)^Security Advanced \(Object Access\): Enabled\(100[.,]00%\)\r?$') $true "$role/$observed excludes the CA-only row from enabled category coverage" + } + + $usable = @(Import-Csv -LiteralPath (Join-Path $script:ScriptRoot 'UsableRules.csv')) + $unusable = @(Import-Csv -LiteralPath (Join-Path $script:ScriptRoot 'UnusableRules.csv')) + $expectedUsable = 3 + if ($observed -eq 'Success' -and $role -eq 'DomainController') { $expectedUsable += 2 } + if ($observed -eq 'Success' -and $role -eq 'ADCS') { $expectedUsable++ } + Assert-Equal $usable.Count $expectedUsable "$role/$observed does not rescue role-inapplicable GUIDs as usable" + Assert-Equal ($usable.Count + $unusable.Count) 8 "$role/$observed retains all unique rules in the utilization denominator" + Assert-Equal ($usable.id -contains 'alternative') $true "$role/$observed permits an applicable alternative source" + Assert-Equal ($usable.id -contains 'fallback') $true "$role/$observed still rescues an enabled GUID outside the catalog" + Assert-Equal ($usable.id -contains 'unknown') $false "$role/$observed leaves an unknown source unavailable" + $expectedUtilization = 'You can utilize {0:N2}% of your detection rules.' -f ($expectedUsable / 8 * 100) + Assert-Equal ($output.Contains($expectedUtilization)) $true "$role/$observed reports utilization from the complete deduplicated corpus" + foreach ($ruleId in @('directory', 'kerberos', 'ca')) { + $rule = $script:heatmapRules | Where-Object id -eq $ruleId + $applicableRole = if ($ruleId -eq 'ca') { 'ADCS' } else { 'DomainController' } + if ($role -ne $applicableRole) { + Assert-Equal $rule.applicable $false "$role/$observed excludes $ruleId from current heatmap coverage" + Assert-Equal $rule.ideal $false "$role/$observed excludes $ruleId from ideal heatmap coverage" + } + } + } + } + Write-Host "PASS: $script:assertions audit profile output assertions (mocked observations; temporary CSV files only)." +} finally { + Remove-Item -LiteralPath $script:ScriptRoot -Recurse -Force +} diff --git a/tests/DomainNtlm.Tests.ps1 b/tests/DomainNtlm.Tests.ps1 index 719d0278..761676c3 100644 --- a/tests/DomainNtlm.Tests.ps1 +++ b/tests/DomainNtlm.Tests.ps1 @@ -18,8 +18,9 @@ function Assert-Throws([scriptblock]$Action, [string]$Message) { try { & $Action } catch { $threw = $true } Assert-Equal $threw $true $Message } -function Reset-Policy($Value, $ProductType = 2) { +function Reset-Policy($Value, $ProductType = 2, [string]$Type = 'DWord') { $script:value = $Value + $script:type = $Type $script:productType = $ProductType $script:writes = 0 $script:prompts = 0 @@ -27,8 +28,10 @@ function Reset-Policy($Value, $ProductType = 2) { $script:keyExists = $true $script:roleFails = $false $script:readFails = $false + $script:typeReadFails = $false $script:writeFails = $false $script:ignoreWrite = $false + $script:ignoreTypeWrite = $false $script:response = 'Y' } function Get-CimInstance { @@ -44,13 +47,26 @@ function Get-ItemProperty { if ($null -eq $script:value) { return [pscustomobject]@{} } return [pscustomobject]@{ AuditNTLMInDomain = $script:value } } +function Get-Item { + param($LiteralPath, $ErrorAction) + $key = [pscustomobject]@{} + $key | Add-Member ScriptMethod GetValueKind { + param($Name) + if ($script:typeReadFails) { throw 'Value kind unavailable' } + return [Microsoft.Win32.RegistryValueKind]$script:type + } + return $key +} 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 } + if (-not $script:ignoreWrite) { + $script:value = $Value + if (-not $script:ignoreTypeWrite) { $script:type = $Type } + } } function Read-Host { param($Prompt) $script:prompts++; return $script:response } @@ -119,4 +135,23 @@ Assert-Throws { Set-WelaDomainNtlmAudit -Auto } 'Write failure propagates' Reset-Policy 2 $script:ignoreWrite = $true Assert-Throws { Set-WelaDomainNtlmAudit -Auto } 'Read-back mismatch propagates' +foreach ($kind in @('String', 'QWord')) { + Reset-Policy '7' 2 $kind + $state = Get-WelaDomainNtlmState + Assert-Equal $state.Type $kind 'Domain state retains registry kind' + Assert-Equal ($state.Description -like 'Unknown registry type*expected DWord*') $true 'A non-DWORD 7 is not reported as Enable all' + Set-WelaDomainNtlmAudit -Auto + Assert-Equal $script:writes 1 'A numerically matching value with the wrong type is repaired' + Assert-Equal $script:type 'DWord' 'Domain repair writes DWORD' + Assert-Equal ((Get-WelaDomainNtlmState).Description) 'Enable all (7)' 'Only the repaired DWORD is reported as Enable all' +} +Reset-Policy '7' 2 'String' +$script:ignoreTypeWrite = $true +Assert-Throws { Set-WelaDomainNtlmAudit -Auto } 'Domain read-back rejects the right value with the wrong type' +Assert-Equal $script:writes 1 'Domain read-back type failure occurs after an attempted repair' +Reset-Policy 7 +$script:typeReadFails = $true +Assert-Equal ((Get-WelaDomainNtlmState).Readable) $false 'A registry kind read failure is not a readable domain state' +Assert-Throws { Set-WelaDomainNtlmAudit -Auto } 'Domain configuration fails closed when registry kind cannot be read' +Assert-Equal $script:writes 0 'Unknown domain registry kind is never overwritten' Write-Host "PASS: $script:assertions domain NTLM assertions (mocked; no host changes)." diff --git a/tests/IntegrationNtlmConfiguration.Tests.ps1 b/tests/IntegrationNtlmConfiguration.Tests.ps1 index 540f70d1..732a7b84 100644 --- a/tests/IntegrationNtlmConfiguration.Tests.ps1 +++ b/tests/IntegrationNtlmConfiguration.Tests.ps1 @@ -27,6 +27,8 @@ function New-TestContext([switch]$DryRun) { } function Reset-Mocks($Outgoing = 0, $Domain = 2, $ProductType = 2) { $script:registry = @{ RestrictSendingNTLMTraffic = $Outgoing; AuditNTLMInDomain = $Domain } + $script:registryTypes = @{ RestrictSendingNTLMTraffic = 'DWord'; AuditNTLMInDomain = 'DWord' } + $script:typeReadFails = $false; $script:ignoreTypeWrite = $false $script:productType = $ProductType $script:writes = 0; $script:readFails = $false; $script:writeFails = '' $script:roleFails = $false @@ -49,10 +51,20 @@ function Get-ItemProperty { if ($script:readFails) { throw 'Mock registry read failure' } return [pscustomobject]$script:registry } +function Get-Item { + param($LiteralPath, $ErrorAction) + $key = [pscustomobject]@{} + $key | Add-Member ScriptMethod GetValueKind { + param($Name) + if ($script:typeReadFails) { throw 'Mock registry kind read failure' } + return [Microsoft.Win32.RegistryValueKind]$script:registryTypes[$Name] + } + return $key +} function Get-WelaRegistryState { param($Path, $Name) if ($script:readFails) { throw 'Mock registry read failure' } - [pscustomobject]@{ KeyExists = $true; ValueExists = ($null -ne $script:registry[$Name]); Value = $script:registry[$Name]; Type = 'DWord' } + [pscustomobject]@{ KeyExists = $true; ValueExists = ($null -ne $script:registry[$Name]); Value = $script:registry[$Name]; Type = $script:registryTypes[$Name] } } function Set-ItemProperty { param($LiteralPath, $Name, $Value, $Type, $ErrorAction) @@ -64,6 +76,7 @@ function Set-ItemProperty { if ($script:writeFails -eq $Name) { throw 'Mock NTLM write failure' } $script:writes++ $script:registry[$Name] = $Value + if (-not $script:ignoreTypeWrite) { $script:registryTypes[$Name] = $Type } } try { Reset-Mocks @@ -145,6 +158,47 @@ try { Set-WelaOutgoingNtlmPolicy -Context $context -WhatIf Set-WelaDomainNtlmAudit -Context $context -WhatIf Assert ($script:writes -eq 0) 'Context adapters also preserve standalone WhatIf behavior' + + foreach ($value in @('0', '1', '2')) { + Reset-Mocks $value + $script:registryTypes.RestrictSendingNTLMTraffic = 'String' + $context = New-TestContext + Set-WelaOutgoingNtlmPolicy -Context $context + $row = $context.Results[0] + Assert ($script:writes -eq 0 -and $row.Status -eq 'Skipped') 'Integrated default preserves numeric strings' + Assert ($row.Diagnostic -match 'unknown.*value/type' -and $row.Before.Type -eq 'String') 'Integrated early decision records unknown type without mislabeling string 2 as enforcement' + } + foreach ($mode in @('Audit', 'Deny')) { + $desired = if ($mode -eq 'Audit') { 1 } else { 2 } + Reset-Mocks ([string]$desired) '7' + $script:registryTypes.RestrictSendingNTLMTraffic = 'String' + $script:registryTypes.AuditNTLMInDomain = 'String' + $context = New-TestContext + Set-WelaOutgoingNtlmPolicy -Context $context -Mode $mode + Set-WelaDomainNtlmAudit -Context $context + $result = Complete-WelaConfiguration $context + Assert ($result.ExitCode -eq 0 -and $script:writes -eq 2) 'Explicit outgoing and domain configuration repair matching numeric strings' + Assert ($script:registryTypes.RestrictSendingNTLMTraffic -eq 'DWord' -and $script:registryTypes.AuditNTLMInDomain -eq 'DWord') 'Both integrated repairs verify DWORD types' + $journal = @(Get-Content -LiteralPath (Join-Path $context.BackupPath 'before.jsonl') | ConvertFrom-Json) + Assert ($journal.Count -eq 2 -and $journal[0].Before.Type -eq 'String' -and $journal[1].Before.Type -eq 'String') 'Type repairs journal original string types for recovery' + } + Reset-Mocks '1' '7' + $script:registryTypes.RestrictSendingNTLMTraffic = 'String' + $script:registryTypes.AuditNTLMInDomain = 'String' + $script:ignoreTypeWrite = $true + $context = New-TestContext + Set-WelaOutgoingNtlmPolicy -Context $context -Mode Audit + Set-WelaDomainNtlmAudit -Context $context + $result = Complete-WelaConfiguration $context + Assert ($script:writes -eq 2 -and $result.Failed -eq 2 -and $result.ExitCode -eq 1) 'Integrated read-back rejects numeric matches with unchanged invalid types' + + Reset-Mocks 1 7 + $script:typeReadFails = $true + $context = New-TestContext + Set-WelaOutgoingNtlmPolicy -Context $context -Mode Audit + Set-WelaDomainNtlmAudit -Context $context + $result = Complete-WelaConfiguration $context + Assert ($script:writes -eq 0 -and $result.Failed -eq 2 -and $result.ExitCode -eq 1) 'Unreadable registry kinds fail closed before integrated writes' Write-Host "PASS: $script:assertions NTLM integration assertions (mocked; no Windows changes)." } finally { foreach ($context in $script:contexts) { diff --git a/tests/IntegrationSafety.Tests.ps1 b/tests/IntegrationSafety.Tests.ps1 index a149dee4..29d6dab1 100644 --- a/tests/IntegrationSafety.Tests.ps1 +++ b/tests/IntegrationSafety.Tests.ps1 @@ -58,7 +58,7 @@ function New-RaceContext([switch]$Prompt) { } function Get-WelaOutgoingNtlmState { # The first display is deliberately stale; the shared runner must trust its own fresh read. - [pscustomobject]@{ Readable = $true; Value = 0; Description = 'Allow all (initial read)'; PolicySource = 'mock' } + [pscustomobject]@{ Readable = $true; Value = 0; Type = 'DWord'; Description = 'Allow all (initial read)'; PolicySource = 'mock' } } function Get-WelaRegistryState { param($Path, $Name) diff --git a/tests/OutgoingNtlm.Tests.ps1 b/tests/OutgoingNtlm.Tests.ps1 index e2065b8b..d1b2c5f0 100644 --- a/tests/OutgoingNtlm.Tests.ps1 +++ b/tests/OutgoingNtlm.Tests.ps1 @@ -19,14 +19,17 @@ function Assert-Throws([scriptblock]$Action, [string]$Message) { try { & $Action } catch { $threw = $true } Assert-Equal $threw $true $Message } -function Reset-Policy($Value) { +function Reset-Policy($Value, [string]$Type = 'DWord') { $script:value = $Value + $script:type = $Type $script:keyExists = $true $script:writes = 0 $script:prompts = 0 $script:readFails = $false + $script:typeReadFails = $false $script:writeFails = $false $script:ignoreWrite = $false + $script:ignoreTypeWrite = $false $script:response = 'Y' $script:rsop = @() $script:onPrompt = $null @@ -42,12 +45,25 @@ function Get-ItemProperty { if ($null -eq $script:value) { return [pscustomobject]@{} } return [pscustomobject]@{ RestrictSendingNTLMTraffic = $script:value } } +function Get-Item { + param($LiteralPath, $ErrorAction) + $key = [pscustomobject]@{} + $key | Add-Member ScriptMethod GetValueKind { + param($Name) + if ($script:typeReadFails) { throw 'Value kind unavailable' } + return [Microsoft.Win32.RegistryValueKind]$script:type + } + return $key +} 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' } $script:writes++ - if (-not $script:ignoreWrite) { $script:value = $Value } + if (-not $script:ignoreWrite) { + $script:value = $Value + if (-not $script:ignoreTypeWrite) { $script:type = $Type } + } } function Get-CimInstance { param($Namespace, $ClassName, $ErrorAction) if ($ClassName -eq 'RSOP_RegistryPolicySetting') { return $script:rsop } } function Read-Host { param($Prompt) $script:prompts++; if ($script:onPrompt) { & $script:onPrompt }; return $script:response } @@ -136,4 +152,42 @@ $script:rsop = @( ) $source = (Get-WelaOutgoingNtlmState).PolicySource Assert-Equal ($source -like 'Last-applied RSoP GPO: Winning GPO*may be stale*') $true 'Matching RSoP priority and freshness limits are reported' +foreach ($kind in @('String', 'QWord')) { + foreach ($initial in @('0', '1', '2')) { + Reset-Policy $initial $kind + $state = Get-WelaOutgoingNtlmState + Assert-Equal $state.Type $kind 'Outgoing state retains registry kind' + Assert-Equal ($state.Description -like 'Unknown registry type*expected DWord*') $true 'A non-DWORD mode is reported as unknown' + Set-WelaOutgoingNtlmPolicy -Auto + Assert-Equal $script:writes 0 'Default mode preserves malformed outgoing types' + Assert-Equal $script:type $kind 'Default mode preserves the original registry type' + } + foreach ($mode in @('Audit', 'Deny')) { + $desired = if ($mode -eq 'Audit') { 1 } else { 2 } + Reset-Policy ([string]$desired) $kind + Set-WelaOutgoingNtlmPolicy -Mode $mode -Auto + Assert-Equal $script:writes 1 'Explicit mode repairs a matching value with the wrong type' + Assert-Equal $script:type 'DWord' 'Explicit mode writes DWORD' + Assert-Equal $script:value $desired 'Explicit repair preserves the requested policy value' + } +} +Reset-Policy '1' 'String' +$script:ignoreTypeWrite = $true +Assert-Throws { Set-WelaOutgoingNtlmPolicy -Mode Audit -Auto } 'Outgoing read-back rejects the right value with the wrong type' +Assert-Equal $script:writes 1 'Outgoing read-back type failure occurs after an attempted repair' +Reset-Policy 1 +$script:typeReadFails = $true +Assert-Equal ((Get-WelaOutgoingNtlmState).Readable) $false 'A registry kind read failure is not a readable outgoing state' +Assert-Throws { Set-WelaOutgoingNtlmPolicy -Mode Audit -Auto } 'Explicit mode fails closed when registry kind cannot be read' +Assert-Equal $script:writes 0 'Unknown outgoing registry kind is never overwritten' +Reset-Policy 0 +$script:onPrompt = { $script:value = '1'; $script:type = 'String' } +Set-WelaOutgoingNtlmPolicy +Assert-Equal $script:writes 0 'Default mode preserves malformed types introduced during confirmation' +Assert-Equal $script:type 'String' 'The final pre-write check retains a newly introduced malformed type' +Reset-Policy 0 +$script:onPrompt = { $script:value = '1'; $script:type = 'String' } +Set-WelaOutgoingNtlmPolicy -Mode Audit +Assert-Equal $script:writes 1 'Explicit mode repairs a malformed type introduced during confirmation' +Assert-Equal $script:type 'DWord' 'Explicit pre-write decision checks the registry type' Write-Host "PASS: $script:assertions outgoing NTLM assertions (mocked; no host changes)." diff --git a/website/docs/resources/changelog.ja.md b/website/docs/resources/changelog.ja.md index 91ed1b07..6238e9e0 100644 --- a/website/docs/resources/changelog.ja.md +++ b/website/docs/resources/changelog.ja.md @@ -15,6 +15,7 @@ **バグ修正:** +- `audit-settings`でホストの役割に適用されない監査ポリシーを`Not applicable`と表示し、カテゴリの有効・無効の集計から除外するようにした。NTLMポリシーの値は、DWORD型で保存されている場合にのみ有効な設定値として解釈・検証する。 (#392) (@Shirofune-Security) - 設定時に外部コマンドの終了コードと変更後の設定値を確認し、処理の終了前にも再確認するようにした。書き込み失敗、設定の未反映、CAサービスの再起動失敗、最終確認時の設定の不一致を明示的に報告し、一律に成功とせず、0以外の終了コードを返すようにした。 (#392) (@Shirofune-Security) - `configure`で全てのホストに`AuditNTLMInDomain=2`を設定していた問題を修正し、ドメインコントローラと確認できたホストにのみ`7` (Enable all)を設定するようにした。その他のホストや役割を判定できないホストでは、この設定を変更しない。ドメインNTLM監査設定を明示的に表示し、設定後の値の確認とレジストリエラーの報告にも対応した。 (#389) (@Shirofune-Security) - ルールのフィルタ条件が全て適用されず最後の条件のみが適用されていたため、ルール数が正確ではなかった。 (#358) (@fukusuket) diff --git a/website/docs/resources/changelog.md b/website/docs/resources/changelog.md index 534841e2..67982b28 100644 --- a/website/docs/resources/changelog.md +++ b/website/docs/resources/changelog.md @@ -17,6 +17,7 @@ **Bug Fixes:** +- `audit-settings` now reports role-inapplicable audit policies as `Not applicable` and excludes them from category enablement totals. NTLM policy values are interpreted and verified only when stored as DWORDs. (#392) (@Shirofune-Security) - Configuration now checks native command exit codes, verifies settings after applying changes, and checks them again before finishing. Failed writes, ineffective changes, CA restart failures and settings that no longer match at the final check produce explicit results and a nonzero exit code instead of unconditional success. (#392) (@Shirofune-Security) - 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)