From ade681ff1b9af78ed1c15c5f955d3830f9519770 Mon Sep 17 00:00:00 2001 From: Shirofune-Security <43838376+Shirofune-Security@users.noreply.github.com> Date: Fri, 18 Sep 2026 21:43:17 +0900 Subject: [PATCH 01/21] Make outgoing NTLM configuration audit-only by default --- .github/workflows/test-outgoing-ntlm.yml | 19 ++++ WELA.ps1 | 134 ++++++++++++++++++++++- tests/OutgoingNtlm.Tests.ps1 | 106 ++++++++++++++++++ website/docs/commands/usage.md | 30 +++++ 4 files changed, 286 insertions(+), 3 deletions(-) create mode 100644 .github/workflows/test-outgoing-ntlm.yml create mode 100644 tests/OutgoingNtlm.Tests.ps1 diff --git a/.github/workflows/test-outgoing-ntlm.yml b/.github/workflows/test-outgoing-ntlm.yml new file mode 100644 index 00000000..73df9a8a --- /dev/null +++ b/.github/workflows/test-outgoing-ntlm.yml @@ -0,0 +1,19 @@ +name: Outgoing NTLM regressions +on: + push: + branches: ['**'] + pull_request: + workflow_dispatch: +permissions: + contents: read +jobs: + test: + runs-on: windows-latest + strategy: + matrix: + shell: [powershell, pwsh] + steps: + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + - name: Test outgoing NTLM policy without changing host settings + shell: ${{ matrix.shell }} + run: ./tests/OutgoingNtlm.Tests.ps1 diff --git a/WELA.ps1 b/WELA.ps1 index a5e00cd6..601a2a32 100644 --- a/WELA.ps1 +++ b/WELA.ps1 @@ -4,6 +4,8 @@ [switch]$Debug, [string]$Baseline, [switch]$Auto, + [ValidateSet("PreserveOrAudit", "Audit", "Deny")] + [string]$OutgoingNtlmMode = "PreserveOrAudit", [switch]$Help ) @@ -437,6 +439,12 @@ function AuditLogSetting { $_ | Add-Member -MemberType NoteProperty -Name "ideal" -Value $false } $auditResult = BuildAuditResult -all_rules $all_rules -Baseline $Baseline -enabledguid $enabledguid + $outgoingNtlm = Get-WelaOutgoingNtlmState + $auditResult += [WELA]::new( + "NTLM Authentication", "Outgoing NTLM policy", $outgoingNtlm.Description, @(), + "Not configured (Allow all)", "Audit all (1); preserve intentional Deny all (2)", "", + "RestrictSendingNTLMTraffic. Policy source: $($outgoingNtlm.PolicySource)" + ) # ベースラインが扱っていないサブカテゴリでも、そのサブカテゴリが有効ならルールは動く。 # ルール自身が持つ subcategory_guids を見て救済する。 @@ -1030,9 +1038,126 @@ function Set-RegistryConfig { } +function Get-WelaOutgoingNtlmPolicySource { + # RSoP is a last-applied policy snapshot, not proof of the current registry writer. + $key = 'SYSTEM\CurrentControlSet\Control\Lsa\MSV1_0' + $name = 'RestrictSendingNTLMTraffic' + $matches = @() + foreach ($class in @('RSOP_RegistryPolicySetting', 'RSOP_SecuritySettingNumeric')) { + try { + $matches += @(Get-CimInstance -Namespace 'root\RSOP\Computer' -ClassName $class -ErrorAction Stop | + Where-Object { + $normalizedKey = $_.keyName -replace '^(MACHINE|HKEY_LOCAL_MACHINE|HKLM)\\', '' + ($normalizedKey -eq $key -and $_.valueName -eq $name) -or + $normalizedKey -eq "$key\$name" + }) + } catch { + # RSoP may be unavailable, including on standalone computers. Never infer "local". + } + } + $policy = $matches | Sort-Object precedence | Select-Object -First 1 + if ($policy -and $policy.GPOID) { + return "Last-applied RSoP GPO: $($policy.GPOID) (may be stale; current registry writer unknown)" + } + return 'Unknown (no matching RSoP source available; local, GPO or MDM provenance is not established)' +} + +function Get-WelaOutgoingNtlmState { + $path = 'HKLM:\SYSTEM\CurrentControlSet\Control\Lsa\MSV1_0' + $name = 'RestrictSendingNTLMTraffic' + $value = $null + $readable = $true + $description = 'Not configured (Allow all)' + try { + if (Test-Path -LiteralPath $path -ErrorAction Stop) { + # Reading the key distinguishes an absent value from a failed read. + $properties = Get-ItemProperty -LiteralPath $path -ErrorAction Stop + $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)" } + } + } + } + } catch { + $readable = $false + $description = "Unknown (registry read failed: $($_.Exception.Message))" + } + [pscustomobject]@{ + Value = $value + Readable = $readable + Description = $description + PolicySource = Get-WelaOutgoingNtlmPolicySource + } +} + +function Set-WelaOutgoingNtlmPolicy { + [CmdletBinding(SupportsShouldProcess = $true)] + param ( + [ValidateSet('PreserveOrAudit', 'Audit', 'Deny')] + [string]$Mode = 'PreserveOrAudit', + [switch]$Auto + ) + $path = 'HKLM:\SYSTEM\CurrentControlSet\Control\Lsa\MSV1_0' + $name = 'RestrictSendingNTLMTraffic' + $state = Get-WelaOutgoingNtlmState + Write-Host "Outgoing NTLM: $($state.Description)" + Write-Host "Policy source: $($state.PolicySource)" + 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) { + 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.' + 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) { + Write-Host "[SKIPPED] Outgoing NTLM is already $description." -ForegroundColor Yellow + return + } + if ($desired -eq 2) { + Write-Warning 'Explicit Deny mode can break NTLM authentication. This is enforcement, not audit-only configuration.' + } + if (-not $PSCmdlet.ShouldProcess("$path\$name", $description)) { return } + if (-not $Auto) { + $response = Read-Host "Change outgoing NTLM from '$($state.Description)' to '$description'? (Y/n)" + if ($response -ne '' -and $response -ne 'Y') { + Write-Host '[SKIPPED] Outgoing NTLM.' -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 $name -Value $desired -Type DWord -ErrorAction Stop + $after = Get-WelaOutgoingNtlmState + if (-not $after.Readable -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 + Write-Host "Policy source: $($after.PolicySource)" + Write-Host 'Registry state was verified; Group Policy or MDM may reapply a different value.' + } catch { + throw "Outgoing NTLM configuration failed: $($_.Exception.Message)" + } +} + + function ConfigureAuditSettings { param ( [switch] $Auto, + [ValidateSet("PreserveOrAudit", "Audit", "Deny")] + [string] $OutgoingNtlmMode = "PreserveOrAudit", [switch] $Debug ) @@ -1289,11 +1414,13 @@ function ConfigureAuditSettings { } Write-Host "" + # Outgoing restriction and audit-only modes must be selected independently. + Set-WelaOutgoingNtlmPolicy -Mode $OutgoingNtlmMode -Auto:$Auto + # NTLM認証の監査設定 Write-Host "Configuring NTLM Audit Settings..." 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} ) @@ -1774,10 +1901,11 @@ switch ($Cmd.ToLower()) { if ($Help){ Write-Host "Configure Windows Event Log audit settings based on the YamatoSecurity baseline" Write-Host "" - Write-Host "Usage: ./WELA.ps1 configure [-Auto]" + Write-Host "Usage: ./WELA.ps1 configure [-Auto] [-OutgoingNtlmMode ]" Write-Host "" Write-Host "Options:" Write-Host " -Auto Automatically configure without prompts" + Write-Host " -OutgoingNtlmMode PreserveOrAudit (default): audit, preserving existing deny; Audit: explicitly replace deny; Deny: opt into enforcement" Write-Host "" Write-Host "Note: only the YamatoSecurity baseline is currently supported for 'configure'." Write-Host "" @@ -1788,7 +1916,7 @@ switch ($Cmd.ToLower()) { Write-Host "Re-run with '-Baseline YamatoSecurity' (or omit -Baseline) if that is what you want." break } - ConfigureAuditSettings -Auto:$Auto -Debug:$Debug + ConfigureAuditSettings -Auto:$Auto -Debug:$Debug -OutgoingNtlmMode $OutgoingNtlmMode } "configure-sacl" { diff --git a/tests/OutgoingNtlm.Tests.ps1 b/tests/OutgoingNtlm.Tests.ps1 new file mode 100644 index 00000000..fe26668f --- /dev/null +++ b/tests/OutgoingNtlm.Tests.ps1 @@ -0,0 +1,106 @@ +# Safe unit regressions: load only function definitions, never dispatch WELA or touch Windows policy. +$ErrorActionPreference = 'Stop' +$sourcePath = Join-Path $PSScriptRoot '../WELA.ps1' +$tokens = $null +$parseErrors = $null +$ast = [System.Management.Automation.Language.Parser]::ParseFile($sourcePath, [ref]$tokens, [ref]$parseErrors) +if ($parseErrors.Count) { throw ($parseErrors | Out-String) } +foreach ($functionName in @('Get-WelaOutgoingNtlmPolicySource', 'Get-WelaOutgoingNtlmState', 'Set-WelaOutgoingNtlmPolicy')) { + $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) { + $script:value = $Value + $script:keyExists = $true + $script:writes = 0 + $script:prompts = 0 + $script:readFails = $false + $script:writeFails = $false + $script:ignoreWrite = $false + $script:response = 'Y' + $script:rsop = @() +} +function Test-Path { param($LiteralPath, $ErrorAction) return $script:keyExists } +function Get-ItemProperty { + param($LiteralPath, $ErrorAction) + if ($script:readFails) { throw 'Access denied' } + if ($null -eq $script:value) { return [pscustomobject]@{} } + return [pscustomobject]@{ RestrictSendingNTLMTraffic = $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' } + $script:writes++ + if (-not $script:ignoreWrite) { $script:value = $Value } +} +function Get-CimInstance { param($Namespace, $ClassName, $ErrorAction) if ($ClassName -eq 'RSOP_RegistryPolicySetting') { return $script:rsop } } +function Read-Host { param($Prompt) $script:prompts++; return $script:response } + +$script:assertions = 0 +foreach ($initial in @($null, 0, 1)) { + Reset-Policy $initial + Set-WelaOutgoingNtlmPolicy -Auto + Assert-Equal $script:value 1 "Default audits initial value '$initial'" + $expectedWrites = if ($initial -eq 1) { 0 } else { 1 } + Assert-Equal $script:writes $expectedWrites 'Already audited hosts are idempotent' +} +Reset-Policy $null +$script:keyExists = $false +Set-WelaOutgoingNtlmPolicy -Auto +Assert-Equal $script:keyExists $true 'Missing key is created' +Assert-Equal $script:value 1 'Missing key gets audit mode' +Reset-Policy 2 +Set-WelaOutgoingNtlmPolicy -Auto +Assert-Equal $script:value 2 'Auto preserves intentional deny' +Assert-Equal $script:writes 0 'Auto does not rewrite deny' +Assert-Equal ((Get-WelaOutgoingNtlmState).Description -like 'Deny all*authentication restriction*') $true 'Deny is reported as enforcement' +Set-WelaOutgoingNtlmPolicy -Mode Audit -Auto +Assert-Equal $script:value 1 'Explicit Audit may replace deny' +Set-WelaOutgoingNtlmPolicy -Mode Deny -Auto +Assert-Equal $script:value 2 'Only explicit Deny opts into enforcement' +Reset-Policy 42 +Set-WelaOutgoingNtlmPolicy -Auto +Assert-Equal $script:writes 0 'Unknown value is preserved' +Assert-Equal ((Get-WelaOutgoingNtlmState).Description) 'Unknown registry value (42)' 'Unknown values are reported honestly' +Reset-Policy 0 +$script:readFails = $true +Assert-Throws { Set-WelaOutgoingNtlmPolicy -Auto } 'Unreadable policy fails visibly' +Assert-Equal $script:writes 0 'Unreadable policy is never overwritten' +Reset-Policy 0 +Set-WelaOutgoingNtlmPolicy -WhatIf +Assert-Equal $script:writes 0 'WhatIf does not mutate policy' +Assert-Equal $script:prompts 0 'WhatIf does not prompt without Auto' +Reset-Policy 2 +$script:response = 'n' +Set-WelaOutgoingNtlmPolicy -Mode Audit +Assert-Equal $script:writes 0 'Declining preserves deny' +$script:response = '' +Set-WelaOutgoingNtlmPolicy -Mode Audit +Assert-Equal $script:value 1 'Confirmed explicit override succeeds' +Reset-Policy 0 +$script:writeFails = $true +Assert-Throws { Set-WelaOutgoingNtlmPolicy -Auto } 'Write failure propagates' +Reset-Policy 0 +$script:ignoreWrite = $true +Assert-Throws { Set-WelaOutgoingNtlmPolicy -Auto } 'Read-back mismatch propagates' +Reset-Policy 0 +Assert-Equal ((Get-WelaOutgoingNtlmState).PolicySource -like 'Unknown*') $true 'No RSoP does not imply local provenance' +$script:rsop = @( + [pscustomobject]@{ keyName = 'SYSTEM\CurrentControlSet\Control\Lsa\MSV1_0'; valueName = 'RestrictSendingNTLMTraffic'; precedence = 2; GPOID = 'Lower priority GPO' }, + [pscustomobject]@{ keyName = 'HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\Lsa\MSV1_0'; valueName = 'RestrictSendingNTLMTraffic'; precedence = 1; GPOID = 'Winning GPO' }, + [pscustomobject]@{ keyName = 'SYSTEM\Other'; valueName = 'RestrictSendingNTLMTraffic'; precedence = 0; GPOID = 'Unrelated GPO' } +) +$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' +Write-Host "PASS: $script:assertions outgoing NTLM assertions (mocked; no host changes)." diff --git a/website/docs/commands/usage.md b/website/docs/commands/usage.md index 7843242e..d2747921 100644 --- a/website/docs/commands/usage.md +++ b/website/docs/commands/usage.md @@ -53,3 +53,33 @@ Update WELA's Sigma rules config files: ``` ./WELA.ps1 update-rules ``` + +### Outgoing NTLM auditing and restrictions + +`configure` defaults to audit-only outgoing NTLM (`RestrictSendingNTLMTraffic=1`). +An existing `Deny all` value (`2`) is preserved, including with `-Auto`. Unknown +values and unreadable policy are also preserved for review. + +```powershell +# Audit outgoing NTLM, preserving an existing restriction. +./WELA.ps1 configure -Auto +# Explicitly replace an existing restriction with audit-only mode. +./WELA.ps1 configure -OutgoingNtlmMode Audit -Auto +# Explicitly opt into denying outgoing NTLM (can break authentication). +./WELA.ps1 configure -OutgoingNtlmMode Deny +``` + +`-OutgoingNtlmMode PreserveOrAudit` is the default. `Audit` and `Deny` are explicit +operator choices; omitting `-Auto` asks before changing the policy. This option +only affects outgoing NTLM. Incoming and domain auditing remain separate controls. +`audit-settings` includes the current outgoing NTLM value and distinguishes audit +from enforcement in its console and CSV results. Policy provenance is reported as +last-applied RSoP GPO data when available, otherwise **Unknown**. RSoP can be stale, +and neither it nor a registry read proves which component last wrote a value. +After a change WELA verifies the registry value; GPO or MDM can subsequently +reapply another value. Validate benign NTLM events in +`Microsoft-Windows-NTLM/Operational` on an isolated Windows host before deployment. + +See [Microsoft's outgoing NTLM policy documentation](https://learn.microsoft.com/en-us/previous-versions/windows/it-pro/windows-10/security/threat-protection/security-policy-settings/network-security-restrict-ntlm-outgoing-ntlm-traffic-to-remote-servers). +The safe mocked regression script is `tests/OutgoingNtlm.Tests.ps1`; its Windows +workflow runs both Windows PowerShell 5.1 and PowerShell 7. From 1ae4930438d56fc6b325749847d8b905e7ad1a9a Mon Sep 17 00:00:00 2001 From: Shirofune-Security <43838376+Shirofune-Security@users.noreply.github.com> Date: Fri, 18 Sep 2026 21:48:27 +0900 Subject: [PATCH 02/21] Verify configure changes and propagate per-control failures --- .github/workflows/configuration-results.yml | 22 ++ .github/workflows/release.yml | 1 + WELA.ps1 | 406 +++----------------- docs/configuration-results.md | 101 +++++ scripts/Configuration.ps1 | 222 +++++++++++ tests/Test-ConfigurationReadOnlyWindows.ps1 | 13 + tests/Test-ConfigurationResults.ps1 | 182 +++++++++ 7 files changed, 601 insertions(+), 346 deletions(-) create mode 100644 .github/workflows/configuration-results.yml create mode 100644 docs/configuration-results.md create mode 100644 scripts/Configuration.ps1 create mode 100644 tests/Test-ConfigurationReadOnlyWindows.ps1 create mode 100644 tests/Test-ConfigurationResults.ps1 diff --git a/.github/workflows/configuration-results.yml b/.github/workflows/configuration-results.yml new file mode 100644 index 00000000..688cba9a --- /dev/null +++ b/.github/workflows/configuration-results.yml @@ -0,0 +1,22 @@ +name: Configuration result regressions +on: + push: + branches: ['**'] + pull_request: + workflow_dispatch: +permissions: + contents: read +jobs: + configuration-results: + runs-on: windows-latest + strategy: + matrix: + shell: [powershell, pwsh] + steps: + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + - name: Mocked regression tests (no Windows configuration changes) + shell: ${{ matrix.shell }} + run: ./tests/Test-ConfigurationResults.ps1 + - name: Real Windows read-only smoke tests + shell: ${{ matrix.shell }} + run: ./tests/Test-ConfigurationReadOnlyWindows.ps1 diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 96c929f5..4785fb1f 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -38,6 +38,7 @@ jobs: mkdir -p release-binaries Copy-Item -Path WELA.ps1 -Destination release-binaries/ Copy-Item -Recurse -Path ./config -Destination release-binaries/ + Copy-Item -Recurse -Path ./scripts -Destination release-binaries/ - name: Set Artifact Name if: contains(matrix.info.os, 'windows') == true diff --git a/WELA.ps1 b/WELA.ps1 index a5e00cd6..e0d87f97 100644 --- a/WELA.ps1 +++ b/WELA.ps1 @@ -4,6 +4,9 @@ [switch]$Debug, [string]$Baseline, [switch]$Auto, + [switch]$DryRun, + [string]$BackupPath, + [string]$ResultsPath, [switch]$Help ) @@ -17,6 +20,7 @@ $SecurityRulesPath = Join-Path $ScriptRoot "config/security_rules.json" $EidMappingPath = Join-Path $ScriptRoot "config/eid_subcategory_mapping.csv" $AuditpolTxtPath = Join-Path $ScriptRoot "auditpol.txt" $SaclTargetsPath = Join-Path $ScriptRoot "config/audit_sacl_targets.json" +. (Join-Path $ScriptRoot "scripts/Configuration.ps1") # 64bit の PowerShell と GPO が読むのは Wow6432Node の無いパス。32bit 用に両方を扱う。 $PowerShellPolicyRoots = @( @@ -986,10 +990,23 @@ function Set-RegistryConfig { [array]$RegPaths, [Parameter(Mandatory = $false)] - [switch]$Auto + [switch]$Auto, + $Context ) foreach ($reg in $RegPaths) { + if ($Context) { + if ($PSCmdlet.ShouldProcess("$($reg.Path)\$($reg.Name)", "Set to $($reg.Value)")) { + Set-WelaRegistryControl -Context $Context -Path $reg.Path -Name $reg.Name -Value $reg.Value + } else { + $Context.Results.Add([pscustomobject]@{ + Id = "Registry/$($reg.Path)/$($reg.Name)"; Kind = 'Registry' + Target = @{ Path = $reg.Path; Name = $reg.Name }; Desired = $reg.Value + Before = $null; After = $null; Status = 'Skipped'; Diagnostic = 'ShouldProcess declined the change.' + }) + } + continue + } try { $currentValue = "Not Set" $pathExists = Test-Path $reg.Path @@ -1031,69 +1048,18 @@ function Set-RegistryConfig { function ConfigureAuditSettings { - param ( - [switch] $Auto, - [switch] $Debug - ) + param ([switch]$Auto, [switch]$Debug, [switch]$DryRun, [string]$BackupPath, [string]$ResultsPath) - if (-not (TestWindows)) { - Write-Host "[ERROR] 'configure' changes Windows settings and can only run on Windows." -ForegroundColor Red - return + if (-not (TestWindows)) { throw "'configure' can only run on Windows." } + if (-not (TestAdministrator)) { throw 'This script requires Administrator privileges.' } + # Never use the debug cache to decide whether mutating controls are compliant. + if ($Debug) { Write-Host 'configure always reads live state; the auditpol debug cache is not used.' -ForegroundColor Yellow } + $context = New-WelaConfigurationContext -Auto:$Auto -DryRun:$DryRun -BackupPath $BackupPath + if (-not $DryRun) { Write-Host "Recovery journal: $($context.BackupPath)" } + + foreach ($log in @('Security', 'Microsoft-Windows-PowerShell/Operational', 'Windows PowerShell')) { + Set-WelaEventLogControl -Context $context -Log $log -Property MaximumSizeInBytes -Desired 1073741824 } - - # 管理者権限の確認 - if (-not (TestAdministrator)) { - Write-Error "This script requires Administrator privileges" - exit 1 - } - - if (-not (CollectAuditpol -UseCached:$Debug)) { - return - } - - # ログサイズ定数 - $oneGB = 1073741824 - $oneTwentyEightMB = 134217728 - - # セキュリティおよびPowerShellログを1GBに設定 - Write-Host "Configuring Event Logs..." - Write-Host "" - $largeLogs = @( - "Security", - "Microsoft-Windows-PowerShell/Operational", - "Windows PowerShell" - ) - - foreach ($log in $largeLogs) { - try { - $logInfo = Get-WinEvent -ListLog $log -ErrorAction Stop - $currentSize = [math]::Floor($logInfo.MaximumSizeInBytes / 1MB) - $newSize = 1024 - Write-Host "Log: $log" - if ($currentSize -ge $newSize) { - Write-Host "[SKIPPED] $log : Current size ($currentSize MB) is already greater than or equal to $newSize MB." -ForegroundColor Yellow - Write-Host "" - continue - } - if ($Auto) { - $response = "Y" - } else { - $response = Read-Host "Your current setting is $currentSize MB. Do you want to change it to 1024 MB? (Y/n)" - } - if ($response -eq "" -or $response -eq "Y" -or $response -eq "y") { - wevtutil sl $log /ms:$oneGB 2>&1 | Out-Null - Write-Host "[OK] $log : 1024 MB" -ForegroundColor Green - } else { - Write-Host "[SKIPPED] $log" -ForegroundColor Yellow - } - } - catch { - Write-Host "[ERROR] $log : $_" -ForegroundColor Red - } - Write-Host "" - } - - # その他の重要なログを128MBに設定 $mediumLogs = @( "System", "Application", @@ -1120,199 +1086,38 @@ function ConfigureAuditSettings { ) foreach ($log in $mediumLogs) { - try { - $logInfo = Get-WinEvent -ListLog $log -ErrorAction Stop - $currentSize = [math]::Floor($logInfo.MaximumSizeInBytes / 1MB) - $newSize = 128 - Write-Host "Log: $log" - if ($currentSize -ge $newSize) { - Write-Host "[SKIPPED] $log : Current size ($currentSize MB) is already greater than or equal to $newSize MB." -ForegroundColor Yellow - Write-Host "" - continue - } - if ($Auto) { - $response = "Y" - } else { - $response = Read-Host "Your current setting is $currentSize MB. Do you want to change it to 128 MB? (Y/n)" - } - if ($response -eq "" -or $response -eq "Y" -or $response -eq "y") { - wevtutil sl $log /ms:$oneTwentyEightMB 2>&1 | Out-Null - Write-Host "[OK] $log : 128 MB" -ForegroundColor Green - } else { - Write-Host "[SKIPPED] $log" -ForegroundColor Yellow - } - } - catch { - Write-Host "[ERROR] $log : $_" -ForegroundColor Red - } - Write-Host "" + Set-WelaEventLogControl -Context $context -Log $log -Property MaximumSizeInBytes -Desired 134217728 + } + foreach ($log in @('Microsoft-Windows-TaskScheduler/Operational', 'Microsoft-Windows-DriverFrameworks-UserMode/Operational', 'Microsoft-Windows-Crypto-DPAPI/Debug')) { + Set-WelaEventLogControl -Context $context -Log $log -Property IsEnabled -Desired $true } - # 特定のログの有効化 - Write-Host "Enabling Event Logs..." - Write-Host "" - foreach ($log in @("Microsoft-Windows-TaskScheduler/Operational", "Microsoft-Windows-DriverFrameworks-UserMode/Operational", "Microsoft-Windows-Crypto-DPAPI/Debug")) { - try { - $logInfo = Get-WinEvent -ListLog $log -ErrorAction Stop - $currentState = if ($logInfo.IsEnabled) { "Enabled" } else { "Disabled" } - $newState = "Enabled" - Write-Host "Log: $log" - if ($currentState -eq $newState) { - Write-Host "[SKIPPED] $log : Already Enabled." -ForegroundColor Yellow - Write-Host "" - continue - } - if ($Auto) { - $response = "Y" - } else { - $response = Read-Host "Your current setting is $currentState. Do you want to change it to Enabled? (Y/n)" - } - if ($response -eq "" -or $response -eq "Y" -or $response -eq "y") { - wevtutil sl $log /e:true 2>&1 | Out-Null - Write-Host "[OK] Enabled: $log" -ForegroundColor Green - } else { - Write-Host "[SKIPPED] $log" -ForegroundColor Yellow - } - } - catch { - Write-Host "[ERROR] Failed to enable $log : $_" -ForegroundColor Red - } - Write-Host "" - } - - # PowerShell ロギングの設定 - Write-Host "Configuring PowerShell Logging..." - Write-Host "" - # 64bit の PowerShell と GPO が読むのは Wow6432Node の無いパス。 - # 32bit の PowerShell 用に Wow6432Node 側も併せて設定する。 $regPaths = @() foreach ($root in $script:PowerShellPolicyRoots) { - $regPaths += @{Path = "$root\ModuleLogging"; Name = "EnableModuleLogging"; Value = 1} - $regPaths += @{Path = "$root\ScriptBlockLogging"; Name = "EnableScriptBlockLogging"; Value = 1} + $regPaths += @{Path = "$root\ModuleLogging"; Name = 'EnableModuleLogging'; Value = 1} + $regPaths += @{Path = "$root\ScriptBlockLogging"; Name = 'EnableScriptBlockLogging'; Value = 1} } - Set-RegistryConfig -RegPaths $regPaths -Auto:$Auto - - # モジュール名レジストリの設定 + Set-RegistryConfig -RegPaths $regPaths -Auto:$Auto -Context $context foreach ($root in $script:PowerShellPolicyRoots) { - try { - $moduleLoggingPath = "$root\ModuleLogging\ModuleNames" - $currentValue = "Not Set" - $pathExists = Test-Path $moduleLoggingPath - if ($pathExists) { - $prop = Get-ItemProperty -Path $moduleLoggingPath -Name "*" -ErrorAction SilentlyContinue - if ($prop) { - $currentValue = $prop."*" - } - } - Write-Host "Registry: $moduleLoggingPath" - if ($currentValue -eq "*") { - Write-Host "[SKIPPED] Module logging : Already set to * (all modules)." -ForegroundColor Yellow - Write-Host "" - } else - { - if ($Auto) - { - $response = "Y" - } - else - { - $response = Read-Host "Your current setting is $currentValue. Do you want to change it to * (all modules)? (Y/n)" - } - if ($response -eq "" -or $response -eq "Y" -or $response -eq "y") - { - if (-not $pathExists) - { - New-Item -Path $moduleLoggingPath -Force | Out-Null - } - Set-ItemProperty -Path $moduleLoggingPath -Name "*" -Value "*" -Type String - Write-Host "[OK] Module logging enabled for all modules" -ForegroundColor Green - } - else - { - Write-Host "[SKIPPED] Module logging" -ForegroundColor Yellow - } - } - } - catch { - Write-Host "[ERROR] Failed to configure module names: $_" -ForegroundColor Red - } - Write-Host "" + Set-WelaRegistryControl -Context $context -Path "$root\ModuleLogging\ModuleNames" -Name '*' -Value '*' -Type String } + Set-WelaRegistryControl -Context $context -Path 'HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Policies\System\Audit' ` + -Name ProcessCreationIncludeCmdLine_Enabled -Value 1 - # コマンドライン監査の有効化 - Write-Host "Enabling Command Line Auditing..." - Write-Host "" - $regPath = "HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Policies\System\Audit" - $valueName = "ProcessCreationIncludeCmdLine_Enabled" - try { - $currentValue = "Not Set" - if (Test-Path $regPath) { - $prop = Get-ItemProperty -Path $regPath -Name $valueName -ErrorAction SilentlyContinue - $currentValue = $prop.$valueName - } - Write-Host "Registry: $regPath" - if ($currentValue -eq 1) { - Write-Host "[SKIPPED] Command Line Auditing : Already Enabled." -ForegroundColor Yellow - Write-Host "" - } else - { - if ($Auto) - { - $response = "Y" - } - else - { - $response = Read-Host "Your current setting is $currentValue. Do you want to change it to 1 (Enabled)? (Y/n)" - } - if ($response -eq "" -or $response -eq "Y" -or $response -eq "y") - { - $regPath = $regPath -replace "HKLM:", "HKLM" - $arguments = "add $regPath /v $valueName /f /t REG_DWORD /d 1" - $process = Start-Process -FilePath "reg.exe" -ArgumentList $arguments -Wait -PassThru -NoNewWindow -RedirectStandardOutput "NUL" - if ($process.ExitCode -eq 0) - { - Write-Host "[OK] Command line auditing enabled" -ForegroundColor Green - } - else - { - Write-Host "[ERROR] Command line auditing failed (ExitCode: $( $process.ExitCode ))" -ForegroundColor Red - } - } - else - { - Write-Host "[SKIPPED] Command line auditing" -ForegroundColor Yellow - } - } - } - catch { - Write-Host "[ERROR] Failed to check command line auditing: $_" -ForegroundColor Red - } - Write-Host "" - - # NTLM認証の監査設定 - Write-Host "Configuring NTLM Audit Settings..." - Write-Host "" + # NTLM policy values are unchanged here; separate policy corrections can use + # the same verified registry-control helper. $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} ) - Set-RegistryConfig -RegPaths $regPaths -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. - if (Test-Path "HKLM:\SYSTEM\CurrentControlSet\Services\NTDS\Parameters") { - Write-Host "Configuring LDAP query logging (1644) on this domain controller..." - Write-Host "" + Set-RegistryConfig -RegPaths $regPaths -Auto:$Auto -Context $context + if (Test-Path 'HKLM:\SYSTEM\CurrentControlSet\Services\NTDS\Parameters') { Set-RegistryConfig -RegPaths @( - @{Path = "HKLM:\SYSTEM\CurrentControlSet\Services\NTDS\Diagnostics"; Name = "15 Field Engineering"; Value = 5} - ) -Auto:$Auto + @{Path = 'HKLM:\SYSTEM\CurrentControlSet\Services\NTDS\Diagnostics'; Name = '15 Field Engineering'; Value = 5} + ) -Auto:$Auto -Context $context } - # 監査ポリシーの設定 - Write-Host "Configuring Audit Policies..." - Write-Host "" $auditPolicies = @( @{Category = "Account Logon"; Name = "Credential Validation"; GUID = "0CCE923F-69AE-11D9-BED3-505054503030"}, @{Category = "Account Logon"; Name = "Kerberos Authentication Service"; GUID = "0CCE9242-69AE-11D9-BED3-505054503030"}, @@ -1350,112 +1155,11 @@ function ConfigureAuditSettings { @{Category = "System"; Name = "Other System Events"; GUID = "0CCE9214-69AE-11D9-BED3-505054503030"} ) - $currentAuditPol = GetAuditpol - - foreach ($policy in $auditPolicies) - { - $newSetting = "Success and Failure" - $currentSetting = if ($currentAuditPol.ContainsKey($policy.GUID)) - { - $currentAuditPol[$policy.GUID] - } - else - { - "Unknown" - } - - Write-Host "Audit Policy: $( $policy.Category ) - $( $policy.Name )" - if ($currentSetting -eq $newSetting) - { - Write-Host "[SKIPPED] $( $policy.Category ) - $( $policy.Name ) : Already set to $newSetting." -ForegroundColor Yellow - Write-Host "" - continue - } - if ($Auto) { - $response = "Y" - } else { - $response = Read-Host "Your current setting is $currentSetting. Do you want to change it to $newSetting? (Y/n)" - } - if ($response -eq "" -or $response -eq "Y" -or $response -eq "y") { - $arguments = "/set /subcategory:{$($policy.GUID)} /success:enable /failure:enable" - $process = Start-Process -FilePath "auditpol.exe" -ArgumentList $arguments -Wait -PassThru -NoNewWindow -RedirectStandardOutput "NUL" - - if ($process.ExitCode -eq 0) { - Write-Host "[OK] $($policy.Category) - $($policy.Name)" -ForegroundColor Green - } - else { - Write-Host "[ERROR] $($policy.Category) - $($policy.Name) (ExitCode: $($process.ExitCode))" -ForegroundColor Red - } - } else { - Write-Host "[SKIPPED] $($policy.Category) - $($policy.Name)" -ForegroundColor Yellow - } - Write-Host "" + foreach ($policy in $auditPolicies) { + Set-WelaAuditPolicyControl -Context $context -Policy $policy } - - # AD CS AuditFilter の設定 - Write-Host "Configuring AD CS Audit Settings..." - try { - $installed = (Get-WindowsFeature -Name AD-Certificate).InstallState -eq "Installed" - } catch { - $installed = $false - } - - if ($installed) { - try { - $csRootKey = "HKLM:\SYSTEM\CurrentControlSet\Services\CertSvc\Configuration\" - $caName = (Get-ItemProperty $csRootKey -ErrorAction Stop).Active - $regPath = "HKLM:\SYSTEM\CurrentControlSet\Services\CertSvc\Configuration\$caName" - $prop = Get-ItemProperty -Path $regPath -Name "AuditFilter" -ErrorAction SilentlyContinue - $currentValue = if ($null -ne $prop) { [int]$prop.AuditFilter } else { "Not Set" } - if ($currentValue -eq 127) { - Write-Host "[OK] AuditFilter is already 127" -ForegroundColor Green - } - else { - $proceed = $false - if ($Auto) { - $proceed = $true - } - else { - $response = Read-Host "Do you want to set AuditFilter to 127 and restart Certificate Services? (Y/n)" - $proceed = ($response -eq "" -or $response -match "^[Yy]$") - } - - if ($proceed) { - try { - # AuditFilter の設定 - & certutil.exe -setreg "CA\AuditFilter" 127 >$null 2>&1 - # 証明書サービスの再起動 - Restart-Service -Name "CertSvc" -Force -ErrorAction Stop - # 反映確認 - $propAfter = Get-ItemProperty -Path $regPath -Name "AuditFilter" -ErrorAction SilentlyContinue - $newValue = if ($null -ne $propAfter) { [int]$propAfter.AuditFilter } else { $null } - - if ($newValue -eq 127) { - Write-Host "[OK] AuditFilter set to 127 and CertSvc restarted" -ForegroundColor Green - } - else { - Write-Host "[ERROR] AuditFilter did not apply as expected (current: $newValue)" -ForegroundColor Red - } - } - catch { - Write-Host "[ERROR] Failed to set AuditFilter or restart CertSvc: $_" -ForegroundColor Red - } - } - else { - Write-Host "[SKIP] No changes applied to AuditFilter" - } - } - } - catch { - Write-Host "[ERROR] Failed to process AD CS audit settings: $_" -ForegroundColor Red - } - } - else { - Write-Host "[INFO] AD Certificate Services is not installed. Skipping." -ForegroundColor Yellow - } - Write-Host "" - - Write-Host "Configuration completed successfully" -ForegroundColor Green + Set-WelaCertificateAuditControl -Context $context + Complete-WelaConfiguration -Context $context -ResultsPath $ResultsPath } $logo = @" @@ -1774,10 +1478,13 @@ switch ($Cmd.ToLower()) { if ($Help){ Write-Host "Configure Windows Event Log audit settings based on the YamatoSecurity baseline" Write-Host "" - Write-Host "Usage: ./WELA.ps1 configure [-Auto]" + Write-Host "Usage: ./WELA.ps1 configure [-Auto] [-DryRun] [-BackupPath ] [-ResultsPath ]" Write-Host "" Write-Host "Options:" Write-Host " -Auto Automatically configure without prompts" + Write-Host " -DryRun Read live state and report proposed changes without writing Windows settings" + Write-Host " -BackupPath New directory for the pre-change recovery journal (unique default beside WELA)" + Write-Host " -ResultsPath Save structured per-control outcomes as JSON" Write-Host "" Write-Host "Note: only the YamatoSecurity baseline is currently supported for 'configure'." Write-Host "" @@ -1788,7 +1495,14 @@ switch ($Cmd.ToLower()) { Write-Host "Re-run with '-Baseline YamatoSecurity' (or omit -Baseline) if that is what you want." break } - ConfigureAuditSettings -Auto:$Auto -Debug:$Debug + try { + $report = ConfigureAuditSettings -Auto:$Auto -Debug:$Debug -DryRun:$DryRun -BackupPath $BackupPath -ResultsPath $ResultsPath + $report + if ($report.ExitCode -ne 0) { exit $report.ExitCode } + } catch { + Write-Host "[Failed] Configuration aborted: $_" -ForegroundColor Red + exit 1 + } } "configure-sacl" { diff --git a/docs/configuration-results.md b/docs/configuration-results.md new file mode 100644 index 00000000..63f787c8 --- /dev/null +++ b/docs/configuration-results.md @@ -0,0 +1,101 @@ +# Verified configuration and recovery + +`configure` reads live state, records each proposed write before executing it, +checks native exit codes, and reads the resulting state. It returns an object with +`ExitCode`, `DryRun`, `BackupPath`, `Failed`, `Skipped`, and a `Results` array. +`-ResultsPath` also saves that object as JSON. The command exits with status 1 when +any control fails or changes again before the final verification. A fatal preflight +or result-file error also exits with status 1. + +```powershell +# Read live settings; do not change Windows settings, restart services or create a journal. +.\WELA.ps1 configure -DryRun -ResultsPath .\proposed-results.json + +# Apply with interactive approval for each change, including the CA restart. +.\WELA.ps1 configure -BackupPath C:\WELA-Recovery\run-001 -ResultsPath .\results.json + +# Apply the existing WELA choices without individual prompts. +.\WELA.ps1 configure -Auto -ResultsPath .\results.json +``` + +Keep the complete WELA directory, including `scripts/Configuration.ps1`. Choose a +recovery path whose parent directory is writable only by the operators who manage +these settings. The backup directory must not already exist. Without `-BackupPath`, +a unique directory is created beside WELA. `-Debug` does not substitute cached +audit policy data during configuration. `-DryRun` may write the explicitly requested +result file, but performs no Windows configuration writes. + +| Status | Meaning | +| --- | --- | +| Applied | Write succeeded and immediate read-back matched. | +| AlreadyCompliant | The initial live value already met the requirement; no write. | +| Skipped | Dry run, operator decline, or no configured local CA. | +| Failed | State could not be read, journaling failed, write/restart failed, or verification failed. | +| Overridden | A value verified earlier became noncompliant by the final read. Cause is unknown. | + +Unknown and unavailable channels are reported as failed observations rather than +silently claiming that logging is enabled. Partial runs and runs with skipped +controls do not claim universal success. Verification is an observation at that +moment; it does not prove future GPO persistence, event production, collection or +Sigma rule coverage. A zero exit code with skipped controls is not full compliance. + +Audit policy reads use GUIDs and the numeric value in `auditpol /r` output, rather +than localized setting names. Registry writes use terminating errors and verify +both the value and registry type. Log sizes retain larger existing buffers. A CA +is detected from its configured registry state; certutil must succeed before a +restart is attempted, and the restart must return to Running. A stopped CA is not +started automatically. A restart failure remains failed even if the registry value +was already written. + +## Recovery journal and rollback design + +Each line of `before.jsonl` records the computer, timestamp, control identity, +requested setting and exact pre-change state. Registry entries include whether the +key/value existed and the previous registry type. Event-log entries capture size or +enabled state; audit policies capture the numeric mask; CA entries also capture +service state. A journal write failure prevents that control's mutation. The +journal is per control, not a full system backup, and can contain records for failed +or declined downstream actions. Save the final result file alongside it. + +This change provides a guarded **manual recovery procedure**, not an automatic +rollback command. Automatic bulk rollback could overwrite a later administrator or +GPO change and could interrupt certificate services. Before recovery: + +1. Use an elevated shell on the journal's recorded computer. Review the specific + failed or applied control and capture its current live state. +2. Compare current state with the recorded requested/verified after-state. If it + differs, stop and determine whether another writer made an intentional change. + Do not blindly replay a journal or restore an entire audit policy backup. +3. Restore only the intended controls, normally in reverse application order: + - **EventLog:** `wevtutil sl /ms:` or `/e:`. + Review shrinking buffers or disabling a channel before proceeding. + - **AuditPolicy:** `auditpol /set /subcategory:{} /success: + /failure:`. Previous mask bit 1 means success, bit 2 means + failure. Restore that subcategory, not unrelated policy. + - **Registry:** restore the previous value using its recorded registry type. + If the value did not exist, remove only that value. Preserve unrelated values + and never recursively delete a newly created parent key. Binary and multistring + old values must be reconstructed with their original types from the JSON. + - **CertificateService:** restore the active CA's previous AuditFilter value (or + its original absence) and separately approve the necessary service restart. + Do not start a CA that was deliberately stopped. A failed restart can leave + the registry and running service out of sync; an operator must resolve this. +4. Check every native exit code and read the restored state. Keep the recovery + commands and observations with the original journal. + +A future automated rollback command should require the same host and control +identity, validate journal schema and allowlisted types, check current state against +recorded after-state, refuse unexpected drift, journal recovery itself, and require +explicit approval for CA restarts. It should never import the whole registry or +force a Group Policy setting. These are design constraints, not implemented claims. + +## Testing + +`tests/Test-ConfigurationResults.ps1` uses mock Windows APIs and disposable temp +journals. It exercises nonzero native exits and stderr, false-success writes, +read-back, idempotence, final drift, dry runs, journal failure, localized audit CSV +labels, and CA write/restart failure. It does not change Windows settings. +`tests/Test-ConfigurationReadOnlyWindows.ps1` runs real read-only `auditpol /get` +and a child `cmd.exe` diagnostic/exit test. CI runs both scripts in Windows PowerShell +5.1 and PowerShell 7. Mutating behavior still requires isolated Windows/CA lab +validation; mock and read-only tests do not establish end-to-end event production. diff --git a/scripts/Configuration.ps1 b/scripts/Configuration.ps1 new file mode 100644 index 00000000..feec7e0d --- /dev/null +++ b/scripts/Configuration.ps1 @@ -0,0 +1,222 @@ +# Execution helpers for configure. Compatible with Windows PowerShell 5.1. +function Invoke-WelaNative { + param([string]$FilePath, [string[]]$Arguments) + # Windows PowerShell sends native stderr through the error stream. Collect it + # without treating stderr alone as failure; the process exit code is decisive. + $ErrorActionPreference = 'Continue' + $PSNativeCommandUseErrorActionPreference = $false + $null = Get-Command $FilePath -ErrorAction Stop + $global:LASTEXITCODE = $null + $output = @(& $FilePath @Arguments 2>&1) + $exitCode = $global:LASTEXITCODE # Capture immediately, before invoking anything else. + $diagnostic = ($output | ForEach-Object { $_.ToString() }) -join [Environment]::NewLine + if ($null -eq $exitCode -or $exitCode -ne 0) { + throw "$FilePath $($Arguments -join ' ') failed (exit: $exitCode). $diagnostic" + } + [pscustomobject]@{ ExitCode = $exitCode; Output = $output; Diagnostic = $diagnostic } +} + +function New-WelaConfigurationContext { + param([switch]$Auto, [switch]$DryRun, [string]$BackupPath) + if (-not $DryRun) { + if (-not $BackupPath) { + $BackupPath = Join-Path $script:ScriptRoot ("wela-backup-{0}-{1}" -f (Get-Date -Format 'yyyyMMdd-HHmmss'), [guid]::NewGuid().ToString('N')) + } + # Refuse reuse: a prior run's recovery evidence must never be overwritten. + $null = New-Item -ItemType Directory -Path $BackupPath -ErrorAction Stop + $BackupPath = (Resolve-Path -LiteralPath $BackupPath -ErrorAction Stop).Path + } + [pscustomobject]@{ + Auto = [bool]$Auto; DryRun = [bool]$DryRun; BackupPath = $BackupPath + Results = New-Object 'System.Collections.Generic.List[object]' + Checks = New-Object 'System.Collections.Generic.List[object]' + } +} + +function Invoke-WelaConfigurationControl { + param($Context, [string]$Id, [string]$Kind, $Target, $Desired, + [scriptblock]$Read, [scriptblock]$Compliant, [scriptblock]$Apply, + [string]$Description = '') + $result = [pscustomobject][ordered]@{ + Id = $Id; Kind = $Kind; Target = $Target; Desired = $Desired + Before = $null; After = $null; Status = 'Failed'; Diagnostic = '' + } + try { + $result.Before = & $Read + if (& $Compliant $result.Before) { + $result.Status = 'AlreadyCompliant' + $result.After = $result.Before + } elseif ($Context.DryRun) { + $result.Status = 'Skipped'; $result.Diagnostic = 'Dry run: change required; no write or restart performed.' + } else { + $proceed = $Context.Auto + if (-not $proceed) { + $response = Read-Host "$Id : $Description Apply this change? (Y/n)" + $proceed = ($response -eq '' -or $response -match '^[Yy]$') + } + if (-not $proceed) { + $result.Status = 'Skipped'; $result.Diagnostic = 'Declined by operator.' + } else { + # Persist the exact pre-change value before any mutation. A journal + # failure stops this control, including service restarts. + $entry = [ordered]@{ + Version = 1; ComputerName = $env:COMPUTERNAME + RecordedUtc = [DateTime]::UtcNow.ToString('o') + Id = $Id; Kind = $Kind; Target = $Target + Before = $result.Before; Desired = $Desired + } + $entry | ConvertTo-Json -Depth 12 -Compress | + Add-Content -LiteralPath (Join-Path $Context.BackupPath 'before.jsonl') -Encoding UTF8 -ErrorAction Stop + $applied = @(& $Apply) + $result.Diagnostic = ($applied | ForEach-Object { + if ($_.PSObject.Properties['Diagnostic']) { $_.Diagnostic } else { $_.ToString() } + }) -join [Environment]::NewLine + $result.After = & $Read + if (-not (& $Compliant $result.After)) { + throw "Post-apply verification did not match the requested state. $($result.Diagnostic)" + } + $result.Status = 'Applied' + } + } + if ($result.Status -in @('Applied', 'AlreadyCompliant')) { + $Context.Checks.Add([pscustomobject]@{ Result = $result; Read = $Read; Compliant = $Compliant }) + } + } catch { + $result.Status = 'Failed'; $result.Diagnostic = $_.ToString() + } + $Context.Results.Add($result) + $color = if ($result.Status -eq 'Failed') { 'Red' } elseif ($result.Status -eq 'Skipped') { 'Yellow' } else { 'Green' } + Write-Host "[$($result.Status)] $Id $($result.Diagnostic)" -ForegroundColor $color +} + +function Complete-WelaConfiguration { + param($Context, [string]$ResultsPath) + # A second read detects a value that was compliant earlier but changed during + # this run. It does not establish whether GPO or another writer caused drift. + foreach ($check in $Context.Checks) { + try { + $check.Result.After = & $check.Read + if (-not (& $check.Compliant $check.Result.After)) { + $check.Result.Status = 'Overridden' + $check.Result.Diagnostic = 'State was compliant earlier but changed before the final check; cause unknown.' + } + } catch { + $check.Result.Status = 'Failed' + $check.Result.Diagnostic = "Final verification failed: $_" + } + } + $failed = @($Context.Results | Where-Object { $_.Status -in @('Failed', 'Overridden') }).Count + $skipped = @($Context.Results | Where-Object { $_.Status -eq 'Skipped' }).Count + $report = [pscustomobject][ordered]@{ + ExitCode = $(if ($failed) { 1 } else { 0 }); DryRun = $Context.DryRun + BackupPath = $Context.BackupPath; Failed = $failed; Skipped = $skipped + Results = @($Context.Results.ToArray()) + } + if ($ResultsPath) { + try { $report | ConvertTo-Json -Depth 12 | Set-Content -LiteralPath $ResultsPath -Encoding UTF8 -ErrorAction Stop } + catch { $report.ExitCode = 1; Write-Host "[Failed] Writing results: $_" -ForegroundColor Red } + } + if ($report.ExitCode) { Write-Host "Configuration incomplete: $failed failed or overridden control(s). Review results and recovery journal." -ForegroundColor Red } + elseif ($Context.DryRun) { Write-Host 'Dry run completed. No Windows configuration was changed.' -ForegroundColor Cyan } + elseif ($skipped) { Write-Host "Configuration completed with $skipped skipped control(s)." -ForegroundColor Yellow } + else { Write-Host 'Configuration completed; all requested controls verified.' -ForegroundColor Green } + return $report +} + +function Set-WelaEventLogControl { + param($Context, [string]$Log, [string]$Property, $Desired) + $read = { (Get-WinEvent -ListLog $Log -ErrorAction Stop).$Property }.GetNewClosure() + $test = if ($Property -eq 'MaximumSizeInBytes') { + { param($value) $value -ge $Desired }.GetNewClosure() + } else { { param($value) $value -eq $Desired }.GetNewClosure() } + $argument = if ($Property -eq 'MaximumSizeInBytes') { "/ms:$Desired" } else { '/e:true' } + $apply = { Invoke-WelaNative -FilePath 'wevtutil.exe' -Arguments @('sl', $Log, $argument) }.GetNewClosure() + Invoke-WelaConfigurationControl -Context $Context -Id "EventLog/$Log/$Property" -Kind EventLog ` + -Target @{ Log = $Log; Property = $Property } -Desired $Desired -Read $read -Compliant $test -Apply $apply +} + +function Get-WelaRegistryState { + param([string]$Path, [string]$Name) + if (-not (Test-Path -LiteralPath $Path -ErrorAction Stop)) { + return [pscustomobject]@{ KeyExists = $false; ValueExists = $false; Value = $null; Type = $null } + } + $key = Get-Item -LiteralPath $Path -ErrorAction Stop + if ($key.GetValueNames() -notcontains $Name) { + return [pscustomobject]@{ KeyExists = $true; ValueExists = $false; Value = $null; Type = $null } + } + [pscustomobject]@{ + KeyExists = $true; ValueExists = $true + Value = $key.GetValue($Name, $null, [Microsoft.Win32.RegistryValueOptions]::DoNotExpandEnvironmentNames) + Type = $key.GetValueKind($Name).ToString() + } +} + +function Set-WelaRegistryControl { + param($Context, [string]$Path, [string]$Name, $Value, [string]$Type = 'DWord') + $read = { Get-WelaRegistryState -Path $Path -Name $Name }.GetNewClosure() + $test = { param($state) $state.ValueExists -and $state.Value -eq $Value -and $state.Type -eq $Type }.GetNewClosure() + $apply = { + if (-not (Test-Path -LiteralPath $Path -ErrorAction Stop)) { + $null = New-Item -Path $Path -ErrorAction Stop + } + Set-ItemProperty -LiteralPath $Path -Name $Name -Value $Value -Type $Type -ErrorAction Stop + }.GetNewClosure() + Invoke-WelaConfigurationControl -Context $Context -Id "Registry/$Path/$Name" -Kind Registry ` + -Target @{ Path = $Path; Name = $Name } -Desired @{ Value = $Value; Type = $Type } ` + -Read $read -Compliant $test -Apply $apply +} + +function Get-WelaAuditPolicyMask { + param([string]$Guid) + $native = Invoke-WelaNative -FilePath 'auditpol.exe' -Arguments @('/get', "/subcategory:{$Guid}", '/r') + # Column order is stable; names and Inclusion Setting text are localized. + $rows = $native.Output | ConvertFrom-Csv -Header Machine, Target, Name, Guid, Inclusion, Exclusion, SettingValue + $row = @($rows | Where-Object { $_.Guid -and $_.Guid.Trim('{}') -eq $Guid }) + if ($row.Count -ne 1 -or $row[0].SettingValue -notmatch '^[0-3]$') { + throw "auditpol returned no unambiguous numeric setting for $Guid. $($native.Diagnostic)" + } + return [int]$row[0].SettingValue +} + +function Set-WelaAuditPolicyControl { + param($Context, $Policy) + $guid = $Policy.GUID + $read = { Get-WelaAuditPolicyMask -Guid $guid }.GetNewClosure() + $apply = { Invoke-WelaNative -FilePath 'auditpol.exe' -Arguments @('/set', "/subcategory:{$guid}", '/success:enable', '/failure:enable') }.GetNewClosure() + Invoke-WelaConfigurationControl -Context $Context -Id "AuditPolicy/$($Policy.Name)" -Kind AuditPolicy ` + -Target @{ Guid = $guid } -Desired 3 -Read $read -Compliant { param($value) $value -eq 3 } -Apply $apply +} + +function Set-WelaCertificateAuditControl { + param($Context) + $root = 'HKLM:\SYSTEM\CurrentControlSet\Services\CertSvc\Configuration' + try { + if (-not (Test-Path -LiteralPath $root -ErrorAction Stop)) { + $Context.Results.Add([pscustomobject]@{ Id = 'ADCS/AuditFilter'; Kind = 'CertificateService'; Target = $root; Desired = 127; Before = $null; After = $null; Status = 'Skipped'; Diagnostic = 'No configured local CA.' }) + return + } + $caName = (Get-ItemProperty -LiteralPath $root -Name Active -ErrorAction Stop).Active + if (-not $caName) { throw 'CA configuration has no active CA name.' } + $path = Join-Path $root $caName + $read = { + [pscustomobject]@{ + Registry = Get-WelaRegistryState -Path $path -Name AuditFilter + ServiceStatus = (Get-Service -Name CertSvc -ErrorAction Stop).Status.ToString() + } + }.GetNewClosure() + $test = { param($value) $value.Registry.ValueExists -and $value.Registry.Value -eq 127 -and $value.ServiceStatus -eq 'Running' } + $apply = { + $state = Get-Service -Name CertSvc -ErrorAction Stop + if ($state.Status -ne 'Running') { throw 'CertSvc is not running; refusing to start a previously stopped CA. Start it deliberately before retrying.' } + Invoke-WelaNative -FilePath 'certutil.exe' -Arguments @('-setreg', 'CA\AuditFilter', '127') + Restart-Service -Name CertSvc -Force -ErrorAction Stop + $service = Get-Service -Name CertSvc -ErrorAction Stop + $service.WaitForStatus([System.ServiceProcess.ServiceControllerStatus]::Running, [TimeSpan]::FromSeconds(30)) + } + Invoke-WelaConfigurationControl -Context $Context -Id 'ADCS/AuditFilter' -Kind CertificateService ` + -Target @{ Path = $path; Name = 'AuditFilter'; Service = 'CertSvc' } -Desired 127 ` + -Read $read -Compliant $test -Apply $apply -Description 'Set AuditFilter=127 and restart Certificate Services.' + } catch { + $Context.Results.Add([pscustomobject]@{ Id = 'ADCS/AuditFilter'; Kind = 'CertificateService'; Target = $root; Desired = 127; Before = $null; After = $null; Status = 'Failed'; Diagnostic = $_.ToString() }) + } +} diff --git a/tests/Test-ConfigurationReadOnlyWindows.ps1 b/tests/Test-ConfigurationReadOnlyWindows.ps1 new file mode 100644 index 00000000..4b3b38f8 --- /dev/null +++ b/tests/Test-ConfigurationReadOnlyWindows.ps1 @@ -0,0 +1,13 @@ +# Read-only smoke test of real Windows commands, independent of the mock suite. +$ErrorActionPreference = 'Stop' +if ($env:OS -ne 'Windows_NT') { throw 'Run this smoke test on Windows.' } +. (Join-Path (Split-Path $PSScriptRoot -Parent) 'scripts/Configuration.ps1') +$mask = Get-WelaAuditPolicyMask '0CCE922B-69AE-11D9-BED3-505054503030' +if ($mask -notin @(0, 1, 2, 3)) { throw "Unexpected process-creation audit mask: $mask" } +$caught = '' +try { Invoke-WelaNative -FilePath $env:ComSpec -Arguments @('/d', '/c', 'echo WELA-smoke-diagnostic 1>&2 & exit /b 9') } +catch { $caught = $_.ToString() } +if ($caught -notmatch 'exit: 9' -or $caught -notmatch 'WELA-smoke-diagnostic') { + throw "Native exit/stderr capture failed: $caught" +} +Write-Host "Read-only Windows smoke checks passed (process creation audit mask: $mask). No Windows settings changed." diff --git a/tests/Test-ConfigurationResults.ps1 b/tests/Test-ConfigurationResults.ps1 new file mode 100644 index 00000000..146583b9 --- /dev/null +++ b/tests/Test-ConfigurationResults.ps1 @@ -0,0 +1,182 @@ +# No Windows settings are changed. Run with powershell.exe 5.1 or pwsh. +$ErrorActionPreference = 'Stop' +$repo = Split-Path $PSScriptRoot -Parent +$script:ScriptRoot = $repo +. (Join-Path $repo 'scripts/Configuration.ps1') +$script:passed = 0 +function Assert($Condition, [string]$Message) { + if (-not $Condition) { throw "FAIL: $Message" } + $script:passed++ +} +function New-TestContext([switch]$DryRun) { + $path = Join-Path ([IO.Path]::GetTempPath()) ('wela-results-test-' + [guid]::NewGuid().ToString('N')) + if (-not $DryRun) { $script:cleanup.Add($path) } + New-WelaConfigurationContext -Auto -DryRun:$DryRun -BackupPath $path +} +$script:cleanup = New-Object 'System.Collections.Generic.List[string]' +try { + foreach ($path in @('WELA.ps1', 'scripts/Configuration.ps1')) { + $parseErrors = $null; $tokens = $null + $null = [Management.Automation.Language.Parser]::ParseFile((Join-Path $repo $path), [ref]$tokens, [ref]$parseErrors) + Assert ($parseErrors.Count -eq 0) "Parser accepts $path" + } + + # An actual child process exercises exit capture and stderr retention. The + # child only emits text and exits; it never calls Windows configuration tools. + $engine = (Get-Process -Id $PID).Path + $caught = '' + try { Invoke-WelaNative -FilePath $engine -Arguments @('-NoProfile', '-Command', "[Console]::Error.WriteLine('injected native diagnostic'); exit 7") } + catch { $caught = $_.ToString() } + Assert ($caught -match 'exit: 7' -and $caught -match 'injected native diagnostic') 'Native failure retains exit code and stderr' + $ok = Invoke-WelaNative -FilePath $engine -Arguments @('-NoProfile', '-Command', "[Console]::Error.WriteLine('non-fatal diagnostic'); exit 0") + Assert ($ok.ExitCode -eq 0 -and $ok.Diagnostic -match 'non-fatal diagnostic') 'Stderr alone is not a native failure' + + $script:state = 1; $script:writes = 0 + $read = { $script:state }; $test = { param($value) $value -eq 2 } + $apply = { $script:writes++; $script:state = 2 } + $c = New-TestContext + Invoke-WelaConfigurationControl $c test Registry @{ Path = 'mock'; Name = 'value' } 2 $read $test $apply + Assert ($c.Results[0].Status -eq 'Applied' -and $script:writes -eq 1) 'Changed state is read back before Applied' + $journal = Get-Content -LiteralPath (Join-Path $c.BackupPath 'before.jsonl') | ConvertFrom-Json + Assert ($journal.Before -eq 1 -and $journal.Desired -eq 2) 'Journal contains exact before and requested state' + Invoke-WelaConfigurationControl $c repeated Registry @{} 2 $read $test $apply + Assert ($c.Results[1].Status -eq 'AlreadyCompliant' -and $script:writes -eq 1) 'Rerun is idempotent' + $r = Complete-WelaConfiguration $c + Assert ($r.ExitCode -eq 0) 'Verified controls produce successful overall status' + $script:state = 1 + $r = Complete-WelaConfiguration $c + Assert ($r.ExitCode -eq 1 -and $r.Results[0].Status -eq 'Overridden') 'Final check detects observed drift without attributing its cause' + + $c = New-TestContext -DryRun + $script:writes = 0 + Invoke-WelaConfigurationControl $c dry Registry @{} 2 $read $test $apply + Assert ($c.Results[0].Status -eq 'Skipped' -and $script:writes -eq 0) 'Dry run never invokes mutation' + Assert (-not (Test-Path -LiteralPath $c.BackupPath)) 'Dry run creates no backup or journal' + + $c = New-TestContext + Invoke-WelaConfigurationControl $c false_success Registry @{} 2 $read $test { } + Assert ($c.Results[0].Status -eq 'Failed') 'Successful write command with wrong read-back is Failed' + Assert ((Complete-WelaConfiguration $c).ExitCode -eq 1) 'Read-back failure makes overall status nonzero' + + $c = New-TestContext + $c.BackupPath = Join-Path $c.BackupPath 'missing-parent' + $script:writes = 0 + Invoke-WelaConfigurationControl $c journal_failed Registry @{} 2 $read $test $apply + Assert ($c.Results[0].Status -eq 'Failed' -and $script:writes -eq 0) 'Journal failure prevents mutation' + + # Registry provider failures and false-success writes use the same verified + # control runner; no actual registry provider is touched in these tests. + $script:registryValue = 0; $script:registryWrites = 0; $script:registryThrows = $true + function global:Get-WelaRegistryState { + param($Path, $Name) + [pscustomobject]@{ KeyExists = $true; ValueExists = $true; Value = $script:registryValue; Type = 'DWord' } + } + function global:Test-Path { + param($LiteralPath, $Path, $ErrorAction) + if ($LiteralPath -like 'HKLM:*') { return $true } + Microsoft.PowerShell.Management\Test-Path -LiteralPath $(if ($LiteralPath) { $LiteralPath } else { $Path }) + } + function global:Set-ItemProperty { + param($LiteralPath, $Name, $Value, $Type, $ErrorAction) + $script:registryWrites++ + if ($script:registryThrows) { throw 'Injected registry access denied' } + $script:registryValue = $Value + } + $c = New-TestContext + Set-WelaRegistryControl $c 'HKLM:\mock' Value 1 + Assert ($c.Results[0].Status -eq 'Failed' -and $c.Results[0].Diagnostic -match 'access denied') 'Registry write errors produce failed results' + $script:registryThrows = $false + $c = New-TestContext + Set-WelaRegistryControl $c 'HKLM:\mock' Value 1 + Assert ($c.Results[0].Status -eq 'Applied' -and $c.Results[0].After.Value -eq 1) 'Registry writes require verified value and type' + $beforeWrites = $script:registryWrites + Set-WelaRegistryControl $c 'HKLM:\mock' Value 1 + Assert ($c.Results[1].Status -eq 'AlreadyCompliant' -and $script:registryWrites -eq $beforeWrites) 'Registry reruns preserve compliant values' + + # Function stubs stand in for the Windows APIs from this point onward. + $script:logSize = 1048576; $script:nativeFails = $true; $script:nativeWrites = 0 + function global:Get-WinEvent { param($ListLog, $ErrorAction) [pscustomobject]@{ MaximumSizeInBytes = $script:logSize; IsEnabled = $false } } + function global:Invoke-WelaNative { + param($FilePath, $Arguments) + $script:nativeWrites++ + if ($script:nativeFails) { throw 'wevtutil.exe failed (exit: 5). Injected access denied' } + $script:logSize = 134217728 + [pscustomobject]@{ ExitCode = 0; Output = @(); Diagnostic = 'mock success' } + } + $c = New-TestContext + Set-WelaEventLogControl $c Security MaximumSizeInBytes 134217728 + $r = Complete-WelaConfiguration $c + Assert ($r.ExitCode -eq 1 -and $r.Results[0].Diagnostic -match 'access denied') 'Injected wevtutil failure survives through the final report' + $script:nativeFails = $false + $c = New-TestContext + Set-WelaEventLogControl $c Security MaximumSizeInBytes 134217728 + Assert ($c.Results[0].Status -eq 'Applied') 'Event log helper reads verified size' + Set-WelaEventLogControl $c Security MaximumSizeInBytes 134217728 + Assert ($c.Results[1].Status -eq 'AlreadyCompliant') 'Event log helper avoids repeated writes' + + function global:Invoke-WelaNative { + param($FilePath, $Arguments) + [pscustomobject]@{ ExitCode = 0; Output = @('Localized,header,labels,here,x,y,z', 'host,System,localized name,{0CCE922B-69AE-11D9-BED3-505054503030},localized text,,3'); Diagnostic = '' } + } + Assert ((Get-WelaAuditPolicyMask '0CCE922B-69AE-11D9-BED3-505054503030') -eq 3) 'Audit policy parser uses numeric mask and GUID, not localized labels' + function global:Invoke-WelaNative { param($FilePath, $Arguments) [pscustomobject]@{ Output = @('unparseable'); Diagnostic = 'bad data' } } + $caught = '' + try { Get-WelaAuditPolicyMask '0CCE922B-69AE-11D9-BED3-505054503030' } catch { $caught = $_.ToString() } + Assert ($caught -ne '') 'Unparseable audit state cannot be marked compliant' + + # Extract ConfigureAuditSettings without running the WELA command dispatcher. + $tokens = $null; $errors = $null + $ast = [Management.Automation.Language.Parser]::ParseFile((Join-Path $repo 'WELA.ps1'), [ref]$tokens, [ref]$errors) + $configure = $ast.Find({ param($node) $node -is [Management.Automation.Language.FunctionDefinitionAst] -and $node.Name -eq 'ConfigureAuditSettings' }, $false) + Assert ($configure.Extent.Text -notmatch 'Configuration completed successfully|Start-Process|Out-Null') 'Configure has no unverified native execution or unconditional success' + + # Run the actual configure dispatcher in a child process with only the + # configuration function replaced by a harmless failed-report fixture. + $dispatch = $ast.Find({ param($node) $node -is [Management.Automation.Language.SwitchStatementAst] -and $node.Condition.Extent.Text -eq '$Cmd.ToLower()' }, $false) + $clause = @($dispatch.Clauses | Where-Object { $_.Item1.Value -eq 'configure' })[0].Item2.Extent.Text + $child = 'function ConfigureAuditSettings { [pscustomobject]@{ ExitCode = 1; Failed = 1; Results = @() } }; & ' + $clause + $encoded = [Convert]::ToBase64String([Text.Encoding]::Unicode.GetBytes($child)) + $childOutput = @(& $engine -NoProfile -EncodedCommand $encoded 2>&1) + $childExit = $global:LASTEXITCODE + Assert ($childExit -eq 1) 'The actual configure dispatcher returns nonzero for a failed control report' + + # CA-specific wrapper: registry read succeeds, certutil succeeds, restart + # fails. All APIs below are mocks, including Test-Path for the mock CA only. + $realTestPath = (Get-Command Test-Path).Name + function global:Test-Path { + param($LiteralPath, $Path, $ErrorAction) + if ($LiteralPath -like 'HKLM:*') { return $true } + Microsoft.PowerShell.Management\Test-Path -LiteralPath $(if ($LiteralPath) { $LiteralPath } else { $Path }) + } + function global:Get-ItemProperty { param($LiteralPath, $Name, $ErrorAction) [pscustomobject]@{ Active = 'MockCA' } } + function global:Join-Path { + param($Path, $ChildPath) + if ($Path -like 'HKLM:*') { return "$Path\$ChildPath" } + Microsoft.PowerShell.Management\Join-Path -Path $Path -ChildPath $ChildPath + } + $script:filter = 0; $script:restartCalls = 0 + function global:Get-WelaRegistryState { param($Path, $Name) [pscustomobject]@{ ValueExists = $true; Value = $script:filter; Type = 'DWord'; KeyExists = $true } } + function global:Get-Service { param($Name, $ErrorAction) [pscustomobject]@{ Status = 'Running' } } + function global:Restart-Service { param($Name, [switch]$Force, $ErrorAction) $script:restartCalls++; throw 'Injected CertSvc restart failure' } + function global:Invoke-WelaNative { param($FilePath, $Arguments) $script:filter = 127; [pscustomobject]@{ ExitCode = 0; Diagnostic = 'mock certutil' } } + $c = New-TestContext + Set-WelaCertificateAuditControl $c + $r = Complete-WelaConfiguration $c + Assert ($r.ExitCode -eq 1 -and $r.Results[0].Diagnostic -match 'restart failure') 'CA restart failure cannot report success even when registry now equals 127' + $script:filter = 0; $script:restartCalls = 0 + function global:Invoke-WelaNative { param($FilePath, $Arguments) throw 'certutil failed (exit: 5)' } + $c = New-TestContext + Set-WelaCertificateAuditControl $c + Assert ($c.Results[0].Status -eq 'Failed' -and $script:restartCalls -eq 0) 'Failed certutil never restarts the CA' + $c = New-TestContext -DryRun + Set-WelaCertificateAuditControl $c + Assert ($c.Results[0].Status -eq 'Skipped' -and $script:restartCalls -eq 0) 'CA dry run never writes or restarts' + + Write-Host "$script:passed configuration-result regression assertions passed. No Windows settings changed." +} finally { + foreach ($path in $script:cleanup) { + if (Microsoft.PowerShell.Management\Test-Path -LiteralPath $path) { + Remove-Item -LiteralPath $path -Recurse -Force + } + } +} From 36c4b4018f2aa265d0222e61f15b29e44a1a5a43 Mon Sep 17 00:00:00 2001 From: Shirofune-Security <43838376+Shirofune-Security@users.noreply.github.com> Date: Fri, 18 Sep 2026 21:49:46 +0900 Subject: [PATCH 03/21] Run configuration checks with explicit PowerShell shells --- .github/workflows/configuration-results.yml | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/.github/workflows/configuration-results.yml b/.github/workflows/configuration-results.yml index 688cba9a..6da70e8e 100644 --- a/.github/workflows/configuration-results.yml +++ b/.github/workflows/configuration-results.yml @@ -9,14 +9,17 @@ permissions: jobs: configuration-results: runs-on: windows-latest - strategy: - matrix: - shell: [powershell, pwsh] steps: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - - name: Mocked regression tests (no Windows configuration changes) - shell: ${{ matrix.shell }} + - name: Mocked regressions in Windows PowerShell 5.1 + shell: powershell run: ./tests/Test-ConfigurationResults.ps1 - - name: Real Windows read-only smoke tests - shell: ${{ matrix.shell }} + - name: Read-only Windows smoke in Windows PowerShell 5.1 + shell: powershell + run: ./tests/Test-ConfigurationReadOnlyWindows.ps1 + - name: Mocked regressions in PowerShell 7 + shell: pwsh + run: ./tests/Test-ConfigurationResults.ps1 + - name: Read-only Windows smoke in PowerShell 7 + shell: pwsh run: ./tests/Test-ConfigurationReadOnlyWindows.ps1 From ca54b5cf742e6f3c959d2009d32c4473cfa2b7b7 Mon Sep 17 00:00:00 2001 From: Shirofune-Security <43838376+Shirofune-Security@users.noreply.github.com> Date: Fri, 18 Sep 2026 21:49:46 +0900 Subject: [PATCH 04/21] Use explicit PowerShell shells in regression workflow --- .github/workflows/test-outgoing-ntlm.yml | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/.github/workflows/test-outgoing-ntlm.yml b/.github/workflows/test-outgoing-ntlm.yml index 73df9a8a..6d0a6b8a 100644 --- a/.github/workflows/test-outgoing-ntlm.yml +++ b/.github/workflows/test-outgoing-ntlm.yml @@ -9,11 +9,11 @@ permissions: jobs: test: runs-on: windows-latest - strategy: - matrix: - shell: [powershell, pwsh] steps: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - - name: Test outgoing NTLM policy without changing host settings - shell: ${{ matrix.shell }} + - name: Test outgoing NTLM policy in Windows PowerShell 5.1 + shell: powershell + run: ./tests/OutgoingNtlm.Tests.ps1 + - name: Test outgoing NTLM policy in PowerShell 7 + shell: pwsh run: ./tests/OutgoingNtlm.Tests.ps1 From eb3232faf5762784b6229973a5f8fbdb6311a9fb Mon Sep 17 00:00:00 2001 From: Shirofune-Security <43838376+Shirofune-Security@users.noreply.github.com> Date: Fri, 18 Sep 2026 21:53:44 +0900 Subject: [PATCH 05/21] Read audit masks through Windows API and preserve missing registry parents --- docs/configuration-results.md | 11 +-- scripts/Configuration.ps1 | 77 +++++++++++++++++---- tests/Test-ConfigurationReadOnlyWindows.ps1 | 3 + tests/Test-ConfigurationResults.ps1 | 53 +++++++++++--- 4 files changed, 117 insertions(+), 27 deletions(-) diff --git a/docs/configuration-results.md b/docs/configuration-results.md index 63f787c8..419cf112 100644 --- a/docs/configuration-results.md +++ b/docs/configuration-results.md @@ -39,8 +39,10 @@ controls do not claim universal success. Verification is an observation at that moment; it does not prove future GPO persistence, event production, collection or Sigma rule coverage. A zero exit code with skipped controls is not full compliance. -Audit policy reads use GUIDs and the numeric value in `auditpol /r` output, rather -than localized setting names. Registry writes use terminating errors and verify +Audit policy reads use GUIDs and numeric flags from the Windows +[AuditQuerySystemPolicy API](https://learn.microsoft.com/en-us/windows/win32/api/ntsecapi/nf-ntsecapi-auditquerysystempolicy). +`auditpol /get /r` contains localized labels and no numeric setting column; it is +not parsed as though it were `auditpol /backup` output. Registry writes use terminating errors and verify both the value and registry type. Log sizes retain larger existing buffers. A CA is detected from its configured registry state; certutil must succeed before a restart is attempted, and the restart must return to Running. A stopped CA is not @@ -93,9 +95,8 @@ force a Group Policy setting. These are design constraints, not implemented clai `tests/Test-ConfigurationResults.ps1` uses mock Windows APIs and disposable temp journals. It exercises nonzero native exits and stderr, false-success writes, -read-back, idempotence, final drift, dry runs, journal failure, localized audit CSV -labels, and CA write/restart failure. It does not change Windows settings. -`tests/Test-ConfigurationReadOnlyWindows.ps1` runs real read-only `auditpol /get` +read-back, idempotence, final drift, dry runs, journal failure, locale-independent native audit flags, and CA write/restart failure. It does not change Windows settings. +`tests/Test-ConfigurationReadOnlyWindows.ps1` runs real read-only Windows audit-policy API and `auditpol /get` queries and a child `cmd.exe` diagnostic/exit test. CI runs both scripts in Windows PowerShell 5.1 and PowerShell 7. Mutating behavior still requires isolated Windows/CA lab validation; mock and read-only tests do not establish end-to-end event production. diff --git a/scripts/Configuration.ps1 b/scripts/Configuration.ps1 index feec7e0d..0ec682a1 100644 --- a/scripts/Configuration.ps1 +++ b/scripts/Configuration.ps1 @@ -151,14 +151,24 @@ function Get-WelaRegistryState { } } +function New-WelaRegistryKey { + param([string]$Path) + if (Test-Path -LiteralPath $Path -ErrorAction Stop) { return } + $separator = $Path.TrimEnd('\').LastIndexOf('\') + if ($separator -lt 1) { throw "Registry root is unavailable: $Path" } + $parent = $Path.Substring(0, $separator) + # Registry New-Item without Force requires its immediate parent. Build only + # missing ancestors; never run New-Item -Force against an existing key. + New-WelaRegistryKey -Path $parent + $null = New-Item -Path $Path -ErrorAction Stop +} + function Set-WelaRegistryControl { param($Context, [string]$Path, [string]$Name, $Value, [string]$Type = 'DWord') $read = { Get-WelaRegistryState -Path $Path -Name $Name }.GetNewClosure() $test = { param($state) $state.ValueExists -and $state.Value -eq $Value -and $state.Type -eq $Type }.GetNewClosure() $apply = { - if (-not (Test-Path -LiteralPath $Path -ErrorAction Stop)) { - $null = New-Item -Path $Path -ErrorAction Stop - } + New-WelaRegistryKey -Path $Path Set-ItemProperty -LiteralPath $Path -Name $Name -Value $Value -Type $Type -ErrorAction Stop }.GetNewClosure() Invoke-WelaConfigurationControl -Context $Context -Id "Registry/$Path/$Name" -Kind Registry ` @@ -166,16 +176,59 @@ function Set-WelaRegistryControl { -Read $read -Compliant $test -Apply $apply } +function Initialize-WelaConfigurationAuditApi { + if ('Wela.ConfigurationAuditApi' -as [type]) { return } + # Querying the Windows API avoids localized auditpol /get CSV (six columns; + # unlike /backup output, it has no numeric Setting Value column). + Add-Type -TypeDefinition @' +using System; +using System.ComponentModel; +using System.Runtime.InteropServices; +namespace Wela { + public static class ConfigurationAuditApi { + [StructLayout(LayoutKind.Sequential)] + private struct AuditPolicyInformation { + public Guid Subcategory; + public UInt32 Information; + public Guid Category; + } + [DllImport("advapi32.dll", SetLastError = true)] + [return: MarshalAs(UnmanagedType.U1)] + private static extern bool AuditQuerySystemPolicy( + [In] Guid[] subcategories, UInt32 count, out IntPtr policy); + [DllImport("advapi32.dll")] + private static extern void AuditFree(IntPtr buffer); + public static UInt32 Query(Guid subcategory) { + IntPtr buffer = IntPtr.Zero; + if (!AuditQuerySystemPolicy(new Guid[] { subcategory }, 1, out buffer)) { + throw new Win32Exception(Marshal.GetLastWin32Error()); + } + try { + if (buffer == IntPtr.Zero) throw new InvalidOperationException("Audit policy query returned no buffer."); + AuditPolicyInformation policy = (AuditPolicyInformation)Marshal.PtrToStructure(buffer, typeof(AuditPolicyInformation)); + if (policy.Subcategory != subcategory) throw new InvalidOperationException("Audit policy query returned a different subcategory."); + return policy.Information; + } finally { + if (buffer != IntPtr.Zero) AuditFree(buffer); + } + } + } +} +'@ -ErrorAction Stop +} + +function Get-WelaNativeAuditPolicy { + param([string]$Guid) + Initialize-WelaConfigurationAuditApi + return [Wela.ConfigurationAuditApi]::Query([guid]$Guid) +} + function Get-WelaAuditPolicyMask { param([string]$Guid) - $native = Invoke-WelaNative -FilePath 'auditpol.exe' -Arguments @('/get', "/subcategory:{$Guid}", '/r') - # Column order is stable; names and Inclusion Setting text are localized. - $rows = $native.Output | ConvertFrom-Csv -Header Machine, Target, Name, Guid, Inclusion, Exclusion, SettingValue - $row = @($rows | Where-Object { $_.Guid -and $_.Guid.Trim('{}') -eq $Guid }) - if ($row.Count -ne 1 -or $row[0].SettingValue -notmatch '^[0-3]$') { - throw "auditpol returned no unambiguous numeric setting for $Guid. $($native.Diagnostic)" - } - return [int]$row[0].SettingValue + $flags = Get-WelaNativeAuditPolicy -Guid $Guid + if ($flags -notin @(0, 1, 2, 3, 4)) { throw "Unexpected audit policy flags $flags for $Guid." } + # POLICY_AUDIT_EVENT_NONE is 4; the success/failure mask is zero. + return [int]($flags -band 3) } function Set-WelaAuditPolicyControl { @@ -204,7 +257,7 @@ function Set-WelaCertificateAuditControl { ServiceStatus = (Get-Service -Name CertSvc -ErrorAction Stop).Status.ToString() } }.GetNewClosure() - $test = { param($value) $value.Registry.ValueExists -and $value.Registry.Value -eq 127 -and $value.ServiceStatus -eq 'Running' } + $test = { param($value) $value.Registry.ValueExists -and $value.Registry.Value -eq 127 -and $value.Registry.Type -eq 'DWord' -and $value.ServiceStatus -eq 'Running' } $apply = { $state = Get-Service -Name CertSvc -ErrorAction Stop if ($state.Status -ne 'Running') { throw 'CertSvc is not running; refusing to start a previously stopped CA. Start it deliberately before retrying.' } diff --git a/tests/Test-ConfigurationReadOnlyWindows.ps1 b/tests/Test-ConfigurationReadOnlyWindows.ps1 index 4b3b38f8..87597fa9 100644 --- a/tests/Test-ConfigurationReadOnlyWindows.ps1 +++ b/tests/Test-ConfigurationReadOnlyWindows.ps1 @@ -4,6 +4,9 @@ if ($env:OS -ne 'Windows_NT') { throw 'Run this smoke test on Windows.' } . (Join-Path (Split-Path $PSScriptRoot -Parent) 'scripts/Configuration.ps1') $mask = Get-WelaAuditPolicyMask '0CCE922B-69AE-11D9-BED3-505054503030' if ($mask -notin @(0, 1, 2, 3)) { throw "Unexpected process-creation audit mask: $mask" } +# Also exercise the real read-only auditpol command and its native exit status. +$csv = Invoke-WelaNative -FilePath auditpol.exe -Arguments @('/get', '/subcategory:{0CCE922B-69AE-11D9-BED3-505054503030}', '/r') +if ($csv.Diagnostic -notmatch '0CCE922B-69AE-11D9-BED3-505054503030') { throw 'auditpol query returned no requested subcategory.' } $caught = '' try { Invoke-WelaNative -FilePath $env:ComSpec -Arguments @('/d', '/c', 'echo WELA-smoke-diagnostic 1>&2 & exit /b 9') } catch { $caught = $_.ToString() } diff --git a/tests/Test-ConfigurationResults.ps1 b/tests/Test-ConfigurationResults.ps1 index 146583b9..f7b7b954 100644 --- a/tests/Test-ConfigurationResults.ps1 +++ b/tests/Test-ConfigurationResults.ps1 @@ -2,7 +2,10 @@ $ErrorActionPreference = 'Stop' $repo = Split-Path $PSScriptRoot -Parent $script:ScriptRoot = $repo -. (Join-Path $repo 'scripts/Configuration.ps1') +# Load trusted source functions into the same scope as the mocks. Windows +# PowerShell 5.1 otherwise resolves a script-local original ahead of global mocks. +$definitions = Get-Content -LiteralPath (Join-Path $repo 'scripts/Configuration.ps1') -Raw +Invoke-Expression ($definitions -replace '(?m)^function ', 'function global:') $script:passed = 0 function Assert($Condition, [string]$Message) { if (-not $Condition) { throw "FAIL: $Message" } @@ -93,6 +96,29 @@ try { Set-WelaRegistryControl $c 'HKLM:\mock' Value 1 Assert ($c.Results[1].Status -eq 'AlreadyCompliant' -and $script:registryWrites -eq $beforeWrites) 'Registry reruns preserve compliant values' + # Missing nested registry parents must be created individually, retaining + # existing parent keys/values. Mock provider rejects children without parents. + $script:mockKeys = @{'HKLM:' = $true; 'HKLM:\SOFTWARE' = $true} + $script:createdKeys = New-Object 'System.Collections.Generic.List[string]' + function global:Test-Path { + param($LiteralPath, $Path, $ErrorAction) + if ($LiteralPath -like 'HKLM:*') { return $script:mockKeys.ContainsKey($LiteralPath) } + Microsoft.PowerShell.Management\Test-Path -LiteralPath $(if ($LiteralPath) { $LiteralPath } else { $Path }) + } + function global:New-Item { + param($Path, $ItemType, [switch]$Force, $ErrorAction) + if ($Path -notlike 'HKLM:*') { return Microsoft.PowerShell.Management\New-Item @PSBoundParameters } + if ($Force) { throw 'Test refuses Force on registry keys' } + if ($script:mockKeys.ContainsKey($Path)) { throw 'Existing parent would be recreated' } + $parent = $Path.Substring(0, $Path.LastIndexOf('\')) + if (-not $script:mockKeys.ContainsKey($parent)) { throw "Missing registry parent: $parent" } + $script:createdKeys.Add($Path); $script:mockKeys[$Path] = $true + } + New-WelaRegistryKey 'HKLM:\SOFTWARE\Policies\Microsoft\Windows\PowerShell\ModuleLogging' + Assert ($script:createdKeys.Count -eq 5 -and $script:mockKeys.ContainsKey('HKLM:\SOFTWARE\Policies\Microsoft\Windows\PowerShell\ModuleLogging')) 'Missing registry ancestors are created safely in order' + New-WelaRegistryKey 'HKLM:\SOFTWARE\Policies\Microsoft\Windows\PowerShell\ModuleLogging' + Assert ($script:createdKeys.Count -eq 5) 'Existing registry parents are preserved on rerun' + # Function stubs stand in for the Windows APIs from this point onward. $script:logSize = 1048576; $script:nativeFails = $true; $script:nativeWrites = 0 function global:Get-WinEvent { param($ListLog, $ErrorAction) [pscustomobject]@{ MaximumSizeInBytes = $script:logSize; IsEnabled = $false } } @@ -114,15 +140,17 @@ try { Set-WelaEventLogControl $c Security MaximumSizeInBytes 134217728 Assert ($c.Results[1].Status -eq 'AlreadyCompliant') 'Event log helper avoids repeated writes' - function global:Invoke-WelaNative { - param($FilePath, $Arguments) - [pscustomobject]@{ ExitCode = 0; Output = @('Localized,header,labels,here,x,y,z', 'host,System,localized name,{0CCE922B-69AE-11D9-BED3-505054503030},localized text,,3'); Diagnostic = '' } - } - Assert ((Get-WelaAuditPolicyMask '0CCE922B-69AE-11D9-BED3-505054503030') -eq 3) 'Audit policy parser uses numeric mask and GUID, not localized labels' - function global:Invoke-WelaNative { param($FilePath, $Arguments) [pscustomobject]@{ Output = @('unparseable'); Diagnostic = 'bad data' } } + # Compile the interop declaration without invoking Windows APIs on this host. + Initialize-WelaConfigurationAuditApi + Assert ($null -ne ('Wela.ConfigurationAuditApi' -as [type])) 'Audit query interop compiles' + function global:Get-WelaNativeAuditPolicy { param($Guid) return 3 } + Assert ((Get-WelaAuditPolicyMask '0CCE922B-69AE-11D9-BED3-505054503030') -eq 3) 'Audit policy uses native numeric flags independent of locale' + function global:Get-WelaNativeAuditPolicy { param($Guid) return 4 } + Assert ((Get-WelaAuditPolicyMask '0CCE922B-69AE-11D9-BED3-505054503030') -eq 0) 'Native NONE flag normalizes to no success/failure audit' + function global:Get-WelaNativeAuditPolicy { param($Guid) return 16 } $caught = '' try { Get-WelaAuditPolicyMask '0CCE922B-69AE-11D9-BED3-505054503030' } catch { $caught = $_.ToString() } - Assert ($caught -ne '') 'Unparseable audit state cannot be marked compliant' + Assert ($caught -ne '') 'Unexpected native flags cannot be marked compliant' # Extract ConfigureAuditSettings without running the WELA command dispatcher. $tokens = $null; $errors = $null @@ -154,8 +182,8 @@ try { if ($Path -like 'HKLM:*') { return "$Path\$ChildPath" } Microsoft.PowerShell.Management\Join-Path -Path $Path -ChildPath $ChildPath } - $script:filter = 0; $script:restartCalls = 0 - function global:Get-WelaRegistryState { param($Path, $Name) [pscustomobject]@{ ValueExists = $true; Value = $script:filter; Type = 'DWord'; KeyExists = $true } } + $script:filter = 0; $script:restartCalls = 0; $script:filterType = 'DWord' + function global:Get-WelaRegistryState { param($Path, $Name) [pscustomobject]@{ ValueExists = $true; Value = $script:filter; Type = $script:filterType; KeyExists = $true } } function global:Get-Service { param($Name, $ErrorAction) [pscustomobject]@{ Status = 'Running' } } function global:Restart-Service { param($Name, [switch]$Force, $ErrorAction) $script:restartCalls++; throw 'Injected CertSvc restart failure' } function global:Invoke-WelaNative { param($FilePath, $Arguments) $script:filter = 127; [pscustomobject]@{ ExitCode = 0; Diagnostic = 'mock certutil' } } @@ -172,6 +200,11 @@ try { Set-WelaCertificateAuditControl $c Assert ($c.Results[0].Status -eq 'Skipped' -and $script:restartCalls -eq 0) 'CA dry run never writes or restarts' + $script:filter = '127'; $script:filterType = 'String' + $c = New-TestContext -DryRun + Set-WelaCertificateAuditControl $c + Assert ($c.Results[0].Status -eq 'Skipped') 'REG_SZ 127 is not accepted as a compliant CA DWORD AuditFilter' + Write-Host "$script:passed configuration-result regression assertions passed. No Windows settings changed." } finally { foreach ($path in $script:cleanup) { From ee7a0e2216f767b58fb1a38e2e199bc07d9f3a27 Mon Sep 17 00:00:00 2001 From: Shirofune-Security <43838376+Shirofune-Security@users.noreply.github.com> Date: Fri, 18 Sep 2026 21:54:38 +0900 Subject: [PATCH 06/21] Unify advanced audit policy audit, plan and configure profiles --- .github/workflows/audit-profiles.yml | 25 + .github/workflows/release.yml | 1 + README.md | 2 + WELA.ps1 | 182 +- config/audit_profiles.json | 4944 ++++++++++++++++++++++++ docs/audit-profiles.md | 74 + modules/AuditProfiles.psm1 | 253 ++ tests/audit-profiles.Tests.ps1 | 115 + tests/audit-profiles.Windows.Tests.ps1 | 15 + 9 files changed, 5530 insertions(+), 81 deletions(-) create mode 100644 .github/workflows/audit-profiles.yml create mode 100644 config/audit_profiles.json create mode 100644 docs/audit-profiles.md create mode 100644 modules/AuditProfiles.psm1 create mode 100644 tests/audit-profiles.Tests.ps1 create mode 100644 tests/audit-profiles.Windows.Tests.ps1 diff --git a/.github/workflows/audit-profiles.yml b/.github/workflows/audit-profiles.yml new file mode 100644 index 00000000..189ad52e --- /dev/null +++ b/.github/workflows/audit-profiles.yml @@ -0,0 +1,25 @@ +name: Audit profile regression tests +on: + push: + branches: ['**'] + pull_request: + workflow_dispatch: +permissions: + contents: read +jobs: + profiles: + runs-on: windows-latest + steps: + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + - name: Test shared profiles in Windows PowerShell 5.1 + shell: powershell + run: ./tests/audit-profiles.Tests.ps1 + - name: Test shared profiles in PowerShell 7 + shell: pwsh + run: ./tests/audit-profiles.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 diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 96c929f5..ad1e002b 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -38,6 +38,7 @@ jobs: mkdir -p release-binaries Copy-Item -Path WELA.ps1 -Destination release-binaries/ Copy-Item -Recurse -Path ./config -Destination release-binaries/ + Copy-Item -Recurse -Path ./modules -Destination release-binaries/ - name: Set Artifact Name if: contains(matrix.info.os, 'windows') == true diff --git a/README.md b/README.md index 2547b290..26d3b06e 100644 --- a/README.md +++ b/README.md @@ -42,6 +42,8 @@ Windows event logs are a vital source of information for Digital Forensics and I (DFIR) — WELA checks your audit policy and log file sizes against best-practice guidelines and real-world Sigma-rule detectability, and can apply the recommended settings for you. +Advanced audit policy can also use [versioned WELA, Microsoft, CIS and ASD profiles](docs/audit-profiles.md) for shared audit, plan and configure behavior. Profiles cover advanced audit policy only. + ## 📖 Documentation All documentation now lives on a dedicated, searchable, multi-language site: diff --git a/WELA.ps1 b/WELA.ps1 index a5e00cd6..d423ccf2 100644 --- a/WELA.ps1 +++ b/WELA.ps1 @@ -3,6 +3,11 @@ [string]$OutType = "std", [switch]$Debug, [string]$Baseline, + [string]$Profile, + [ValidateSet("Client", "MemberServer", "DomainController", "ADCS")][string]$Role, + [int]$Build, + [string]$PlanPath, + [switch]$IncludeOptional, [switch]$Auto, [switch]$Help ) @@ -17,6 +22,7 @@ $SecurityRulesPath = Join-Path $ScriptRoot "config/security_rules.json" $EidMappingPath = Join-Path $ScriptRoot "config/eid_subcategory_mapping.csv" $AuditpolTxtPath = Join-Path $ScriptRoot "auditpol.txt" $SaclTargetsPath = Join-Path $ScriptRoot "config/audit_sacl_targets.json" +Import-Module (Join-Path $ScriptRoot "modules/AuditProfiles.psm1") -ErrorAction Stop # 64bit の PowerShell と GPO が読むのは Wow6432Node の無いパス。32bit 用に両方を扱う。 $PowerShellPolicyRoots = @( @@ -273,6 +279,47 @@ function GetBaselineNames { return @((GetBaselineConfig).baselines.PSObject.Properties.Name) } +function Get-WelaSelectedContext { + if (($script:Role -and -not $script:Build) -or ($script:Build -and -not $script:Role)) { + throw "Specify both -Role and -Build, or neither to detect this Windows host." + } + if ($script:Role -and $script:Build) { + return [pscustomobject]@{ Role = $script:Role; Build = $script:Build } + } + Get-WelaHostContext +} + +function Invoke-WelaProfileCommand { + param([string]$Command) + if ($script:Baseline) { throw "Use -Profile or -Baseline, not both. Versioned profiles cover advanced audit policy only." } + if (-not $script:Profile) { throw "Specify -Profile. Use './WELA.ps1 profiles' to list versioned profiles." } + $context = Get-WelaSelectedContext + $current = @{} + if (TestWindows) { + $actual = Get-WelaHostContext + if ($actual.Role -eq $context.Role -and $actual.Build -eq $context.Build) { $current = Get-WelaEffectiveAuditPolicy } + elseif ($Command -ne 'plan') { throw "Requested role/build does not match this Windows host." } + else { Write-Host "Planning for another role/build: effective state remains Unknown." } + } + elseif ($Command -ne 'plan') { throw "Audit and configure require Windows. Offline planning requires explicit -Role and -Build." } + $plan = Get-WelaAuditProfilePlan -Profile $script:Profile -Role $context.Role -Build $context.Build -Current $current -IncludeOptional:$script:IncludeOptional + Write-Host "Profile: $($plan.profile); role: $($plan.role); build: $($plan.build)" + Write-Host "Scope: advanced audit policy only. Channels, command-line capture, PowerShell, NTLM, SACLs, CA AuditFilter and forwarding are separate." + $result = $plan + if ($Command -eq 'configure') { + if (-not (TestAdministrator)) { throw "Configuring advanced audit policy requires Administrator privileges." } + $result = Invoke-WelaAuditProfilePlan -Plan $plan -Confirm:(-not $script:Auto) + $result.results | Format-Table id, beforeMask, targetMask, effectiveMask, status -AutoSize + } else { + $plan.policies | Format-Table id, mode, currentMask, requiredMask, action -AutoSize + } + if ($script:PlanPath) { + $result | ConvertTo-Json -Depth 20 | Set-Content -LiteralPath $script:PlanPath -Encoding UTF8 -ErrorAction Stop + Write-Host "Machine-readable result: $($script:PlanPath)" + } + if ($Command -eq 'configure' -and -not $result.success) { throw "One or more advanced audit policies failed. See the effective-state results." } +} + function BuildAuditResult { param ( [object[]] $all_rules, @@ -293,8 +340,16 @@ function BuildAuditResult { $auditpol = GetAuditpol $auditResult = @() + $sharedPlan = $null + if ($baselineName -eq 'YamatoSecurity') { + $context = Get-WelaSelectedContext + $sharedPlan = Get-WelaAuditProfilePlan -Profile 'wela-2.2.0' -Role $context.Role -Build $context.Build -IncludeOptional:$script:IncludeOptional + Write-Host "Advanced audit recommendations: $($sharedPlan.profile), role $($sharedPlan.role), build $($sharedPlan.build). Other controls use the existing baseline metadata." + } foreach ($item in $config.catalog) { + # The versioned profile owns all advanced-audit recommendations and canonical GUIDs. + if ($sharedPlan -and $item.currentSetting.type -eq 'auditpol') { continue } $setting = $settings.($item.id) if (-not $setting) { throw "Baseline '$baselineName' has no entry for catalog id '$($item.id)'." @@ -379,6 +434,26 @@ 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' } + if ($policy.mode -ne 'not-applicable' -and $enabledguid -contains $policy.guid) { + $rules | ForEach-Object { $_.applicable = $true } + } + if ($policy.mode -in @('exact', 'minimum') -and $policy.requiredMask -ne 0) { + $rules | ForEach-Object { $_.ideal = $true } + } + $legacyItem = $config.catalog | Where-Object { $_.subCategory -eq $policy.id -and $_.currentSetting.type -eq 'auditpol' } | Select-Object -First 1 + $legacy = if ($legacyItem) { $settings.($legacyItem.id) } else { $null } + $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, + $defaultSetting, $policy.recommendation, $volume, $note) + } + } + # どのカテゴリにも該当しなかったルールを取りこぼさない。 # 集計対象から黙って消えると、利用率の分母がルール総数と合わなくなる。 $covered = [System.Collections.Generic.HashSet[string]]::new() @@ -1047,6 +1122,12 @@ function ConfigureAuditSettings { exit 1 } + # Validate the complete advanced policy target before any configuration writes. + $context = Get-WelaHostContext + $effectivePolicy = Get-WelaEffectiveAuditPolicy + $profilePlan = Get-WelaAuditProfilePlan -Profile 'wela-2.2.0' -Role $context.Role -Build $context.Build -Current $effectivePolicy -IncludeOptional:$script:IncludeOptional + Assert-WelaAuditProfileTarget -Plan $profilePlan -Context $context -Current $effectivePolicy + if (-not (CollectAuditpol -UseCached:$Debug)) { return } @@ -1310,87 +1391,11 @@ function ConfigureAuditSettings { ) -Auto:$Auto } - # 監査ポリシーの設定 - Write-Host "Configuring Audit Policies..." - Write-Host "" - $auditPolicies = @( - @{Category = "Account Logon"; Name = "Credential Validation"; GUID = "0CCE923F-69AE-11D9-BED3-505054503030"}, - @{Category = "Account Logon"; Name = "Kerberos Authentication Service"; GUID = "0CCE9242-69AE-11D9-BED3-505054503030"}, - @{Category = "Account Logon"; Name = "Kerberos Service Ticket Operations"; GUID = "0CCE9240-69AE-11D9-BED3-505054503030"}, - @{Category = "Account Management"; Name = "Computer Account Management"; GUID = "0CCE9236-69AE-11D9-BED3-505054503030"}, - @{Category = "Account Management"; Name = "Distribution Group Management"; GUID = "0CCE9238-69AE-11D9-BED3-505054503030"}, - @{Category = "Account Management"; Name = "Other Account Management Events"; GUID = "0CCE923A-69AE-11D9-BED3-505054503030"}, - @{Category = "Account Management"; Name = "Security Group Management"; GUID = "0CCE9237-69AE-11D9-BED3-505054503030"}, - @{Category = "Account Management"; Name = "User Account Management"; GUID = "0CCE9235-69AE-11D9-BED3-505054503030"}, - @{Category = "Detailed Tracking"; Name = "Plug and Play"; GUID = "0cce9248-69ae-11d9-bed3-505054503030"}, - @{Category = "Detailed Tracking"; Name = "Process Creation"; GUID = "0CCE922B-69AE-11D9-BED3-505054503030"}, - @{Category = "Detailed Tracking"; Name = "Process Termination"; GUID = "0CCE922C-69AE-11D9-BED3-505054503030"}, - @{Category = "Detailed Tracking"; Name = "RPC Events"; GUID = "0CCE922E-69AE-11D9-BED3-505054503030"}, - @{Category = "DS Access"; Name = "Directory Service Access"; GUID = "0CCE923B-69AE-11D9-BED3-505054503030"}, - @{Category = "DS Access"; Name = "Directory Service Changes"; GUID = "0CCE923C-69AE-11D9-BED3-505054503030"}, - @{Category = "Logon/Logoff"; Name = "Account Lockout"; GUID = "0CCE9217-69AE-11D9-BED3-505054503030"}, - @{Category = "Logon/Logoff"; Name = "Logoff"; GUID = "0CCE9216-69AE-11D9-BED3-505054503030"}, - @{Category = "Logon/Logoff"; Name = "Logon"; GUID = "0CCE9215-69AE-11D9-BED3-505054503030"}, - @{Category = "Logon/Logoff"; Name = "Other Logon/Logoff Events"; GUID = "0CCE921C-69AE-11D9-BED3-505054503030"}, - @{Category = "Logon/Logoff"; Name = "Special Logon"; GUID = "0CCE921B-69AE-11D9-BED3-505054503030"}, - @{Category = "Object Access"; Name = "Certification Services"; GUID = "0CCE9221-69AE-11D9-BED3-505054503030"}, - @{Category = "Object Access"; Name = "File Share"; GUID = "0CCE9224-69AE-11D9-BED3-505054503030"}, - @{Category = "Object Access"; Name = "Detailed File Share"; GUID = "0CCE9244-69AE-11D9-BED3-505054503030"}, - @{Category = "Object Access"; Name = "Filtering Platform Connection"; GUID = "0CCE9226-69AE-11D9-BED3-505054503030"}, - @{Category = "Object Access"; Name = "Other Object Access Events"; GUID = "0CCE9227-69AE-11D9-BED3-505054503030"}, - @{Category = "Object Access"; Name = "Removable Storage"; GUID = "0CCE9245-69AE-11D9-BED3-505054503030"}, - @{Category = "Object Access"; Name = "SAM"; GUID = "0CCE9220-69AE-11D9-BED3-505054503030"}, - @{Category = "Policy Change"; Name = "Audit Policy Change"; GUID = "0CCE922F-69AE-11D9-BED3-505054503030"}, - @{Category = "Policy Change"; Name = "Authentication Policy Change"; GUID = "0CCE9230-69AE-11D9-BED3-505054503030"}, - @{Category = "Policy Change"; Name = "Other Policy Change Events"; GUID = "0CCE9234-69AE-11D9-BED3-505054503030"}, - @{Category = "Privilege Use"; Name = "Sensitive Privilege Use"; GUID = "0CCE9228-69AE-11D9-BED3-505054503030"}, - @{Category = "System"; Name = "Security State Change"; GUID = "0CCE9210-69AE-11D9-BED3-505054503030"}, - @{Category = "System"; Name = "Security System Extension"; GUID = "0CCE9211-69AE-11D9-BED3-505054503030"}, - @{Category = "System"; Name = "System Integrity"; GUID = "0CCE9212-69AE-11D9-BED3-505054503030"}, - @{Category = "System"; Name = "Other System Events"; GUID = "0CCE9214-69AE-11D9-BED3-505054503030"} - ) - - $currentAuditPol = GetAuditpol - - foreach ($policy in $auditPolicies) - { - $newSetting = "Success and Failure" - $currentSetting = if ($currentAuditPol.ContainsKey($policy.GUID)) - { - $currentAuditPol[$policy.GUID] - } - else - { - "Unknown" - } - - Write-Host "Audit Policy: $( $policy.Category ) - $( $policy.Name )" - if ($currentSetting -eq $newSetting) - { - Write-Host "[SKIPPED] $( $policy.Category ) - $( $policy.Name ) : Already set to $newSetting." -ForegroundColor Yellow - Write-Host "" - continue - } - if ($Auto) { - $response = "Y" - } else { - $response = Read-Host "Your current setting is $currentSetting. Do you want to change it to $newSetting? (Y/n)" - } - if ($response -eq "" -or $response -eq "Y" -or $response -eq "y") { - $arguments = "/set /subcategory:{$($policy.GUID)} /success:enable /failure:enable" - $process = Start-Process -FilePath "auditpol.exe" -ArgumentList $arguments -Wait -PassThru -NoNewWindow -RedirectStandardOutput "NUL" - - if ($process.ExitCode -eq 0) { - Write-Host "[OK] $($policy.Category) - $($policy.Name)" -ForegroundColor Green - } - else { - Write-Host "[ERROR] $($policy.Category) - $($policy.Name) (ExitCode: $($process.ExitCode))" -ForegroundColor Red - } - } else { - Write-Host "[SKIPPED] $($policy.Category) - $($policy.Name)" -ForegroundColor Yellow - } - Write-Host "" - } + # Audit and configure consume the same versioned policy definition. + Write-Host "Configuring advanced audit policy from wela-2.2.0..." + $profileResult = Invoke-WelaAuditProfilePlan -Plan $profilePlan -Confirm:(-not $Auto) + $profileResult.results | Format-Table id, beforeMask, targetMask, effectiveMask, status -AutoSize + if (-not $profileResult.success) { throw "Advanced audit-policy configuration failed. Review effective-state results above." } # AD CS AuditFilter の設定 Write-Host "Configuring AD CS Audit Settings..." @@ -1714,6 +1719,11 @@ function Get-WelaUserProfiles { $usage = @" Usage: + ./WELA.ps1 profiles # List versioned advanced audit-policy profiles + ./WELA.ps1 plan -Profile wela-2.2.0 -Role Client -Build 26100 -PlanPath plan.json + ./WELA.ps1 audit-settings -Profile microsoft-sct-win11-24h2 -PlanPath audit.json + ./WELA.ps1 configure -Profile asd-native-2021-10 -PlanPath result.json -Auto + # -Profile changes advanced audit policy ONLY. Optional controls need -IncludeOptional. ./WELA.ps1 audit-settings -Baseline YamatoSecurity # Audit current setting and show in stdout, save to csv ./WELA.ps1 audit-settings -Baseline ASD -OutType gui # Audit current setting and show in gui, save to csv ./WELA.ps1 audit-filesize -Baseline YamatoSecurity # Audit current file size and show in stdout, save to csv @@ -1733,7 +1743,17 @@ Write-Host "" Write-Host "WELA v$WELAVersion - $WELAReleaseName" Write-Host "" +if ($Profile -and $Cmd.ToLower() -in @('plan', 'audit', 'audit-settings', 'configure') -and -not $Help) { + Invoke-WelaProfileCommand -Command $Cmd.ToLower() + return +} + switch ($Cmd.ToLower()) { + "profiles" { + (Import-WelaAuditProfiles).profiles | Select-Object id, version, scope, appliesTo | Format-List + } + "plan" { Invoke-WelaProfileCommand -Command 'plan' } + "audit" { Invoke-WelaProfileCommand -Command 'audit' } "audit-settings" { if ($Help -or [string]::IsNullOrEmpty($Baseline)){ Write-Host "Audit current Windows Event Log settings and compare with baseline" diff --git a/config/audit_profiles.json b/config/audit_profiles.json new file mode 100644 index 00000000..4a467042 --- /dev/null +++ b/config/audit_profiles.json @@ -0,0 +1,4944 @@ +{ + "schemaVersion": 1, + "description": "Versioned advanced audit-policy profiles. Mask bits: success=1, failure=2. Omission and Not Configured preserve effective auditpol state; neither removes a GPO.", + "sources": { + "wela": { + "title": "WELA configure source, 2.2.0 development snapshot", + "version": "8ef938f0966e86adc527395f50f907c43e843d1e", + "url": "https://github.com/Yamato-Security/WELA/blob/8ef938f0966e86adc527395f50f907c43e843d1e/WELA.ps1#L1322" + }, + "ms-defaults": { + "title": "Microsoft Advanced Audit Policy Configuration defaults (documentary reference)", + "version": "reviewed-2026-09-18", + "url": "https://learn.microsoft.com/en-us/windows/security/threat-protection/auditing/advanced-security-audit-policy-settings" + }, + "ms-audit": { + "title": "Microsoft Audit Policy Recommendations", + "version": "reviewed-2026-09-18", + "url": "https://learn.microsoft.com/en-us/windows-server/identity/ad-ds/plan/security-best-practices/audit-policy-recommendations" + }, + "ms-sct": { + "title": "Microsoft Security Compliance Toolkit Policy Analyzer files", + "version": "24H2 / 25H2 / Server 2022 / Server 2025 v2602", + "url": "https://www.microsoft.com/en-us/download/details.aspx?id=55319" + }, + "ms-wef": { + "title": "Microsoft WEF intrusion detection, Appendix A minimum audit policy", + "version": "reviewed-2026-09-18", + "url": "https://learn.microsoft.com/en-us/windows/security/operating-system-security/device-management/use-windows-event-forwarding-to-assist-in-intrusion-detection#appendix-a---minimum-recommended-minimum-audit-policy" + }, + "ms-identity": { + "title": "Microsoft Defender for Identity Windows event collection prerequisites", + "version": "reviewed-2026-09-18", + "url": "https://learn.microsoft.com/en-us/defender-for-identity/deploy/configure-windows-event-collection" + }, + "cis-client": { + "title": "CIS Microsoft Windows 11 Enterprise Benchmark", + "version": "4.0.0 (historical; not current CIS)", + "url": "https://rayasec.com/wp-content/uploads/CIS-Benchmark/Microsoft-Windows-Desktop/CIS_Microsoft_Windows_11_Enterprise_Benchmark_v4.0.0.pdf" + }, + "cis-server": { + "title": "CIS Microsoft Windows Server 2022 Benchmark", + "version": "4.0.0 (historical; not current CIS)", + "url": "https://rayasec.com/wp-content/uploads/CIS-Benchmark/Microsoft-Windows-Server/CIS_Microsoft_Windows_Server_2022_Benchmark_v4.0.0.pdf" + }, + "asd": { + "title": "ASD Windows event logging and forwarding (native fallback)", + "version": "2021-10-06", + "url": "https://www.cyber.gov.au/business-government/detecting-responding-to-threats/event-logging/windows-event-logging-and-forwarding" + } + }, + "catalog": [ + { + "id": "Credential Validation", + "guid": "0CCE923F-69AE-11D9-BED3-505054503030", + "category": "Account Logon", + "roles": [ + "Client", + "MemberServer", + "DomainController", + "ADCS" + ], + "prerequisites": "" + }, + { + "id": "Kerberos Authentication Service", + "guid": "0CCE9242-69AE-11D9-BED3-505054503030", + "category": "Account Logon", + "roles": [ + "DomainController" + ], + "prerequisites": "" + }, + { + "id": "Kerberos Service Ticket Operations", + "guid": "0CCE9240-69AE-11D9-BED3-505054503030", + "category": "Account Logon", + "roles": [ + "DomainController" + ], + "prerequisites": "" + }, + { + "id": "Other Account Logon Events", + "guid": "0CCE9241-69AE-11D9-BED3-505054503030", + "category": "Account Logon", + "roles": [ + "DomainController" + ], + "prerequisites": "" + }, + { + "id": "Application Group Management", + "guid": "0CCE9239-69AE-11D9-BED3-505054503030", + "category": "Account Management", + "roles": [ + "Client", + "MemberServer", + "DomainController", + "ADCS" + ], + "prerequisites": "" + }, + { + "id": "Computer Account Management", + "guid": "0CCE9236-69AE-11D9-BED3-505054503030", + "category": "Account Management", + "roles": [ + "DomainController" + ], + "prerequisites": "" + }, + { + "id": "Distribution Group Management", + "guid": "0CCE9238-69AE-11D9-BED3-505054503030", + "category": "Account Management", + "roles": [ + "DomainController" + ], + "prerequisites": "" + }, + { + "id": "Other Account Management Events", + "guid": "0CCE923A-69AE-11D9-BED3-505054503030", + "category": "Account Management", + "roles": [ + "Client", + "MemberServer", + "DomainController", + "ADCS" + ], + "prerequisites": "" + }, + { + "id": "Security Group Management", + "guid": "0CCE9237-69AE-11D9-BED3-505054503030", + "category": "Account Management", + "roles": [ + "Client", + "MemberServer", + "DomainController", + "ADCS" + ], + "prerequisites": "" + }, + { + "id": "User Account Management", + "guid": "0CCE9235-69AE-11D9-BED3-505054503030", + "category": "Account Management", + "roles": [ + "Client", + "MemberServer", + "DomainController", + "ADCS" + ], + "prerequisites": "" + }, + { + "id": "DPAPI Activity", + "guid": "0CCE922D-69AE-11D9-BED3-505054503030", + "category": "Detailed Tracking", + "roles": [ + "Client", + "MemberServer", + "DomainController", + "ADCS" + ], + "prerequisites": "" + }, + { + "id": "Plug and Play Events", + "guid": "0CCE9248-69AE-11D9-BED3-505054503030", + "category": "Detailed Tracking", + "roles": [ + "Client", + "MemberServer", + "DomainController", + "ADCS" + ], + "prerequisites": "" + }, + { + "id": "Process Creation", + "guid": "0CCE922B-69AE-11D9-BED3-505054503030", + "category": "Detailed Tracking", + "roles": [ + "Client", + "MemberServer", + "DomainController", + "ADCS" + ], + "prerequisites": "Command-line payload requires ProcessCreationIncludeCmdLine_Enabled. This profile only configures audit policy." + }, + { + "id": "Process Termination", + "guid": "0CCE922C-69AE-11D9-BED3-505054503030", + "category": "Detailed Tracking", + "roles": [ + "Client", + "MemberServer", + "DomainController", + "ADCS" + ], + "prerequisites": "" + }, + { + "id": "RPC Events", + "guid": "0CCE922E-69AE-11D9-BED3-505054503030", + "category": "Detailed Tracking", + "roles": [ + "Client", + "MemberServer", + "DomainController", + "ADCS" + ], + "prerequisites": "" + }, + { + "id": "Token Right Adjusted Events", + "guid": "0CCE924A-69AE-11D9-BED3-505054503030", + "category": "Detailed Tracking", + "roles": [ + "Client", + "MemberServer", + "DomainController", + "ADCS" + ], + "prerequisites": "" + }, + { + "id": "Detailed Directory Service Replication", + "guid": "0CCE923E-69AE-11D9-BED3-505054503030", + "category": "DS Access", + "roles": [ + "DomainController" + ], + "prerequisites": "" + }, + { + "id": "Directory Service Access", + "guid": "0CCE923B-69AE-11D9-BED3-505054503030", + "category": "DS Access", + "roles": [ + "DomainController" + ], + "prerequisites": "A matching AD object SACL is required. This profile does not create SACLs." + }, + { + "id": "Directory Service Changes", + "guid": "0CCE923C-69AE-11D9-BED3-505054503030", + "category": "DS Access", + "roles": [ + "DomainController" + ], + "prerequisites": "A matching AD object SACL is required. This profile does not create SACLs." + }, + { + "id": "Directory Service Replication", + "guid": "0CCE923D-69AE-11D9-BED3-505054503030", + "category": "DS Access", + "roles": [ + "DomainController" + ], + "prerequisites": "" + }, + { + "id": "Account Lockout", + "guid": "0CCE9217-69AE-11D9-BED3-505054503030", + "category": "Logon/Logoff", + "roles": [ + "Client", + "MemberServer", + "DomainController", + "ADCS" + ], + "prerequisites": "" + }, + { + "id": "Group Membership", + "guid": "0CCE9249-69AE-11D9-BED3-505054503030", + "category": "Logon/Logoff", + "roles": [ + "Client", + "MemberServer", + "DomainController", + "ADCS" + ], + "prerequisites": "" + }, + { + "id": "IPsec Extended Mode", + "guid": "0CCE921A-69AE-11D9-BED3-505054503030", + "category": "Logon/Logoff", + "roles": [ + "Client", + "MemberServer", + "DomainController", + "ADCS" + ], + "prerequisites": "" + }, + { + "id": "IPsec Main Mode", + "guid": "0CCE9218-69AE-11D9-BED3-505054503030", + "category": "Logon/Logoff", + "roles": [ + "Client", + "MemberServer", + "DomainController", + "ADCS" + ], + "prerequisites": "" + }, + { + "id": "IPsec Quick Mode", + "guid": "0CCE9219-69AE-11D9-BED3-505054503030", + "category": "Logon/Logoff", + "roles": [ + "Client", + "MemberServer", + "DomainController", + "ADCS" + ], + "prerequisites": "" + }, + { + "id": "Logoff", + "guid": "0CCE9216-69AE-11D9-BED3-505054503030", + "category": "Logon/Logoff", + "roles": [ + "Client", + "MemberServer", + "DomainController", + "ADCS" + ], + "prerequisites": "" + }, + { + "id": "Logon", + "guid": "0CCE9215-69AE-11D9-BED3-505054503030", + "category": "Logon/Logoff", + "roles": [ + "Client", + "MemberServer", + "DomainController", + "ADCS" + ], + "prerequisites": "" + }, + { + "id": "Network Policy Server", + "guid": "0CCE9243-69AE-11D9-BED3-505054503030", + "category": "Logon/Logoff", + "roles": [ + "Client", + "MemberServer", + "DomainController", + "ADCS" + ], + "prerequisites": "" + }, + { + "id": "Other Logon/Logoff Events", + "guid": "0CCE921C-69AE-11D9-BED3-505054503030", + "category": "Logon/Logoff", + "roles": [ + "Client", + "MemberServer", + "DomainController", + "ADCS" + ], + "prerequisites": "" + }, + { + "id": "Special Logon", + "guid": "0CCE921B-69AE-11D9-BED3-505054503030", + "category": "Logon/Logoff", + "roles": [ + "Client", + "MemberServer", + "DomainController", + "ADCS" + ], + "prerequisites": "" + }, + { + "id": "User/Device Claims", + "guid": "0CCE9247-69AE-11D9-BED3-505054503030", + "category": "Logon/Logoff", + "roles": [ + "Client", + "MemberServer", + "DomainController", + "ADCS" + ], + "prerequisites": "" + }, + { + "id": "Application Generated", + "guid": "0CCE9222-69AE-11D9-BED3-505054503030", + "category": "Object Access", + "roles": [ + "Client", + "MemberServer", + "DomainController", + "ADCS" + ], + "prerequisites": "" + }, + { + "id": "Central Access Policy Staging", + "guid": "0CCE9246-69AE-11D9-BED3-505054503030", + "category": "Object Access", + "roles": [ + "Client", + "MemberServer", + "DomainController", + "ADCS" + ], + "prerequisites": "" + }, + { + "id": "Certification Services", + "guid": "0CCE9221-69AE-11D9-BED3-505054503030", + "category": "Object Access", + "roles": [ + "ADCS" + ], + "prerequisites": "The CA AuditFilter must also select the events. This profile does not configure AuditFilter." + }, + { + "id": "Detailed File Share", + "guid": "0CCE9244-69AE-11D9-BED3-505054503030", + "category": "Object Access", + "roles": [ + "Client", + "MemberServer", + "DomainController", + "ADCS" + ], + "prerequisites": "" + }, + { + "id": "File Share", + "guid": "0CCE9224-69AE-11D9-BED3-505054503030", + "category": "Object Access", + "roles": [ + "Client", + "MemberServer", + "DomainController", + "ADCS" + ], + "prerequisites": "" + }, + { + "id": "File System", + "guid": "0CCE921D-69AE-11D9-BED3-505054503030", + "category": "Object Access", + "roles": [ + "Client", + "MemberServer", + "DomainController", + "ADCS" + ], + "prerequisites": "A matching file/folder SACL is required. This profile does not create SACLs." + }, + { + "id": "Filtering Platform Connection", + "guid": "0CCE9226-69AE-11D9-BED3-505054503030", + "category": "Object Access", + "roles": [ + "Client", + "MemberServer", + "DomainController", + "ADCS" + ], + "prerequisites": "" + }, + { + "id": "Filtering Platform Packet Drop", + "guid": "0CCE9225-69AE-11D9-BED3-505054503030", + "category": "Object Access", + "roles": [ + "Client", + "MemberServer", + "DomainController", + "ADCS" + ], + "prerequisites": "" + }, + { + "id": "Handle Manipulation", + "guid": "0CCE9223-69AE-11D9-BED3-505054503030", + "category": "Object Access", + "roles": [ + "Client", + "MemberServer", + "DomainController", + "ADCS" + ], + "prerequisites": "A matching object SACL is required. This profile does not create SACLs." + }, + { + "id": "Kernel Object", + "guid": "0CCE921F-69AE-11D9-BED3-505054503030", + "category": "Object Access", + "roles": [ + "Client", + "MemberServer", + "DomainController", + "ADCS" + ], + "prerequisites": "A matching kernel-object SACL is required; enabling the subcategory alone does not generate object events." + }, + { + "id": "Other Object Access Events", + "guid": "0CCE9227-69AE-11D9-BED3-505054503030", + "category": "Object Access", + "roles": [ + "Client", + "MemberServer", + "DomainController", + "ADCS" + ], + "prerequisites": "" + }, + { + "id": "Registry", + "guid": "0CCE921E-69AE-11D9-BED3-505054503030", + "category": "Object Access", + "roles": [ + "Client", + "MemberServer", + "DomainController", + "ADCS" + ], + "prerequisites": "A matching registry SACL is required. This profile does not create SACLs." + }, + { + "id": "Removable Storage", + "guid": "0CCE9245-69AE-11D9-BED3-505054503030", + "category": "Object Access", + "roles": [ + "Client", + "MemberServer", + "DomainController", + "ADCS" + ], + "prerequisites": "" + }, + { + "id": "SAM", + "guid": "0CCE9220-69AE-11D9-BED3-505054503030", + "category": "Object Access", + "roles": [ + "Client", + "MemberServer", + "DomainController", + "ADCS" + ], + "prerequisites": "" + }, + { + "id": "Audit Policy Change", + "guid": "0CCE922F-69AE-11D9-BED3-505054503030", + "category": "Policy Change", + "roles": [ + "Client", + "MemberServer", + "DomainController", + "ADCS" + ], + "prerequisites": "" + }, + { + "id": "Authentication Policy Change", + "guid": "0CCE9230-69AE-11D9-BED3-505054503030", + "category": "Policy Change", + "roles": [ + "Client", + "MemberServer", + "DomainController", + "ADCS" + ], + "prerequisites": "" + }, + { + "id": "Authorization Policy Change", + "guid": "0CCE9231-69AE-11D9-BED3-505054503030", + "category": "Policy Change", + "roles": [ + "Client", + "MemberServer", + "DomainController", + "ADCS" + ], + "prerequisites": "" + }, + { + "id": "Filtering Platform Policy Change", + "guid": "0CCE9233-69AE-11D9-BED3-505054503030", + "category": "Policy Change", + "roles": [ + "Client", + "MemberServer", + "DomainController", + "ADCS" + ], + "prerequisites": "" + }, + { + "id": "MPSSVC Rule-Level Policy Change", + "guid": "0CCE9232-69AE-11D9-BED3-505054503030", + "category": "Policy Change", + "roles": [ + "Client", + "MemberServer", + "DomainController", + "ADCS" + ], + "prerequisites": "" + }, + { + "id": "Other Policy Change Events", + "guid": "0CCE9234-69AE-11D9-BED3-505054503030", + "category": "Policy Change", + "roles": [ + "Client", + "MemberServer", + "DomainController", + "ADCS" + ], + "prerequisites": "" + }, + { + "id": "Non Sensitive Privilege Use", + "guid": "0CCE9229-69AE-11D9-BED3-505054503030", + "category": "Privilege Use", + "roles": [ + "Client", + "MemberServer", + "DomainController", + "ADCS" + ], + "prerequisites": "" + }, + { + "id": "Other Privilege Use Events", + "guid": "0CCE922A-69AE-11D9-BED3-505054503030", + "category": "Privilege Use", + "roles": [ + "Client", + "MemberServer", + "DomainController", + "ADCS" + ], + "prerequisites": "" + }, + { + "id": "Sensitive Privilege Use", + "guid": "0CCE9228-69AE-11D9-BED3-505054503030", + "category": "Privilege Use", + "roles": [ + "Client", + "MemberServer", + "DomainController", + "ADCS" + ], + "prerequisites": "" + }, + { + "id": "IPsec Driver", + "guid": "0CCE9213-69AE-11D9-BED3-505054503030", + "category": "System", + "roles": [ + "Client", + "MemberServer", + "DomainController", + "ADCS" + ], + "prerequisites": "" + }, + { + "id": "Other System Events", + "guid": "0CCE9214-69AE-11D9-BED3-505054503030", + "category": "System", + "roles": [ + "Client", + "MemberServer", + "DomainController", + "ADCS" + ], + "prerequisites": "" + }, + { + "id": "Security State Change", + "guid": "0CCE9210-69AE-11D9-BED3-505054503030", + "category": "System", + "roles": [ + "Client", + "MemberServer", + "DomainController", + "ADCS" + ], + "prerequisites": "" + }, + { + "id": "Security System Extension", + "guid": "0CCE9211-69AE-11D9-BED3-505054503030", + "category": "System", + "roles": [ + "Client", + "MemberServer", + "DomainController", + "ADCS" + ], + "prerequisites": "" + }, + { + "id": "System Integrity", + "guid": "0CCE9212-69AE-11D9-BED3-505054503030", + "category": "System", + "roles": [ + "Client", + "MemberServer", + "DomainController", + "ADCS" + ], + "prerequisites": "" + } + ], + "profiles": [ + { + "id": "wela-2.2.0", + "version": "wela-2.2.0", + "sourceIds": [ + "wela" + ], + "omitted": "unchanged", + "scope": "advanced-audit-policy-only", + "appliesTo": [ + { + "roles": [ + "Client" + ], + "minBuild": 22000, + "maxBuild": 26200 + }, + { + "roles": [ + "MemberServer", + "DomainController", + "ADCS" + ], + "minBuild": 20348, + "maxBuild": 26100 + } + ], + "controls": { + "Credential Validation": { + "mode": "exact", + "mask": 3 + }, + "Kerberos Authentication Service": { + "mode": "exact", + "mask": 3 + }, + "Kerberos Service Ticket Operations": { + "mode": "exact", + "mask": 3 + }, + "Computer Account Management": { + "mode": "exact", + "mask": 3 + }, + "Distribution Group Management": { + "mode": "exact", + "mask": 3 + }, + "Other Account Management Events": { + "mode": "exact", + "mask": 3 + }, + "Security Group Management": { + "mode": "exact", + "mask": 3 + }, + "User Account Management": { + "mode": "exact", + "mask": 3 + }, + "Plug and Play Events": { + "mode": "exact", + "mask": 3 + }, + "Process Creation": { + "mode": "exact", + "mask": 3 + }, + "Process Termination": { + "mode": "exact", + "mask": 3 + }, + "RPC Events": { + "mode": "exact", + "mask": 3 + }, + "Directory Service Access": { + "mode": "exact", + "mask": 3 + }, + "Directory Service Changes": { + "mode": "exact", + "mask": 3 + }, + "Account Lockout": { + "mode": "exact", + "mask": 3 + }, + "Logoff": { + "mode": "exact", + "mask": 3 + }, + "Logon": { + "mode": "exact", + "mask": 3 + }, + "Other Logon/Logoff Events": { + "mode": "exact", + "mask": 3 + }, + "Special Logon": { + "mode": "exact", + "mask": 3 + }, + "Certification Services": { + "mode": "exact", + "mask": 3 + }, + "File Share": { + "mode": "exact", + "mask": 3 + }, + "Detailed File Share": { + "mode": "exact", + "mask": 3 + }, + "Filtering Platform Connection": { + "mode": "exact", + "mask": 3 + }, + "Other Object Access Events": { + "mode": "exact", + "mask": 3 + }, + "Removable Storage": { + "mode": "exact", + "mask": 3 + }, + "SAM": { + "mode": "exact", + "mask": 3 + }, + "Audit Policy Change": { + "mode": "exact", + "mask": 3 + }, + "Authentication Policy Change": { + "mode": "exact", + "mask": 3 + }, + "Other Policy Change Events": { + "mode": "exact", + "mask": 3 + }, + "Sensitive Privilege Use": { + "mode": "exact", + "mask": 3 + }, + "Security State Change": { + "mode": "exact", + "mask": 3 + }, + "Security System Extension": { + "mode": "exact", + "mask": 3 + }, + "System Integrity": { + "mode": "exact", + "mask": 3 + }, + "Other System Events": { + "mode": "exact", + "mask": 3 + }, + "File System": { + "mode": "optional", + "mask": 3, + "note": "Opt-in subcategory prerequisite only. Use configure-sacl separately for targeted file/registry SACLs." + }, + "Registry": { + "mode": "optional", + "mask": 3, + "note": "Opt-in subcategory prerequisite only. Use configure-sacl separately for targeted file/registry SACLs." + }, + "Handle Manipulation": { + "mode": "optional", + "mask": 3, + "note": "Opt-in subcategory prerequisite only. Use configure-sacl separately for targeted file/registry SACLs." + } + }, + "roleOverrides": {}, + "note": "Shared replacement for the 34-policy configure list. Policies irrelevant to the selected role are not applied. The three targeted SACL prerequisites are opt-in." + }, + { + "id": "windows-defaults-reviewed-2026-09", + "version": "windows-defaults-reviewed-2026-09", + "sourceIds": [ + "ms-defaults" + ], + "omitted": "unchanged", + "scope": "advanced-audit-policy-only", + "appliesTo": [ + { + "roles": [ + "Client" + ], + "minBuild": 22000, + "maxBuild": 26200 + }, + { + "roles": [ + "MemberServer", + "DomainController", + "ADCS" + ], + "minBuild": 20348, + "maxBuild": 26100 + } + ], + "controls": {}, + "roleOverrides": { + "Client": { + "Credential Validation": { + "mode": "exact", + "mask": 0, + "note": "Microsoft Advanced Audit Policy Configuration: Default on Server editions: Success ." + }, + "Kerberos Authentication Service": { + "mode": "exact", + "mask": 0, + "note": "Microsoft Advanced Audit Policy Configuration: Default on Server editions: Success ." + }, + "Kerberos Service Ticket Operations": { + "mode": "exact", + "mask": 0, + "note": "Microsoft says Not configured; effective state modeled as no auditing, must validate." + }, + "Other Account Logon Events": { + "mode": "exact", + "mask": 0, + "note": "Microsoft Advanced Audit Policy Configuration: Default: No Auditing ." + }, + "Application Group Management": { + "mode": "exact", + "mask": 0, + "note": "Microsoft Advanced Audit Policy Configuration: Default: No Auditing ." + }, + "Computer Account Management": { + "mode": "exact", + "mask": 0, + "note": "Microsoft Advanced Audit Policy Configuration: Default on Server editions: Success ." + }, + "Distribution Group Management": { + "mode": "exact", + "mask": 0, + "note": "Microsoft Advanced Audit Policy Configuration: Default: No Auditing ." + }, + "Other Account Management Events": { + "mode": "exact", + "mask": 0, + "note": "Microsoft Advanced Audit Policy Configuration: Default: No Auditing ." + }, + "Security Group Management": { + "mode": "exact", + "mask": 1, + "note": "Microsoft Advanced Audit Policy Configuration: Default: Success ." + }, + "User Account Management": { + "mode": "exact", + "mask": 1, + "note": "Microsoft Advanced Audit Policy Configuration: Default: Success ." + }, + "DPAPI Activity": { + "mode": "exact", + "mask": 0, + "note": "Modeled no-auditing starting point; verify effective auditpol on target image." + }, + "Plug and Play Events": { + "mode": "exact", + "mask": 0, + "note": "WELA default metadata; cross-checked where Microsoft explicitly states a value; not measured." + }, + "Process Creation": { + "mode": "exact", + "mask": 0, + "note": "WELA default metadata; cross-checked where Microsoft explicitly states a value; not measured." + }, + "Process Termination": { + "mode": "exact", + "mask": 0, + "note": "WELA default metadata; cross-checked where Microsoft explicitly states a value; not measured." + }, + "RPC Events": { + "mode": "exact", + "mask": 0, + "note": "Microsoft Advanced Audit Policy Configuration: Default: No Auditing ." + }, + "Token Right Adjusted Events": { + "mode": "exact", + "mask": 0, + "note": "Microsoft Advanced Audit Policy Configuration: Default: No Auditing ." + }, + "Detailed Directory Service Replication": { + "mode": "exact", + "mask": 0, + "note": "Microsoft Advanced Audit Policy Configuration: Default: No Auditing ." + }, + "Directory Service Access": { + "mode": "exact", + "mask": 0, + "note": "Microsoft Advanced Audit Policy Configuration: Default on Server editions: Success ." + }, + "Directory Service Changes": { + "mode": "exact", + "mask": 0, + "note": "Microsoft Advanced Audit Policy Configuration: Default: No Auditing ." + }, + "Directory Service Replication": { + "mode": "exact", + "mask": 0, + "note": "Microsoft Advanced Audit Policy Configuration: Default: No Auditing ." + }, + "Account Lockout": { + "mode": "exact", + "mask": 1, + "note": "Microsoft Advanced Audit Policy Configuration: Default setting: Success ." + }, + "Group Membership": { + "mode": "exact", + "mask": 0, + "note": "Microsoft Advanced Audit Policy Configuration: Default: No Auditing ." + }, + "IPsec Extended Mode": { + "mode": "exact", + "mask": 0, + "note": "Microsoft Advanced Audit Policy Configuration: Default: No Auditing ." + }, + "IPsec Main Mode": { + "mode": "exact", + "mask": 0, + "note": "Microsoft Advanced Audit Policy Configuration: Default: No Auditing ." + }, + "IPsec Quick Mode": { + "mode": "exact", + "mask": 0, + "note": "Microsoft Advanced Audit Policy Configuration: Default: No Auditing ." + }, + "Logoff": { + "mode": "exact", + "mask": 1, + "note": "Microsoft Advanced Audit Policy Configuration: Default: Success ." + }, + "Logon": { + "mode": "exact", + "mask": 3, + "note": "Microsoft audit-policy-recommendations footnote: since Windows 10 1809, Logon defaults to Success and Failure. Older per-setting prose and WELA metadata disagree." + }, + "Network Policy Server": { + "mode": "exact", + "mask": 3, + "note": "Microsoft Advanced Audit Policy Configuration: Default: Success and Failure ." + }, + "Other Logon/Logoff Events": { + "mode": "exact", + "mask": 0, + "note": "Microsoft Advanced Audit Policy Configuration: Default: No Auditing ." + }, + "Special Logon": { + "mode": "exact", + "mask": 1, + "note": "Microsoft Advanced Audit Policy Configuration: Default: Success" + }, + "User/Device Claims": { + "mode": "exact", + "mask": 0, + "note": "Microsoft Advanced Audit Policy Configuration: Default: No Auditing ." + }, + "Application Generated": { + "mode": "exact", + "mask": 0, + "note": "Modeled no-auditing starting point; verify effective auditpol on target image." + }, + "Central Access Policy Staging": { + "mode": "exact", + "mask": 0, + "note": "Microsoft Advanced Audit Policy Configuration: Default: No Auditing ." + }, + "Certification Services": { + "mode": "exact", + "mask": 0, + "note": "WELA default metadata; cross-checked where Microsoft explicitly states a value; not measured." + }, + "Detailed File Share": { + "mode": "exact", + "mask": 0, + "note": "WELA default metadata; cross-checked where Microsoft explicitly states a value; not measured." + }, + "File Share": { + "mode": "exact", + "mask": 0, + "note": "WELA default metadata; cross-checked where Microsoft explicitly states a value; not measured." + }, + "File System": { + "mode": "exact", + "mask": 0, + "note": "WELA default metadata; cross-checked where Microsoft explicitly states a value; not measured." + }, + "Filtering Platform Connection": { + "mode": "exact", + "mask": 0, + "note": "WELA default metadata; cross-checked where Microsoft explicitly states a value; not measured." + }, + "Filtering Platform Packet Drop": { + "mode": "exact", + "mask": 0, + "note": "WELA default metadata; cross-checked where Microsoft explicitly states a value; not measured." + }, + "Handle Manipulation": { + "mode": "exact", + "mask": 0, + "note": "WELA default metadata; cross-checked where Microsoft explicitly states a value; not measured." + }, + "Kernel Object": { + "mode": "exact", + "mask": 0, + "note": "WELA default metadata; cross-checked where Microsoft explicitly states a value; not measured." + }, + "Other Object Access Events": { + "mode": "exact", + "mask": 0, + "note": "WELA default metadata; cross-checked where Microsoft explicitly states a value; not measured." + }, + "Registry": { + "mode": "exact", + "mask": 0, + "note": "WELA default metadata; cross-checked where Microsoft explicitly states a value; not measured." + }, + "Removable Storage": { + "mode": "exact", + "mask": 0, + "note": "WELA default metadata; cross-checked where Microsoft explicitly states a value; not measured." + }, + "SAM": { + "mode": "exact", + "mask": 0, + "note": "WELA default metadata; cross-checked where Microsoft explicitly states a value; not measured." + }, + "Audit Policy Change": { + "mode": "exact", + "mask": 1, + "note": "Microsoft Advanced Audit Policy Configuration: Default: Success ." + }, + "Authentication Policy Change": { + "mode": "exact", + "mask": 1, + "note": "Microsoft Advanced Audit Policy Configuration: Default: Success ." + }, + "Authorization Policy Change": { + "mode": "exact", + "mask": 0, + "note": "Microsoft Advanced Audit Policy Configuration: Default: No Auditing ." + }, + "Filtering Platform Policy Change": { + "mode": "exact", + "mask": 0, + "note": "Microsoft Advanced Audit Policy Configuration: Default: No Auditing ." + }, + "MPSSVC Rule-Level Policy Change": { + "mode": "exact", + "mask": 0, + "note": "Microsoft Advanced Audit Policy Configuration: Default: No Auditing ." + }, + "Other Policy Change Events": { + "mode": "exact", + "mask": 0, + "note": "Microsoft Advanced Audit Policy Configuration: Default: No Auditing ." + }, + "Non Sensitive Privilege Use": { + "mode": "exact", + "mask": 0, + "note": "WELA default metadata; cross-checked where Microsoft explicitly states a value; not measured." + }, + "Other Privilege Use Events": { + "mode": "exact", + "mask": 0, + "note": "Modeled no-auditing starting point; verify effective auditpol on target image." + }, + "Sensitive Privilege Use": { + "mode": "exact", + "mask": 0, + "note": "WELA default metadata; cross-checked where Microsoft explicitly states a value; not measured." + }, + "IPsec Driver": { + "mode": "exact", + "mask": 0, + "note": "Microsoft Advanced Audit Policy Configuration: Default: No Auditing ." + }, + "Other System Events": { + "mode": "exact", + "mask": 3, + "note": "Microsoft Advanced Audit Policy Configuration: Default: Success and Failure ." + }, + "Security State Change": { + "mode": "exact", + "mask": 1, + "note": "Microsoft Advanced Audit Policy Configuration: Default: Success ." + }, + "Security System Extension": { + "mode": "exact", + "mask": 0, + "note": "Microsoft Advanced Audit Policy Configuration: Default: No Auditing ." + }, + "System Integrity": { + "mode": "exact", + "mask": 3, + "note": "Microsoft Advanced Audit Policy Configuration: Default: Success and Failure ." + } + }, + "MemberServer": { + "Credential Validation": { + "mode": "exact", + "mask": 1, + "note": "Microsoft Advanced Audit Policy Configuration: Default on Server editions: Success ." + }, + "Kerberos Authentication Service": { + "mode": "exact", + "mask": 1, + "note": "Microsoft Advanced Audit Policy Configuration: Default on Server editions: Success ." + }, + "Kerberos Service Ticket Operations": { + "mode": "exact", + "mask": 0, + "note": "Microsoft says Not configured; effective state modeled as no auditing, must validate." + }, + "Other Account Logon Events": { + "mode": "exact", + "mask": 0, + "note": "Microsoft Advanced Audit Policy Configuration: Default: No Auditing ." + }, + "Application Group Management": { + "mode": "exact", + "mask": 0, + "note": "Microsoft Advanced Audit Policy Configuration: Default: No Auditing ." + }, + "Computer Account Management": { + "mode": "exact", + "mask": 1, + "note": "Microsoft Advanced Audit Policy Configuration: Default on Server editions: Success ." + }, + "Distribution Group Management": { + "mode": "exact", + "mask": 0, + "note": "Microsoft Advanced Audit Policy Configuration: Default: No Auditing ." + }, + "Other Account Management Events": { + "mode": "exact", + "mask": 0, + "note": "Microsoft Advanced Audit Policy Configuration: Default: No Auditing ." + }, + "Security Group Management": { + "mode": "exact", + "mask": 1, + "note": "Microsoft Advanced Audit Policy Configuration: Default: Success ." + }, + "User Account Management": { + "mode": "exact", + "mask": 1, + "note": "Microsoft Advanced Audit Policy Configuration: Default: Success ." + }, + "DPAPI Activity": { + "mode": "exact", + "mask": 0, + "note": "Modeled no-auditing starting point; verify effective auditpol on target image." + }, + "Plug and Play Events": { + "mode": "exact", + "mask": 0, + "note": "WELA default metadata; cross-checked where Microsoft explicitly states a value; not measured." + }, + "Process Creation": { + "mode": "exact", + "mask": 0, + "note": "WELA default metadata; cross-checked where Microsoft explicitly states a value; not measured." + }, + "Process Termination": { + "mode": "exact", + "mask": 0, + "note": "WELA default metadata; cross-checked where Microsoft explicitly states a value; not measured." + }, + "RPC Events": { + "mode": "exact", + "mask": 0, + "note": "Microsoft Advanced Audit Policy Configuration: Default: No Auditing ." + }, + "Token Right Adjusted Events": { + "mode": "exact", + "mask": 0, + "note": "Microsoft Advanced Audit Policy Configuration: Default: No Auditing ." + }, + "Detailed Directory Service Replication": { + "mode": "exact", + "mask": 0, + "note": "Microsoft Advanced Audit Policy Configuration: Default: No Auditing ." + }, + "Directory Service Access": { + "mode": "exact", + "mask": 1, + "note": "Microsoft Advanced Audit Policy Configuration: Default on Server editions: Success ." + }, + "Directory Service Changes": { + "mode": "exact", + "mask": 0, + "note": "Microsoft Advanced Audit Policy Configuration: Default: No Auditing ." + }, + "Directory Service Replication": { + "mode": "exact", + "mask": 0, + "note": "Microsoft Advanced Audit Policy Configuration: Default: No Auditing ." + }, + "Account Lockout": { + "mode": "exact", + "mask": 1, + "note": "Microsoft Advanced Audit Policy Configuration: Default setting: Success ." + }, + "Group Membership": { + "mode": "exact", + "mask": 0, + "note": "Microsoft Advanced Audit Policy Configuration: Default: No Auditing ." + }, + "IPsec Extended Mode": { + "mode": "exact", + "mask": 0, + "note": "Microsoft Advanced Audit Policy Configuration: Default: No Auditing ." + }, + "IPsec Main Mode": { + "mode": "exact", + "mask": 0, + "note": "Microsoft Advanced Audit Policy Configuration: Default: No Auditing ." + }, + "IPsec Quick Mode": { + "mode": "exact", + "mask": 0, + "note": "Microsoft Advanced Audit Policy Configuration: Default: No Auditing ." + }, + "Logoff": { + "mode": "exact", + "mask": 1, + "note": "Microsoft Advanced Audit Policy Configuration: Default: Success ." + }, + "Logon": { + "mode": "exact", + "mask": 3, + "note": "Microsoft audit-policy-recommendations footnote: since Windows 10 1809, Logon defaults to Success and Failure. Older per-setting prose and WELA metadata disagree." + }, + "Network Policy Server": { + "mode": "exact", + "mask": 3, + "note": "Microsoft Advanced Audit Policy Configuration: Default: Success and Failure ." + }, + "Other Logon/Logoff Events": { + "mode": "exact", + "mask": 0, + "note": "Microsoft Advanced Audit Policy Configuration: Default: No Auditing ." + }, + "Special Logon": { + "mode": "exact", + "mask": 1, + "note": "Microsoft Advanced Audit Policy Configuration: Default: Success" + }, + "User/Device Claims": { + "mode": "exact", + "mask": 0, + "note": "Microsoft Advanced Audit Policy Configuration: Default: No Auditing ." + }, + "Application Generated": { + "mode": "exact", + "mask": 0, + "note": "Modeled no-auditing starting point; verify effective auditpol on target image." + }, + "Central Access Policy Staging": { + "mode": "exact", + "mask": 0, + "note": "Microsoft Advanced Audit Policy Configuration: Default: No Auditing ." + }, + "Certification Services": { + "mode": "exact", + "mask": 0, + "note": "WELA default metadata; cross-checked where Microsoft explicitly states a value; not measured." + }, + "Detailed File Share": { + "mode": "exact", + "mask": 0, + "note": "WELA default metadata; cross-checked where Microsoft explicitly states a value; not measured." + }, + "File Share": { + "mode": "exact", + "mask": 0, + "note": "WELA default metadata; cross-checked where Microsoft explicitly states a value; not measured." + }, + "File System": { + "mode": "exact", + "mask": 0, + "note": "WELA default metadata; cross-checked where Microsoft explicitly states a value; not measured." + }, + "Filtering Platform Connection": { + "mode": "exact", + "mask": 0, + "note": "WELA default metadata; cross-checked where Microsoft explicitly states a value; not measured." + }, + "Filtering Platform Packet Drop": { + "mode": "exact", + "mask": 0, + "note": "WELA default metadata; cross-checked where Microsoft explicitly states a value; not measured." + }, + "Handle Manipulation": { + "mode": "exact", + "mask": 0, + "note": "WELA default metadata; cross-checked where Microsoft explicitly states a value; not measured." + }, + "Kernel Object": { + "mode": "exact", + "mask": 0, + "note": "WELA default metadata; cross-checked where Microsoft explicitly states a value; not measured." + }, + "Other Object Access Events": { + "mode": "exact", + "mask": 0, + "note": "WELA default metadata; cross-checked where Microsoft explicitly states a value; not measured." + }, + "Registry": { + "mode": "exact", + "mask": 0, + "note": "WELA default metadata; cross-checked where Microsoft explicitly states a value; not measured." + }, + "Removable Storage": { + "mode": "exact", + "mask": 0, + "note": "WELA default metadata; cross-checked where Microsoft explicitly states a value; not measured." + }, + "SAM": { + "mode": "exact", + "mask": 0, + "note": "WELA default metadata; cross-checked where Microsoft explicitly states a value; not measured." + }, + "Audit Policy Change": { + "mode": "exact", + "mask": 1, + "note": "Microsoft Advanced Audit Policy Configuration: Default: Success ." + }, + "Authentication Policy Change": { + "mode": "exact", + "mask": 1, + "note": "Microsoft Advanced Audit Policy Configuration: Default: Success ." + }, + "Authorization Policy Change": { + "mode": "exact", + "mask": 0, + "note": "Microsoft Advanced Audit Policy Configuration: Default: No Auditing ." + }, + "Filtering Platform Policy Change": { + "mode": "exact", + "mask": 0, + "note": "Microsoft Advanced Audit Policy Configuration: Default: No Auditing ." + }, + "MPSSVC Rule-Level Policy Change": { + "mode": "exact", + "mask": 0, + "note": "Microsoft Advanced Audit Policy Configuration: Default: No Auditing ." + }, + "Other Policy Change Events": { + "mode": "exact", + "mask": 0, + "note": "Microsoft Advanced Audit Policy Configuration: Default: No Auditing ." + }, + "Non Sensitive Privilege Use": { + "mode": "exact", + "mask": 0, + "note": "WELA default metadata; cross-checked where Microsoft explicitly states a value; not measured." + }, + "Other Privilege Use Events": { + "mode": "exact", + "mask": 0, + "note": "Modeled no-auditing starting point; verify effective auditpol on target image." + }, + "Sensitive Privilege Use": { + "mode": "exact", + "mask": 0, + "note": "WELA default metadata; cross-checked where Microsoft explicitly states a value; not measured." + }, + "IPsec Driver": { + "mode": "exact", + "mask": 0, + "note": "Microsoft Advanced Audit Policy Configuration: Default: No Auditing ." + }, + "Other System Events": { + "mode": "exact", + "mask": 3, + "note": "Microsoft Advanced Audit Policy Configuration: Default: Success and Failure ." + }, + "Security State Change": { + "mode": "exact", + "mask": 1, + "note": "Microsoft Advanced Audit Policy Configuration: Default: Success ." + }, + "Security System Extension": { + "mode": "exact", + "mask": 0, + "note": "Microsoft Advanced Audit Policy Configuration: Default: No Auditing ." + }, + "System Integrity": { + "mode": "exact", + "mask": 3, + "note": "Microsoft Advanced Audit Policy Configuration: Default: Success and Failure ." + } + }, + "DomainController": { + "Credential Validation": { + "mode": "exact", + "mask": 1, + "note": "Microsoft Advanced Audit Policy Configuration: Default on Server editions: Success ." + }, + "Kerberos Authentication Service": { + "mode": "exact", + "mask": 1, + "note": "Microsoft Advanced Audit Policy Configuration: Default on Server editions: Success ." + }, + "Kerberos Service Ticket Operations": { + "mode": "exact", + "mask": 0, + "note": "Microsoft says Not configured; effective state modeled as no auditing, must validate." + }, + "Other Account Logon Events": { + "mode": "exact", + "mask": 0, + "note": "Microsoft Advanced Audit Policy Configuration: Default: No Auditing ." + }, + "Application Group Management": { + "mode": "exact", + "mask": 0, + "note": "Microsoft Advanced Audit Policy Configuration: Default: No Auditing ." + }, + "Computer Account Management": { + "mode": "exact", + "mask": 1, + "note": "Microsoft Advanced Audit Policy Configuration: Default on Server editions: Success ." + }, + "Distribution Group Management": { + "mode": "exact", + "mask": 0, + "note": "Microsoft Advanced Audit Policy Configuration: Default: No Auditing ." + }, + "Other Account Management Events": { + "mode": "exact", + "mask": 0, + "note": "Microsoft Advanced Audit Policy Configuration: Default: No Auditing ." + }, + "Security Group Management": { + "mode": "exact", + "mask": 1, + "note": "Microsoft Advanced Audit Policy Configuration: Default: Success ." + }, + "User Account Management": { + "mode": "exact", + "mask": 1, + "note": "Microsoft Advanced Audit Policy Configuration: Default: Success ." + }, + "DPAPI Activity": { + "mode": "exact", + "mask": 0, + "note": "Modeled no-auditing starting point; verify effective auditpol on target image." + }, + "Plug and Play Events": { + "mode": "exact", + "mask": 0, + "note": "WELA default metadata; cross-checked where Microsoft explicitly states a value; not measured." + }, + "Process Creation": { + "mode": "exact", + "mask": 0, + "note": "WELA default metadata; cross-checked where Microsoft explicitly states a value; not measured." + }, + "Process Termination": { + "mode": "exact", + "mask": 0, + "note": "WELA default metadata; cross-checked where Microsoft explicitly states a value; not measured." + }, + "RPC Events": { + "mode": "exact", + "mask": 0, + "note": "Microsoft Advanced Audit Policy Configuration: Default: No Auditing ." + }, + "Token Right Adjusted Events": { + "mode": "exact", + "mask": 0, + "note": "Microsoft Advanced Audit Policy Configuration: Default: No Auditing ." + }, + "Detailed Directory Service Replication": { + "mode": "exact", + "mask": 0, + "note": "Microsoft Advanced Audit Policy Configuration: Default: No Auditing ." + }, + "Directory Service Access": { + "mode": "exact", + "mask": 1, + "note": "Microsoft Advanced Audit Policy Configuration: Default on Server editions: Success ." + }, + "Directory Service Changes": { + "mode": "exact", + "mask": 0, + "note": "Microsoft Advanced Audit Policy Configuration: Default: No Auditing ." + }, + "Directory Service Replication": { + "mode": "exact", + "mask": 0, + "note": "Microsoft Advanced Audit Policy Configuration: Default: No Auditing ." + }, + "Account Lockout": { + "mode": "exact", + "mask": 1, + "note": "Microsoft Advanced Audit Policy Configuration: Default setting: Success ." + }, + "Group Membership": { + "mode": "exact", + "mask": 0, + "note": "Microsoft Advanced Audit Policy Configuration: Default: No Auditing ." + }, + "IPsec Extended Mode": { + "mode": "exact", + "mask": 0, + "note": "Microsoft Advanced Audit Policy Configuration: Default: No Auditing ." + }, + "IPsec Main Mode": { + "mode": "exact", + "mask": 0, + "note": "Microsoft Advanced Audit Policy Configuration: Default: No Auditing ." + }, + "IPsec Quick Mode": { + "mode": "exact", + "mask": 0, + "note": "Microsoft Advanced Audit Policy Configuration: Default: No Auditing ." + }, + "Logoff": { + "mode": "exact", + "mask": 1, + "note": "Microsoft Advanced Audit Policy Configuration: Default: Success ." + }, + "Logon": { + "mode": "exact", + "mask": 3, + "note": "Microsoft audit-policy-recommendations footnote: since Windows 10 1809, Logon defaults to Success and Failure. Older per-setting prose and WELA metadata disagree." + }, + "Network Policy Server": { + "mode": "exact", + "mask": 3, + "note": "Microsoft Advanced Audit Policy Configuration: Default: Success and Failure ." + }, + "Other Logon/Logoff Events": { + "mode": "exact", + "mask": 0, + "note": "Microsoft Advanced Audit Policy Configuration: Default: No Auditing ." + }, + "Special Logon": { + "mode": "exact", + "mask": 1, + "note": "Microsoft Advanced Audit Policy Configuration: Default: Success" + }, + "User/Device Claims": { + "mode": "exact", + "mask": 0, + "note": "Microsoft Advanced Audit Policy Configuration: Default: No Auditing ." + }, + "Application Generated": { + "mode": "exact", + "mask": 0, + "note": "Modeled no-auditing starting point; verify effective auditpol on target image." + }, + "Central Access Policy Staging": { + "mode": "exact", + "mask": 0, + "note": "Microsoft Advanced Audit Policy Configuration: Default: No Auditing ." + }, + "Certification Services": { + "mode": "exact", + "mask": 0, + "note": "WELA default metadata; cross-checked where Microsoft explicitly states a value; not measured." + }, + "Detailed File Share": { + "mode": "exact", + "mask": 0, + "note": "WELA default metadata; cross-checked where Microsoft explicitly states a value; not measured." + }, + "File Share": { + "mode": "exact", + "mask": 0, + "note": "WELA default metadata; cross-checked where Microsoft explicitly states a value; not measured." + }, + "File System": { + "mode": "exact", + "mask": 0, + "note": "WELA default metadata; cross-checked where Microsoft explicitly states a value; not measured." + }, + "Filtering Platform Connection": { + "mode": "exact", + "mask": 0, + "note": "WELA default metadata; cross-checked where Microsoft explicitly states a value; not measured." + }, + "Filtering Platform Packet Drop": { + "mode": "exact", + "mask": 0, + "note": "WELA default metadata; cross-checked where Microsoft explicitly states a value; not measured." + }, + "Handle Manipulation": { + "mode": "exact", + "mask": 0, + "note": "WELA default metadata; cross-checked where Microsoft explicitly states a value; not measured." + }, + "Kernel Object": { + "mode": "exact", + "mask": 0, + "note": "WELA default metadata; cross-checked where Microsoft explicitly states a value; not measured." + }, + "Other Object Access Events": { + "mode": "exact", + "mask": 0, + "note": "WELA default metadata; cross-checked where Microsoft explicitly states a value; not measured." + }, + "Registry": { + "mode": "exact", + "mask": 0, + "note": "WELA default metadata; cross-checked where Microsoft explicitly states a value; not measured." + }, + "Removable Storage": { + "mode": "exact", + "mask": 0, + "note": "WELA default metadata; cross-checked where Microsoft explicitly states a value; not measured." + }, + "SAM": { + "mode": "exact", + "mask": 0, + "note": "WELA default metadata; cross-checked where Microsoft explicitly states a value; not measured." + }, + "Audit Policy Change": { + "mode": "exact", + "mask": 1, + "note": "Microsoft Advanced Audit Policy Configuration: Default: Success ." + }, + "Authentication Policy Change": { + "mode": "exact", + "mask": 1, + "note": "Microsoft Advanced Audit Policy Configuration: Default: Success ." + }, + "Authorization Policy Change": { + "mode": "exact", + "mask": 0, + "note": "Microsoft Advanced Audit Policy Configuration: Default: No Auditing ." + }, + "Filtering Platform Policy Change": { + "mode": "exact", + "mask": 0, + "note": "Microsoft Advanced Audit Policy Configuration: Default: No Auditing ." + }, + "MPSSVC Rule-Level Policy Change": { + "mode": "exact", + "mask": 0, + "note": "Microsoft Advanced Audit Policy Configuration: Default: No Auditing ." + }, + "Other Policy Change Events": { + "mode": "exact", + "mask": 0, + "note": "Microsoft Advanced Audit Policy Configuration: Default: No Auditing ." + }, + "Non Sensitive Privilege Use": { + "mode": "exact", + "mask": 0, + "note": "WELA default metadata; cross-checked where Microsoft explicitly states a value; not measured." + }, + "Other Privilege Use Events": { + "mode": "exact", + "mask": 0, + "note": "Modeled no-auditing starting point; verify effective auditpol on target image." + }, + "Sensitive Privilege Use": { + "mode": "exact", + "mask": 0, + "note": "WELA default metadata; cross-checked where Microsoft explicitly states a value; not measured." + }, + "IPsec Driver": { + "mode": "exact", + "mask": 0, + "note": "Microsoft Advanced Audit Policy Configuration: Default: No Auditing ." + }, + "Other System Events": { + "mode": "exact", + "mask": 3, + "note": "Microsoft Advanced Audit Policy Configuration: Default: Success and Failure ." + }, + "Security State Change": { + "mode": "exact", + "mask": 1, + "note": "Microsoft Advanced Audit Policy Configuration: Default: Success ." + }, + "Security System Extension": { + "mode": "exact", + "mask": 0, + "note": "Microsoft Advanced Audit Policy Configuration: Default: No Auditing ." + }, + "System Integrity": { + "mode": "exact", + "mask": 3, + "note": "Microsoft Advanced Audit Policy Configuration: Default: Success and Failure ." + } + }, + "ADCS": { + "Credential Validation": { + "mode": "exact", + "mask": 1, + "note": "Microsoft Advanced Audit Policy Configuration: Default on Server editions: Success ." + }, + "Kerberos Authentication Service": { + "mode": "exact", + "mask": 1, + "note": "Microsoft Advanced Audit Policy Configuration: Default on Server editions: Success ." + }, + "Kerberos Service Ticket Operations": { + "mode": "exact", + "mask": 0, + "note": "Microsoft says Not configured; effective state modeled as no auditing, must validate." + }, + "Other Account Logon Events": { + "mode": "exact", + "mask": 0, + "note": "Microsoft Advanced Audit Policy Configuration: Default: No Auditing ." + }, + "Application Group Management": { + "mode": "exact", + "mask": 0, + "note": "Microsoft Advanced Audit Policy Configuration: Default: No Auditing ." + }, + "Computer Account Management": { + "mode": "exact", + "mask": 1, + "note": "Microsoft Advanced Audit Policy Configuration: Default on Server editions: Success ." + }, + "Distribution Group Management": { + "mode": "exact", + "mask": 0, + "note": "Microsoft Advanced Audit Policy Configuration: Default: No Auditing ." + }, + "Other Account Management Events": { + "mode": "exact", + "mask": 0, + "note": "Microsoft Advanced Audit Policy Configuration: Default: No Auditing ." + }, + "Security Group Management": { + "mode": "exact", + "mask": 1, + "note": "Microsoft Advanced Audit Policy Configuration: Default: Success ." + }, + "User Account Management": { + "mode": "exact", + "mask": 1, + "note": "Microsoft Advanced Audit Policy Configuration: Default: Success ." + }, + "DPAPI Activity": { + "mode": "exact", + "mask": 0, + "note": "Modeled no-auditing starting point; verify effective auditpol on target image." + }, + "Plug and Play Events": { + "mode": "exact", + "mask": 0, + "note": "WELA default metadata; cross-checked where Microsoft explicitly states a value; not measured." + }, + "Process Creation": { + "mode": "exact", + "mask": 0, + "note": "WELA default metadata; cross-checked where Microsoft explicitly states a value; not measured." + }, + "Process Termination": { + "mode": "exact", + "mask": 0, + "note": "WELA default metadata; cross-checked where Microsoft explicitly states a value; not measured." + }, + "RPC Events": { + "mode": "exact", + "mask": 0, + "note": "Microsoft Advanced Audit Policy Configuration: Default: No Auditing ." + }, + "Token Right Adjusted Events": { + "mode": "exact", + "mask": 0, + "note": "Microsoft Advanced Audit Policy Configuration: Default: No Auditing ." + }, + "Detailed Directory Service Replication": { + "mode": "exact", + "mask": 0, + "note": "Microsoft Advanced Audit Policy Configuration: Default: No Auditing ." + }, + "Directory Service Access": { + "mode": "exact", + "mask": 1, + "note": "Microsoft Advanced Audit Policy Configuration: Default on Server editions: Success ." + }, + "Directory Service Changes": { + "mode": "exact", + "mask": 0, + "note": "Microsoft Advanced Audit Policy Configuration: Default: No Auditing ." + }, + "Directory Service Replication": { + "mode": "exact", + "mask": 0, + "note": "Microsoft Advanced Audit Policy Configuration: Default: No Auditing ." + }, + "Account Lockout": { + "mode": "exact", + "mask": 1, + "note": "Microsoft Advanced Audit Policy Configuration: Default setting: Success ." + }, + "Group Membership": { + "mode": "exact", + "mask": 0, + "note": "Microsoft Advanced Audit Policy Configuration: Default: No Auditing ." + }, + "IPsec Extended Mode": { + "mode": "exact", + "mask": 0, + "note": "Microsoft Advanced Audit Policy Configuration: Default: No Auditing ." + }, + "IPsec Main Mode": { + "mode": "exact", + "mask": 0, + "note": "Microsoft Advanced Audit Policy Configuration: Default: No Auditing ." + }, + "IPsec Quick Mode": { + "mode": "exact", + "mask": 0, + "note": "Microsoft Advanced Audit Policy Configuration: Default: No Auditing ." + }, + "Logoff": { + "mode": "exact", + "mask": 1, + "note": "Microsoft Advanced Audit Policy Configuration: Default: Success ." + }, + "Logon": { + "mode": "exact", + "mask": 3, + "note": "Microsoft audit-policy-recommendations footnote: since Windows 10 1809, Logon defaults to Success and Failure. Older per-setting prose and WELA metadata disagree." + }, + "Network Policy Server": { + "mode": "exact", + "mask": 3, + "note": "Microsoft Advanced Audit Policy Configuration: Default: Success and Failure ." + }, + "Other Logon/Logoff Events": { + "mode": "exact", + "mask": 0, + "note": "Microsoft Advanced Audit Policy Configuration: Default: No Auditing ." + }, + "Special Logon": { + "mode": "exact", + "mask": 1, + "note": "Microsoft Advanced Audit Policy Configuration: Default: Success" + }, + "User/Device Claims": { + "mode": "exact", + "mask": 0, + "note": "Microsoft Advanced Audit Policy Configuration: Default: No Auditing ." + }, + "Application Generated": { + "mode": "exact", + "mask": 0, + "note": "Modeled no-auditing starting point; verify effective auditpol on target image." + }, + "Central Access Policy Staging": { + "mode": "exact", + "mask": 0, + "note": "Microsoft Advanced Audit Policy Configuration: Default: No Auditing ." + }, + "Certification Services": { + "mode": "exact", + "mask": 0, + "note": "WELA default metadata; cross-checked where Microsoft explicitly states a value; not measured." + }, + "Detailed File Share": { + "mode": "exact", + "mask": 0, + "note": "WELA default metadata; cross-checked where Microsoft explicitly states a value; not measured." + }, + "File Share": { + "mode": "exact", + "mask": 0, + "note": "WELA default metadata; cross-checked where Microsoft explicitly states a value; not measured." + }, + "File System": { + "mode": "exact", + "mask": 0, + "note": "WELA default metadata; cross-checked where Microsoft explicitly states a value; not measured." + }, + "Filtering Platform Connection": { + "mode": "exact", + "mask": 0, + "note": "WELA default metadata; cross-checked where Microsoft explicitly states a value; not measured." + }, + "Filtering Platform Packet Drop": { + "mode": "exact", + "mask": 0, + "note": "WELA default metadata; cross-checked where Microsoft explicitly states a value; not measured." + }, + "Handle Manipulation": { + "mode": "exact", + "mask": 0, + "note": "WELA default metadata; cross-checked where Microsoft explicitly states a value; not measured." + }, + "Kernel Object": { + "mode": "exact", + "mask": 0, + "note": "WELA default metadata; cross-checked where Microsoft explicitly states a value; not measured." + }, + "Other Object Access Events": { + "mode": "exact", + "mask": 0, + "note": "WELA default metadata; cross-checked where Microsoft explicitly states a value; not measured." + }, + "Registry": { + "mode": "exact", + "mask": 0, + "note": "WELA default metadata; cross-checked where Microsoft explicitly states a value; not measured." + }, + "Removable Storage": { + "mode": "exact", + "mask": 0, + "note": "WELA default metadata; cross-checked where Microsoft explicitly states a value; not measured." + }, + "SAM": { + "mode": "exact", + "mask": 0, + "note": "WELA default metadata; cross-checked where Microsoft explicitly states a value; not measured." + }, + "Audit Policy Change": { + "mode": "exact", + "mask": 1, + "note": "Microsoft Advanced Audit Policy Configuration: Default: Success ." + }, + "Authentication Policy Change": { + "mode": "exact", + "mask": 1, + "note": "Microsoft Advanced Audit Policy Configuration: Default: Success ." + }, + "Authorization Policy Change": { + "mode": "exact", + "mask": 0, + "note": "Microsoft Advanced Audit Policy Configuration: Default: No Auditing ." + }, + "Filtering Platform Policy Change": { + "mode": "exact", + "mask": 0, + "note": "Microsoft Advanced Audit Policy Configuration: Default: No Auditing ." + }, + "MPSSVC Rule-Level Policy Change": { + "mode": "exact", + "mask": 0, + "note": "Microsoft Advanced Audit Policy Configuration: Default: No Auditing ." + }, + "Other Policy Change Events": { + "mode": "exact", + "mask": 0, + "note": "Microsoft Advanced Audit Policy Configuration: Default: No Auditing ." + }, + "Non Sensitive Privilege Use": { + "mode": "exact", + "mask": 0, + "note": "WELA default metadata; cross-checked where Microsoft explicitly states a value; not measured." + }, + "Other Privilege Use Events": { + "mode": "exact", + "mask": 0, + "note": "Modeled no-auditing starting point; verify effective auditpol on target image." + }, + "Sensitive Privilege Use": { + "mode": "exact", + "mask": 0, + "note": "WELA default metadata; cross-checked where Microsoft explicitly states a value; not measured." + }, + "IPsec Driver": { + "mode": "exact", + "mask": 0, + "note": "Microsoft Advanced Audit Policy Configuration: Default: No Auditing ." + }, + "Other System Events": { + "mode": "exact", + "mask": 3, + "note": "Microsoft Advanced Audit Policy Configuration: Default: Success and Failure ." + }, + "Security State Change": { + "mode": "exact", + "mask": 1, + "note": "Microsoft Advanced Audit Policy Configuration: Default: Success ." + }, + "Security System Extension": { + "mode": "exact", + "mask": 0, + "note": "Microsoft Advanced Audit Policy Configuration: Default: No Auditing ." + }, + "System Integrity": { + "mode": "exact", + "mask": 3, + "note": "Microsoft Advanced Audit Policy Configuration: Default: Success and Failure ." + } + } + }, + "referenceOnly": true, + "note": "Documentary model, not an OS reset profile. Unconfigured policy and effective disabled state are not interchangeable; validate actual installation and domain GPO. Applying this reference is prohibited." + }, + { + "id": "microsoft-sct-win11-24h2", + "version": "microsoft-sct-win11-24h2", + "sourceIds": [ + "ms-sct" + ], + "omitted": "unchanged", + "scope": "advanced-audit-policy-only", + "appliesTo": [ + { + "roles": [ + "Client" + ], + "minBuild": 26100, + "maxBuild": 26100 + } + ], + "controls": {}, + "roleOverrides": { + "Client": { + "Credential Validation": { + "mode": "exact", + "mask": 3, + "evidence": "MSFT Windows 11 24H2 - Computer" + }, + "Security Group Management": { + "mode": "exact", + "mask": 1, + "evidence": "MSFT Windows 11 24H2 - Computer" + }, + "User Account Management": { + "mode": "exact", + "mask": 3, + "evidence": "MSFT Windows 11 24H2 - Computer" + }, + "Plug and Play Events": { + "mode": "exact", + "mask": 1, + "evidence": "MSFT Windows 11 24H2 - Computer" + }, + "Process Creation": { + "mode": "exact", + "mask": 1, + "evidence": "MSFT Windows 11 24H2 - Computer" + }, + "Account Lockout": { + "mode": "exact", + "mask": 2, + "evidence": "MSFT Windows 11 24H2 - Computer" + }, + "Group Membership": { + "mode": "exact", + "mask": 1, + "evidence": "MSFT Windows 11 24H2 - Computer" + }, + "Logon": { + "mode": "exact", + "mask": 3, + "evidence": "MSFT Windows 11 24H2 - Computer" + }, + "Other Logon/Logoff Events": { + "mode": "exact", + "mask": 3, + "evidence": "MSFT Windows 11 24H2 - Computer" + }, + "Special Logon": { + "mode": "exact", + "mask": 1, + "evidence": "MSFT Windows 11 24H2 - Computer" + }, + "Detailed File Share": { + "mode": "exact", + "mask": 2, + "evidence": "MSFT Windows 11 24H2 - Computer" + }, + "File Share": { + "mode": "exact", + "mask": 3, + "evidence": "MSFT Windows 11 24H2 - Computer" + }, + "Other Object Access Events": { + "mode": "exact", + "mask": 3, + "evidence": "MSFT Windows 11 24H2 - Computer" + }, + "Removable Storage": { + "mode": "exact", + "mask": 3, + "evidence": "MSFT Windows 11 24H2 - Computer" + }, + "Audit Policy Change": { + "mode": "exact", + "mask": 1, + "evidence": "MSFT Windows 11 24H2 - Computer" + }, + "Authentication Policy Change": { + "mode": "exact", + "mask": 1, + "evidence": "MSFT Windows 11 24H2 - Computer" + }, + "MPSSVC Rule-Level Policy Change": { + "mode": "exact", + "mask": 3, + "evidence": "MSFT Windows 11 24H2 - Computer" + }, + "Other Policy Change Events": { + "mode": "exact", + "mask": 2, + "evidence": "MSFT Windows 11 24H2 - Computer" + }, + "Sensitive Privilege Use": { + "mode": "exact", + "mask": 1, + "evidence": "MSFT Windows 11 24H2 - Computer" + }, + "Other System Events": { + "mode": "exact", + "mask": 3, + "evidence": "MSFT Windows 11 24H2 - Computer" + }, + "Security State Change": { + "mode": "exact", + "mask": 1, + "evidence": "MSFT Windows 11 24H2 - Computer" + }, + "Security System Extension": { + "mode": "exact", + "mask": 1, + "evidence": "MSFT Windows 11 24H2 - Computer" + }, + "System Integrity": { + "mode": "exact", + "mask": 3, + "evidence": "MSFT Windows 11 24H2 - Computer" + } + } + } + }, + { + "id": "microsoft-sct-win11-25h2", + "version": "microsoft-sct-win11-25h2", + "sourceIds": [ + "ms-sct" + ], + "omitted": "unchanged", + "scope": "advanced-audit-policy-only", + "appliesTo": [ + { + "roles": [ + "Client" + ], + "minBuild": 26200, + "maxBuild": 26200 + } + ], + "controls": {}, + "roleOverrides": { + "Client": { + "Credential Validation": { + "mode": "exact", + "mask": 3, + "evidence": "MSFT Windows 11 25H2 - Computer" + }, + "Security Group Management": { + "mode": "exact", + "mask": 1, + "evidence": "MSFT Windows 11 25H2 - Computer" + }, + "User Account Management": { + "mode": "exact", + "mask": 3, + "evidence": "MSFT Windows 11 25H2 - Computer" + }, + "Plug and Play Events": { + "mode": "exact", + "mask": 1, + "evidence": "MSFT Windows 11 25H2 - Computer" + }, + "Process Creation": { + "mode": "exact", + "mask": 1, + "evidence": "MSFT Windows 11 25H2 - Computer" + }, + "Account Lockout": { + "mode": "exact", + "mask": 2, + "evidence": "MSFT Windows 11 25H2 - Computer" + }, + "Group Membership": { + "mode": "exact", + "mask": 1, + "evidence": "MSFT Windows 11 25H2 - Computer" + }, + "Logon": { + "mode": "exact", + "mask": 3, + "evidence": "MSFT Windows 11 25H2 - Computer" + }, + "Other Logon/Logoff Events": { + "mode": "exact", + "mask": 3, + "evidence": "MSFT Windows 11 25H2 - Computer" + }, + "Special Logon": { + "mode": "exact", + "mask": 1, + "evidence": "MSFT Windows 11 25H2 - Computer" + }, + "Detailed File Share": { + "mode": "exact", + "mask": 2, + "evidence": "MSFT Windows 11 25H2 - Computer" + }, + "File Share": { + "mode": "exact", + "mask": 3, + "evidence": "MSFT Windows 11 25H2 - Computer" + }, + "Other Object Access Events": { + "mode": "exact", + "mask": 3, + "evidence": "MSFT Windows 11 25H2 - Computer" + }, + "Removable Storage": { + "mode": "exact", + "mask": 3, + "evidence": "MSFT Windows 11 25H2 - Computer" + }, + "Audit Policy Change": { + "mode": "exact", + "mask": 1, + "evidence": "MSFT Windows 11 25H2 - Computer" + }, + "Authentication Policy Change": { + "mode": "exact", + "mask": 1, + "evidence": "MSFT Windows 11 25H2 - Computer" + }, + "MPSSVC Rule-Level Policy Change": { + "mode": "exact", + "mask": 3, + "evidence": "MSFT Windows 11 25H2 - Computer" + }, + "Other Policy Change Events": { + "mode": "exact", + "mask": 2, + "evidence": "MSFT Windows 11 25H2 - Computer" + }, + "Sensitive Privilege Use": { + "mode": "exact", + "mask": 1, + "evidence": "MSFT Windows 11 25H2 - Computer" + }, + "Other System Events": { + "mode": "exact", + "mask": 3, + "evidence": "MSFT Windows 11 25H2 - Computer" + }, + "Security State Change": { + "mode": "exact", + "mask": 1, + "evidence": "MSFT Windows 11 25H2 - Computer" + }, + "Security System Extension": { + "mode": "exact", + "mask": 1, + "evidence": "MSFT Windows 11 25H2 - Computer" + }, + "System Integrity": { + "mode": "exact", + "mask": 3, + "evidence": "MSFT Windows 11 25H2 - Computer" + } + } + } + }, + { + "id": "microsoft-sct-server2022", + "version": "microsoft-sct-server2022", + "sourceIds": [ + "ms-sct" + ], + "omitted": "unchanged", + "scope": "advanced-audit-policy-only", + "appliesTo": [ + { + "roles": [ + "MemberServer", + "DomainController", + "ADCS" + ], + "minBuild": 20348, + "maxBuild": 20348 + } + ], + "controls": {}, + "roleOverrides": { + "MemberServer": { + "Credential Validation": { + "mode": "exact", + "mask": 3, + "evidence": "MSFT Windows Server 2022 - Member Server" + }, + "Security Group Management": { + "mode": "exact", + "mask": 1, + "evidence": "MSFT Windows Server 2022 - Member Server" + }, + "User Account Management": { + "mode": "exact", + "mask": 3, + "evidence": "MSFT Windows Server 2022 - Member Server" + }, + "Plug and Play Events": { + "mode": "exact", + "mask": 1, + "evidence": "MSFT Windows Server 2022 - Member Server" + }, + "Process Creation": { + "mode": "exact", + "mask": 1, + "evidence": "MSFT Windows Server 2022 - Member Server" + }, + "Account Lockout": { + "mode": "exact", + "mask": 2, + "evidence": "MSFT Windows Server 2022 - Member Server" + }, + "Group Membership": { + "mode": "exact", + "mask": 1, + "evidence": "MSFT Windows Server 2022 - Member Server" + }, + "Logon": { + "mode": "exact", + "mask": 3, + "evidence": "MSFT Windows Server 2022 - Member Server" + }, + "Other Logon/Logoff Events": { + "mode": "exact", + "mask": 3, + "evidence": "MSFT Windows Server 2022 - Member Server" + }, + "Special Logon": { + "mode": "exact", + "mask": 1, + "evidence": "MSFT Windows Server 2022 - Member Server" + }, + "Detailed File Share": { + "mode": "exact", + "mask": 2, + "evidence": "MSFT Windows Server 2022 - Member Server" + }, + "File Share": { + "mode": "exact", + "mask": 3, + "evidence": "MSFT Windows Server 2022 - Member Server" + }, + "Other Object Access Events": { + "mode": "exact", + "mask": 3, + "evidence": "MSFT Windows Server 2022 - Member Server" + }, + "Removable Storage": { + "mode": "exact", + "mask": 3, + "evidence": "MSFT Windows Server 2022 - Member Server" + }, + "Audit Policy Change": { + "mode": "exact", + "mask": 1, + "evidence": "MSFT Windows Server 2022 - Member Server" + }, + "Authentication Policy Change": { + "mode": "exact", + "mask": 1, + "evidence": "MSFT Windows Server 2022 - Member Server" + }, + "MPSSVC Rule-Level Policy Change": { + "mode": "exact", + "mask": 3, + "evidence": "MSFT Windows Server 2022 - Member Server" + }, + "Other Policy Change Events": { + "mode": "exact", + "mask": 2, + "evidence": "MSFT Windows Server 2022 - Member Server" + }, + "Sensitive Privilege Use": { + "mode": "exact", + "mask": 3, + "evidence": "MSFT Windows Server 2022 - Member Server" + }, + "Other System Events": { + "mode": "exact", + "mask": 3, + "evidence": "MSFT Windows Server 2022 - Member Server" + }, + "Security State Change": { + "mode": "exact", + "mask": 1, + "evidence": "MSFT Windows Server 2022 - Member Server" + }, + "Security System Extension": { + "mode": "exact", + "mask": 1, + "evidence": "MSFT Windows Server 2022 - Member Server" + }, + "System Integrity": { + "mode": "exact", + "mask": 3, + "evidence": "MSFT Windows Server 2022 - Member Server" + } + }, + "DomainController": { + "Credential Validation": { + "mode": "exact", + "mask": 2, + "evidence": "MSFT Windows Server 2022 - Domain Controller" + }, + "Kerberos Authentication Service": { + "mode": "exact", + "mask": 3, + "evidence": "MSFT Windows Server 2022 - Domain Controller" + }, + "Kerberos Service Ticket Operations": { + "mode": "exact", + "mask": 2, + "evidence": "MSFT Windows Server 2022 - Domain Controller" + }, + "Computer Account Management": { + "mode": "exact", + "mask": 1, + "evidence": "MSFT Windows Server 2022 - Domain Controller" + }, + "Other Account Management Events": { + "mode": "exact", + "mask": 1, + "evidence": "MSFT Windows Server 2022 - Domain Controller" + }, + "Security Group Management": { + "mode": "exact", + "mask": 1, + "evidence": "MSFT Windows Server 2022 - Domain Controller" + }, + "User Account Management": { + "mode": "exact", + "mask": 3, + "evidence": "MSFT Windows Server 2022 - Domain Controller" + }, + "Plug and Play Events": { + "mode": "exact", + "mask": 1, + "evidence": "MSFT Windows Server 2022 - Domain Controller" + }, + "Process Creation": { + "mode": "exact", + "mask": 1, + "evidence": "MSFT Windows Server 2022 - Domain Controller" + }, + "Directory Service Access": { + "mode": "exact", + "mask": 2, + "evidence": "MSFT Windows Server 2022 - Domain Controller" + }, + "Directory Service Changes": { + "mode": "exact", + "mask": 1, + "evidence": "MSFT Windows Server 2022 - Domain Controller" + }, + "Account Lockout": { + "mode": "exact", + "mask": 2, + "evidence": "MSFT Windows Server 2022 - Domain Controller" + }, + "Group Membership": { + "mode": "exact", + "mask": 1, + "evidence": "MSFT Windows Server 2022 - Domain Controller" + }, + "Logon": { + "mode": "exact", + "mask": 3, + "evidence": "MSFT Windows Server 2022 - Domain Controller" + }, + "Other Logon/Logoff Events": { + "mode": "exact", + "mask": 3, + "evidence": "MSFT Windows Server 2022 - Domain Controller" + }, + "Special Logon": { + "mode": "exact", + "mask": 1, + "evidence": "MSFT Windows Server 2022 - Domain Controller" + }, + "Detailed File Share": { + "mode": "exact", + "mask": 2, + "evidence": "MSFT Windows Server 2022 - Domain Controller" + }, + "File Share": { + "mode": "exact", + "mask": 3, + "evidence": "MSFT Windows Server 2022 - Domain Controller" + }, + "Other Object Access Events": { + "mode": "exact", + "mask": 3, + "evidence": "MSFT Windows Server 2022 - Domain Controller" + }, + "Removable Storage": { + "mode": "exact", + "mask": 3, + "evidence": "MSFT Windows Server 2022 - Domain Controller" + }, + "Audit Policy Change": { + "mode": "exact", + "mask": 1, + "evidence": "MSFT Windows Server 2022 - Domain Controller" + }, + "Authentication Policy Change": { + "mode": "exact", + "mask": 1, + "evidence": "MSFT Windows Server 2022 - Domain Controller" + }, + "MPSSVC Rule-Level Policy Change": { + "mode": "exact", + "mask": 3, + "evidence": "MSFT Windows Server 2022 - Domain Controller" + }, + "Other Policy Change Events": { + "mode": "exact", + "mask": 2, + "evidence": "MSFT Windows Server 2022 - Domain Controller" + }, + "Sensitive Privilege Use": { + "mode": "exact", + "mask": 3, + "evidence": "MSFT Windows Server 2022 - Domain Controller" + }, + "Other System Events": { + "mode": "exact", + "mask": 3, + "evidence": "MSFT Windows Server 2022 - Domain Controller" + }, + "Security State Change": { + "mode": "exact", + "mask": 1, + "evidence": "MSFT Windows Server 2022 - Domain Controller" + }, + "Security System Extension": { + "mode": "exact", + "mask": 1, + "evidence": "MSFT Windows Server 2022 - Domain Controller" + }, + "System Integrity": { + "mode": "exact", + "mask": 3, + "evidence": "MSFT Windows Server 2022 - Domain Controller" + } + }, + "ADCS": { + "Credential Validation": { + "mode": "exact", + "mask": 3, + "evidence": "MSFT Windows Server 2022 - Member Server" + }, + "Security Group Management": { + "mode": "exact", + "mask": 1, + "evidence": "MSFT Windows Server 2022 - Member Server" + }, + "User Account Management": { + "mode": "exact", + "mask": 3, + "evidence": "MSFT Windows Server 2022 - Member Server" + }, + "Plug and Play Events": { + "mode": "exact", + "mask": 1, + "evidence": "MSFT Windows Server 2022 - Member Server" + }, + "Process Creation": { + "mode": "exact", + "mask": 1, + "evidence": "MSFT Windows Server 2022 - Member Server" + }, + "Account Lockout": { + "mode": "exact", + "mask": 2, + "evidence": "MSFT Windows Server 2022 - Member Server" + }, + "Group Membership": { + "mode": "exact", + "mask": 1, + "evidence": "MSFT Windows Server 2022 - Member Server" + }, + "Logon": { + "mode": "exact", + "mask": 3, + "evidence": "MSFT Windows Server 2022 - Member Server" + }, + "Other Logon/Logoff Events": { + "mode": "exact", + "mask": 3, + "evidence": "MSFT Windows Server 2022 - Member Server" + }, + "Special Logon": { + "mode": "exact", + "mask": 1, + "evidence": "MSFT Windows Server 2022 - Member Server" + }, + "Detailed File Share": { + "mode": "exact", + "mask": 2, + "evidence": "MSFT Windows Server 2022 - Member Server" + }, + "File Share": { + "mode": "exact", + "mask": 3, + "evidence": "MSFT Windows Server 2022 - Member Server" + }, + "Other Object Access Events": { + "mode": "exact", + "mask": 3, + "evidence": "MSFT Windows Server 2022 - Member Server" + }, + "Removable Storage": { + "mode": "exact", + "mask": 3, + "evidence": "MSFT Windows Server 2022 - Member Server" + }, + "Audit Policy Change": { + "mode": "exact", + "mask": 1, + "evidence": "MSFT Windows Server 2022 - Member Server" + }, + "Authentication Policy Change": { + "mode": "exact", + "mask": 1, + "evidence": "MSFT Windows Server 2022 - Member Server" + }, + "MPSSVC Rule-Level Policy Change": { + "mode": "exact", + "mask": 3, + "evidence": "MSFT Windows Server 2022 - Member Server" + }, + "Other Policy Change Events": { + "mode": "exact", + "mask": 2, + "evidence": "MSFT Windows Server 2022 - Member Server" + }, + "Sensitive Privilege Use": { + "mode": "exact", + "mask": 3, + "evidence": "MSFT Windows Server 2022 - Member Server" + }, + "Other System Events": { + "mode": "exact", + "mask": 3, + "evidence": "MSFT Windows Server 2022 - Member Server" + }, + "Security State Change": { + "mode": "exact", + "mask": 1, + "evidence": "MSFT Windows Server 2022 - Member Server" + }, + "Security System Extension": { + "mode": "exact", + "mask": 1, + "evidence": "MSFT Windows Server 2022 - Member Server" + }, + "System Integrity": { + "mode": "exact", + "mask": 3, + "evidence": "MSFT Windows Server 2022 - Member Server" + } + } + } + }, + { + "id": "microsoft-sct-server2025-2602", + "version": "microsoft-sct-server2025-2602", + "sourceIds": [ + "ms-sct" + ], + "omitted": "unchanged", + "scope": "advanced-audit-policy-only", + "appliesTo": [ + { + "roles": [ + "MemberServer", + "DomainController", + "ADCS" + ], + "minBuild": 26100, + "maxBuild": 26100 + } + ], + "controls": {}, + "roleOverrides": { + "MemberServer": { + "Credential Validation": { + "mode": "exact", + "mask": 3, + "evidence": "MSFT Windows Server 2025 v2602 - Member Server" + }, + "Security Group Management": { + "mode": "exact", + "mask": 1, + "evidence": "MSFT Windows Server 2025 v2602 - Member Server" + }, + "User Account Management": { + "mode": "exact", + "mask": 3, + "evidence": "MSFT Windows Server 2025 v2602 - Member Server" + }, + "Plug and Play Events": { + "mode": "exact", + "mask": 1, + "evidence": "MSFT Windows Server 2025 v2602 - Member Server" + }, + "Process Creation": { + "mode": "exact", + "mask": 1, + "evidence": "MSFT Windows Server 2025 v2602 - Member Server" + }, + "Account Lockout": { + "mode": "exact", + "mask": 2, + "evidence": "MSFT Windows Server 2025 v2602 - Member Server" + }, + "Group Membership": { + "mode": "exact", + "mask": 1, + "evidence": "MSFT Windows Server 2025 v2602 - Member Server" + }, + "Logon": { + "mode": "exact", + "mask": 3, + "evidence": "MSFT Windows Server 2025 v2602 - Member Server" + }, + "Other Logon/Logoff Events": { + "mode": "exact", + "mask": 3, + "evidence": "MSFT Windows Server 2025 v2602 - Member Server" + }, + "Special Logon": { + "mode": "exact", + "mask": 1, + "evidence": "MSFT Windows Server 2025 v2602 - Member Server" + }, + "Detailed File Share": { + "mode": "exact", + "mask": 2, + "evidence": "MSFT Windows Server 2025 v2602 - Member Server" + }, + "File Share": { + "mode": "exact", + "mask": 3, + "evidence": "MSFT Windows Server 2025 v2602 - Member Server" + }, + "Other Object Access Events": { + "mode": "exact", + "mask": 3, + "evidence": "MSFT Windows Server 2025 v2602 - Member Server" + }, + "Removable Storage": { + "mode": "exact", + "mask": 3, + "evidence": "MSFT Windows Server 2025 v2602 - Member Server" + }, + "Audit Policy Change": { + "mode": "exact", + "mask": 3, + "evidence": "MSFT Windows Server 2025 v2602 - Member Server" + }, + "Authentication Policy Change": { + "mode": "exact", + "mask": 1, + "evidence": "MSFT Windows Server 2025 v2602 - Member Server" + }, + "Authorization Policy Change": { + "mode": "exact", + "mask": 1, + "evidence": "MSFT Windows Server 2025 v2602 - Member Server" + }, + "MPSSVC Rule-Level Policy Change": { + "mode": "exact", + "mask": 3, + "evidence": "MSFT Windows Server 2025 v2602 - Member Server" + }, + "Other Policy Change Events": { + "mode": "exact", + "mask": 2, + "evidence": "MSFT Windows Server 2025 v2602 - Member Server" + }, + "Sensitive Privilege Use": { + "mode": "exact", + "mask": 3, + "evidence": "MSFT Windows Server 2025 v2602 - Member Server" + }, + "Other System Events": { + "mode": "exact", + "mask": 3, + "evidence": "MSFT Windows Server 2025 v2602 - Member Server" + }, + "Security State Change": { + "mode": "exact", + "mask": 1, + "evidence": "MSFT Windows Server 2025 v2602 - Member Server" + }, + "Security System Extension": { + "mode": "exact", + "mask": 1, + "evidence": "MSFT Windows Server 2025 v2602 - Member Server" + }, + "System Integrity": { + "mode": "exact", + "mask": 3, + "evidence": "MSFT Windows Server 2025 v2602 - Member Server" + } + }, + "DomainController": { + "Credential Validation": { + "mode": "exact", + "mask": 3, + "evidence": "MSFT Windows Server 2025 v2602 - Domain Controller" + }, + "Kerberos Authentication Service": { + "mode": "exact", + "mask": 3, + "evidence": "MSFT Windows Server 2025 v2602 - Domain Controller" + }, + "Kerberos Service Ticket Operations": { + "mode": "exact", + "mask": 2, + "evidence": "MSFT Windows Server 2025 v2602 - Domain Controller" + }, + "Computer Account Management": { + "mode": "exact", + "mask": 1, + "evidence": "MSFT Windows Server 2025 v2602 - Domain Controller" + }, + "Other Account Management Events": { + "mode": "exact", + "mask": 1, + "evidence": "MSFT Windows Server 2025 v2602 - Domain Controller" + }, + "Security Group Management": { + "mode": "exact", + "mask": 1, + "evidence": "MSFT Windows Server 2025 v2602 - Domain Controller" + }, + "User Account Management": { + "mode": "exact", + "mask": 3, + "evidence": "MSFT Windows Server 2025 v2602 - Domain Controller" + }, + "Plug and Play Events": { + "mode": "exact", + "mask": 1, + "evidence": "MSFT Windows Server 2025 v2602 - Domain Controller" + }, + "Process Creation": { + "mode": "exact", + "mask": 1, + "evidence": "MSFT Windows Server 2025 v2602 - Domain Controller" + }, + "Directory Service Access": { + "mode": "exact", + "mask": 2, + "evidence": "MSFT Windows Server 2025 v2602 - Domain Controller" + }, + "Directory Service Changes": { + "mode": "exact", + "mask": 1, + "evidence": "MSFT Windows Server 2025 v2602 - Domain Controller" + }, + "Account Lockout": { + "mode": "exact", + "mask": 2, + "evidence": "MSFT Windows Server 2025 v2602 - Domain Controller" + }, + "Group Membership": { + "mode": "exact", + "mask": 1, + "evidence": "MSFT Windows Server 2025 v2602 - Domain Controller" + }, + "Logon": { + "mode": "exact", + "mask": 3, + "evidence": "MSFT Windows Server 2025 v2602 - Domain Controller" + }, + "Other Logon/Logoff Events": { + "mode": "exact", + "mask": 3, + "evidence": "MSFT Windows Server 2025 v2602 - Domain Controller" + }, + "Special Logon": { + "mode": "exact", + "mask": 1, + "evidence": "MSFT Windows Server 2025 v2602 - Domain Controller" + }, + "Detailed File Share": { + "mode": "exact", + "mask": 2, + "evidence": "MSFT Windows Server 2025 v2602 - Domain Controller" + }, + "File Share": { + "mode": "exact", + "mask": 3, + "evidence": "MSFT Windows Server 2025 v2602 - Domain Controller" + }, + "Other Object Access Events": { + "mode": "exact", + "mask": 3, + "evidence": "MSFT Windows Server 2025 v2602 - Domain Controller" + }, + "Removable Storage": { + "mode": "exact", + "mask": 3, + "evidence": "MSFT Windows Server 2025 v2602 - Domain Controller" + }, + "Audit Policy Change": { + "mode": "exact", + "mask": 3, + "evidence": "MSFT Windows Server 2025 v2602 - Domain Controller" + }, + "Authentication Policy Change": { + "mode": "exact", + "mask": 1, + "evidence": "MSFT Windows Server 2025 v2602 - Domain Controller" + }, + "Authorization Policy Change": { + "mode": "exact", + "mask": 1, + "evidence": "MSFT Windows Server 2025 v2602 - Domain Controller" + }, + "MPSSVC Rule-Level Policy Change": { + "mode": "exact", + "mask": 3, + "evidence": "MSFT Windows Server 2025 v2602 - Domain Controller" + }, + "Other Policy Change Events": { + "mode": "exact", + "mask": 2, + "evidence": "MSFT Windows Server 2025 v2602 - Domain Controller" + }, + "Sensitive Privilege Use": { + "mode": "exact", + "mask": 3, + "evidence": "MSFT Windows Server 2025 v2602 - Domain Controller" + }, + "Other System Events": { + "mode": "exact", + "mask": 3, + "evidence": "MSFT Windows Server 2025 v2602 - Domain Controller" + }, + "Security State Change": { + "mode": "exact", + "mask": 1, + "evidence": "MSFT Windows Server 2025 v2602 - Domain Controller" + }, + "Security System Extension": { + "mode": "exact", + "mask": 1, + "evidence": "MSFT Windows Server 2025 v2602 - Domain Controller" + }, + "System Integrity": { + "mode": "exact", + "mask": 3, + "evidence": "MSFT Windows Server 2025 v2602 - Domain Controller" + } + }, + "ADCS": { + "Credential Validation": { + "mode": "exact", + "mask": 3, + "evidence": "MSFT Windows Server 2025 v2602 - Member Server" + }, + "Security Group Management": { + "mode": "exact", + "mask": 1, + "evidence": "MSFT Windows Server 2025 v2602 - Member Server" + }, + "User Account Management": { + "mode": "exact", + "mask": 3, + "evidence": "MSFT Windows Server 2025 v2602 - Member Server" + }, + "Plug and Play Events": { + "mode": "exact", + "mask": 1, + "evidence": "MSFT Windows Server 2025 v2602 - Member Server" + }, + "Process Creation": { + "mode": "exact", + "mask": 1, + "evidence": "MSFT Windows Server 2025 v2602 - Member Server" + }, + "Account Lockout": { + "mode": "exact", + "mask": 2, + "evidence": "MSFT Windows Server 2025 v2602 - Member Server" + }, + "Group Membership": { + "mode": "exact", + "mask": 1, + "evidence": "MSFT Windows Server 2025 v2602 - Member Server" + }, + "Logon": { + "mode": "exact", + "mask": 3, + "evidence": "MSFT Windows Server 2025 v2602 - Member Server" + }, + "Other Logon/Logoff Events": { + "mode": "exact", + "mask": 3, + "evidence": "MSFT Windows Server 2025 v2602 - Member Server" + }, + "Special Logon": { + "mode": "exact", + "mask": 1, + "evidence": "MSFT Windows Server 2025 v2602 - Member Server" + }, + "Detailed File Share": { + "mode": "exact", + "mask": 2, + "evidence": "MSFT Windows Server 2025 v2602 - Member Server" + }, + "File Share": { + "mode": "exact", + "mask": 3, + "evidence": "MSFT Windows Server 2025 v2602 - Member Server" + }, + "Other Object Access Events": { + "mode": "exact", + "mask": 3, + "evidence": "MSFT Windows Server 2025 v2602 - Member Server" + }, + "Removable Storage": { + "mode": "exact", + "mask": 3, + "evidence": "MSFT Windows Server 2025 v2602 - Member Server" + }, + "Audit Policy Change": { + "mode": "exact", + "mask": 3, + "evidence": "MSFT Windows Server 2025 v2602 - Member Server" + }, + "Authentication Policy Change": { + "mode": "exact", + "mask": 1, + "evidence": "MSFT Windows Server 2025 v2602 - Member Server" + }, + "Authorization Policy Change": { + "mode": "exact", + "mask": 1, + "evidence": "MSFT Windows Server 2025 v2602 - Member Server" + }, + "MPSSVC Rule-Level Policy Change": { + "mode": "exact", + "mask": 3, + "evidence": "MSFT Windows Server 2025 v2602 - Member Server" + }, + "Other Policy Change Events": { + "mode": "exact", + "mask": 2, + "evidence": "MSFT Windows Server 2025 v2602 - Member Server" + }, + "Sensitive Privilege Use": { + "mode": "exact", + "mask": 3, + "evidence": "MSFT Windows Server 2025 v2602 - Member Server" + }, + "Other System Events": { + "mode": "exact", + "mask": 3, + "evidence": "MSFT Windows Server 2025 v2602 - Member Server" + }, + "Security State Change": { + "mode": "exact", + "mask": 1, + "evidence": "MSFT Windows Server 2025 v2602 - Member Server" + }, + "Security System Extension": { + "mode": "exact", + "mask": 1, + "evidence": "MSFT Windows Server 2025 v2602 - Member Server" + }, + "System Integrity": { + "mode": "exact", + "mask": 3, + "evidence": "MSFT Windows Server 2025 v2602 - Member Server" + } + } + } + }, + { + "id": "microsoft-stronger-reviewed-2026-09", + "version": "microsoft-stronger-reviewed-2026-09", + "sourceIds": [ + "ms-audit" + ], + "omitted": "unchanged", + "scope": "advanced-audit-policy-only", + "appliesTo": [ + { + "roles": [ + "Client" + ], + "minBuild": 22000, + "maxBuild": 26200 + }, + { + "roles": [ + "MemberServer", + "DomainController", + "ADCS" + ], + "minBuild": 20348, + "maxBuild": 26100 + } + ], + "controls": { + "Credential Validation": { + "mode": "minimum", + "mask": 3 + }, + "Kerberos Authentication Service": { + "mode": "minimum", + "mask": 3 + }, + "Kerberos Service Ticket Operations": { + "mode": "minimum", + "mask": 3 + }, + "Other Account Logon Events": { + "mode": "minimum", + "mask": 3 + }, + "Computer Account Management": { + "mode": "minimum", + "mask": 3 + }, + "Other Account Management Events": { + "mode": "minimum", + "mask": 3 + }, + "Security Group Management": { + "mode": "minimum", + "mask": 3 + }, + "User Account Management": { + "mode": "minimum", + "mask": 3 + }, + "DPAPI Activity": { + "mode": "minimum", + "mask": 3 + }, + "Process Creation": { + "mode": "minimum", + "mask": 3 + }, + "Account Lockout": { + "mode": "minimum", + "mask": 1 + }, + "IPsec Main Mode": { + "mode": "optional", + "mask": 3, + "note": "Only if IPsec is in use; operator must opt in." + }, + "Logoff": { + "mode": "minimum", + "mask": 1 + }, + "Special Logon": { + "mode": "minimum", + "mask": 3 + }, + "Audit Policy Change": { + "mode": "minimum", + "mask": 3 + }, + "Authentication Policy Change": { + "mode": "minimum", + "mask": 3 + }, + "MPSSVC Rule-Level Policy Change": { + "mode": "unchanged", + "note": "Source table does not distinguish success/failure; deliberately not inferred." + }, + "IPsec Driver": { + "mode": "minimum", + "mask": 3 + }, + "Security State Change": { + "mode": "minimum", + "mask": 3 + }, + "Security System Extension": { + "mode": "minimum", + "mask": 3 + }, + "System Integrity": { + "mode": "minimum", + "mask": 3 + }, + "Directory Service Access": { + "mode": "minimum", + "mask": 3 + }, + "Directory Service Changes": { + "mode": "minimum", + "mask": 3 + }, + "Logon": { + "mode": "minimum", + "mask": 3 + }, + "Other Logon/Logoff Events": { + "mode": "minimum", + "mask": 3 + } + }, + "roleOverrides": {}, + "note": "Stronger recommendation column, interpreted as minimum enabled flags; conditional IPsec is opt-in. Unspecified flags are preserved." + }, + { + "id": "microsoft-wef-reviewed-2026-09", + "version": "microsoft-wef-reviewed-2026-09", + "sourceIds": [ + "ms-wef" + ], + "omitted": "unchanged", + "scope": "advanced-audit-policy-only", + "appliesTo": [ + { + "roles": [ + "Client" + ], + "minBuild": 22000, + "maxBuild": 26200 + }, + { + "roles": [ + "MemberServer", + "DomainController", + "ADCS" + ], + "minBuild": 20348, + "maxBuild": 26100 + } + ], + "controls": { + "Credential Validation": { + "mode": "minimum", + "mask": 3 + }, + "Security Group Management": { + "mode": "minimum", + "mask": 1 + }, + "User Account Management": { + "mode": "minimum", + "mask": 3 + }, + "Computer Account Management": { + "mode": "minimum", + "mask": 3 + }, + "Other Account Management Events": { + "mode": "minimum", + "mask": 3 + }, + "Process Creation": { + "mode": "minimum", + "mask": 1 + }, + "Process Termination": { + "mode": "minimum", + "mask": 1 + }, + "User/Device Claims": { + "mode": "not-configured" + }, + "IPsec Extended Mode": { + "mode": "not-configured" + }, + "IPsec Quick Mode": { + "mode": "not-configured" + }, + "Logon": { + "mode": "minimum", + "mask": 3 + }, + "Logoff": { + "mode": "minimum", + "mask": 1 + }, + "Other Logon/Logoff Events": { + "mode": "minimum", + "mask": 3 + }, + "Special Logon": { + "mode": "minimum", + "mask": 3 + }, + "Account Lockout": { + "mode": "minimum", + "mask": 1 + }, + "Application Generated": { + "mode": "not-configured" + }, + "File Share": { + "mode": "minimum", + "mask": 1 + }, + "File System": { + "mode": "not-configured" + }, + "Other Object Access Events": { + "mode": "not-configured" + }, + "Registry": { + "mode": "not-configured" + }, + "Removable Storage": { + "mode": "minimum", + "mask": 1 + }, + "Audit Policy Change": { + "mode": "minimum", + "mask": 3 + }, + "MPSSVC Rule-Level Policy Change": { + "mode": "minimum", + "mask": 3 + }, + "Other Policy Change Events": { + "mode": "minimum", + "mask": 3 + }, + "Authentication Policy Change": { + "mode": "minimum", + "mask": 3 + }, + "Authorization Policy Change": { + "mode": "minimum", + "mask": 3 + }, + "Sensitive Privilege Use": { + "mode": "not-configured" + }, + "Security State Change": { + "mode": "minimum", + "mask": 3 + }, + "Security System Extension": { + "mode": "minimum", + "mask": 3 + }, + "System Integrity": { + "mode": "minimum", + "mask": 3 + } + }, + "roleOverrides": {}, + "note": "Appendix A minimum policy only. Does not provision subscriptions, collector, channel permissions, SACLs or retention." + }, + { + "id": "microsoft-identity-reviewed-2026-09", + "version": "microsoft-identity-reviewed-2026-09", + "sourceIds": [ + "ms-identity" + ], + "omitted": "unchanged", + "scope": "advanced-audit-policy-only", + "appliesTo": [ + { + "roles": [ + "DomainController", + "ADCS" + ], + "minBuild": 20348, + "maxBuild": 26100 + } + ], + "controls": {}, + "roleOverrides": { + "DomainController": { + "Credential Validation": { + "mode": "minimum", + "mask": 3 + }, + "Computer Account Management": { + "mode": "minimum", + "mask": 3 + }, + "Distribution Group Management": { + "mode": "minimum", + "mask": 3 + }, + "Security Group Management": { + "mode": "minimum", + "mask": 3 + }, + "User Account Management": { + "mode": "minimum", + "mask": 3 + }, + "Directory Service Changes": { + "mode": "minimum", + "mask": 3 + }, + "Directory Service Access": { + "mode": "minimum", + "mask": 3 + }, + "Security System Extension": { + "mode": "minimum", + "mask": 3 + } + }, + "ADCS": { + "Certification Services": { + "mode": "minimum", + "mask": 3 + } + } + }, + "note": "Advanced audit-policy subset only. AD SACLs, CA AuditFilter and other sensor prerequisites remain separate. This does not install any external sensor." + }, + { + "id": "cis-win11-v4-l1", + "version": "cis-win11-v4-l1", + "sourceIds": [ + "cis-client" + ], + "omitted": "unchanged", + "scope": "advanced-audit-policy-only", + "appliesTo": [ + { + "roles": [ + "Client" + ], + "minBuild": 22000, + "maxBuild": 26100 + } + ], + "controls": {}, + "roleOverrides": { + "Client": { + "Credential Validation": { + "mode": "exact", + "mask": 3, + "evidence": "17.1.1; printed page 437; L1" + }, + "Application Group Management": { + "mode": "exact", + "mask": 3, + "evidence": "17.2.1; printed page 440; L1" + }, + "Security Group Management": { + "mode": "minimum", + "mask": 1, + "evidence": "17.2.2; printed page 442; L1" + }, + "User Account Management": { + "mode": "exact", + "mask": 3, + "evidence": "17.2.3; printed page 444; L1" + }, + "Plug and Play Events": { + "mode": "minimum", + "mask": 1, + "evidence": "17.3.1; printed page 448; L1" + }, + "Process Creation": { + "mode": "minimum", + "mask": 1, + "evidence": "17.3.2; printed page 450; L1" + }, + "Account Lockout": { + "mode": "minimum", + "mask": 2, + "evidence": "17.5.1; printed page 453; L1" + }, + "Group Membership": { + "mode": "minimum", + "mask": 1, + "evidence": "17.5.2; printed page 455; L1" + }, + "Logoff": { + "mode": "minimum", + "mask": 1, + "evidence": "17.5.3; printed page 457; L1" + }, + "Logon": { + "mode": "exact", + "mask": 3, + "evidence": "17.5.4; printed page 459; L1" + }, + "Other Logon/Logoff Events": { + "mode": "exact", + "mask": 3, + "evidence": "17.5.5; printed page 461; L1" + }, + "Special Logon": { + "mode": "minimum", + "mask": 1, + "evidence": "17.5.6; printed page 463; L1" + }, + "Detailed File Share": { + "mode": "minimum", + "mask": 2, + "evidence": "17.6.1; printed page 466; L1" + }, + "File Share": { + "mode": "exact", + "mask": 3, + "evidence": "17.6.2; printed page 468; L1" + }, + "Other Object Access Events": { + "mode": "exact", + "mask": 3, + "evidence": "17.6.3; printed page 470; L1" + }, + "Removable Storage": { + "mode": "exact", + "mask": 3, + "evidence": "17.6.4; printed page 472; L1" + }, + "Audit Policy Change": { + "mode": "minimum", + "mask": 1, + "evidence": "17.7.1; printed page 475; L1" + }, + "Authentication Policy Change": { + "mode": "minimum", + "mask": 1, + "evidence": "17.7.2; printed page 477; L1" + }, + "Authorization Policy Change": { + "mode": "minimum", + "mask": 1, + "evidence": "17.7.3; printed page 479; L1" + }, + "MPSSVC Rule-Level Policy Change": { + "mode": "exact", + "mask": 3, + "evidence": "17.7.4; printed page 481; L1" + }, + "Other Policy Change Events": { + "mode": "minimum", + "mask": 2, + "evidence": "17.7.5; printed page 484; L1" + }, + "Sensitive Privilege Use": { + "mode": "exact", + "mask": 3, + "evidence": "17.8.1; printed page 487; L1" + }, + "IPsec Driver": { + "mode": "exact", + "mask": 3, + "evidence": "17.9.1; printed page 491; L1" + }, + "Other System Events": { + "mode": "exact", + "mask": 3, + "evidence": "17.9.2; printed page 494; L1" + }, + "Security State Change": { + "mode": "minimum", + "mask": 1, + "evidence": "17.9.3; printed page 496; L1" + }, + "Security System Extension": { + "mode": "minimum", + "mask": 1, + "evidence": "17.9.4; printed page 498; L1" + }, + "System Integrity": { + "mode": "exact", + "mask": 3, + "evidence": "17.9.5; printed page 500; L1" + } + } + }, + "note": "Historical v4.0.0. L1/L2 may have identical advanced audit policies; PowerShell and other controls are outside this profile scope." + }, + { + "id": "cis-win11-v4-l2", + "version": "cis-win11-v4-l2", + "sourceIds": [ + "cis-client" + ], + "omitted": "unchanged", + "scope": "advanced-audit-policy-only", + "appliesTo": [ + { + "roles": [ + "Client" + ], + "minBuild": 22000, + "maxBuild": 26100 + } + ], + "controls": {}, + "roleOverrides": { + "Client": { + "Credential Validation": { + "mode": "exact", + "mask": 3, + "evidence": "17.1.1; printed page 437; L1" + }, + "Application Group Management": { + "mode": "exact", + "mask": 3, + "evidence": "17.2.1; printed page 440; L1" + }, + "Security Group Management": { + "mode": "minimum", + "mask": 1, + "evidence": "17.2.2; printed page 442; L1" + }, + "User Account Management": { + "mode": "exact", + "mask": 3, + "evidence": "17.2.3; printed page 444; L1" + }, + "Plug and Play Events": { + "mode": "minimum", + "mask": 1, + "evidence": "17.3.1; printed page 448; L1" + }, + "Process Creation": { + "mode": "minimum", + "mask": 1, + "evidence": "17.3.2; printed page 450; L1" + }, + "Account Lockout": { + "mode": "minimum", + "mask": 2, + "evidence": "17.5.1; printed page 453; L1" + }, + "Group Membership": { + "mode": "minimum", + "mask": 1, + "evidence": "17.5.2; printed page 455; L1" + }, + "Logoff": { + "mode": "minimum", + "mask": 1, + "evidence": "17.5.3; printed page 457; L1" + }, + "Logon": { + "mode": "exact", + "mask": 3, + "evidence": "17.5.4; printed page 459; L1" + }, + "Other Logon/Logoff Events": { + "mode": "exact", + "mask": 3, + "evidence": "17.5.5; printed page 461; L1" + }, + "Special Logon": { + "mode": "minimum", + "mask": 1, + "evidence": "17.5.6; printed page 463; L1" + }, + "Detailed File Share": { + "mode": "minimum", + "mask": 2, + "evidence": "17.6.1; printed page 466; L1" + }, + "File Share": { + "mode": "exact", + "mask": 3, + "evidence": "17.6.2; printed page 468; L1" + }, + "Other Object Access Events": { + "mode": "exact", + "mask": 3, + "evidence": "17.6.3; printed page 470; L1" + }, + "Removable Storage": { + "mode": "exact", + "mask": 3, + "evidence": "17.6.4; printed page 472; L1" + }, + "Audit Policy Change": { + "mode": "minimum", + "mask": 1, + "evidence": "17.7.1; printed page 475; L1" + }, + "Authentication Policy Change": { + "mode": "minimum", + "mask": 1, + "evidence": "17.7.2; printed page 477; L1" + }, + "Authorization Policy Change": { + "mode": "minimum", + "mask": 1, + "evidence": "17.7.3; printed page 479; L1" + }, + "MPSSVC Rule-Level Policy Change": { + "mode": "exact", + "mask": 3, + "evidence": "17.7.4; printed page 481; L1" + }, + "Other Policy Change Events": { + "mode": "minimum", + "mask": 2, + "evidence": "17.7.5; printed page 484; L1" + }, + "Sensitive Privilege Use": { + "mode": "exact", + "mask": 3, + "evidence": "17.8.1; printed page 487; L1" + }, + "IPsec Driver": { + "mode": "exact", + "mask": 3, + "evidence": "17.9.1; printed page 491; L1" + }, + "Other System Events": { + "mode": "exact", + "mask": 3, + "evidence": "17.9.2; printed page 494; L1" + }, + "Security State Change": { + "mode": "minimum", + "mask": 1, + "evidence": "17.9.3; printed page 496; L1" + }, + "Security System Extension": { + "mode": "minimum", + "mask": 1, + "evidence": "17.9.4; printed page 498; L1" + }, + "System Integrity": { + "mode": "exact", + "mask": 3, + "evidence": "17.9.5; printed page 500; L1" + } + } + }, + "note": "Historical v4.0.0. L1/L2 may have identical advanced audit policies; PowerShell and other controls are outside this profile scope." + }, + { + "id": "cis-server2022-v4-l1", + "version": "cis-server2022-v4-l1", + "sourceIds": [ + "cis-server" + ], + "omitted": "unchanged", + "scope": "advanced-audit-policy-only", + "appliesTo": [ + { + "roles": [ + "MemberServer", + "DomainController", + "ADCS" + ], + "minBuild": 20348, + "maxBuild": 20348 + } + ], + "controls": {}, + "roleOverrides": { + "MemberServer": { + "Credential Validation": { + "mode": "exact", + "mask": 3, + "evidence": "17.1.1; printed page 391; L1" + }, + "Application Group Management": { + "mode": "exact", + "mask": 3, + "evidence": "17.2.1; printed page 398; L1" + }, + "Security Group Management": { + "mode": "minimum", + "mask": 1, + "evidence": "17.2.5; printed page 407; L1" + }, + "User Account Management": { + "mode": "exact", + "mask": 3, + "evidence": "17.2.6; printed page 410; L1" + }, + "Plug and Play Events": { + "mode": "minimum", + "mask": 1, + "evidence": "17.3.1; printed page 414; L1" + }, + "Process Creation": { + "mode": "minimum", + "mask": 1, + "evidence": "17.3.2; printed page 416; L1" + }, + "Account Lockout": { + "mode": "minimum", + "mask": 2, + "evidence": "17.5.1; printed page 424; L1" + }, + "Group Membership": { + "mode": "minimum", + "mask": 1, + "evidence": "17.5.2; printed page 426; L1" + }, + "Logoff": { + "mode": "minimum", + "mask": 1, + "evidence": "17.5.3; printed page 428; L1" + }, + "Logon": { + "mode": "exact", + "mask": 3, + "evidence": "17.5.4; printed page 430; L1" + }, + "Other Logon/Logoff Events": { + "mode": "exact", + "mask": 3, + "evidence": "17.5.5; printed page 432; L1" + }, + "Special Logon": { + "mode": "minimum", + "mask": 1, + "evidence": "17.5.6; printed page 434; L1" + }, + "Detailed File Share": { + "mode": "minimum", + "mask": 2, + "evidence": "17.6.1; printed page 437; L1" + }, + "File Share": { + "mode": "exact", + "mask": 3, + "evidence": "17.6.2; printed page 439; L1" + }, + "Other Object Access Events": { + "mode": "exact", + "mask": 3, + "evidence": "17.6.3; printed page 441; L1" + }, + "Removable Storage": { + "mode": "exact", + "mask": 3, + "evidence": "17.6.4; printed page 443; L1" + }, + "Audit Policy Change": { + "mode": "minimum", + "mask": 1, + "evidence": "17.7.1; printed page 446; L1" + }, + "Authentication Policy Change": { + "mode": "minimum", + "mask": 1, + "evidence": "17.7.2; printed page 448; L1" + }, + "Authorization Policy Change": { + "mode": "minimum", + "mask": 1, + "evidence": "17.7.3; printed page 450; L1" + }, + "MPSSVC Rule-Level Policy Change": { + "mode": "exact", + "mask": 3, + "evidence": "17.7.4; printed page 452; L1" + }, + "Other Policy Change Events": { + "mode": "minimum", + "mask": 2, + "evidence": "17.7.5; printed page 455; L1" + }, + "Sensitive Privilege Use": { + "mode": "exact", + "mask": 3, + "evidence": "17.8.1; printed page 458; L1" + }, + "IPsec Driver": { + "mode": "exact", + "mask": 3, + "evidence": "17.9.1; printed page 462; L1" + }, + "Other System Events": { + "mode": "exact", + "mask": 3, + "evidence": "17.9.2; printed page 465; L1" + }, + "Security State Change": { + "mode": "minimum", + "mask": 1, + "evidence": "17.9.3; printed page 467; L1" + }, + "Security System Extension": { + "mode": "minimum", + "mask": 1, + "evidence": "17.9.4; printed page 469; L1" + }, + "System Integrity": { + "mode": "exact", + "mask": 3, + "evidence": "17.9.5; printed page 471; L1" + } + }, + "DomainController": { + "Credential Validation": { + "mode": "exact", + "mask": 3, + "evidence": "17.1.1; printed page 391; L1" + }, + "Kerberos Authentication Service": { + "mode": "exact", + "mask": 3, + "evidence": "17.1.2; printed page 393; L1" + }, + "Kerberos Service Ticket Operations": { + "mode": "exact", + "mask": 3, + "evidence": "17.1.3; printed page 395; L1" + }, + "Application Group Management": { + "mode": "exact", + "mask": 3, + "evidence": "17.2.1; printed page 398; L1" + }, + "Computer Account Management": { + "mode": "minimum", + "mask": 1, + "evidence": "17.2.2; printed page 400; L1" + }, + "Distribution Group Management": { + "mode": "minimum", + "mask": 1, + "evidence": "17.2.3; printed page 402; L1" + }, + "Other Account Management Events": { + "mode": "minimum", + "mask": 1, + "evidence": "17.2.4; printed page 405; L1" + }, + "Security Group Management": { + "mode": "minimum", + "mask": 1, + "evidence": "17.2.5; printed page 407; L1" + }, + "User Account Management": { + "mode": "exact", + "mask": 3, + "evidence": "17.2.6; printed page 410; L1" + }, + "Plug and Play Events": { + "mode": "minimum", + "mask": 1, + "evidence": "17.3.1; printed page 414; L1" + }, + "Process Creation": { + "mode": "minimum", + "mask": 1, + "evidence": "17.3.2; printed page 416; L1" + }, + "Directory Service Access": { + "mode": "minimum", + "mask": 2, + "evidence": "17.4.1; printed page 419; L1" + }, + "Directory Service Changes": { + "mode": "minimum", + "mask": 1, + "evidence": "17.4.2; printed page 421; L1" + }, + "Account Lockout": { + "mode": "minimum", + "mask": 2, + "evidence": "17.5.1; printed page 424; L1" + }, + "Group Membership": { + "mode": "minimum", + "mask": 1, + "evidence": "17.5.2; printed page 426; L1" + }, + "Logoff": { + "mode": "minimum", + "mask": 1, + "evidence": "17.5.3; printed page 428; L1" + }, + "Logon": { + "mode": "exact", + "mask": 3, + "evidence": "17.5.4; printed page 430; L1" + }, + "Other Logon/Logoff Events": { + "mode": "exact", + "mask": 3, + "evidence": "17.5.5; printed page 432; L1" + }, + "Special Logon": { + "mode": "minimum", + "mask": 1, + "evidence": "17.5.6; printed page 434; L1" + }, + "Detailed File Share": { + "mode": "minimum", + "mask": 2, + "evidence": "17.6.1; printed page 437; L1" + }, + "File Share": { + "mode": "exact", + "mask": 3, + "evidence": "17.6.2; printed page 439; L1" + }, + "Other Object Access Events": { + "mode": "exact", + "mask": 3, + "evidence": "17.6.3; printed page 441; L1" + }, + "Removable Storage": { + "mode": "exact", + "mask": 3, + "evidence": "17.6.4; printed page 443; L1" + }, + "Audit Policy Change": { + "mode": "minimum", + "mask": 1, + "evidence": "17.7.1; printed page 446; L1" + }, + "Authentication Policy Change": { + "mode": "minimum", + "mask": 1, + "evidence": "17.7.2; printed page 448; L1" + }, + "Authorization Policy Change": { + "mode": "minimum", + "mask": 1, + "evidence": "17.7.3; printed page 450; L1" + }, + "MPSSVC Rule-Level Policy Change": { + "mode": "exact", + "mask": 3, + "evidence": "17.7.4; printed page 452; L1" + }, + "Other Policy Change Events": { + "mode": "minimum", + "mask": 2, + "evidence": "17.7.5; printed page 455; L1" + }, + "Sensitive Privilege Use": { + "mode": "exact", + "mask": 3, + "evidence": "17.8.1; printed page 458; L1" + }, + "IPsec Driver": { + "mode": "exact", + "mask": 3, + "evidence": "17.9.1; printed page 462; L1" + }, + "Other System Events": { + "mode": "exact", + "mask": 3, + "evidence": "17.9.2; printed page 465; L1" + }, + "Security State Change": { + "mode": "minimum", + "mask": 1, + "evidence": "17.9.3; printed page 467; L1" + }, + "Security System Extension": { + "mode": "minimum", + "mask": 1, + "evidence": "17.9.4; printed page 469; L1" + }, + "System Integrity": { + "mode": "exact", + "mask": 3, + "evidence": "17.9.5; printed page 471; L1" + } + }, + "ADCS": { + "Credential Validation": { + "mode": "exact", + "mask": 3, + "evidence": "17.1.1; printed page 391; L1" + }, + "Application Group Management": { + "mode": "exact", + "mask": 3, + "evidence": "17.2.1; printed page 398; L1" + }, + "Security Group Management": { + "mode": "minimum", + "mask": 1, + "evidence": "17.2.5; printed page 407; L1" + }, + "User Account Management": { + "mode": "exact", + "mask": 3, + "evidence": "17.2.6; printed page 410; L1" + }, + "Plug and Play Events": { + "mode": "minimum", + "mask": 1, + "evidence": "17.3.1; printed page 414; L1" + }, + "Process Creation": { + "mode": "minimum", + "mask": 1, + "evidence": "17.3.2; printed page 416; L1" + }, + "Account Lockout": { + "mode": "minimum", + "mask": 2, + "evidence": "17.5.1; printed page 424; L1" + }, + "Group Membership": { + "mode": "minimum", + "mask": 1, + "evidence": "17.5.2; printed page 426; L1" + }, + "Logoff": { + "mode": "minimum", + "mask": 1, + "evidence": "17.5.3; printed page 428; L1" + }, + "Logon": { + "mode": "exact", + "mask": 3, + "evidence": "17.5.4; printed page 430; L1" + }, + "Other Logon/Logoff Events": { + "mode": "exact", + "mask": 3, + "evidence": "17.5.5; printed page 432; L1" + }, + "Special Logon": { + "mode": "minimum", + "mask": 1, + "evidence": "17.5.6; printed page 434; L1" + }, + "Detailed File Share": { + "mode": "minimum", + "mask": 2, + "evidence": "17.6.1; printed page 437; L1" + }, + "File Share": { + "mode": "exact", + "mask": 3, + "evidence": "17.6.2; printed page 439; L1" + }, + "Other Object Access Events": { + "mode": "exact", + "mask": 3, + "evidence": "17.6.3; printed page 441; L1" + }, + "Removable Storage": { + "mode": "exact", + "mask": 3, + "evidence": "17.6.4; printed page 443; L1" + }, + "Audit Policy Change": { + "mode": "minimum", + "mask": 1, + "evidence": "17.7.1; printed page 446; L1" + }, + "Authentication Policy Change": { + "mode": "minimum", + "mask": 1, + "evidence": "17.7.2; printed page 448; L1" + }, + "Authorization Policy Change": { + "mode": "minimum", + "mask": 1, + "evidence": "17.7.3; printed page 450; L1" + }, + "MPSSVC Rule-Level Policy Change": { + "mode": "exact", + "mask": 3, + "evidence": "17.7.4; printed page 452; L1" + }, + "Other Policy Change Events": { + "mode": "minimum", + "mask": 2, + "evidence": "17.7.5; printed page 455; L1" + }, + "Sensitive Privilege Use": { + "mode": "exact", + "mask": 3, + "evidence": "17.8.1; printed page 458; L1" + }, + "IPsec Driver": { + "mode": "exact", + "mask": 3, + "evidence": "17.9.1; printed page 462; L1" + }, + "Other System Events": { + "mode": "exact", + "mask": 3, + "evidence": "17.9.2; printed page 465; L1" + }, + "Security State Change": { + "mode": "minimum", + "mask": 1, + "evidence": "17.9.3; printed page 467; L1" + }, + "Security System Extension": { + "mode": "minimum", + "mask": 1, + "evidence": "17.9.4; printed page 469; L1" + }, + "System Integrity": { + "mode": "exact", + "mask": 3, + "evidence": "17.9.5; printed page 471; L1" + } + } + }, + "note": "Historical v4.0.0. L1/L2 may have identical advanced audit policies; PowerShell and other controls are outside this profile scope." + }, + { + "id": "cis-server2022-v4-l2", + "version": "cis-server2022-v4-l2", + "sourceIds": [ + "cis-server" + ], + "omitted": "unchanged", + "scope": "advanced-audit-policy-only", + "appliesTo": [ + { + "roles": [ + "MemberServer", + "DomainController", + "ADCS" + ], + "minBuild": 20348, + "maxBuild": 20348 + } + ], + "controls": {}, + "roleOverrides": { + "MemberServer": { + "Credential Validation": { + "mode": "exact", + "mask": 3, + "evidence": "17.1.1; printed page 391; L1" + }, + "Application Group Management": { + "mode": "exact", + "mask": 3, + "evidence": "17.2.1; printed page 398; L1" + }, + "Security Group Management": { + "mode": "minimum", + "mask": 1, + "evidence": "17.2.5; printed page 407; L1" + }, + "User Account Management": { + "mode": "exact", + "mask": 3, + "evidence": "17.2.6; printed page 410; L1" + }, + "Plug and Play Events": { + "mode": "minimum", + "mask": 1, + "evidence": "17.3.1; printed page 414; L1" + }, + "Process Creation": { + "mode": "minimum", + "mask": 1, + "evidence": "17.3.2; printed page 416; L1" + }, + "Account Lockout": { + "mode": "minimum", + "mask": 2, + "evidence": "17.5.1; printed page 424; L1" + }, + "Group Membership": { + "mode": "minimum", + "mask": 1, + "evidence": "17.5.2; printed page 426; L1" + }, + "Logoff": { + "mode": "minimum", + "mask": 1, + "evidence": "17.5.3; printed page 428; L1" + }, + "Logon": { + "mode": "exact", + "mask": 3, + "evidence": "17.5.4; printed page 430; L1" + }, + "Other Logon/Logoff Events": { + "mode": "exact", + "mask": 3, + "evidence": "17.5.5; printed page 432; L1" + }, + "Special Logon": { + "mode": "minimum", + "mask": 1, + "evidence": "17.5.6; printed page 434; L1" + }, + "Detailed File Share": { + "mode": "minimum", + "mask": 2, + "evidence": "17.6.1; printed page 437; L1" + }, + "File Share": { + "mode": "exact", + "mask": 3, + "evidence": "17.6.2; printed page 439; L1" + }, + "Other Object Access Events": { + "mode": "exact", + "mask": 3, + "evidence": "17.6.3; printed page 441; L1" + }, + "Removable Storage": { + "mode": "exact", + "mask": 3, + "evidence": "17.6.4; printed page 443; L1" + }, + "Audit Policy Change": { + "mode": "minimum", + "mask": 1, + "evidence": "17.7.1; printed page 446; L1" + }, + "Authentication Policy Change": { + "mode": "minimum", + "mask": 1, + "evidence": "17.7.2; printed page 448; L1" + }, + "Authorization Policy Change": { + "mode": "minimum", + "mask": 1, + "evidence": "17.7.3; printed page 450; L1" + }, + "MPSSVC Rule-Level Policy Change": { + "mode": "exact", + "mask": 3, + "evidence": "17.7.4; printed page 452; L1" + }, + "Other Policy Change Events": { + "mode": "minimum", + "mask": 2, + "evidence": "17.7.5; printed page 455; L1" + }, + "Sensitive Privilege Use": { + "mode": "exact", + "mask": 3, + "evidence": "17.8.1; printed page 458; L1" + }, + "IPsec Driver": { + "mode": "exact", + "mask": 3, + "evidence": "17.9.1; printed page 462; L1" + }, + "Other System Events": { + "mode": "exact", + "mask": 3, + "evidence": "17.9.2; printed page 465; L1" + }, + "Security State Change": { + "mode": "minimum", + "mask": 1, + "evidence": "17.9.3; printed page 467; L1" + }, + "Security System Extension": { + "mode": "minimum", + "mask": 1, + "evidence": "17.9.4; printed page 469; L1" + }, + "System Integrity": { + "mode": "exact", + "mask": 3, + "evidence": "17.9.5; printed page 471; L1" + } + }, + "DomainController": { + "Credential Validation": { + "mode": "exact", + "mask": 3, + "evidence": "17.1.1; printed page 391; L1" + }, + "Kerberos Authentication Service": { + "mode": "exact", + "mask": 3, + "evidence": "17.1.2; printed page 393; L1" + }, + "Kerberos Service Ticket Operations": { + "mode": "exact", + "mask": 3, + "evidence": "17.1.3; printed page 395; L1" + }, + "Application Group Management": { + "mode": "exact", + "mask": 3, + "evidence": "17.2.1; printed page 398; L1" + }, + "Computer Account Management": { + "mode": "minimum", + "mask": 1, + "evidence": "17.2.2; printed page 400; L1" + }, + "Distribution Group Management": { + "mode": "minimum", + "mask": 1, + "evidence": "17.2.3; printed page 402; L1" + }, + "Other Account Management Events": { + "mode": "minimum", + "mask": 1, + "evidence": "17.2.4; printed page 405; L1" + }, + "Security Group Management": { + "mode": "minimum", + "mask": 1, + "evidence": "17.2.5; printed page 407; L1" + }, + "User Account Management": { + "mode": "exact", + "mask": 3, + "evidence": "17.2.6; printed page 410; L1" + }, + "Plug and Play Events": { + "mode": "minimum", + "mask": 1, + "evidence": "17.3.1; printed page 414; L1" + }, + "Process Creation": { + "mode": "minimum", + "mask": 1, + "evidence": "17.3.2; printed page 416; L1" + }, + "Directory Service Access": { + "mode": "minimum", + "mask": 2, + "evidence": "17.4.1; printed page 419; L1" + }, + "Directory Service Changes": { + "mode": "minimum", + "mask": 1, + "evidence": "17.4.2; printed page 421; L1" + }, + "Account Lockout": { + "mode": "minimum", + "mask": 2, + "evidence": "17.5.1; printed page 424; L1" + }, + "Group Membership": { + "mode": "minimum", + "mask": 1, + "evidence": "17.5.2; printed page 426; L1" + }, + "Logoff": { + "mode": "minimum", + "mask": 1, + "evidence": "17.5.3; printed page 428; L1" + }, + "Logon": { + "mode": "exact", + "mask": 3, + "evidence": "17.5.4; printed page 430; L1" + }, + "Other Logon/Logoff Events": { + "mode": "exact", + "mask": 3, + "evidence": "17.5.5; printed page 432; L1" + }, + "Special Logon": { + "mode": "minimum", + "mask": 1, + "evidence": "17.5.6; printed page 434; L1" + }, + "Detailed File Share": { + "mode": "minimum", + "mask": 2, + "evidence": "17.6.1; printed page 437; L1" + }, + "File Share": { + "mode": "exact", + "mask": 3, + "evidence": "17.6.2; printed page 439; L1" + }, + "Other Object Access Events": { + "mode": "exact", + "mask": 3, + "evidence": "17.6.3; printed page 441; L1" + }, + "Removable Storage": { + "mode": "exact", + "mask": 3, + "evidence": "17.6.4; printed page 443; L1" + }, + "Audit Policy Change": { + "mode": "minimum", + "mask": 1, + "evidence": "17.7.1; printed page 446; L1" + }, + "Authentication Policy Change": { + "mode": "minimum", + "mask": 1, + "evidence": "17.7.2; printed page 448; L1" + }, + "Authorization Policy Change": { + "mode": "minimum", + "mask": 1, + "evidence": "17.7.3; printed page 450; L1" + }, + "MPSSVC Rule-Level Policy Change": { + "mode": "exact", + "mask": 3, + "evidence": "17.7.4; printed page 452; L1" + }, + "Other Policy Change Events": { + "mode": "minimum", + "mask": 2, + "evidence": "17.7.5; printed page 455; L1" + }, + "Sensitive Privilege Use": { + "mode": "exact", + "mask": 3, + "evidence": "17.8.1; printed page 458; L1" + }, + "IPsec Driver": { + "mode": "exact", + "mask": 3, + "evidence": "17.9.1; printed page 462; L1" + }, + "Other System Events": { + "mode": "exact", + "mask": 3, + "evidence": "17.9.2; printed page 465; L1" + }, + "Security State Change": { + "mode": "minimum", + "mask": 1, + "evidence": "17.9.3; printed page 467; L1" + }, + "Security System Extension": { + "mode": "minimum", + "mask": 1, + "evidence": "17.9.4; printed page 469; L1" + }, + "System Integrity": { + "mode": "exact", + "mask": 3, + "evidence": "17.9.5; printed page 471; L1" + } + }, + "ADCS": { + "Credential Validation": { + "mode": "exact", + "mask": 3, + "evidence": "17.1.1; printed page 391; L1" + }, + "Application Group Management": { + "mode": "exact", + "mask": 3, + "evidence": "17.2.1; printed page 398; L1" + }, + "Security Group Management": { + "mode": "minimum", + "mask": 1, + "evidence": "17.2.5; printed page 407; L1" + }, + "User Account Management": { + "mode": "exact", + "mask": 3, + "evidence": "17.2.6; printed page 410; L1" + }, + "Plug and Play Events": { + "mode": "minimum", + "mask": 1, + "evidence": "17.3.1; printed page 414; L1" + }, + "Process Creation": { + "mode": "minimum", + "mask": 1, + "evidence": "17.3.2; printed page 416; L1" + }, + "Account Lockout": { + "mode": "minimum", + "mask": 2, + "evidence": "17.5.1; printed page 424; L1" + }, + "Group Membership": { + "mode": "minimum", + "mask": 1, + "evidence": "17.5.2; printed page 426; L1" + }, + "Logoff": { + "mode": "minimum", + "mask": 1, + "evidence": "17.5.3; printed page 428; L1" + }, + "Logon": { + "mode": "exact", + "mask": 3, + "evidence": "17.5.4; printed page 430; L1" + }, + "Other Logon/Logoff Events": { + "mode": "exact", + "mask": 3, + "evidence": "17.5.5; printed page 432; L1" + }, + "Special Logon": { + "mode": "minimum", + "mask": 1, + "evidence": "17.5.6; printed page 434; L1" + }, + "Detailed File Share": { + "mode": "minimum", + "mask": 2, + "evidence": "17.6.1; printed page 437; L1" + }, + "File Share": { + "mode": "exact", + "mask": 3, + "evidence": "17.6.2; printed page 439; L1" + }, + "Other Object Access Events": { + "mode": "exact", + "mask": 3, + "evidence": "17.6.3; printed page 441; L1" + }, + "Removable Storage": { + "mode": "exact", + "mask": 3, + "evidence": "17.6.4; printed page 443; L1" + }, + "Audit Policy Change": { + "mode": "minimum", + "mask": 1, + "evidence": "17.7.1; printed page 446; L1" + }, + "Authentication Policy Change": { + "mode": "minimum", + "mask": 1, + "evidence": "17.7.2; printed page 448; L1" + }, + "Authorization Policy Change": { + "mode": "minimum", + "mask": 1, + "evidence": "17.7.3; printed page 450; L1" + }, + "MPSSVC Rule-Level Policy Change": { + "mode": "exact", + "mask": 3, + "evidence": "17.7.4; printed page 452; L1" + }, + "Other Policy Change Events": { + "mode": "minimum", + "mask": 2, + "evidence": "17.7.5; printed page 455; L1" + }, + "Sensitive Privilege Use": { + "mode": "exact", + "mask": 3, + "evidence": "17.8.1; printed page 458; L1" + }, + "IPsec Driver": { + "mode": "exact", + "mask": 3, + "evidence": "17.9.1; printed page 462; L1" + }, + "Other System Events": { + "mode": "exact", + "mask": 3, + "evidence": "17.9.2; printed page 465; L1" + }, + "Security State Change": { + "mode": "minimum", + "mask": 1, + "evidence": "17.9.3; printed page 467; L1" + }, + "Security System Extension": { + "mode": "minimum", + "mask": 1, + "evidence": "17.9.4; printed page 469; L1" + }, + "System Integrity": { + "mode": "exact", + "mask": 3, + "evidence": "17.9.5; printed page 471; L1" + } + } + }, + "note": "Historical v4.0.0. L1/L2 may have identical advanced audit policies; PowerShell and other controls are outside this profile scope." + }, + { + "id": "asd-native-2021-10", + "version": "asd-native-2021-10", + "sourceIds": [ + "asd" + ], + "omitted": "unchanged", + "scope": "advanced-audit-policy-only", + "appliesTo": [ + { + "roles": [ + "Client" + ], + "minBuild": 22000, + "maxBuild": 26200 + }, + { + "roles": [ + "MemberServer", + "DomainController", + "ADCS" + ], + "minBuild": 20348, + "maxBuild": 26100 + } + ], + "controls": { + "Computer Account Management": { + "mode": "exact", + "mask": 3 + }, + "Other Account Management Events": { + "mode": "exact", + "mask": 3 + }, + "Security Group Management": { + "mode": "exact", + "mask": 3 + }, + "User Account Management": { + "mode": "exact", + "mask": 3 + }, + "Audit Policy Change": { + "mode": "exact", + "mask": 3 + }, + "Other Policy Change Events": { + "mode": "exact", + "mask": 3 + }, + "System Integrity": { + "mode": "exact", + "mask": 3 + }, + "Logon": { + "mode": "exact", + "mask": 3 + }, + "Other Logon/Logoff Events": { + "mode": "exact", + "mask": 3 + }, + "Special Logon": { + "mode": "exact", + "mask": 3 + }, + "File Share": { + "mode": "exact", + "mask": 3 + }, + "Other Object Access Events": { + "mode": "exact", + "mask": 3 + }, + "File System": { + "mode": "optional", + "mask": 3 + }, + "Registry": { + "mode": "optional", + "mask": 3 + }, + "Kernel Object": { + "mode": "exact", + "mask": 3 + }, + "Account Lockout": { + "mode": "exact", + "mask": 2 + }, + "Group Membership": { + "mode": "exact", + "mask": 1 + }, + "Logoff": { + "mode": "exact", + "mask": 1 + }, + "Process Creation": { + "mode": "exact", + "mask": 1 + }, + "Process Termination": { + "mode": "exact", + "mask": 1 + }, + "Detailed File Share": { + "mode": "not-configured" + } + }, + "roleOverrides": {}, + "note": "Native fallback only. Sysmon excluded. File System and Registry are optional and require SACLs. Detailed File Share Not Configured preserves effective state; it does not disable auditing." + } + ] +} diff --git a/docs/audit-profiles.md b/docs/audit-profiles.md new file mode 100644 index 00000000..8437ad62 --- /dev/null +++ b/docs/audit-profiles.md @@ -0,0 +1,74 @@ +# Versioned advanced audit-policy profiles + +`audit-settings`, `plan`, and `configure` share `config/audit_profiles.json` for advanced Security audit policy. The ordinary `audit-settings -Baseline YamatoSecurity` and ordinary `configure` also use `wela-2.2.0`, eliminating a separate hard-coded configuration list. All 59 subcategories use canonical GUIDs, including categories missing from the older display catalog. + +**Profile scope is advanced audit policy only.** Selecting Microsoft, CIS or ASD does not configure their PowerShell settings, command-line capture, channel buffers, NTLM policy, firewall logs, SACLs, CA AuditFilter, forwarding or retention. This is not a claim of full baseline compliance or detection coverage. Sysmon and external sensors are outside this feature. Ordinary `configure` without `-Profile` continues the existing broader WELA setup, with its advanced audit portion supplied by the shared profile. + +## Commands + +```powershell +# List exact profile ids and role/build applicability. +.\WELA.ps1 profiles + +# Offline planning is available on any platform; unknown effective state stays Unknown. +.\WELA.ps1 plan -Profile wela-2.2.0 -Role Client -Build 26100 -PlanPath plan.json + +# On Windows, omit Role/Build to detect this host and read effective auditpol values. +.\WELA.ps1 audit-settings -Profile microsoft-sct-win11-24h2 -PlanPath audit.json + +# Apply ONLY advanced audit policy. Interactive unless -Auto is supplied. +.\WELA.ps1 configure -Profile asd-native-2021-10 -Auto -PlanPath result.json + +# Select optional File System/Registry policy flags, without creating SACLs. +.\WELA.ps1 configure -Profile asd-native-2021-10 -IncludeOptional -PlanPath result.json +``` + +Supply both `-Role` and `-Build`, or omit both for Windows host detection. Roles are `Client`, `MemberServer`, `DomainController`, and `ADCS` (CA on a member server). Combined DC/CA deployments are not a separate profile: detection identifies them as DCs; review CA requirements separately. Build means the base build, for example 20348 (Server 2022), 26100 (Windows 11 24H2 / Server 2025), or 26200 (Windows 11 25H2). Live application checks the actual Windows host; a supplied role/build cannot authorize applying a mismatched plan. Versioned SCT profiles reject other base builds. Unsupported profiles/hosts and unreadable policies fail before writes. + +The WELA and documentary guide profiles currently cover the reviewed Windows 11/Server 2022/Server 2025 range. Older/future operating systems require a reviewed applicability update. `-Baseline` retains the legacy display interface for non-Yamato guides; use `-Profile` to select the versioned shared definitions. Do not combine `-Baseline` and `-Profile`. + +## Included sources + +| Profile | Version / meaning | +| --- | --- | +| `wela-2.2.0` | Reviewed WELA development snapshot `8ef938f0966e86adc527395f50f907c43e843d1e`; retains the 34 existing success/failure policies, with irrelevant roles skipped and three SACL prerequisites optional | +| `windows-defaults-reviewed-2026-09` | Documentary effective-default model; **reference only**, cannot be applied or used to reset an OS | +| `microsoft-sct-win11-24h2`, `microsoft-sct-win11-25h2` | Official SCT Policy Analyzer settings, exact masks | +| `microsoft-sct-server2022`, `microsoft-sct-server2025-2602` | Official SCT member/DC settings; AD CS uses the member-server baseline | +| `microsoft-stronger-reviewed-2026-09` | Stronger audit recommendation column; minimum enabled flags, conditional IPsec opt-in; ambiguous unspecified success/failure values preserved | +| `microsoft-wef-reviewed-2026-09` | WEF Appendix A minimum audit policy, preserving explicit Not Configured | +| `microsoft-identity-reviewed-2026-09` | Identity collection's DC/CA advanced audit requirements only; other prerequisites remain separate | +| `cis-win11-v4-l1`, `cis-win11-v4-l2` | Historical Windows 11 Enterprise v4.0.0, retaining “includes” minimum semantics | +| `cis-server2022-v4-l1`, `cis-server2022-v4-l2` | Historical Server 2022 v4.0.0, role-aware DC requirements | +| `asd-native-2021-10` | ASD native fallback; optional object auditing and explicit Detailed File Share Not Configured | + +CIS v4.0.0 is not the latest CIS edition. Defaults combine documentary evidence that is not a clean-install measurement, and some Server values are shared across roles. The defaults profile is deliberately blocked from application. Source URLs, versions, setting-level evidence, notes and prerequisites are in the JSON and exported plans. The catalog GUID reference is [Microsoft MS-GPAC](https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-gpac/77878370-0712-47cd-997d-b07053429f6d). + +## Policy semantics + +Mask bits are Success `1`, Failure `2`, both `3`, neither `0`. + +| Mode | Behavior | +| --- | --- | +| `exact` | Set the exact mask; may remove an existing success/failure flag | +| `minimum` | Bitwise OR with fresh effective state, preserving additional auditing | +| `unchanged` | Preserve current state; every omitted control becomes an explicit unchanged plan row | +| `not-configured` | Preserve effective policy; do not interpret it as disabled and do not remove a GPO | +| `optional` | Preserve unless `-IncludeOptional` is supplied; then set the explicit mask | +| `not-applicable` | Preserve; skip a subcategory outside the selected role | + +For example Detailed File Share is exact S+F in WELA, minimum Failure in the reviewed CIS profiles, and Not Configured in ASD. These are deliberate differences, not a universal “enable everything” preset. Omitted values and unknown effective state are different: unknown state blocks application instead of becoming mask zero. + +Effective policy is read through the Windows [AuditQuerySystemPolicy API](https://learn.microsoft.com/en-us/windows/win32/api/ntsecapi/nf-ntsecapi-auditquerysystempolicy); localized `auditpol /get /r` text is not parsed. Apply reads effective state again, checks native command errors, then verifies each changed mask. A command that exits successfully but does not change effective policy is reported as failed. Group Policy can reapply after a successful verification: these are local effective-policy changes, not GPO authoring. Exported plan/current state and apply results include the profile version, schema SHA-256, source provenance, before/target/effective masks and failure details. This feature does not validate event generation, SACL correctness, ingestion, or Sigma field compatibility. + +## Extending the schema and testing + +Add a catalog entry with a unique GUID, category, supported roles and prerequisite text. Add or override profile controls using `mode`, `mask` (only for exact/minimum/optional), optional `note`, `evidence`, and `sourceIds`. A profile supplies `sourceIds`, an explicit role/build range, `omitted: unchanged`, and `scope: advanced-audit-policy-only`. Optional control source ids are added to profile provenance. Do not silently revise a published source version when its semantics change. + +```powershell +# Pure tests, including injected native boundaries; no policy changes or elevation. +pwsh -NoProfile -File tests/audit-profiles.Tests.ps1 +powershell -NoProfile -File tests/audit-profiles.Tests.ps1 +``` + +The tests cover source/schema validation, role/build gating, exact/minimum/optional/NC behavior, locale-independent native policy reads, unknown-state refusal, fresh-state merging, idempotence, failed commands and verification, and the ordinary Yamato audit display. CI runs these on Windows PowerShell 5.1 and PowerShell 7. Source review and mocked tests are not substitutes for checking effective policy and benign event XML on isolated Windows clients, member servers, DCs and CAs. diff --git a/modules/AuditProfiles.psm1 b/modules/AuditProfiles.psm1 new file mode 100644 index 00000000..1f7d101c --- /dev/null +++ b/modules/AuditProfiles.psm1 @@ -0,0 +1,253 @@ +# Requires Windows PowerShell 5.1 or PowerShell 7. No Windows dependency for schema/planning. +Set-StrictMode -Version 2.0 + +function Get-WelaProperty { + param($Object, [string]$Name, $Default = $null) + if ($null -ne $Object -and $null -ne $Object.PSObject.Properties[$Name]) { return $Object.$Name } + return $Default +} + +function Import-WelaAuditProfiles { + [CmdletBinding()] + param([string]$Path = (Join-Path $PSScriptRoot '../config/audit_profiles.json')) + $data = Get-Content -LiteralPath $Path -Raw -ErrorAction Stop | ConvertFrom-Json -ErrorAction Stop + if ($data.schemaVersion -ne 1) { throw 'Unsupported audit profile schema version.' } + $roles = @('Client', 'MemberServer', 'DomainController', 'ADCS') + $ids = @{}; $guids = @{}; $profileIds = @{} + foreach ($policy in $data.catalog) { + if (-not $policy.id -or $ids.ContainsKey($policy.id)) { throw "Duplicate or empty policy id: $($policy.id)" } + if ($policy.guid -notmatch '^[0-9A-Fa-f]{8}(-[0-9A-Fa-f]{4}){3}-[0-9A-Fa-f]{12}$' -or $guids.ContainsKey($policy.guid)) { throw "Invalid or duplicate GUID: $($policy.guid)" } + if (@($policy.roles).Count -eq 0 -or @($policy.roles | Where-Object { $_ -notin $roles }).Count) { throw "Invalid policy roles: $($policy.id)" } + $ids[$policy.id] = $true; $guids[$policy.guid] = $true + } + foreach ($profile in $data.profiles) { + if (-not $profile.id -or $profileIds.ContainsKey($profile.id)) { throw "Duplicate or empty profile id: $($profile.id)" } + $profileIds[$profile.id] = $true + if ($profile.omitted -ne 'unchanged' -or $profile.scope -ne 'advanced-audit-policy-only' -or -not $profile.version) { throw "Invalid profile metadata: $($profile.id)" } + if (@($profile.sourceIds).Count -eq 0) { throw "Missing profile provenance: $($profile.id)" } + foreach ($source in $profile.sourceIds) { + if (-not $data.sources.PSObject.Properties[$source]) { throw "Unknown profile source: $source" } + } + if (@($profile.appliesTo).Count -eq 0) { throw "Missing applicability: $($profile.id)" } + foreach ($range in $profile.appliesTo) { + if (@($range.roles).Count -eq 0 -or @($range.roles | Where-Object { $_ -notin $roles }).Count -or $range.minBuild -lt 0 -or $range.maxBuild -lt $range.minBuild) { throw "Invalid applicability: $($profile.id)" } + } + $sets = @($profile.controls) + foreach ($override in $profile.roleOverrides.PSObject.Properties) { + if ($override.Name -notin $roles) { throw "Unknown role override: $($override.Name)" } + $sets += $override.Value + } + foreach ($set in $sets) { + foreach ($property in $set.PSObject.Properties) { + if (-not $ids.ContainsKey($property.Name)) { throw "Unknown audit policy: $($property.Name)" } + $control = $property.Value + foreach ($sourceId in @(Get-WelaProperty $control 'sourceIds' @())) { + if (-not $data.sources.PSObject.Properties[$sourceId]) { throw "Unknown control source: $sourceId" } + } + if ($control.mode -notin @('exact', 'minimum', 'unchanged', 'not-configured', 'optional', 'not-applicable')) { throw "Invalid mode: $($control.mode)" } + $hasMask = $null -ne $control.PSObject.Properties['mask'] + if ($control.mode -in @('exact', 'minimum', 'optional')) { + if (-not $hasMask -or $control.mask -isnot [ValueType] -or $control.mask -is [bool] -or $control.mask -notin @(0, 1, 2, 3) -or [double]$control.mask -ne [int]$control.mask) { throw "Invalid mask: $($property.Name)" } + } elseif ($hasMask) { throw "Non-setting mode cannot have a mask: $($property.Name)" } + } + } + } + return $data +} + +function Format-WelaAuditMask { + param($Mask) + if ($null -eq $Mask) { return 'Unknown' } + switch ([int]$Mask) { 0 { 'No Auditing' } 1 { 'Success' } 2 { 'Failure' } 3 { 'Success and Failure' } default { throw "Invalid mask: $Mask" } } +} + +function Get-WelaAuditProfilePlan { + [CmdletBinding()] + param( + [Parameter(Mandatory)][string]$Profile, + [Parameter(Mandatory)][ValidateSet('Client', 'MemberServer', 'DomainController', 'ADCS')][string]$Role, + [Parameter(Mandatory)][ValidateRange(1, 999999)][int]$Build, + [hashtable]$Current = @{}, [switch]$IncludeOptional, + [string]$Path = (Join-Path $PSScriptRoot '../config/audit_profiles.json') + ) + $data = Import-WelaAuditProfiles -Path $Path + $selected = @($data.profiles | Where-Object { $_.id -eq $Profile }) + if ($selected.Count -ne 1) { throw "Unknown audit profile '$Profile'. Use -Cmd profiles to list profiles." } + $selected = $selected[0] + $matches = @($selected.appliesTo | Where-Object { $Role -in $_.roles -and $Build -ge $_.minBuild -and $Build -le $_.maxBuild }) + if ($matches.Count -eq 0) { throw "Profile '$Profile' does not support role '$Role', build '$Build'." } + foreach ($value in $Current.Values) { + if ($null -ne $value -and ($value -is [bool] -or $value -notin @(0, 1, 2, 3))) { throw "Invalid effective audit mask: $value" } + } + $controls = @{} + foreach ($property in $selected.controls.PSObject.Properties) { $controls[$property.Name] = $property.Value } + $override = Get-WelaProperty $selected.roleOverrides $Role + if ($override) { foreach ($property in $override.PSObject.Properties) { $controls[$property.Name] = $property.Value } } + $rows = foreach ($policy in $data.catalog) { + $control = $controls[$policy.id] + $mode = if ($control) { $control.mode } else { 'unchanged' } + if ($Role -notin $policy.roles) { $mode = 'not-applicable' } + $mask = Get-WelaProperty $control 'mask' + $currentMask = if ($Current.ContainsKey($policy.guid)) { $Current[$policy.guid] } else { $null } + $desired = $null; $action = 'Preserve'; $compliance = 'Not assessed' + if ($mode -eq 'not-applicable') { $action = 'Not applicable'; $mask = $null } + elseif ($mode -eq 'optional' -and -not $IncludeOptional) { $action = 'Optional (not selected)' } + elseif ($mode -in @('exact', 'minimum', 'optional')) { + if ($null -eq $currentMask) { $action = 'Unknown'; $compliance = 'Unknown' } + else { + $desired = if ($mode -eq 'minimum') { [int]$currentMask -bor [int]$mask } else { [int]$mask } + $action = if ($currentMask -eq $desired) { 'No change' } else { 'Set' } + $compliance = if ($action -eq 'No change') { 'Compliant' } else { 'Drift' } + } + } + [pscustomobject][ordered]@{ + id = $policy.id; guid = $policy.guid; category = $policy.category; mode = $mode + requiredMask = $mask; currentMask = $currentMask; targetMask = $desired + recommendation = if ($mode -in @('exact', 'minimum', 'optional')) { "$(Format-WelaAuditMask $mask) [$mode]" } else { $mode } + action = $action; compliance = $compliance; prerequisites = $policy.prerequisites + note = Get-WelaProperty $control 'note' ''; evidence = Get-WelaProperty $control 'evidence' '' + sourceIds = @(@($selected.sourceIds) + @(Get-WelaProperty $control 'sourceIds' @()) | Select-Object -Unique) + } + } + $sourceIds = @($rows | ForEach-Object { $_.sourceIds } | Select-Object -Unique) + $sources = foreach ($id in $sourceIds) { [pscustomobject]@{ id = $id; source = $data.sources.$id } } + [pscustomobject][ordered]@{ + schemaVersion = 1; profile = $selected.id; version = $selected.version + scope = $selected.scope; role = $Role; build = $Build; includeOptional = [bool]$IncludeOptional + referenceOnly = [bool](Get-WelaProperty $selected 'referenceOnly' $false) + generatedUtc = [DateTime]::UtcNow.ToString('o'); schemaSha256 = (Get-FileHash -LiteralPath $Path -Algorithm SHA256).Hash + note = Get-WelaProperty $selected 'note' ''; provenance = @($sources); policies = @($rows) + } +} + +function Get-WelaEffectiveAuditPolicy { + [CmdletBinding()] + param() + # auditpol /get /r has localized text and no numeric mask column. Query the native API instead. + if (-not ('Wela.AuditProfiles.NativePolicy' -as [type])) { + Add-Type -TypeDefinition @' +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.Runtime.InteropServices; +namespace Wela.AuditProfiles { + public static class NativePolicy { + [StructLayout(LayoutKind.Sequential)] + private struct PolicyInformation { + public Guid Subcategory; + public UInt32 Information; + public Guid Category; + } + [DllImport("advapi32.dll", SetLastError = true)] + [return: MarshalAs(UnmanagedType.U1)] + private static extern bool AuditQuerySystemPolicy( + [In, MarshalAs(UnmanagedType.LPArray, SizeParamIndex = 1)] Guid[] subcategories, + UInt32 count, out IntPtr information); + [DllImport("advapi32.dll")] + private static extern void AuditFree(IntPtr buffer); + public static Dictionary Read(Guid[] subcategories) { + IntPtr buffer = IntPtr.Zero; + try { + if (!AuditQuerySystemPolicy(subcategories, (UInt32)subcategories.Length, out buffer)) + throw new Win32Exception(Marshal.GetLastWin32Error(), "AuditQuerySystemPolicy failed"); + if (buffer == IntPtr.Zero) throw new InvalidOperationException("Audit policy API returned a null buffer."); + int size = Marshal.SizeOf(typeof(PolicyInformation)); + var result = new Dictionary(StringComparer.OrdinalIgnoreCase); + for (int i = 0; i < subcategories.Length; i++) { + var policy = (PolicyInformation)Marshal.PtrToStructure(IntPtr.Add(buffer, i * size), typeof(PolicyInformation)); + // POLICY_AUDIT_EVENT_NONE = 4; success/failure are bits 1 and 2. + if (policy.Information > 4U) throw new InvalidOperationException("Unrecognized native audit flags."); + result.Add(policy.Subcategory.ToString().ToUpperInvariant(), (int)(policy.Information & 3U)); + } + return result; + } finally { if (buffer != IntPtr.Zero) AuditFree(buffer); } + } + } +} +'@ -ErrorAction Stop + } + $catalog = (Import-WelaAuditProfiles).catalog + [guid[]]$guids = @($catalog | ForEach-Object { [guid]$_.guid }) + $native = [Wela.AuditProfiles.NativePolicy]::Read($guids) + $current = @{} + foreach ($policy in $catalog) { + if (-not $native.ContainsKey($policy.guid)) { throw "Audit policy API omitted $($policy.id)." } + $current[$policy.guid] = $native[$policy.guid] + } + return $current +} + +function Set-WelaEffectiveAuditPolicy { + param([ValidatePattern('^[0-9A-Fa-f]{8}(-[0-9A-Fa-f]{4}){3}-[0-9A-Fa-f]{12}$')][string]$Guid, [ValidateRange(0, 3)][int]$Mask) + $success = if ($Mask -band 1) { 'enable' } else { 'disable' } + $failure = if ($Mask -band 2) { 'enable' } else { 'disable' } + $output = & auditpol.exe /set "/subcategory:{$Guid}" "/success:$success" "/failure:$failure" 2>&1 + if ($LASTEXITCODE -ne 0) { throw "auditpol /set failed ($LASTEXITCODE): $($output -join ' ')" } +} + +function Get-WelaHostContext { + [CmdletBinding()] + param() + $os = Get-CimInstance -ClassName Win32_OperatingSystem -ErrorAction Stop + $system = Get-CimInstance -ClassName Win32_ComputerSystem -ErrorAction Stop + if ([int]$os.ProductType -notin @(1, 2, 3) -or [int]$system.DomainRole -notin @(0, 1, 2, 3, 4, 5) -or [int]$os.BuildNumber -le 0) { throw 'Cannot determine a valid Windows role/build.' } + if (([int]$os.ProductType -eq 1 -and [int]$system.DomainRole -notin @(0, 1)) -or + ([int]$os.ProductType -eq 2 -and [int]$system.DomainRole -notin @(4, 5)) -or + ([int]$os.ProductType -eq 3 -and [int]$system.DomainRole -notin @(2, 3))) { throw 'Windows ProductType and DomainRole disagree.' } + $role = if ([int]$os.ProductType -eq 1) { 'Client' } + elseif ([int]$system.DomainRole -in @(4, 5)) { 'DomainController' } + elseif (Test-Path 'HKLM:\SYSTEM\CurrentControlSet\Services\CertSvc\Configuration') { 'ADCS' } + else { 'MemberServer' } + [pscustomobject]@{ Role = $role; Build = [int]$os.BuildNumber } +} + +function Assert-WelaAuditProfileTarget { + [CmdletBinding()] + param([Parameter(Mandatory)]$Plan, [Parameter(Mandatory)]$Context, [Parameter(Mandatory)]$Current) + if ($Plan.referenceOnly) { throw 'Windows defaults are a reference, not an apply/restore profile.' } + if ($Context.Role -ne $Plan.role -or $Context.Build -ne $Plan.build) { throw 'Plan role/build does not match the actual Windows host.' } + if ($Current -isnot [hashtable]) { throw 'Effective policy reader did not return a GUID-to-mask map.' } + $selected = @($Plan.policies | Where-Object { $_.mode -in @('exact', 'minimum') -or ($_.mode -eq 'optional' -and $Plan.includeOptional) }) + foreach ($policy in $selected) { + if (-not $Current.ContainsKey($policy.guid) -or $null -eq $Current[$policy.guid] -or $Current[$policy.guid] -notin @(0, 1, 2, 3)) { throw "Cannot apply with unknown current policy: $($policy.id). No policies changed." } + } +} + +function Invoke-WelaAuditProfilePlan { + [CmdletBinding(SupportsShouldProcess)] + param( + [Parameter(Mandatory)]$Plan, + [scriptblock]$ReadPolicy = { Get-WelaEffectiveAuditPolicy }, + [scriptblock]$WritePolicy = { param($Guid, $Mask) Set-WelaEffectiveAuditPolicy -Guid $Guid -Mask $Mask }, + [scriptblock]$ReadContext = { Get-WelaHostContext } + ) + $hostContext = & $ReadContext + $before = & $ReadPolicy + Assert-WelaAuditProfileTarget -Plan $Plan -Context $hostContext -Current $before + $selected = @($Plan.policies | Where-Object { $_.mode -in @('exact', 'minimum') -or ($_.mode -eq 'optional' -and $Plan.includeOptional) }) + $results = foreach ($policy in $selected) { + $initial = $before[$policy.guid]; $effective = $initial; $errorText = $null + $target = if ($policy.mode -eq 'minimum') { [int]$initial -bor [int]$policy.requiredMask } else { [int]$policy.requiredMask } + $status = 'No change' + if ($initial -ne $target) { + if ($PSCmdlet.ShouldProcess($policy.id, "Set audit policy to $(Format-WelaAuditMask $target)")) { + try { + & $WritePolicy $policy.guid $target | Out-Null + $verified = & $ReadPolicy + $effective = if ($verified.ContainsKey($policy.guid)) { $verified[$policy.guid] } else { $null } + if ($effective -ne $target) { throw 'Effective policy does not match the requested mask (GPO or command failure).' } + $status = 'Applied' + } catch { $status = 'Failed'; $errorText = $_.Exception.Message; $effective = $null } + } else { $status = 'Skipped' } + } + [pscustomobject]@{ id = $policy.id; guid = $policy.guid; mode = $policy.mode; beforeMask = $initial; targetMask = $target; effectiveMask = $effective; status = $status; error = $errorText } + } + [pscustomobject]@{ + profile = $Plan.profile; scope = $Plan.scope; role = $Plan.role; build = $Plan.build + schemaSha256 = $Plan.schemaSha256; provenance = $Plan.provenance + success = (@($results | Where-Object { $_.status -eq 'Failed' }).Count -eq 0) + results = @($results) + } +} + +Export-ModuleMember -Function Import-WelaAuditProfiles, Format-WelaAuditMask, Get-WelaAuditProfilePlan, Get-WelaEffectiveAuditPolicy, Set-WelaEffectiveAuditPolicy, Get-WelaHostContext, Assert-WelaAuditProfileTarget, Invoke-WelaAuditProfilePlan diff --git a/tests/audit-profiles.Tests.ps1 b/tests/audit-profiles.Tests.ps1 new file mode 100644 index 00000000..6d61d5b3 --- /dev/null +++ b/tests/audit-profiles.Tests.ps1 @@ -0,0 +1,115 @@ +# Deterministic tests: no elevation, Windows policy writes, or Pester dependency. +$ErrorActionPreference = 'Stop' +Import-Module (Join-Path $PSScriptRoot '../modules/AuditProfiles.psm1') -Force +$script:Checks = 0 +function Assert([bool]$Condition, [string]$Message) { + $script:Checks++ + if (-not $Condition) { throw "Assertion failed: $Message" } +} +function Assert-Throws([scriptblock]$Action, [string]$Pattern) { + try { & $Action | Out-Null } catch { Assert ($_.Exception.Message -match $Pattern) "Expected '$Pattern', got '$($_.Exception.Message)'"; return } + throw "Expected exception matching '$Pattern'." +} +function Policy($Plan, $Id) { $Plan.policies | Where-Object { $_.id -eq $Id } } +$data = Import-WelaAuditProfiles +Assert ($data.catalog.Count -eq 59) 'all canonical audit subcategories are represented' +$zero = @{} +foreach ($policy in $data.catalog) { $zero[$policy.guid] = 0 } +foreach ($profile in $data.profiles) { + foreach ($range in $profile.appliesTo) { + foreach ($role in $range.roles) { + $plan = Get-WelaAuditProfilePlan -Profile $profile.id -Role $role -Build $range.minBuild -Current $zero + Assert ($plan.policies.Count -eq 59) "$($profile.id)/$role preserves omitted policies explicitly" + Assert ($plan.provenance.Count -gt 0 -and $plan.schemaSha256.Length -eq 64) 'versioned source and schema fingerprints' + } + } +} +$wela = Get-WelaAuditProfilePlan -Profile wela-2.2.0 -Role Client -Build 26100 -Current $zero +foreach ($id in @('Process Termination', 'RPC Events', 'Detailed File Share', 'Other Policy Change Events')) { + $row = Policy $wela $id + Assert ($row.mode -eq 'exact' -and $row.targetMask -eq 3) "$id recommendation matches existing configure SF policy" +} +Assert ((Policy $wela 'File System').action -eq 'Optional (not selected)') 'optional controls preserve current state by default' +$opt = Get-WelaAuditProfilePlan -Profile wela-2.2.0 -Role Client -Build 26100 -Current $zero -IncludeOptional +Assert ((Policy $opt 'File System').targetMask -eq 3) 'optional control is explicit opt-in' +Assert ((Policy $opt 'File System').prerequisites -match 'SACL') 'SACL dependency is visible' +Assert ((Policy $wela 'Directory Service Access').mode -eq 'not-applicable') 'DC auditing is role scoped' +$adcs = Get-WelaAuditProfilePlan -Profile wela-2.2.0 -Role ADCS -Build 20348 -Current $zero +Assert ((Policy $adcs 'Certification Services').targetMask -eq 3) 'CA role is supported' +Assert ((Policy $adcs 'Certification Services').prerequisites -match 'AuditFilter') 'CA prerequisite not silently claimed applied' +$shareGuid = (Policy $wela 'Detailed File Share').guid +$current = $zero.Clone(); $current[$shareGuid] = 1 +$cis = Get-WelaAuditProfilePlan -Profile cis-win11-v4-l1 -Role Client -Build 26100 -Current $current +Assert ((Policy $cis 'Detailed File Share').mode -eq 'minimum') 'CIS includes Failure is represented as minimum' +Assert ((Policy $cis 'Detailed File Share').targetMask -eq 3) 'minimum Failure preserves preexisting Success' +$asd = Get-WelaAuditProfilePlan -Profile asd-native-2021-10 -Role Client -Build 26100 -Current $current +Assert ((Policy $asd 'Detailed File Share').mode -eq 'not-configured') 'ASD explicit NC is retained' +Assert ($null -eq (Policy $asd 'Detailed File Share').targetMask) 'NC does not become disabled' +Assert ((Policy $asd 'RPC Events').mode -eq 'unchanged') 'omission is unchanged, not no-auditing' +$unknown = Get-WelaAuditProfilePlan -Profile cis-win11-v4-l1 -Role Client -Build 26100 +Assert ($null -eq (Policy $unknown 'Detailed File Share').targetMask -and (Policy $unknown 'Detailed File Share').action -eq 'Unknown') 'unknown current does not become disabled before minimum merge' +Assert-Throws { Get-WelaAuditProfilePlan -Profile microsoft-sct-win11-24h2 -Role Client -Build 26200 } 'does not support' +Assert-Throws { Get-WelaAuditProfilePlan -Profile microsoft-sct-win11-24h2 -Role DomainController -Build 26100 } 'does not support' +Assert-Throws { Get-WelaAuditProfilePlan -Profile typo -Role Client -Build 26100 } 'Unknown audit profile' +# Inject a stateful native boundary, exercising actual selection, merge, verify and failure behavior. +$script:State = $zero.Clone(); $script:Writes = @() +$reader = { return $script:State.Clone() } +$writer = { param($Guid, $Mask) $script:Writes += $Guid; $script:State[$Guid] = $Mask } +$context = { [pscustomobject]@{ Role = 'Client'; Build = 26100 } } +$applied = Invoke-WelaAuditProfilePlan -Plan $wela -ReadPolicy $reader -WritePolicy $writer -ReadContext $context -Confirm:$false +Assert $applied.success 'apply succeeds after verified effective reads' +Assert ($script:Writes.Count -gt 0) 'selected exact policies were applied' +Assert (@($applied.results | Where-Object { $_.status -eq 'Applied' -and $_.effectiveMask -ne $_.targetMask }).Count -eq 0) 'applied always means verified' +$count = $script:Writes.Count +$again = Invoke-WelaAuditProfilePlan -Plan $wela -ReadPolicy $reader -WritePolicy $writer -ReadContext $context -Confirm:$false +Assert ($again.success -and $script:Writes.Count -eq $count) 'applying twice is idempotent using fresh current state' +# Apply a stale minimum plan after a preexisting Success flag is introduced: merge fresh state. +$script:State = $zero.Clone(); $script:State[$shareGuid] = 1 +$minimum = Invoke-WelaAuditProfilePlan -Plan $cis -ReadPolicy $reader -WritePolicy $writer -ReadContext $context -Confirm:$false +Assert ($minimum.success -and $script:State[$shareGuid] -eq 3) 'fresh effective flags are preserved in minimum apply' +$script:State = $zero.Clone() +$failed = Invoke-WelaAuditProfilePlan -Plan $wela -ReadPolicy $reader -WritePolicy { throw 'command failed' } -ReadContext $context -Confirm:$false +Assert (-not $failed.success -and @($failed.results | Where-Object { $_.status -eq 'Failed' }).Count -gt 0) 'native failure is machine-readable' +$mismatch = Invoke-WelaAuditProfilePlan -Plan $wela -ReadPolicy $reader -WritePolicy { param($Guid, $Mask) } -ReadContext $context -Confirm:$false +Assert (-not $mismatch.success) 'zero exit without effective change does not count as success' +$script:Writes = @() +$whatIf = Invoke-WelaAuditProfilePlan -Plan $wela -ReadPolicy $reader -WritePolicy $writer -ReadContext $context -WhatIf +Assert ($script:Writes.Count -eq 0) 'WhatIf never invokes native writer' +Assert-Throws { Invoke-WelaAuditProfilePlan -Plan $wela -ReadPolicy { @{} } -WritePolicy $writer -ReadContext $context -Confirm:$false } 'unknown current' +Assert ($script:Writes.Count -eq 0) 'unknown preflight refuses all writes' +Assert-Throws { Invoke-WelaAuditProfilePlan -Plan $wela -ReadPolicy $reader -WritePolicy $writer -ReadContext { [pscustomobject]@{ Role = 'DomainController'; Build = 26100 } } } 'actual Windows host' +$defaults = Get-WelaAuditProfilePlan -Profile windows-defaults-reviewed-2026-09 -Role Client -Build 26100 -Current $zero +Assert-Throws { Invoke-WelaAuditProfilePlan -Plan $defaults -ReadPolicy $reader -WritePolicy $writer -ReadContext $context } 'reference' +# Schema rejects bad policy names, duplicate GUIDs, invalid masks/modes, and unknown provenance. +$temp = Join-Path ([System.IO.Path]::GetTempPath()) ('wela-profile-test-' + [guid]::NewGuid().ToString() + '.json') +try { + foreach ($case in @('guid', 'mask', 'mode', 'source', 'unknown')) { + $copy = Get-Content (Join-Path $PSScriptRoot '../config/audit_profiles.json') -Raw | ConvertFrom-Json + switch ($case) { + 'guid' { $copy.catalog[1].guid = $copy.catalog[0].guid } + 'mask' { $copy.profiles[0].controls.'Process Creation'.mask = 7 } + 'mode' { $copy.profiles[0].controls.'Process Creation'.mode = 'invented' } + 'source' { $copy.profiles[0].sourceIds = @('unreviewed') } + 'unknown' { $copy.profiles[0].controls | Add-Member NoteProperty 'Typo Policy' ([pscustomobject]@{ mode = 'exact'; mask = 3 }) } + } + $copy | ConvertTo-Json -Depth 30 | Set-Content -LiteralPath $temp -Encoding UTF8 + Assert-Throws { Import-WelaAuditProfiles -Path $temp } 'Invalid|Unknown|duplicate' + } +} finally { Remove-Item -LiteralPath $temp -Force -ErrorAction SilentlyContinue } +# Legacy Yamato audit display now takes recommendations from the shared profile, including omitted policies. +. (Join-Path $PSScriptRoot '../WELA.ps1') help -Role Client -Build 26100 +function GetAuditpol { return @{} } +$legacy = BuildAuditResult -all_rules @() -Baseline YamatoSecurity -enabledguid @() +foreach ($id in @('Process Termination', 'RPC Events', 'Detailed File Share', 'Other Policy Change Events')) { + $entry = $legacy | Where-Object { $_.SubCategory -eq $id } + Assert ($entry.RecommendedSetting -eq 'Success and Failure [exact]') "legacy audit/settings shares $id recommendation" +} +Assert (@($legacy | Where-Object { $_.Category -like 'Security Advanced*' }).Count -eq 59) 'legacy display includes all canonical GUIDs' +# An unsupported legacy configure target must fail before reaching the old setup body. +function TestWindows { return $true } +function TestAdministrator { return $true } +function Get-WelaHostContext { [pscustomobject]@{ Role = 'Client'; Build = 19045 } } +function Get-WelaEffectiveAuditPolicy { return $zero.Clone() } +function CollectAuditpol { throw 'Reached the old configuration body before profile validation' } +Assert-Throws { ConfigureAuditSettings -Auto } 'does not support' +Write-Host "PASS: $script:Checks audit profile checks; no Windows settings changed." diff --git a/tests/audit-profiles.Windows.Tests.ps1 b/tests/audit-profiles.Windows.Tests.ps1 new file mode 100644 index 00000000..aecc06ce --- /dev/null +++ b/tests/audit-profiles.Windows.Tests.ps1 @@ -0,0 +1,15 @@ +# Read-only Windows smoke test: requires administrator or audit-policy query permission. +$ErrorActionPreference = 'Stop' +Import-Module (Join-Path $PSScriptRoot '../modules/AuditProfiles.psm1') -Force +$current = Get-WelaEffectiveAuditPolicy +$catalog = (Import-WelaAuditProfiles).catalog +if ($current.Count -ne $catalog.Count) { throw "Native API returned $($current.Count) policies; expected $($catalog.Count)." } +foreach ($policy in $catalog) { + if (-not $current.ContainsKey($policy.guid) -or $current[$policy.guid] -notin @(0, 1, 2, 3)) { + throw "Missing/invalid effective state: $($policy.id)" + } +} +$context = Get-WelaHostContext +$plan = Get-WelaAuditProfilePlan -Profile wela-2.2.0 -Role $context.Role -Build $context.Build -Current $current +Assert-WelaAuditProfileTarget -Plan $plan -Context $context -Current $current +Write-Host "PASS: queried all $($current.Count) effective audit policies on $($context.Role) build $($context.Build); no settings changed." From bf82f2c458cf1fa34c7fd79749f0b211c5d63c98 Mon Sep 17 00:00:00 2001 From: Shirofune-Security <43838376+Shirofune-Security@users.noreply.github.com> Date: Fri, 18 Sep 2026 21:56:01 +0900 Subject: [PATCH 07/21] Clear expected child failure codes after regression assertions --- tests/Test-ConfigurationReadOnlyWindows.ps1 | 3 +++ tests/Test-ConfigurationResults.ps1 | 3 +++ 2 files changed, 6 insertions(+) diff --git a/tests/Test-ConfigurationReadOnlyWindows.ps1 b/tests/Test-ConfigurationReadOnlyWindows.ps1 index 87597fa9..a95f35d3 100644 --- a/tests/Test-ConfigurationReadOnlyWindows.ps1 +++ b/tests/Test-ConfigurationReadOnlyWindows.ps1 @@ -13,4 +13,7 @@ catch { $caught = $_.ToString() } if ($caught -notmatch 'exit: 9' -or $caught -notmatch 'WELA-smoke-diagnostic') { throw "Native exit/stderr capture failed: $caught" } +# The intentionally failed child was asserted above; do not leak its expected +# exit code into a CI shell wrapper after a successful smoke test. +$global:LASTEXITCODE = 0 Write-Host "Read-only Windows smoke checks passed (process creation audit mask: $mask). No Windows settings changed." diff --git a/tests/Test-ConfigurationResults.ps1 b/tests/Test-ConfigurationResults.ps1 index f7b7b954..2e552e56 100644 --- a/tests/Test-ConfigurationResults.ps1 +++ b/tests/Test-ConfigurationResults.ps1 @@ -166,6 +166,9 @@ try { $encoded = [Convert]::ToBase64String([Text.Encoding]::Unicode.GetBytes($child)) $childOutput = @(& $engine -NoProfile -EncodedCommand $encoded 2>&1) $childExit = $global:LASTEXITCODE + # GitHub's PowerShell wrapper propagates LASTEXITCODE after the script. This + # child was deliberately failed; assertions below decide the test outcome. + $global:LASTEXITCODE = 0 Assert ($childExit -eq 1) 'The actual configure dispatcher returns nonzero for a failed control report' # CA-specific wrapper: registry read succeeds, certutil succeeds, restart From f2dc28a66bcaa0732aebc2d1731d1dd8879ff5d0 Mon Sep 17 00:00:00 2001 From: Shirofune-Security <43838376+Shirofune-Security@users.noreply.github.com> Date: Fri, 18 Sep 2026 21:58:06 +0900 Subject: [PATCH 08/21] Test composed NTLM policy journaling dry runs and failures --- .github/workflows/configuration-results.yml | 6 + docs/configuration-results.md | 13 ++ tests/IntegrationNtlmConfiguration.Tests.ps1 | 153 +++++++++++++++++++ 3 files changed, 172 insertions(+) create mode 100644 tests/IntegrationNtlmConfiguration.Tests.ps1 diff --git a/.github/workflows/configuration-results.yml b/.github/workflows/configuration-results.yml index 6da70e8e..cce1ace3 100644 --- a/.github/workflows/configuration-results.yml +++ b/.github/workflows/configuration-results.yml @@ -23,3 +23,9 @@ jobs: - name: Read-only Windows smoke in PowerShell 7 shell: pwsh run: ./tests/Test-ConfigurationReadOnlyWindows.ps1 + - name: NTLM integration in Windows PowerShell 5.1 + shell: powershell + run: ./tests/IntegrationNtlmConfiguration.Tests.ps1 + - name: NTLM integration in PowerShell 7 + shell: pwsh + run: ./tests/IntegrationNtlmConfiguration.Tests.ps1 diff --git a/docs/configuration-results.md b/docs/configuration-results.md index 419cf112..c2c37646 100644 --- a/docs/configuration-results.md +++ b/docs/configuration-results.md @@ -100,3 +100,16 @@ read-back, idempotence, final drift, dry runs, journal failure, locale-independe and a child `cmd.exe` diagnostic/exit test. CI runs both scripts in Windows PowerShell 5.1 and PowerShell 7. Mutating behavior still requires isolated Windows/CA lab validation; mock and read-only tests do not establish end-to-end event production. + +## NTLM policy integration + +Outgoing and domain NTLM decisions use the same configuration context. `-DryRun` +prevents both writes, and actual changes are journaled with their original registry +types before execution. Applied values participate in the final drift check. +`PreserveOrAudit` preserves an existing outgoing deny (`2`) and unknown numeric +values, recording the reason as `Skipped`; explicit `Audit` and `Deny` remain +available through `-OutgoingNtlmMode`. Non-DC domain auditing is `Skipped`. +Unknown domain role, unreadable policy and failed writes produce `Failed` outcomes +and a nonzero overall result while allowing other controls to be assessed. +`tests/IntegrationNtlmConfiguration.Tests.ps1` exercises this composed behavior +using mocked registry/CIM calls and temporary journals only. diff --git a/tests/IntegrationNtlmConfiguration.Tests.ps1 b/tests/IntegrationNtlmConfiguration.Tests.ps1 new file mode 100644 index 00000000..f5e546e4 --- /dev/null +++ b/tests/IntegrationNtlmConfiguration.Tests.ps1 @@ -0,0 +1,153 @@ +# Composed #362/#363/#365 behavior, using mock registry/CIM and temporary journals only. +$ErrorActionPreference = 'Stop' +$repo = Split-Path $PSScriptRoot -Parent +$script:ScriptRoot = $repo +. (Join-Path $repo 'scripts/Configuration.ps1') +$tokens = $null; $errors = $null +$ast = [Management.Automation.Language.Parser]::ParseFile((Join-Path $repo 'WELA.ps1'), [ref]$tokens, [ref]$errors) +if ($errors.Count) { throw ($errors | Out-String) } +foreach ($name in @('Get-WelaOutgoingNtlmPolicySource', 'Get-WelaOutgoingNtlmState', 'Set-WelaOutgoingNtlmPolicy', 'Get-WelaDomainNtlmState', 'Set-WelaDomainNtlmAudit')) { + $function = $ast.Find({ param($node) $node -is [Management.Automation.Language.FunctionDefinitionAst] -and $node.Name -eq $name }, $true) + . ([scriptblock]::Create($function.Extent.Text)) +} +$script:assertions = 0 +$script:contexts = New-Object 'System.Collections.Generic.List[object]' +function Assert($Condition, [string]$Message) { + if (-not $Condition) { throw "FAIL: $Message" } + $script:assertions++ +} +function New-TestContext([switch]$DryRun) { + $path = Join-Path ([IO.Path]::GetTempPath()) ('wela-ntlm-integration-' + [guid]::NewGuid().ToString('N')) + $context = New-WelaConfigurationContext -Auto -DryRun:$DryRun -BackupPath $path + $script:contexts.Add($context) + $script:currentContext = $context + return $context +} +function Reset-Mocks($Outgoing = 0, $Domain = 2, $ProductType = 2) { + $script:registry = @{ RestrictSendingNTLMTraffic = $Outgoing; AuditNTLMInDomain = $Domain } + $script:productType = $ProductType + $script:writes = 0; $script:readFails = $false; $script:writeFails = '' + $script:roleFails = $false +} +function global:Get-CimInstance { + param($ClassName, $Property, $Namespace, $ErrorAction) + if ($ClassName -eq 'Win32_OperatingSystem') { + if ($script:roleFails) { throw 'Mock role query failure' } + return [pscustomobject]@{ ProductType = $script:productType } + } +} +function global:Test-Path { + param($LiteralPath, $Path, $ErrorAction) + $target = if ($LiteralPath) { $LiteralPath } else { $Path } + if ($target -like 'HKLM:*') { return $true } + Microsoft.PowerShell.Management\Test-Path -LiteralPath $target +} +function global:Get-ItemProperty { + param($LiteralPath, $ErrorAction) + if ($script:readFails) { throw 'Mock registry read failure' } + return [pscustomobject]$script:registry +} +function global: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' } +} +function global:Set-ItemProperty { + param($LiteralPath, $Name, $Value, $Type, $ErrorAction) + # Assert the actual mutation cannot run before its matching journal entry. + $journal = Join-Path $script:currentContext.BackupPath 'before.jsonl' + if (-not (Microsoft.PowerShell.Management\Test-Path -LiteralPath $journal)) { throw 'Mutation occurred before journal existed' } + $entries = @(Get-Content -LiteralPath $journal | ConvertFrom-Json) + if ($entries[-1].Target.Name -ne $Name) { throw 'Mutation occurred before its own journal entry' } + if ($script:writeFails -eq $Name) { throw 'Mock NTLM write failure' } + $script:writes++ + $script:registry[$Name] = $Value +} +try { + Reset-Mocks + $context = New-TestContext -DryRun + Set-WelaOutgoingNtlmPolicy -Context $context + Set-WelaDomainNtlmAudit -Context $context + Assert ($script:writes -eq 0) 'Both NTLM controls honor shared DryRun' + Assert ($context.Results.Count -eq 2 -and @($context.Results | Where-Object Status -ne Skipped).Count -eq 0) 'Both dry-run changes are reported as skipped' + Assert (-not (Microsoft.PowerShell.Management\Test-Path -LiteralPath $context.BackupPath)) 'NTLM dry run creates no journal or backup directory' + + Reset-Mocks 2 + $context = New-TestContext + Set-WelaOutgoingNtlmPolicy -Context $context + Assert ($script:writes -eq 0 -and $script:registry.RestrictSendingNTLMTraffic -eq 2) 'Context Auto preserves existing deny' + Assert ($context.Results[0].Status -eq 'Skipped' -and $context.Results[0].Diagnostic -match 'Deny all enforcement') 'Preserved enforcement is explicit in results' + + Reset-Mocks 42 + $context = New-TestContext + Set-WelaOutgoingNtlmPolicy -Context $context + Assert ($script:writes -eq 0 -and $context.Results[0].Status -eq 'Skipped') 'Unknown outgoing value is preserved and reported' + + Reset-Mocks + $context = New-TestContext + Set-WelaOutgoingNtlmPolicy -Context $context + Set-WelaDomainNtlmAudit -Context $context + $result = Complete-WelaConfiguration -Context $context + Assert ($script:registry.RestrictSendingNTLMTraffic -eq 1 -and $script:registry.AuditNTLMInDomain -eq 7) 'Context applies audit-only outgoing and DC Enable all' + Assert ($script:writes -eq 2 -and $result.ExitCode -eq 0) 'Both actual writes are verified successfully' + $journal = @(Get-Content -LiteralPath (Join-Path $context.BackupPath 'before.jsonl') | ConvertFrom-Json) + Assert ($journal.Count -eq 2 -and $journal[0].Before.Value -eq 0 -and $journal[1].Before.Value -eq 2) 'Journal records exact values before both NTLM changes' + Assert ($journal[0].Before.Type -eq 'DWord' -and $journal[1].Desired.Value -eq 7) 'Journal retains registry type and requested domain value' + Set-WelaOutgoingNtlmPolicy -Context $context + Set-WelaDomainNtlmAudit -Context $context + Assert ($script:writes -eq 2) 'Verified NTLM controls are idempotent' + $script:registry.AuditNTLMInDomain = 0 + Assert ((Complete-WelaConfiguration $context).ExitCode -eq 1) 'Final verification aggregates later NTLM drift' + + Reset-Mocks 2 + $context = New-TestContext + Set-WelaOutgoingNtlmPolicy -Context $context -Mode Audit + Assert ($script:registry.RestrictSendingNTLMTraffic -eq 1) 'Explicit Audit override goes through shared journal and verification' + Set-WelaOutgoingNtlmPolicy -Context $context -Mode Deny + Assert ($script:registry.RestrictSendingNTLMTraffic -eq 2) 'Explicit Deny remains a separate operator choice' + + Reset-Mocks 0 2 3 + $context = New-TestContext + Set-WelaDomainNtlmAudit -Context $context + Assert ($script:writes -eq 0 -and $context.Results[0].Status -eq 'Skipped') 'Non-DC domain policy is skipped' + Assert ($context.Results[0].Diagnostic -match 'Not applicable') 'Non-DC reason is explicit' + + Reset-Mocks + $script:roleFails = $true + $context = New-TestContext + Set-WelaDomainNtlmAudit -Context $context + Set-WelaOutgoingNtlmPolicy -Context $context + $result = Complete-WelaConfiguration $context + Assert ($result.ExitCode -eq 1 -and $result.Failed -eq 1) 'Unknown role produces an aggregated failure' + Assert ($script:registry.RestrictSendingNTLMTraffic -eq 1) 'Other controls continue after unknown domain role' + + Reset-Mocks + $script:readFails = $true + $context = New-TestContext + Set-WelaOutgoingNtlmPolicy -Context $context + Set-WelaDomainNtlmAudit -Context $context + $result = Complete-WelaConfiguration $context + Assert ($result.ExitCode -eq 1 -and $result.Failed -eq 2 -and $script:writes -eq 0) 'Both unreadable NTLM states aggregate as failures with no writes' + + Reset-Mocks + $script:writeFails = 'RestrictSendingNTLMTraffic' + $context = New-TestContext + Set-WelaOutgoingNtlmPolicy -Context $context + Set-WelaDomainNtlmAudit -Context $context + $result = Complete-WelaConfiguration $context + Assert ($result.ExitCode -eq 1 -and $result.Failed -eq 1) 'NTLM write failure aggregates in final exit code' + Assert ($script:registry.AuditNTLMInDomain -eq 7) 'A failed outgoing control does not prevent domain configuration' + + Reset-Mocks + $context = New-TestContext + Set-WelaOutgoingNtlmPolicy -Context $context -WhatIf + Set-WelaDomainNtlmAudit -Context $context -WhatIf + Assert ($script:writes -eq 0) 'Context adapters also preserve standalone WhatIf behavior' + Write-Host "PASS: $script:assertions NTLM integration assertions (mocked; no Windows changes)." +} finally { + foreach ($context in $script:contexts) { + if (Microsoft.PowerShell.Management\Test-Path -LiteralPath $context.BackupPath) { + Remove-Item -LiteralPath $context.BackupPath -Recurse -Force + } + } +} From 5be186f952792935f27d57ebc8b19138b4252b16 Mon Sep 17 00:00:00 2001 From: Shirofune-Security <43838376+Shirofune-Security@users.noreply.github.com> Date: Fri, 18 Sep 2026 21:58:33 +0900 Subject: [PATCH 09/21] Add six missing native audit subcategories with source-specific masks --- .../workflows/test-native-audit-controls.yml | 19 ++++++ config/audit_profiles.json | 65 +++++++++++++++++- docs/audit-profiles.md | 2 +- tests/NativeAuditControls.Tests.ps1 | 68 +++++++++++++++++++ .../docs/commands/native-audit-controls.md | 45 ++++++++++++ 5 files changed, 195 insertions(+), 4 deletions(-) create mode 100644 .github/workflows/test-native-audit-controls.yml create mode 100644 tests/NativeAuditControls.Tests.ps1 create mode 100644 website/docs/commands/native-audit-controls.md diff --git a/.github/workflows/test-native-audit-controls.yml b/.github/workflows/test-native-audit-controls.yml new file mode 100644 index 00000000..02faeac6 --- /dev/null +++ b/.github/workflows/test-native-audit-controls.yml @@ -0,0 +1,19 @@ +name: Native audit control 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: Verify native policy masks and dependencies (Windows PowerShell 5.1) + shell: powershell + run: ./tests/NativeAuditControls.Tests.ps1 + - name: Verify native policy masks and dependencies (PowerShell 7) + shell: pwsh + run: ./tests/NativeAuditControls.Tests.ps1 diff --git a/config/audit_profiles.json b/config/audit_profiles.json index 4a467042..cc16eb39 100644 --- a/config/audit_profiles.json +++ b/config/audit_profiles.json @@ -98,7 +98,7 @@ "DomainController", "ADCS" ], - "prerequisites": "" + "prerequisites": "Only generates application-group management evidence when the corresponding application-group activity occurs." }, { "id": "Computer Account Management", @@ -509,7 +509,7 @@ "DomainController", "ADCS" ], - "prerequisites": "A matching kernel-object SACL is required; enabling the subcategory alone does not generate object events." + "prerequisites": "Requires a matching target-object SACL and access rights/type. Enabling this audit policy does not install kernel-object SACLs or guarantee detection coverage." }, { "id": "Other Object Access Events", @@ -906,10 +906,69 @@ "mode": "optional", "mask": 3, "note": "Opt-in subcategory prerequisite only. Use configure-sacl separately for targeted file/registry SACLs." + }, + "Group Membership": { + "mode": "exact", + "mask": 1, + "sourceIds": [ + "ms-sct", + "cis-client", + "cis-server", + "asd" + ], + "evidence": "CIS v4 17.5.2; Microsoft SCT; ASD native Group Membership Success." + }, + "Application Group Management": { + "mode": "exact", + "mask": 3, + "sourceIds": [ + "cis-client", + "cis-server" + ], + "evidence": "CIS v4 17.2.1: Success and Failure." + }, + "Authorization Policy Change": { + "mode": "exact", + "mask": 1, + "sourceIds": [ + "cis-client", + "cis-server", + "ms-sct" + ], + "evidence": "CIS v4 17.7.3 and Server 2025 SCT: Success. The separate WEF profile retains Success and Failure." + }, + "MPSSVC Rule-Level Policy Change": { + "mode": "exact", + "mask": 3, + "sourceIds": [ + "ms-sct", + "cis-client", + "cis-server", + "ms-wef" + ], + "evidence": "CIS v4 17.7.4; Microsoft SCT and WEF: Success and Failure." + }, + "IPsec Driver": { + "mode": "exact", + "mask": 3, + "sourceIds": [ + "cis-client", + "cis-server", + "ms-audit" + ], + "evidence": "CIS v4 17.9.1 and Microsoft generic audit recommendation: Success and Failure." + }, + "Kernel Object": { + "mode": "exact", + "mask": 3, + "sourceIds": [ + "asd" + ], + "evidence": "ASD native audit policy: Success and Failure; matching object SACLs are a separate prerequisite." } }, "roleOverrides": {}, - "note": "Shared replacement for the 34-policy configure list. Policies irrelevant to the selected role are not applied. The three targeted SACL prerequisites are opt-in." + "note": "WELA native audit-policy profile: original configure controls plus six source-backed baseline gaps. Role-specific controls are not applied outside their roles. File/Registry/Handle optional policies and object SACL dependencies remain separate. Source profiles retain their own exact/minimum masks." }, { "id": "windows-defaults-reviewed-2026-09", diff --git a/docs/audit-profiles.md b/docs/audit-profiles.md index 8437ad62..5710c9d5 100644 --- a/docs/audit-profiles.md +++ b/docs/audit-profiles.md @@ -31,7 +31,7 @@ The WELA and documentary guide profiles currently cover the reviewed Windows 11/ | Profile | Version / meaning | | --- | --- | -| `wela-2.2.0` | Reviewed WELA development snapshot `8ef938f0966e86adc527395f50f907c43e843d1e`; retains the 34 existing success/failure policies, with irrelevant roles skipped and three SACL prerequisites optional | +| `wela-2.2.0` | Reviewed WELA development snapshot `8ef938f0966e86adc527395f50f907c43e843d1e`; extends the 34 existing policies with [six native audit controls](../website/docs/commands/native-audit-controls.md), with irrelevant roles skipped and three SACL prerequisites optional | | `windows-defaults-reviewed-2026-09` | Documentary effective-default model; **reference only**, cannot be applied or used to reset an OS | | `microsoft-sct-win11-24h2`, `microsoft-sct-win11-25h2` | Official SCT Policy Analyzer settings, exact masks | | `microsoft-sct-server2022`, `microsoft-sct-server2025-2602` | Official SCT member/DC settings; AD CS uses the member-server baseline | diff --git a/tests/NativeAuditControls.Tests.ps1 b/tests/NativeAuditControls.Tests.ps1 new file mode 100644 index 00000000..d2002dd5 --- /dev/null +++ b/tests/NativeAuditControls.Tests.ps1 @@ -0,0 +1,68 @@ +# Pure policy regressions. No Windows settings are read or changed. +$ErrorActionPreference = 'Stop' +Import-Module (Join-Path $PSScriptRoot '../modules/AuditProfiles.psm1') -Force +$config = Import-WelaAuditProfiles +$assertions = 0 +function Assert-Equal($Actual, $Expected, [string]$Message) { + if ($Actual -cne $Expected) { throw "$Message : expected '$Expected', got '$Actual'." } + $script:assertions++ +} +$expected = @{ + 'Group Membership' = @{ Guid = '0CCE9249-69AE-11D9-BED3-505054503030'; Mask = 1 } + 'Application Group Management' = @{ Guid = '0CCE9239-69AE-11D9-BED3-505054503030'; Mask = 3 } + 'Authorization Policy Change' = @{ Guid = '0CCE9231-69AE-11D9-BED3-505054503030'; Mask = 1 } + 'MPSSVC Rule-Level Policy Change' = @{ Guid = '0CCE9232-69AE-11D9-BED3-505054503030'; Mask = 3 } + 'IPsec Driver' = @{ Guid = '0CCE9213-69AE-11D9-BED3-505054503030'; Mask = 3 } + 'Kernel Object' = @{ Guid = '0CCE921F-69AE-11D9-BED3-505054503030'; Mask = 3 } +} +$current = @{} +foreach ($policy in $config.catalog) { $current[$policy.guid] = 0 } +foreach ($role in @('Client', 'MemberServer', 'DomainController', 'ADCS')) { + $plan = Get-WelaAuditProfilePlan -Profile 'wela-2.2.0' -Role $role -Build 26100 -Current $current + foreach ($name in $expected.Keys) { + $row = @($plan.policies | Where-Object { $_.id -eq $name }) + Assert-Equal $row.Count 1 "$role/$name has exactly one plan row" + Assert-Equal $row[0].guid $expected[$name].Guid "$role/$name canonical GUID" + Assert-Equal $row[0].targetMask $expected[$name].Mask "$role/$name applies the intended mask" + Assert-Equal ($row[0].sourceIds.Count -gt 0) $true "$role/$name retains provenance" + } + $kernel = $plan.policies | Where-Object { $_.id -eq 'Kernel Object' } + Assert-Equal ($kernel.prerequisites -match 'SACL') $true "$role kernel auditing reports object-SACL dependency" +} +# The WELA extension must not overwrite another guide's semantics. +$inherited = $current.Clone() +$inherited[$expected['Group Membership'].Guid] = 2 +$inherited[$expected['Authorization Policy Change'].Guid] = 2 +$cis = Get-WelaAuditProfilePlan -Profile 'cis-win11-v4-l1' -Role Client -Build 26100 -Current $inherited +foreach ($name in @('Group Membership', 'Authorization Policy Change')) { + $row = $cis.policies | Where-Object { $_.id -eq $name } + Assert-Equal $row.mode 'minimum' "CIS $name is a minimum" + Assert-Equal $row.targetMask 3 "CIS $name preserves inherited failure auditing" +} +$wef = Get-WelaAuditProfilePlan -Profile 'microsoft-wef-reviewed-2026-09' -Role Client -Build 26100 -Current $current +Assert-Equal (($wef.policies | Where-Object { $_.id -eq 'Authorization Policy Change' }).targetMask) 3 'WEF authorization auditing retains both outcomes' +$server = Get-WelaAuditProfilePlan -Profile 'microsoft-sct-server2025-2602' -Role MemberServer -Build 26100 -Current $current +Assert-Equal (($server.policies | Where-Object { $_.id -eq 'Authorization Policy Change' }).targetMask) 1 'Server 2025 SCT authorization target remains success' +$asd = Get-WelaAuditProfilePlan -Profile 'asd-native-2021-10' -Role Client -Build 26100 -Current $current +Assert-Equal (($asd.policies | Where-Object { $_.id -eq 'Kernel Object' }).targetMask) 3 'ASD kernel target remains both outcomes' +Assert-Equal (($asd.policies | Where-Object { $_.id -eq 'Detailed File Share' }).mode) 'not-configured' 'ASD detailed-share setting is not silently enabled' +# Exercise the actual shared apply path using an in-memory provider. +$script:policyState = $current.Clone() +$script:writes = @{} +$plan = Get-WelaAuditProfilePlan -Profile 'wela-2.2.0' -Role Client -Build 26100 -Current $script:policyState +$result = Invoke-WelaAuditProfilePlan -Plan $plan -ReadPolicy { $script:policyState } -WritePolicy { + param($Guid, $Mask) + $script:policyState[$Guid] = $Mask + $script:writes[$Guid] = $Mask +} -ReadContext { [pscustomobject]@{ Role = 'Client'; Build = 26100 } } -Confirm:$false +Assert-Equal $result.success $true 'Shared apply reports verified success with matching readback' +foreach ($name in $expected.Keys) { + Assert-Equal $script:writes[$expected[$name].Guid] $expected[$name].Mask "Apply requests correct $name mask" +} +$script:writes = @{} +$result = Invoke-WelaAuditProfilePlan -Plan $plan -ReadPolicy { $script:policyState } -WritePolicy { + param($Guid, $Mask) + $script:writes[$Guid] = $Mask +} -ReadContext { [pscustomobject]@{ Role = 'Client'; Build = 26100 } } -Confirm:$false +Assert-Equal $script:writes.Count 0 'Reapply is idempotent after all controls match' +Write-Host "PASS: $assertions native-audit-control assertions (mocked; no host changes)." diff --git a/website/docs/commands/native-audit-controls.md b/website/docs/commands/native-audit-controls.md new file mode 100644 index 00000000..a7e6a93a --- /dev/null +++ b/website/docs/commands/native-audit-controls.md @@ -0,0 +1,45 @@ +# Native audit controls and their prerequisites + +The WELA native profile supports the following audit subcategories in addition to +its original configure policy list. Source-specific profiles keep their own +success/failure masks; selecting a profile does not combine all guides globally. + +| Subcategory | WELA target | Other reviewed requirements | +| --- | --- | --- | +| Group Membership | Success | Microsoft SCT, CIS v4 and ASD native: Success; CIS specifies a minimum. | +| Application Group Management | Success and Failure | CIS v4 section 17.2.1: both outcomes. | +| Authorization Policy Change | Success | CIS v4 and Server 2025 SCT: Success; Microsoft WEF Appendix A: both outcomes. | +| MPSSVC Rule-Level Policy Change | Success and Failure | Microsoft SCT, CIS v4 and Microsoft WEF: both outcomes. | +| IPsec Driver | Success and Failure | CIS v4 and Microsoft generic audit guidance: both outcomes. | +| Kernel Object | Success and Failure | ASD native: both outcomes; matching object SACLs and access semantics are separate prerequisites. | + +The mask describes policy configuration, not a promise that every operation emits +both kinds of event. Event generation depends on Windows version, role, object +access, and whether the activity occurs. Application Group Management events are +only relevant when application groups are used. Group Membership events provide +logon group context; they do not substitute for Security Group Management events. +IPsec Driver auditing does not enable IPsec or define connection security rules. + +Enabling Kernel Object auditing does not create a matching audit ACE on every +object. The plan records this dependency; WELA must not count a rule as verified +solely because the auditpol setting is enabled. The existing `configure-sacl` +command handles selected file/registry targets, not arbitrary kernel objects or +AD directory objects. + +Use `plan` to inspect the selected profile and its source provenance before +applying it. The plan distinguishes exact and minimum masks, settings the source +leaves unconfigured, and controls that do not apply to the selected role/build. +Minimum Success or Failure requirements preserve the other effective audit bit. +The advanced-audit profile does not install Sysmon or change firewall enforcement. + +## Source versions + +- [Microsoft Security Compliance Toolkit](https://www.microsoft.com/en-us/download/details.aspx?id=55319): Windows 11 24H2/25H2 and Windows Server 2022/2025 v2602 packages. +- [Microsoft audit recommendations](https://learn.microsoft.com/en-us/windows-server/identity/ad-ds/plan/security-best-practices/audit-policy-recommendations). +- [Microsoft WEF Appendix A](https://learn.microsoft.com/en-us/windows/security/operating-system-security/device-management/use-windows-event-forwarding-to-assist-in-intrusion-detection). +- [ASD Windows event logging and forwarding](https://www.cyber.gov.au/business-government/detecting-responding-to-threats/event-logging/windows-event-logging-and-forwarding): 2021 publication, native fallback. +- CIS Windows 11 Enterprise and Windows Server 2022 **v4.0.0**: historical reviewed benchmarks, not a claim about current CIS requirements. The profile data records control numbers and source links. + +Tests exercise policy masks, source-profile differences and the object-auditing +dependency. They do not establish live event production or detection coverage; +validate those on the applicable Windows roles with representative benign events. From 98d37fbf3715ba4375b12239ea46dcf1d1f8cb54 Mon Sep 17 00:00:00 2001 From: Shirofune-Security <43838376+Shirofune-Security@users.noreply.github.com> Date: Fri, 18 Sep 2026 21:59:59 +0900 Subject: [PATCH 10/21] Retain policy prerequisites and evidence in apply results --- WELA.ps1 | 11 +++++++++++ modules/AuditProfiles.psm1 | 8 ++++++-- tests/audit-profiles.Tests.ps1 | 5 +++++ 3 files changed, 22 insertions(+), 2 deletions(-) diff --git a/WELA.ps1 b/WELA.ps1 index d423ccf2..541bb0e2 100644 --- a/WELA.ps1 +++ b/WELA.ps1 @@ -289,6 +289,15 @@ function Get-WelaSelectedContext { Get-WelaHostContext } +function Show-WelaAuditProfilePrerequisites { + param($Plan) + foreach ($policy in $Plan.policies) { + if ($policy.prerequisites -and ($policy.mode -in @('exact', 'minimum') -or ($policy.mode -eq 'optional' -and $Plan.includeOptional))) { + Write-Host "Prerequisite - $($policy.id): $($policy.prerequisites)" -ForegroundColor DarkYellow + } + } +} + function Invoke-WelaProfileCommand { param([string]$Command) if ($script:Baseline) { throw "Use -Profile or -Baseline, not both. Versioned profiles cover advanced audit policy only." } @@ -305,6 +314,7 @@ function Invoke-WelaProfileCommand { $plan = Get-WelaAuditProfilePlan -Profile $script:Profile -Role $context.Role -Build $context.Build -Current $current -IncludeOptional:$script:IncludeOptional Write-Host "Profile: $($plan.profile); role: $($plan.role); build: $($plan.build)" Write-Host "Scope: advanced audit policy only. Channels, command-line capture, PowerShell, NTLM, SACLs, CA AuditFilter and forwarding are separate." + Show-WelaAuditProfilePrerequisites -Plan $plan $result = $plan if ($Command -eq 'configure') { if (-not (TestAdministrator)) { throw "Configuring advanced audit policy requires Administrator privileges." } @@ -1393,6 +1403,7 @@ function ConfigureAuditSettings { # Audit and configure consume the same versioned policy definition. Write-Host "Configuring advanced audit policy from wela-2.2.0..." + Show-WelaAuditProfilePrerequisites -Plan $profilePlan $profileResult = Invoke-WelaAuditProfilePlan -Plan $profilePlan -Confirm:(-not $Auto) $profileResult.results | Format-Table id, beforeMask, targetMask, effectiveMask, status -AutoSize if (-not $profileResult.success) { throw "Advanced audit-policy configuration failed. Review effective-state results above." } diff --git a/modules/AuditProfiles.psm1 b/modules/AuditProfiles.psm1 index 1f7d101c..8a53aba3 100644 --- a/modules/AuditProfiles.psm1 +++ b/modules/AuditProfiles.psm1 @@ -240,10 +240,14 @@ function Invoke-WelaAuditProfilePlan { } catch { $status = 'Failed'; $errorText = $_.Exception.Message; $effective = $null } } else { $status = 'Skipped' } } - [pscustomobject]@{ id = $policy.id; guid = $policy.guid; mode = $policy.mode; beforeMask = $initial; targetMask = $target; effectiveMask = $effective; status = $status; error = $errorText } + [pscustomobject]@{ + id = $policy.id; guid = $policy.guid; mode = $policy.mode + beforeMask = $initial; targetMask = $target; effectiveMask = $effective; status = $status; error = $errorText + prerequisites = $policy.prerequisites; evidence = $policy.evidence; sourceIds = @($policy.sourceIds) + } } [pscustomobject]@{ - profile = $Plan.profile; scope = $Plan.scope; role = $Plan.role; build = $Plan.build + profile = $Plan.profile; version = $Plan.version; scope = $Plan.scope; role = $Plan.role; build = $Plan.build schemaSha256 = $Plan.schemaSha256; provenance = $Plan.provenance success = (@($results | Where-Object { $_.status -eq 'Failed' }).Count -eq 0) results = @($results) diff --git a/tests/audit-profiles.Tests.ps1 b/tests/audit-profiles.Tests.ps1 index 6d61d5b3..9700fc07 100644 --- a/tests/audit-profiles.Tests.ps1 +++ b/tests/audit-profiles.Tests.ps1 @@ -58,6 +58,9 @@ $writer = { param($Guid, $Mask) $script:Writes += $Guid; $script:State[$Guid] = $context = { [pscustomobject]@{ Role = 'Client'; Build = 26100 } } $applied = Invoke-WelaAuditProfilePlan -Plan $wela -ReadPolicy $reader -WritePolicy $writer -ReadContext $context -Confirm:$false Assert $applied.success 'apply succeeds after verified effective reads' +$processResult = $applied.results | Where-Object { $_.id -eq 'Process Creation' } +Assert ($processResult.prerequisites -match 'Command-line' -and $processResult.sourceIds -contains 'wela') 'apply results retain source and event-generation prerequisites' +Assert ($applied.version -eq $wela.version) 'apply result includes selected source version' Assert ($script:Writes.Count -gt 0) 'selected exact policies were applied' Assert (@($applied.results | Where-Object { $_.status -eq 'Applied' -and $_.effectiveMask -ne $_.targetMask }).Count -eq 0) 'applied always means verified' $count = $script:Writes.Count @@ -72,6 +75,8 @@ $failed = Invoke-WelaAuditProfilePlan -Plan $wela -ReadPolicy $reader -WritePoli Assert (-not $failed.success -and @($failed.results | Where-Object { $_.status -eq 'Failed' }).Count -gt 0) 'native failure is machine-readable' $mismatch = Invoke-WelaAuditProfilePlan -Plan $wela -ReadPolicy $reader -WritePolicy { param($Guid, $Mask) } -ReadContext $context -Confirm:$false Assert (-not $mismatch.success) 'zero exit without effective change does not count as success' +$failedProcess = $mismatch.results | Where-Object { $_.id -eq 'Process Creation' } +Assert ($failedProcess.status -eq 'Failed' -and $null -eq $failedProcess.effectiveMask -and $failedProcess.prerequisites -match 'Command-line') 'failed/unknown effective state still retains prerequisites' $script:Writes = @() $whatIf = Invoke-WelaAuditProfilePlan -Plan $wela -ReadPolicy $reader -WritePolicy $writer -ReadContext $context -WhatIf Assert ($script:Writes.Count -eq 0) 'WhatIf never invokes native writer' From a6cef75c126802ce59f657d6599e9a0f4bd6e988 Mon Sep 17 00:00:00 2001 From: Shirofune-Security <43838376+Shirofune-Security@users.noreply.github.com> Date: Fri, 18 Sep 2026 22:00:52 +0900 Subject: [PATCH 11/21] Verify source attribution and prerequisites in native control results --- tests/NativeAuditControls.Tests.ps1 | 20 +++++++++++++------- 1 file changed, 13 insertions(+), 7 deletions(-) diff --git a/tests/NativeAuditControls.Tests.ps1 b/tests/NativeAuditControls.Tests.ps1 index d2002dd5..34f815b3 100644 --- a/tests/NativeAuditControls.Tests.ps1 +++ b/tests/NativeAuditControls.Tests.ps1 @@ -8,12 +8,12 @@ function Assert-Equal($Actual, $Expected, [string]$Message) { $script:assertions++ } $expected = @{ - 'Group Membership' = @{ Guid = '0CCE9249-69AE-11D9-BED3-505054503030'; Mask = 1 } - 'Application Group Management' = @{ Guid = '0CCE9239-69AE-11D9-BED3-505054503030'; Mask = 3 } - 'Authorization Policy Change' = @{ Guid = '0CCE9231-69AE-11D9-BED3-505054503030'; Mask = 1 } - 'MPSSVC Rule-Level Policy Change' = @{ Guid = '0CCE9232-69AE-11D9-BED3-505054503030'; Mask = 3 } - 'IPsec Driver' = @{ Guid = '0CCE9213-69AE-11D9-BED3-505054503030'; Mask = 3 } - 'Kernel Object' = @{ Guid = '0CCE921F-69AE-11D9-BED3-505054503030'; Mask = 3 } + 'Group Membership' = @{ Guid = '0CCE9249-69AE-11D9-BED3-505054503030'; Mask = 1; Source = 'ms-sct' } + 'Application Group Management' = @{ Guid = '0CCE9239-69AE-11D9-BED3-505054503030'; Mask = 3; Source = 'cis-client' } + 'Authorization Policy Change' = @{ Guid = '0CCE9231-69AE-11D9-BED3-505054503030'; Mask = 1; Source = 'cis-client' } + 'MPSSVC Rule-Level Policy Change' = @{ Guid = '0CCE9232-69AE-11D9-BED3-505054503030'; Mask = 3; Source = 'ms-wef' } + 'IPsec Driver' = @{ Guid = '0CCE9213-69AE-11D9-BED3-505054503030'; Mask = 3; Source = 'ms-audit' } + 'Kernel Object' = @{ Guid = '0CCE921F-69AE-11D9-BED3-505054503030'; Mask = 3; Source = 'asd' } } $current = @{} foreach ($policy in $config.catalog) { $current[$policy.guid] = 0 } @@ -24,7 +24,7 @@ foreach ($role in @('Client', 'MemberServer', 'DomainController', 'ADCS')) { Assert-Equal $row.Count 1 "$role/$name has exactly one plan row" Assert-Equal $row[0].guid $expected[$name].Guid "$role/$name canonical GUID" Assert-Equal $row[0].targetMask $expected[$name].Mask "$role/$name applies the intended mask" - Assert-Equal ($row[0].sourceIds.Count -gt 0) $true "$role/$name retains provenance" + Assert-Equal ($row[0].sourceIds -contains $expected[$name].Source) $true "$role/$name retains provenance" } $kernel = $plan.policies | Where-Object { $_.id -eq 'Kernel Object' } Assert-Equal ($kernel.prerequisites -match 'SACL') $true "$role kernel auditing reports object-SACL dependency" @@ -56,6 +56,12 @@ $result = Invoke-WelaAuditProfilePlan -Plan $plan -ReadPolicy { $script:policySt $script:writes[$Guid] = $Mask } -ReadContext { [pscustomobject]@{ Role = 'Client'; Build = 26100 } } -Confirm:$false Assert-Equal $result.success $true 'Shared apply reports verified success with matching readback' +$kernelResult = $result.results | Where-Object { $_.id -eq 'Kernel Object' } +Assert-Equal ($kernelResult.prerequisites -match 'SACL') $true 'Apply result retains kernel SACL prerequisite' +Assert-Equal ($kernelResult.sourceIds -contains 'asd') $true 'Apply result identifies ASD kernel requirement' +Assert-Equal ($kernelResult.evidence -match 'ASD') $true 'Apply result retains source evidence' +Assert-Equal $result.role 'Client' 'Apply result retains selected role' +Assert-Equal $result.build 26100 'Apply result retains selected build' foreach ($name in $expected.Keys) { Assert-Equal $script:writes[$expected[$name].Guid] $expected[$name].Mask "Apply requests correct $name mask" } From d3160a2033a10ec9772e3d6991334d0878bdc6db Mon Sep 17 00:00:00 2001 From: Shirofune-Security <43838376+Shirofune-Security@users.noreply.github.com> Date: Fri, 18 Sep 2026 22:04:43 +0900 Subject: [PATCH 12/21] Preserve additional minimum audit flags and reject unknown CA roles --- docs/audit-profiles.md | 4 +- modules/AuditProfiles.psm1 | 88 ++++++++++++++++++++++++++-------- tests/audit-profiles.Tests.ps1 | 45 +++++++++++++++++ 3 files changed, 114 insertions(+), 23 deletions(-) diff --git a/docs/audit-profiles.md b/docs/audit-profiles.md index 8437ad62..f63f494e 100644 --- a/docs/audit-profiles.md +++ b/docs/audit-profiles.md @@ -23,7 +23,7 @@ .\WELA.ps1 configure -Profile asd-native-2021-10 -IncludeOptional -PlanPath result.json ``` -Supply both `-Role` and `-Build`, or omit both for Windows host detection. Roles are `Client`, `MemberServer`, `DomainController`, and `ADCS` (CA on a member server). Combined DC/CA deployments are not a separate profile: detection identifies them as DCs; review CA requirements separately. Build means the base build, for example 20348 (Server 2022), 26100 (Windows 11 24H2 / Server 2025), or 26200 (Windows 11 25H2). Live application checks the actual Windows host; a supplied role/build cannot authorize applying a mismatched plan. Versioned SCT profiles reject other base builds. Unsupported profiles/hosts and unreadable policies fail before writes. +Supply both `-Role` and `-Build`, or omit both for Windows host detection. Roles are `Client`, `MemberServer`, `DomainController`, and `ADCS` (CA on a member server). Combined DC/CA deployments are not supported by these role profiles: host detection refuses them before configuration writes, rather than silently omitting CA auditing. A failure to read the CA installation state is also an error, not evidence of a member server without CA. Build means the base build, for example 20348 (Server 2022), 26100 (Windows 11 24H2 / Server 2025), or 26200 (Windows 11 25H2). Live application checks the actual Windows host; a supplied role/build cannot authorize applying a mismatched plan. Versioned SCT profiles reject other base builds. Unsupported profiles/hosts and unreadable policies fail before writes. The WELA and documentary guide profiles currently cover the reviewed Windows 11/Server 2022/Server 2025 range. Older/future operating systems require a reviewed applicability update. `-Baseline` retains the legacy display interface for non-Yamato guides; use `-Profile` to select the versioned shared definitions. Do not combine `-Baseline` and `-Profile`. @@ -59,7 +59,7 @@ Mask bits are Success `1`, Failure `2`, both `3`, neither `0`. For example Detailed File Share is exact S+F in WELA, minimum Failure in the reviewed CIS profiles, and Not Configured in ASD. These are deliberate differences, not a universal “enable everything” preset. Omitted values and unknown effective state are different: unknown state blocks application instead of becoming mask zero. -Effective policy is read through the Windows [AuditQuerySystemPolicy API](https://learn.microsoft.com/en-us/windows/win32/api/ntsecapi/nf-ntsecapi-auditquerysystempolicy); localized `auditpol /get /r` text is not parsed. Apply reads effective state again, checks native command errors, then verifies each changed mask. A command that exits successfully but does not change effective policy is reported as failed. Group Policy can reapply after a successful verification: these are local effective-policy changes, not GPO authoring. Exported plan/current state and apply results include the profile version, schema SHA-256, source provenance, before/target/effective masks and failure details. This feature does not validate event generation, SACL correctness, ingestion, or Sigma field compatibility. +Effective policy is read through the Windows [AuditQuerySystemPolicy API](https://learn.microsoft.com/en-us/windows/win32/api/ntsecapi/nf-ntsecapi-auditquerysystempolicy); localized `auditpol /get /r` text is not parsed. Apply reads effective state immediately before each control, checks native command errors, then verifies each changed policy. Minimum policies only enable required native flags, never disable additional flags, and accept any effective state containing the required bits. Exact policies require the exact mask. A command that exits successfully but does not change effective policy is reported as failed. Group Policy can reapply after a successful verification: these are local effective-policy changes, not GPO authoring. Exported plan/current state and apply results include the profile version, schema SHA-256, source provenance, before/target/effective masks and failure details. This feature does not validate event generation, SACL correctness, ingestion, or Sigma field compatibility. ## Extending the schema and testing diff --git a/modules/AuditProfiles.psm1 b/modules/AuditProfiles.psm1 index 8a53aba3..445e497a 100644 --- a/modules/AuditProfiles.psm1 +++ b/modules/AuditProfiles.psm1 @@ -177,26 +177,58 @@ namespace Wela.AuditProfiles { return $current } +function Get-WelaAuditSetArguments { + param( + [ValidatePattern('^[0-9A-Fa-f]{8}(-[0-9A-Fa-f]{4}){3}-[0-9A-Fa-f]{12}$')][string]$Guid, + [ValidateRange(0, 3)][int]$Mask, + [ValidateSet('exact', 'minimum')][string]$Mode = 'exact' + ) + $arguments = @('/set', "/subcategory:{$Guid}") + if ($Mode -eq 'minimum') { + # Only enable required bits; never clear another actor's newly enabled bit. + if ($Mask -band 1) { $arguments += '/success:enable' } + if ($Mask -band 2) { $arguments += '/failure:enable' } + } else { + $arguments += if ($Mask -band 1) { '/success:enable' } else { '/success:disable' } + $arguments += if ($Mask -band 2) { '/failure:enable' } else { '/failure:disable' } + } + return $arguments +} + function Set-WelaEffectiveAuditPolicy { - param([ValidatePattern('^[0-9A-Fa-f]{8}(-[0-9A-Fa-f]{4}){3}-[0-9A-Fa-f]{12}$')][string]$Guid, [ValidateRange(0, 3)][int]$Mask) - $success = if ($Mask -band 1) { 'enable' } else { 'disable' } - $failure = if ($Mask -band 2) { 'enable' } else { 'disable' } - $output = & auditpol.exe /set "/subcategory:{$Guid}" "/success:$success" "/failure:$failure" 2>&1 + param( + [ValidatePattern('^[0-9A-Fa-f]{8}(-[0-9A-Fa-f]{4}){3}-[0-9A-Fa-f]{12}$')][string]$Guid, + [ValidateRange(0, 3)][int]$Mask, + [ValidateSet('exact', 'minimum')][string]$Mode = 'exact' + ) + 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 ' ')" } } function Get-WelaHostContext { [CmdletBinding()] - param() - $os = Get-CimInstance -ClassName Win32_OperatingSystem -ErrorAction Stop - $system = Get-CimInstance -ClassName Win32_ComputerSystem -ErrorAction Stop + param( + [scriptblock]$ReadOperatingSystem = { Get-CimInstance -ClassName Win32_OperatingSystem -ErrorAction Stop }, + [scriptblock]$ReadComputerSystem = { Get-CimInstance -ClassName Win32_ComputerSystem -ErrorAction Stop }, + [scriptblock]$ReadCertificateAuthority = { Test-Path 'HKLM:\SYSTEM\CurrentControlSet\Services\CertSvc\Configuration' -ErrorAction Stop } + ) + $os = & $ReadOperatingSystem + $system = & $ReadComputerSystem if ([int]$os.ProductType -notin @(1, 2, 3) -or [int]$system.DomainRole -notin @(0, 1, 2, 3, 4, 5) -or [int]$os.BuildNumber -le 0) { throw 'Cannot determine a valid Windows role/build.' } if (([int]$os.ProductType -eq 1 -and [int]$system.DomainRole -notin @(0, 1)) -or ([int]$os.ProductType -eq 2 -and [int]$system.DomainRole -notin @(4, 5)) -or ([int]$os.ProductType -eq 3 -and [int]$system.DomainRole -notin @(2, 3))) { throw 'Windows ProductType and DomainRole disagree.' } + $hasCA = $false + if ([int]$os.ProductType -ne 1) { + $hasCA = & $ReadCertificateAuthority + if ($hasCA -isnot [bool]) { throw 'Cannot determine whether Certificate Services is installed.' } + if ($hasCA -and [int]$system.DomainRole -in @(4, 5)) { throw 'Combined domain-controller/CA hosts are unsupported by the current role profiles. No configuration should be applied.' } + } $role = if ([int]$os.ProductType -eq 1) { 'Client' } elseif ([int]$system.DomainRole -in @(4, 5)) { 'DomainController' } - elseif (Test-Path 'HKLM:\SYSTEM\CurrentControlSet\Services\CertSvc\Configuration') { 'ADCS' } + elseif ($hasCA) { 'ADCS' } else { 'MemberServer' } [pscustomobject]@{ Role = $role; Build = [int]$os.BuildNumber } } @@ -218,7 +250,7 @@ function Invoke-WelaAuditProfilePlan { param( [Parameter(Mandatory)]$Plan, [scriptblock]$ReadPolicy = { Get-WelaEffectiveAuditPolicy }, - [scriptblock]$WritePolicy = { param($Guid, $Mask) Set-WelaEffectiveAuditPolicy -Guid $Guid -Mask $Mask }, + [scriptblock]$WritePolicy, [scriptblock]$ReadContext = { Get-WelaHostContext } ) $hostContext = & $ReadContext @@ -226,20 +258,34 @@ function Invoke-WelaAuditProfilePlan { Assert-WelaAuditProfileTarget -Plan $Plan -Context $hostContext -Current $before $selected = @($Plan.policies | Where-Object { $_.mode -in @('exact', 'minimum') -or ($_.mode -eq 'optional' -and $Plan.includeOptional) }) $results = foreach ($policy in $selected) { - $initial = $before[$policy.guid]; $effective = $initial; $errorText = $null - $target = if ($policy.mode -eq 'minimum') { [int]$initial -bor [int]$policy.requiredMask } else { [int]$policy.requiredMask } - $status = 'No change' - if ($initial -ne $target) { - if ($PSCmdlet.ShouldProcess($policy.id, "Set audit policy to $(Format-WelaAuditMask $target)")) { - try { - & $WritePolicy $policy.guid $target | Out-Null + $initial = $null; $effective = $null; $target = $null; $errorText = $null; $status = 'No change' + try { + # Whole-plan preflight is not a current-state cache: re-read immediately before each control. + $fresh = & $ReadPolicy + if ($fresh -isnot [hashtable] -or -not $fresh.ContainsKey($policy.guid) -or $null -eq $fresh[$policy.guid] -or $fresh[$policy.guid] -notin @(0, 1, 2, 3)) { throw 'Current audit policy became unknown before application.' } + $initial = $fresh[$policy.guid]; $effective = $initial + $isMinimum = $policy.mode -eq 'minimum' + $target = if ($isMinimum) { [int]$initial -bor [int]$policy.requiredMask } else { [int]$policy.requiredMask } + if ($initial -ne $target) { + if ($PSCmdlet.ShouldProcess($policy.id, "Set audit policy to $(Format-WelaAuditMask $target)")) { + $writeMode = if ($isMinimum) { 'minimum' } else { 'exact' } + if ($WritePolicy) { + # Existing two-argument test providers retain their merged-mask contract. + # A third mode argument lets providers preserve concurrent additional flags. + & $WritePolicy $policy.guid $target $writeMode | Out-Null + } else { + $writeMask = if ($isMinimum) { $policy.requiredMask } else { $target } + Set-WelaEffectiveAuditPolicy -Guid $policy.guid -Mask $writeMask -Mode $writeMode + } $verified = & $ReadPolicy - $effective = if ($verified.ContainsKey($policy.guid)) { $verified[$policy.guid] } else { $null } - if ($effective -ne $target) { throw 'Effective policy does not match the requested mask (GPO or command failure).' } + $effective = if ($verified -is [hashtable] -and $verified.ContainsKey($policy.guid)) { $verified[$policy.guid] } else { $null } + if ($null -eq $effective -or $effective -notin @(0, 1, 2, 3)) { throw 'Effective policy is unknown after application.' } + $matches = if ($isMinimum) { ([int]$effective -band [int]$policy.requiredMask) -eq [int]$policy.requiredMask } else { $effective -eq $target } + if (-not $matches) { throw 'Effective policy does not meet the requested audit requirement (GPO or command failure).' } $status = 'Applied' - } catch { $status = 'Failed'; $errorText = $_.Exception.Message; $effective = $null } - } else { $status = 'Skipped' } - } + } else { $status = 'Skipped' } + } + } catch { $status = 'Failed'; $errorText = $_.Exception.Message; $effective = $null } [pscustomobject]@{ id = $policy.id; guid = $policy.guid; mode = $policy.mode beforeMask = $initial; targetMask = $target; effectiveMask = $effective; status = $status; error = $errorText diff --git a/tests/audit-profiles.Tests.ps1 b/tests/audit-profiles.Tests.ps1 index 9700fc07..8423ed98 100644 --- a/tests/audit-profiles.Tests.ps1 +++ b/tests/audit-profiles.Tests.ps1 @@ -70,6 +70,51 @@ Assert ($again.success -and $script:Writes.Count -eq $count) 'applying twice is $script:State = $zero.Clone(); $script:State[$shareGuid] = 1 $minimum = Invoke-WelaAuditProfilePlan -Plan $cis -ReadPolicy $reader -WritePolicy $writer -ReadContext $context -Confirm:$false Assert ($minimum.success -and $script:State[$shareGuid] -eq 3) 'fresh effective flags are preserved in minimum apply' +# Minimum readback permits additional flags enabled by Windows/GPO after the write. +$single = Get-WelaAuditProfilePlan -Profile cis-win11-v4-l1 -Role Client -Build 26100 -Current $zero +$single.policies = @($single.policies | Where-Object { $_.id -eq 'Detailed File Share' }) +$script:State = $zero.Clone() +$extra = Invoke-WelaAuditProfilePlan -Plan $single -ReadPolicy $reader -WritePolicy { + param($Guid, $Mask, $Mode) + Assert ($Mode -eq 'minimum') 'injected writer receives policy semantics' + $script:State[$Guid] = 3 +} -ReadContext $context -Confirm:$false +Assert ($extra.success -and $extra.results[0].targetMask -eq 2 -and $extra.results[0].effectiveMask -eq 3) 'minimum Failure accepts post-write Success+Failure' +# A flag introduced after whole-plan preflight is included in the immediate control read. +$script:State = $zero.Clone(); $script:Reads = 0; $script:WrittenMask = $null +$race = Invoke-WelaAuditProfilePlan -Plan $single -ReadPolicy { + $script:Reads++ + if ($script:Reads -eq 2) { $script:State[$shareGuid] = 1 } + $script:State.Clone() +} -WritePolicy { + param($Guid, $Mask, $Mode) + $script:WrittenMask = $Mask + $script:State[$Guid] = $Mask +} -ReadContext $context -Confirm:$false +Assert ($race.success -and $script:WrittenMask -eq 3 -and $race.results[0].beforeMask -eq 1) 'fresh per-control read preserves a flag introduced after preflight' +$script:Reads = 0; $script:WrittenMask = $null +$unknownRace = Invoke-WelaAuditProfilePlan -Plan $single -ReadPolicy { + $script:Reads++ + if ($script:Reads -eq 1) { $zero.Clone() } else { @{} } +} -WritePolicy { $script:WrittenMask = 1 } -ReadContext $context -Confirm:$false +Assert (-not $unknownRace.success -and $null -eq $script:WrittenMask -and $unknownRace.results[0].sourceIds.Count -gt 0) 'state becoming unknown blocks that write and retains evidence' +# Verify the real native command contract: minimum never supplies an unrequired disable. +$minimumArgs = @(& (Get-Module AuditProfiles) { param($Guid) Get-WelaAuditSetArguments -Guid $Guid -Mask 2 -Mode minimum } $shareGuid) +Assert ($minimumArgs -contains '/failure:enable' -and @($minimumArgs | Where-Object { $_ -like '/success:*' -or $_ -like '*:disable' }).Count -eq 0) 'minimum Failure writes only failure-enable, preserving concurrent Success' +$exactArgs = @(& (Get-Module AuditProfiles) { param($Guid) Get-WelaAuditSetArguments -Guid $Guid -Mask 1 -Mode exact } $shareGuid) +Assert ($exactArgs -contains '/success:enable' -and $exactArgs -contains '/failure:disable') 'exact Success deliberately clears Failure' +# Role classification must not guess when CA presence is unreadable, or omit CA policy on a DC. +$serverOS = { [pscustomobject]@{ ProductType = 3; BuildNumber = 26100 } } +$dcOS = { [pscustomobject]@{ ProductType = 2; BuildNumber = 26100 } } +$memberSystem = { [pscustomobject]@{ DomainRole = 3 } } +$dcSystem = { [pscustomobject]@{ DomainRole = 5 } } +Assert-Throws { Get-WelaHostContext -ReadOperatingSystem $serverOS -ReadComputerSystem $memberSystem -ReadCertificateAuthority { throw 'CA registry access denied' } } 'access denied' +Assert-Throws { Get-WelaHostContext -ReadOperatingSystem $serverOS -ReadComputerSystem $memberSystem -ReadCertificateAuthority { $null } } 'Cannot determine' +Assert-Throws { Get-WelaHostContext -ReadOperatingSystem $dcOS -ReadComputerSystem $dcSystem -ReadCertificateAuthority { $true } } 'Combined domain-controller/CA' +$ca = Get-WelaHostContext -ReadOperatingSystem $serverOS -ReadComputerSystem $memberSystem -ReadCertificateAuthority { $true } +Assert ($ca.Role -eq 'ADCS') 'member-server CA remains supported' +$dc = Get-WelaHostContext -ReadOperatingSystem $dcOS -ReadComputerSystem $dcSystem -ReadCertificateAuthority { $false } +Assert ($dc.Role -eq 'DomainController') 'DC without CA remains supported' $script:State = $zero.Clone() $failed = Invoke-WelaAuditProfilePlan -Plan $wela -ReadPolicy $reader -WritePolicy { throw 'command failed' } -ReadContext $context -Confirm:$false Assert (-not $failed.success -and @($failed.results | Where-Object { $_.status -eq 'Failed' }).Count -gt 0) 'native failure is machine-readable' From f4f9c33de234a066557ecfb9964533e2a7d6b88e Mon Sep 17 00:00:00 2001 From: Shirofune-Security <43838376+Shirofune-Security@users.noreply.github.com> Date: Fri, 18 Sep 2026 22:05:13 +0900 Subject: [PATCH 13/21] Exercise real audit CLI export in Windows read-only smoke --- tests/audit-profiles.Windows.Tests.ps1 | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/tests/audit-profiles.Windows.Tests.ps1 b/tests/audit-profiles.Windows.Tests.ps1 index aecc06ce..b89ac5a5 100644 --- a/tests/audit-profiles.Windows.Tests.ps1 +++ b/tests/audit-profiles.Windows.Tests.ps1 @@ -12,4 +12,16 @@ foreach ($policy in $catalog) { $context = Get-WelaHostContext $plan = Get-WelaAuditProfilePlan -Profile wela-2.2.0 -Role $context.Role -Build $context.Build -Current $current Assert-WelaAuditProfileTarget -Plan $plan -Context $context -Current $current -Write-Host "PASS: queried all $($current.Count) effective audit policies on $($context.Role) build $($context.Build); no settings changed." +$temp = Join-Path ([System.IO.Path]::GetTempPath()) ('wela-cli-audit-' + [guid]::NewGuid().ToString() + '.json') +try { + # Exercise real script dispatch and export without printing the 59-row table or changing policy. + & (Join-Path $PSScriptRoot '../WELA.ps1') audit -Profile wela-2.2.0 -PlanPath $temp 6>$null | Out-Null + $export = Get-Content -LiteralPath $temp -Raw -ErrorAction Stop | ConvertFrom-Json -ErrorAction Stop + if ($export.policies.Count -ne 59 -or $export.role -ne $context.Role -or $export.build -ne $context.Build -or $export.profile -ne 'wela-2.2.0') { + throw 'CLI audit export did not preserve all policies and the detected role/build.' + } + if (@($export.policies | Where-Object { $null -eq $_.currentMask }).Count -ne 0) { + throw 'CLI audit unexpectedly exported unknown effective policy values.' + } +} finally { Remove-Item -LiteralPath $temp -Force -ErrorAction SilentlyContinue } +Write-Host "PASS: queried all $($current.Count) effective audit policies on $($context.Role) build $($context.Build); CLI audit JSON verified; no settings changed." From d96f04dcef7702b1ee36b5328d68863351de7a73 Mon Sep 17 00:00:00 2001 From: Shirofune-Security <43838376+Shirofune-Security@users.noreply.github.com> Date: Fri, 18 Sep 2026 22:05:25 +0900 Subject: [PATCH 14/21] Integrate profile masks metadata and dry runs with verified execution --- .github/workflows/configuration-results.yml | 6 + WELA.ps1 | 9 +- docs/configuration-results.md | 32 ++++ scripts/Configuration.ps1 | 33 ++-- .../IntegrationProfileConfiguration.Tests.ps1 | 152 ++++++++++++++++++ 5 files changed, 215 insertions(+), 17 deletions(-) create mode 100644 tests/IntegrationProfileConfiguration.Tests.ps1 diff --git a/.github/workflows/configuration-results.yml b/.github/workflows/configuration-results.yml index cce1ace3..ef3aac70 100644 --- a/.github/workflows/configuration-results.yml +++ b/.github/workflows/configuration-results.yml @@ -29,3 +29,9 @@ jobs: - name: NTLM integration in PowerShell 7 shell: pwsh run: ./tests/IntegrationNtlmConfiguration.Tests.ps1 + - name: Profile integration in Windows PowerShell 5.1 + shell: powershell + run: ./tests/IntegrationProfileConfiguration.Tests.ps1 + - name: Profile integration in PowerShell 7 + shell: pwsh + run: ./tests/IntegrationProfileConfiguration.Tests.ps1 diff --git a/WELA.ps1 b/WELA.ps1 index 09ff0dd6..ecf53e8f 100644 --- a/WELA.ps1 +++ b/WELA.ps1 @@ -327,7 +327,7 @@ function Invoke-WelaProfileCommand { Assert-WelaAuditProfileTarget -Plan $plan -Context $actual -Current $current $configurationContext = New-WelaConfigurationContext -Auto:$script:Auto -DryRun:$script:DryRun -BackupPath $script:BackupPath Set-WelaProfileAuditControls -Context $configurationContext -Plan $plan - $result = Complete-WelaConfiguration -Context $configurationContext -ResultsPath $script:ResultsPath -Plan $plan + $result = Complete-WelaConfiguration -Context $configurationContext -ResultsPath $script:ResultsPath -Plan $plan -Scope advanced-audit-policy-only $result.Results | Format-Table Id, Before, Desired, After, Status -AutoSize } else { $plan.policies | Format-Table id, mode, currentMask, requiredMask, action -AutoSize @@ -1430,7 +1430,7 @@ function ConfigureAuditSettings { ) Set-RegistryConfig -RegPaths $regPaths -Auto:$Auto -Context $context Set-WelaDomainNtlmAudit -Auto:$Auto -Context $context - if (Test-Path 'HKLM:\SYSTEM\CurrentControlSet\Services\NTDS\Parameters') { + if ($hostContext.Role -eq 'DomainController') { Set-RegistryConfig -RegPaths @( @{Path = 'HKLM:\SYSTEM\CurrentControlSet\Services\NTDS\Diagnostics'; Name = '15 Field Engineering'; Value = 5} ) -Auto:$Auto -Context $context @@ -1774,16 +1774,17 @@ switch ($Cmd.ToLower()) { if ($Help){ Write-Host "Configure Windows Event Log audit settings based on the YamatoSecurity baseline" Write-Host "" - Write-Host "Usage: ./WELA.ps1 configure [-Auto] [-DryRun] [-BackupPath ] [-ResultsPath ] [-OutgoingNtlmMode ]" + Write-Host "Usage: ./WELA.ps1 configure [-Profile ] [-Auto] [-DryRun] [-BackupPath ] [-ResultsPath ] [-OutgoingNtlmMode ]" Write-Host "" Write-Host "Options:" + Write-Host " -Profile Configure advanced audit policy only from a versioned profile; list IDs with profiles" Write-Host " -Auto Automatically configure without prompts" Write-Host " -OutgoingNtlmMode PreserveOrAudit (default): audit, preserving existing deny; Audit: explicitly replace deny; Deny: opt into enforcement" Write-Host " -DryRun Read live state and report proposed changes without writing Windows settings" Write-Host " -BackupPath New directory for the pre-change recovery journal (unique default beside WELA)" Write-Host " -ResultsPath Save structured per-control outcomes as JSON" Write-Host "" - Write-Host "Note: only the YamatoSecurity baseline is currently supported for 'configure'." + Write-Host "Without -Profile, configure applies the YamatoSecurity native logging settings. -Profile applies advanced audit policy only. -DryRun and recovery/results options work with both." Write-Host "" return } diff --git a/docs/configuration-results.md b/docs/configuration-results.md index c2c37646..986acf85 100644 --- a/docs/configuration-results.md +++ b/docs/configuration-results.md @@ -113,3 +113,35 @@ Unknown domain role, unreadable policy and failed writes produce `Failed` outcom and a nonzero overall result while allowing other controls to be assessed. `tests/IntegrationNtlmConfiguration.Tests.ps1` exercises this composed behavior using mocked registry/CIM calls and temporary journals only. + +## Versioned profile integration + +`configure -Profile ` uses the same dry-run, recovery-journal and verification +runner as the broader default `configure` command. Host role/build and all required +effective audit settings are validated before creating a journal or changing any +Windows setting. The Windows-defaults profile remains read-only. + +```powershell +.\WELA.ps1 configure -Profile cis-win11-v4-l1 -DryRun -ResultsPath .\cis-plan.json +.\WELA.ps1 configure -Profile microsoft-sct-win11-24h2 -Auto -BackupPath C:\WELA-Recovery\sct-001 -ResultsPath .\sct-results.json +``` + +Exact recommendations set the named mask; minimum recommendations only enable +required flags and accept a compliant superset. They never disable an unrequested +flag, including one added by another writer between observation and application. +Omitted, Not Configured and non-applicable policies are preserved. Opt-in policies +require `-IncludeOptional`. Both result paths retain version, host role/build, +source identifiers and prerequisites such as SACLs; recording an enabled audit +subcategory does not claim its prerequisite was installed. + +The result `Scope` is `native-windows-configuration` for default configure and +`advanced-audit-policy-only` for `configure -Profile`. `ProfileScope` describes the +advanced-policy subset within either result. `-PlanPath` remains available for +profile JSON output; `-ResultsPath` saves the verified configuration report. + +This composed change depends on the outgoing/domain NTLM corrections, versioned +audit profiles and six additional native audit controls. It preserves their +selection behavior while adding shared execution and recovery reporting. +`tests/IntegrationProfileConfiguration.Tests.ps1` tests the composed command +paths without touching Windows policy, including exact/minimum behavior, concurrent +flags, unknown-state preflight, reference-only defaults, metadata and dry runs. diff --git a/scripts/Configuration.ps1 b/scripts/Configuration.ps1 index 5d0d7410..3f0d5632 100644 --- a/scripts/Configuration.ps1 +++ b/scripts/Configuration.ps1 @@ -90,7 +90,9 @@ function Invoke-WelaConfigurationControl { } function Complete-WelaConfiguration { - param($Context, [string]$ResultsPath, $Plan) + param($Context, [string]$ResultsPath, $Plan, + [ValidateSet("native-windows-configuration", "advanced-audit-policy-only")] + [string]$Scope = "native-windows-configuration") # A second read detects a value that was compliant earlier but changed during # this run. It does not establish whether GPO or another writer caused drift. foreach ($check in $Context.Checks) { @@ -109,16 +111,17 @@ function Complete-WelaConfiguration { $skipped = @($Context.Results | Where-Object { $_.Status -eq 'Skipped' }).Count $report = [pscustomobject][ordered]@{ ExitCode = $(if ($failed) { 1 } else { 0 }); DryRun = $Context.DryRun - BackupPath = $Context.BackupPath; Failed = $failed; Skipped = $skipped + BackupPath = $Context.BackupPath; Failed = $failed; Skipped = $skipped; Scope = $Scope Results = @($Context.Results.ToArray()) } if ($Plan) { $report | Add-Member NoteProperty Profile $Plan.profile + $report | Add-Member NoteProperty Version $Plan.version $report | Add-Member NoteProperty Role $Plan.role $report | Add-Member NoteProperty Build $Plan.build $report | Add-Member NoteProperty SchemaSha256 $Plan.schemaSha256 $report | Add-Member NoteProperty Provenance $Plan.provenance - $report | Add-Member NoteProperty Scope $Plan.scope + $report | Add-Member NoteProperty ProfileScope $Plan.scope } if ($ResultsPath) { try { $report | ConvertTo-Json -Depth 12 | Set-Content -LiteralPath $ResultsPath -Encoding UTF8 -ErrorAction Stop } @@ -243,22 +246,24 @@ function Set-WelaAuditPolicyControl { param($Context, $Policy, [ValidateRange(0, 3)][int]$Mask = 3, [ValidateSet('exact', 'minimum')][string]$Mode = 'exact') $guid = $Policy.GUID - $observed = @{ Mask = $null } - $read = { - $observed.Mask = Get-WelaAuditPolicyMask -Guid $guid - return $observed.Mask - }.GetNewClosure() + $read = { Get-WelaAuditPolicyMask -Guid $guid }.GetNewClosure() $test = { param($value) if ($Mode -eq 'minimum') { return ($value -band $Mask) -eq $Mask } return $value -eq $Mask }.GetNewClosure() $apply = { - # Minimum requirements preserve the flags observed immediately before journaling. - $target = if ($Mode -eq 'minimum') { $observed.Mask -bor $Mask } else { $Mask } - $success = if ($target -band 1) { 'enable' } else { 'disable' } - $failure = if ($target -band 2) { 'enable' } else { 'disable' } - Invoke-WelaNative -FilePath 'auditpol.exe' -Arguments @('/set', "/subcategory:{$guid}", "/success:$success", "/failure:$failure") + $arguments = @('/set', "/subcategory:{$guid}") + if ($Mode -eq 'minimum') { + # Only enable required flags: never disable another writer's added flag. + if ($Mask -band 1) { $arguments += '/success:enable' } + if ($Mask -band 2) { $arguments += '/failure:enable' } + } else { + $success = if ($Mask -band 1) { 'enable' } else { 'disable' } + $failure = if ($Mask -band 2) { 'enable' } else { 'disable' } + $arguments += "/success:$success", "/failure:$failure" + } + Invoke-WelaNative -FilePath 'auditpol.exe' -Arguments $arguments }.GetNewClosure() Invoke-WelaConfigurationControl -Context $Context -Id "AuditPolicy/$($Policy.Name)" -Kind AuditPolicy ` -Target @{ Guid = $guid } -Desired @{ Mask = $Mask; Mode = $Mode } -Read $read -Compliant $test -Apply $apply @@ -273,6 +278,8 @@ function Set-WelaProfileAuditControls { Set-WelaAuditPolicyControl -Context $Context -Policy @{ GUID = $policy.guid; Name = $policy.id } -Mask $policy.requiredMask -Mode $mode $row = $Context.Results[$Context.Results.Count - 1] $row | Add-Member NoteProperty Profile $Plan.profile + $row | Add-Member NoteProperty Version $Plan.version + $row | Add-Member NoteProperty SchemaSha256 $Plan.schemaSha256 $row | Add-Member NoteProperty Role $Plan.role $row | Add-Member NoteProperty Build $Plan.build $row | Add-Member NoteProperty Mode $policy.mode diff --git a/tests/IntegrationProfileConfiguration.Tests.ps1 b/tests/IntegrationProfileConfiguration.Tests.ps1 new file mode 100644 index 00000000..e4188cf2 --- /dev/null +++ b/tests/IntegrationProfileConfiguration.Tests.ps1 @@ -0,0 +1,152 @@ +# Profile command + verified configuration integration. No Windows policy is touched. +$ErrorActionPreference = 'Stop' +$repo = Split-Path $PSScriptRoot -Parent +$script:ScriptRoot = $repo +Import-Module (Join-Path $repo 'modules/AuditProfiles.psm1') -Force +. (Join-Path $repo 'scripts/Configuration.ps1') +$tokens = $null; $errors = $null +$ast = [Management.Automation.Language.Parser]::ParseFile((Join-Path $repo 'WELA.ps1'), [ref]$tokens, [ref]$errors) +if ($errors.Count) { throw ($errors | Out-String) } +foreach ($name in @('Get-WelaSelectedContext', 'Show-WelaAuditProfilePrerequisites', 'Invoke-WelaProfileCommand', 'ConfigureAuditSettings')) { + $function = $ast.Find({ param($node) $node -is [Management.Automation.Language.FunctionDefinitionAst] -and $node.Name -eq $name }, $true) + . ([scriptblock]::Create($function.Extent.Text)) +} +$script:assertions = 0 +$script:cleanup = New-Object 'System.Collections.Generic.List[string]' +$data = Import-WelaAuditProfiles +$zero = @{} +foreach ($policy in $data.catalog) { $zero[$policy.guid] = 0 } +$shareGuid = ($data.catalog | Where-Object id -eq 'Detailed File Share').guid +$processGuid = ($data.catalog | Where-Object id -eq 'Process Creation').guid +$privilegeGuid = ($data.catalog | Where-Object id -eq 'Sensitive Privilege Use').guid +function Assert($Condition, [string]$Message) { + if (-not $Condition) { throw "FAIL: $Message" } + $script:assertions++ +} +function Assert-Throws([scriptblock]$Action, [string]$Pattern) { + $caught = '' + try { & $Action | Out-Null } catch { $caught = $_.ToString() } + Assert ($caught -match $Pattern) "Expected failure matching '$Pattern', got '$caught'" +} +function Reset-Run([string]$Profile = 'cis-win11-v4-l1', [switch]$DryRun) { + $script:state = $zero.Clone() + $script:writes = @() + $script:failGuid = '' + $script:concurrentGuid = '' + $script:Profile = $Profile + $script:Role = 'Client'; $script:Build = 26100 + $script:hostBuild = 26100 + $script:Baseline = $null; $script:IncludeOptional = $false + $script:Auto = $true; $script:DryRun = [bool]$DryRun + $script:BackupPath = Join-Path ([IO.Path]::GetTempPath()) ('wela-profile-integration-' + [guid]::NewGuid().ToString('N')) + $script:ResultsPath = $script:BackupPath + '-results.json' + $script:PlanPath = $null + $script:cleanup.Add($script:BackupPath) + $script:cleanup.Add($script:ResultsPath) +} +function global:TestWindows { return $true } +function global:TestAdministrator { return $true } +function global:Get-WelaHostContext { [pscustomobject]@{ Role = 'Client'; Build = $script:hostBuild } } +function global:Get-WelaEffectiveAuditPolicy { return $script:state.Clone() } +function global:Get-WelaNativeAuditPolicy { + param($Guid) + if (-not $script:state.ContainsKey($Guid)) { throw "Mock missing policy: $Guid" } + return $script:state[$Guid] +} +function global:Invoke-WelaNative { + param($FilePath, $Arguments) + if ($FilePath -ne 'auditpol.exe' -or $Arguments[0] -ne '/set') { throw 'Unexpected native mutation' } + $guid = ($Arguments | Where-Object { $_ -like '/subcategory:*' }) -replace '^/subcategory:\{([^}]+)\}$', '$1' + $journal = Join-Path $script:BackupPath 'before.jsonl' + if (-not (Test-Path -LiteralPath $journal)) { throw 'Audit mutation occurred before journal' } + $entries = @(Get-Content -LiteralPath $journal | ConvertFrom-Json) + if ($entries[-1].Target.Guid -ne $guid) { throw 'Audit mutation occurred before matching journal entry' } + if ($guid -eq $script:failGuid) { throw 'Mock auditpol write failure' } + # Simulate a concurrent writer enabling Failure after WELA observed the policy. + if ($guid -eq $script:concurrentGuid) { $script:state[$guid] = $script:state[$guid] -bor 2 } + $mask = $script:state[$guid] + foreach ($arg in $Arguments) { + switch ($arg) { + '/success:enable' { $mask = $mask -bor 1 } + '/success:disable' { $mask = $mask -band 2 } + '/failure:enable' { $mask = $mask -bor 2 } + '/failure:disable' { $mask = $mask -band 1 } + } + } + $script:state[$guid] = $mask + $script:writes += [pscustomobject]@{ Guid = $guid; Arguments = $Arguments } + [pscustomobject]@{ ExitCode = 0; Diagnostic = ''; Output = @() } +} +try { + Reset-Run -DryRun + Invoke-WelaProfileCommand configure | Out-Null + $report = Get-Content -LiteralPath $script:ResultsPath -Raw | ConvertFrom-Json + Assert ($script:writes.Count -eq 0 -and $report.DryRun) 'configure -Profile -DryRun makes no audit writes' + Assert ($report.Scope -eq 'advanced-audit-policy-only' -and $report.ProfileScope -eq 'advanced-audit-policy-only') 'Profile-only results declare their narrower scope' + Assert (-not (Test-Path -LiteralPath $script:BackupPath)) 'Profile dry run creates no journal directory' + Assert ($report.Results.Count -gt 0 -and @($report.Results | Where-Object Status -ne Skipped).Count -eq 0) 'Profile dry-run proposals remain explicit skipped results' + + Reset-Run + $script:state[$shareGuid] = 1 + $script:concurrentGuid = $processGuid + Invoke-WelaProfileCommand configure | Out-Null + $report = Get-Content -LiteralPath $script:ResultsPath -Raw | ConvertFrom-Json + Assert ($script:state[$shareGuid] -eq 3) 'Minimum Failure preserves existing Success' + Assert ($script:state[$processGuid] -eq 3) 'Minimum Success preserves concurrently added Failure' + $processWrite = $script:writes | Where-Object Guid -eq $processGuid + Assert ($processWrite.Arguments -contains '/success:enable' -and @($processWrite.Arguments | Where-Object { $_ -like '*:disable' }).Count -eq 0) 'Minimum native command only enables required bits' + Assert ($report.ExitCode -eq 0) 'Minimum supersets pass final compliance verification' + Assert ($report.Profile -eq 'cis-win11-v4-l1' -and $report.Role -eq 'Client' -and $report.Build -eq 26100) 'Final report retains profile role and build' + Assert ($report.Version -and $report.SchemaSha256.Length -eq 64 -and $report.Provenance.Count -gt 0) 'Final report retains version and provenance' + $row = $report.Results | Where-Object { $_.Target.Guid -eq $shareGuid } + Assert ($row.Mode -eq 'minimum' -and $row.Evidence -and $row.SourceIds.Count -gt 0) 'Per-control mode and source evidence survive the context adapter' + $journal = @(Get-Content -LiteralPath (Join-Path $script:BackupPath 'before.jsonl') | ConvertFrom-Json) + Assert (@($journal | Where-Object { $_.Target.Guid -eq $shareGuid -and $_.Before -eq 1 -and $_.Desired.Mask -eq 2 -and $_.Desired.Mode -eq 'minimum' }).Count -eq 1) 'Minimum policy journal preserves before state and requirement semantics' + + Reset-Run 'microsoft-sct-win11-24h2' + $script:state[$privilegeGuid] = 3 + Invoke-WelaProfileCommand configure | Out-Null + Assert ($script:state[$privilegeGuid] -eq 1) 'Exact SCT mask remains exact rather than hardcoded SF' + $privilegeWrite = $script:writes | Where-Object Guid -eq $privilegeGuid + Assert ($privilegeWrite.Arguments -contains '/failure:disable') 'Exact Success deliberately clears unrequested Failure' + + Reset-Run 'wela-2.2.0' + $script:IncludeOptional = $true + Invoke-WelaProfileCommand configure | Out-Null + $report = Get-Content -LiteralPath $script:ResultsPath -Raw | ConvertFrom-Json + $kernel = $report.Results | Where-Object Id -eq 'AuditPolicy/Kernel Object' + Assert ($kernel.Prerequisites -match 'SACL' -and $kernel.Evidence -and $kernel.SourceIds.Count -gt 0) 'Added native controls retain dependency and source evidence after apply' + + Reset-Run + $script:failGuid = $processGuid + Assert-Throws { Invoke-WelaProfileCommand configure } 'advanced audit policies failed' + $report = Get-Content -LiteralPath $script:ResultsPath -Raw | ConvertFrom-Json + Assert ($report.ExitCode -eq 1 -and $report.Failed -eq 1) 'Profile native failure reaches final machine-readable result' + Assert ($script:writes.Count -gt 0) 'Other policy controls continue after one failure' + + Reset-Run 'windows-defaults-reviewed-2026-09' + Assert-Throws { Invoke-WelaProfileCommand configure } 'reference' + Assert ($script:writes.Count -eq 0 -and -not (Test-Path -LiteralPath $script:BackupPath)) 'Reference-only defaults fail before journal creation or mutation' + + Reset-Run + $script:state.Remove($processGuid) + Assert-Throws { Invoke-WelaProfileCommand configure } 'unknown current' + Assert ($script:writes.Count -eq 0 -and -not (Test-Path -LiteralPath $script:BackupPath)) 'Missing required state refuses the entire profile before mutation' + + Reset-Run + $script:hostBuild = 19045 + Assert-Throws { ConfigureAuditSettings -Auto -BackupPath $script:BackupPath } 'does not support' + Assert ($script:writes.Count -eq 0 -and -not (Test-Path -LiteralPath $script:BackupPath)) 'Legacy configure rejects unsupported hosts before any control or journal' + $referencePlan = Get-WelaAuditProfilePlan -Profile wela-2.2.0 -Role Client -Build 26100 -Current $zero + $emptyContext = New-WelaConfigurationContext -DryRun + $broaderReport = Complete-WelaConfiguration -Context $emptyContext -Plan $referencePlan + Assert ($broaderReport.Scope -eq 'native-windows-configuration' -and $broaderReport.ProfileScope -eq 'advanced-audit-policy-only') 'Broad configure scope is not mislabeled as its audit-policy profile scope' + $engine = (Get-Process -Id $PID).Path + $helpOutput = & $engine -NoProfile -File (Join-Path $repo 'WELA.ps1') configure -Profile cis-win11-v4-l1 -Help 2>&1 + Assert ($LASTEXITCODE -eq 0 -and ($helpOutput -join ' ') -match 'Usage:.*-Profile') 'Profile configure help returns usage without entering the Windows mutation path' + Write-Host "PASS: $script:assertions profile configuration integration assertions (mocked; no Windows changes)." +} finally { + foreach ($path in $script:cleanup) { + if (Test-Path -LiteralPath $path) { Remove-Item -LiteralPath $path -Recurse -Force } + } +} From bc9e098c8113588d183531a32a3b4923c704449f Mon Sep 17 00:00:00 2001 From: Shirofune-Security <43838376+Shirofune-Security@users.noreply.github.com> Date: Fri, 18 Sep 2026 22:09:37 +0900 Subject: [PATCH 15/21] Recheck outgoing NTLM enforcement after confirmation --- WELA.ps1 | 18 ++++++++++++++++++ tests/OutgoingNtlm.Tests.ps1 | 35 ++++++++++++++++++++++++++++++++++- 2 files changed, 52 insertions(+), 1 deletion(-) diff --git a/WELA.ps1 b/WELA.ps1 index 601a2a32..f018d149 100644 --- a/WELA.ps1 +++ b/WELA.ps1 @@ -1136,6 +1136,24 @@ function Set-WelaOutgoingNtlmPolicy { } } try { + # A prompt or ShouldProcess confirmation may outlive a Group Policy refresh. + # Recheck immediately before mutation so default audit setup cannot undo new enforcement. + $freshState = Get-WelaOutgoingNtlmState + 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) { + 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." + return + } + if ($freshState.Value -eq $desired) { + Write-Host "[SKIPPED] Outgoing NTLM is now already $description." -ForegroundColor Yellow + return + } if (-not (Test-Path -LiteralPath $path -ErrorAction Stop)) { New-Item -Path $path -Force -ErrorAction Stop | Out-Null } diff --git a/tests/OutgoingNtlm.Tests.ps1 b/tests/OutgoingNtlm.Tests.ps1 index fe26668f..e2065b8b 100644 --- a/tests/OutgoingNtlm.Tests.ps1 +++ b/tests/OutgoingNtlm.Tests.ps1 @@ -29,10 +29,15 @@ function Reset-Policy($Value) { $script:ignoreWrite = $false $script:response = 'Y' $script:rsop = @() + $script:onPrompt = $null + $script:onRead = $null + $script:reads = 0 } function Test-Path { param($LiteralPath, $ErrorAction) return $script:keyExists } function Get-ItemProperty { param($LiteralPath, $ErrorAction) + $script:reads++ + if ($script:onRead) { & $script:onRead } if ($script:readFails) { throw 'Access denied' } if ($null -eq $script:value) { return [pscustomobject]@{} } return [pscustomobject]@{ RestrictSendingNTLMTraffic = $script:value } @@ -45,7 +50,7 @@ function Set-ItemProperty { if (-not $script:ignoreWrite) { $script:value = $Value } } function Get-CimInstance { param($Namespace, $ClassName, $ErrorAction) if ($ClassName -eq 'RSOP_RegistryPolicySetting') { return $script:rsop } } -function Read-Host { param($Prompt) $script:prompts++; return $script:response } +function Read-Host { param($Prompt) $script:prompts++; if ($script:onPrompt) { & $script:onPrompt }; return $script:response } $script:assertions = 0 foreach ($initial in @($null, 0, 1)) { @@ -88,6 +93,34 @@ Assert-Equal $script:writes 0 'Declining preserves deny' $script:response = '' Set-WelaOutgoingNtlmPolicy -Mode Audit Assert-Equal $script:value 1 'Confirmed explicit override succeeds' +# A user confirmation must not authorize replacing enforcement that appeared during the prompt. +foreach ($changed in @(2, 42)) { + Reset-Policy 0 + $script:changedDuringPrompt = $changed + $script:onPrompt = { $script:value = $script:changedDuringPrompt } + Set-WelaOutgoingNtlmPolicy + Assert-Equal $script:prompts 1 'State changes while the user is confirming' + Assert-Equal $script:value $changed 'Default mode preserves newly applied deny or unknown policy' + Assert-Equal $script:writes 0 'A stale initial read never authorizes replacing new enforcement' +} +Reset-Policy 0 +$script:onPrompt = { $script:readFails = $true } +Assert-Throws { Set-WelaOutgoingNtlmPolicy } 'State becoming unreadable during confirmation aborts' +Assert-Equal $script:writes 0 'Unreadable refreshed state is not overwritten' +Reset-Policy 0 +$script:onPrompt = { $script:value = 2 } +Set-WelaOutgoingNtlmPolicy -Mode Audit +Assert-Equal $script:value 1 'Explicit Audit still authorizes replacing deny introduced during confirmation' +Assert-Equal $script:writes 1 'Explicit override writes once after a fresh readable state' +Reset-Policy 0 +$script:onPrompt = { $script:value = 1 } +Set-WelaOutgoingNtlmPolicy +Assert-Equal $script:writes 0 'A concurrently applied audit setting is not redundantly rewritten' +Reset-Policy 0 +$script:onRead = { if ($script:reads -eq 2) { $script:value = 2 } } +Set-WelaOutgoingNtlmPolicy -Auto +Assert-Equal $script:value 2 'Auto also preserves deny introduced after the initial read' +Assert-Equal $script:writes 0 'Auto rechecks immediately before writing' Reset-Policy 0 $script:writeFails = $true Assert-Throws { Set-WelaOutgoingNtlmPolicy -Auto } 'Write failure propagates' From fa5141755b78aa921fcd650bf8204bdf7acc8eb2 Mon Sep 17 00:00:00 2001 From: Shirofune-Security <43838376+Shirofune-Security@users.noreply.github.com> Date: Fri, 18 Sep 2026 22:10:17 +0900 Subject: [PATCH 16/21] Reject unsupported dry runs and preserve freshly observed NTLM restrictions --- WELA.ps1 | 5 +++++ scripts/Configuration.ps1 | 31 ++++++++++++++++++++++++++----- 2 files changed, 31 insertions(+), 5 deletions(-) diff --git a/WELA.ps1 b/WELA.ps1 index ecf53e8f..07029a91 100644 --- a/WELA.ps1 +++ b/WELA.ps1 @@ -1723,6 +1723,11 @@ Write-Host "" Write-Host "WELA v$WELAVersion - $WELAReleaseName" Write-Host "" +# Reject unsupported dry-run requests before reaching any command's mutation path. +if ($DryRun -and $Cmd -ne 'configure') { + throw "-DryRun is supported only by configure (including configure -Profile). No command was run." +} + if ($Profile -and $Cmd.ToLower() -in @('plan', 'audit', 'audit-settings', 'configure') -and -not $Help) { Invoke-WelaProfileCommand -Command $Cmd.ToLower() return diff --git a/scripts/Configuration.ps1 b/scripts/Configuration.ps1 index 3f0d5632..5816c294 100644 --- a/scripts/Configuration.ps1 +++ b/scripts/Configuration.ps1 @@ -36,14 +36,17 @@ function New-WelaConfigurationContext { function Invoke-WelaConfigurationControl { param($Context, [string]$Id, [string]$Kind, $Target, $Desired, [scriptblock]$Read, [scriptblock]$Compliant, [scriptblock]$Apply, - [string]$Description = '') + [string]$Description = '', [scriptblock]$PreserveWhen) $result = [pscustomobject][ordered]@{ Id = $Id; Kind = $Kind; Target = $Target; Desired = $Desired Before = $null; After = $null; Status = 'Failed'; Diagnostic = '' } try { $result.Before = & $Read - if (& $Compliant $result.Before) { + $preserveReason = if ($PreserveWhen) { & $PreserveWhen $result.Before } else { $null } + if ($preserveReason) { + $result.Status = 'Skipped'; $result.After = $result.Before; $result.Diagnostic = [string]$preserveReason + } elseif (& $Compliant $result.Before) { $result.Status = 'AlreadyCompliant' $result.After = $result.Before } elseif ($Context.DryRun) { @@ -175,16 +178,22 @@ function New-WelaRegistryKey { } function Set-WelaRegistryControl { - param($Context, [string]$Path, [string]$Name, $Value, [string]$Type = 'DWord') + param($Context, [string]$Path, [string]$Name, $Value, [string]$Type = 'DWord', [scriptblock]$PreserveWhen) $read = { Get-WelaRegistryState -Path $Path -Name $Name }.GetNewClosure() $test = { param($state) $state.ValueExists -and $state.Value -eq $Value -and $state.Type -eq $Type }.GetNewClosure() $apply = { New-WelaRegistryKey -Path $Path + if ($PreserveWhen) { + # Recheck after the prompt and journal, immediately before the value write. + $fresh = Get-WelaRegistryState -Path $Path -Name $Name + $preserveReason = & $PreserveWhen $fresh + if ($preserveReason) { throw "Refused registry write after state changed: $preserveReason" } + } Set-ItemProperty -LiteralPath $Path -Name $Name -Value $Value -Type $Type -ErrorAction Stop }.GetNewClosure() Invoke-WelaConfigurationControl -Context $Context -Id "Registry/$Path/$Name" -Kind Registry ` -Target @{ Path = $Path; Name = $Name } -Desired @{ Value = $Value; Type = $Type } ` - -Read $read -Compliant $test -Apply $apply + -Read $read -Compliant $test -Apply $apply -PreserveWhen $PreserveWhen } function Initialize-WelaConfigurationAuditApi { @@ -361,7 +370,19 @@ function Set-WelaNtlmConfigurationControl { } else { # Shared runner owns prompts, dry-run suppression, exact registry # before-state journal, type/value read-back and final drift check. - Set-WelaRegistryControl -Context $Context -Path $path -Name $name -Value $desired + $preserve = $null + if ($Scope -eq 'Outgoing' -and $Mode -eq 'PreserveOrAudit') { + $preserve = { + param($snapshot) + if ($snapshot.ValueExists -and $snapshot.Type -eq 'DWord' -and $snapshot.Value -eq 2) { + return 'Preserved newly observed Deny all enforcement (2); explicit Audit mode is required to replace it.' + } + if ($snapshot.ValueExists -and ($snapshot.Type -ne 'DWord' -or $snapshot.Value -notin @(0, 1, 2))) { + return "Preserved newly observed unknown outgoing NTLM value/type ($($snapshot.Value)/$($snapshot.Type))." + } + } + } + Set-WelaRegistryControl -Context $Context -Path $path -Name $name -Value $desired -PreserveWhen $preserve return } } From 595f123327d9c449923a1a8334f6f9cc08f692fd Mon Sep 17 00:00:00 2001 From: Shirofune-Security <43838376+Shirofune-Security@users.noreply.github.com> Date: Fri, 18 Sep 2026 22:12:30 +0900 Subject: [PATCH 17/21] Test unsupported dry-run rejection and NTLM policy races safely --- docs/configuration-results.md | 11 +++ tests/IntegrationSafety.Tests.ps1 | 132 ++++++++++++++++++++++++++++++ 2 files changed, 143 insertions(+) create mode 100644 tests/IntegrationSafety.Tests.ps1 diff --git a/docs/configuration-results.md b/docs/configuration-results.md index 986acf85..6f4fc9dc 100644 --- a/docs/configuration-results.md +++ b/docs/configuration-results.md @@ -145,3 +145,14 @@ selection behavior while adding shared execution and recovery reporting. `tests/IntegrationProfileConfiguration.Tests.ps1` tests the composed command paths without touching Windows policy, including exact/minimum behavior, concurrent flags, unknown-state preflight, reference-only defaults, metadata and dry runs. + +`-DryRun` is supported only by `configure`, including its `-Profile` form. Other +commands reject the flag before dispatch, so `configure-sacl -DryRun` and +`update-rules -DryRun` cannot silently perform their normal mutations. + +Outgoing `PreserveOrAudit` checks the shared runner's fresh registry snapshot and +checks again after prompting and journaling, immediately before the value write. +Newly observed deny or unknown states are preserved or refused with an explicit +result; changing them requires an explicit `Audit` or `Deny` choice. Windows does +not provide an atomic compare-and-set through this registry provider, so a +concurrent writer after the final check remains outside this guarantee. diff --git a/tests/IntegrationSafety.Tests.ps1 b/tests/IntegrationSafety.Tests.ps1 new file mode 100644 index 00000000..d655b9ac --- /dev/null +++ b/tests/IntegrationSafety.Tests.ps1 @@ -0,0 +1,132 @@ +# Safety regressions use extracted dispatcher statements with stub mutators and mocked registry APIs. +# Never dot-source WELA or invoke configure-sacl/update-rules implementations from this test. +$ErrorActionPreference = 'Stop' +$repo = Split-Path $PSScriptRoot -Parent +$tokens = $null; $errors = $null +$ast = [Management.Automation.Language.Parser]::ParseFile((Join-Path $repo 'WELA.ps1'), [ref]$tokens, [ref]$errors) +if ($errors.Count) { throw ($errors | Out-String) } +$dispatch = $ast.Find({ param($node) $node -is [Management.Automation.Language.SwitchStatementAst] -and $node.Condition.Extent.Text -eq '$Cmd.ToLower()' }, $false) +$guard = $ast.EndBlock.Statements | Where-Object { + $_ -is [Management.Automation.Language.IfStatementAst] -and $_.Extent.Text -match '-DryRun is supported only by configure' +} | Select-Object -First 1 +if (-not $guard -or $guard.Extent.StartOffset -ge $dispatch.Extent.StartOffset) { throw 'DryRun rejection guard must precede command dispatch.' } +$source = Get-Content -LiteralPath (Join-Path $repo 'WELA.ps1') -Raw +$dispatchOnly = [scriptblock]::Create($source.Substring($guard.Extent.StartOffset, $dispatch.Extent.EndOffset - $guard.Extent.StartOffset)) +$script:assertions = 0 +function Assert($Condition, [string]$Message) { + if (-not $Condition) { throw "FAIL: $Message" } + $script:assertions++ +} +# These stubs are the only mutators visible to the extracted command dispatcher. +function Set-AuditSacl { param([switch]$Auto) $script:saclCalls++ } +function UpdateRules { $script:updateCalls++ } +function ConfigureAuditSettings { $script:configureCalls++; [pscustomobject]@{ ExitCode = 0 } } +function Invoke-WelaProfileCommand { param($Command) $script:profileCalls++ } +$Help = $false; $Auto = $true; $Baseline = $null; $Profile = $null; $Debug = $false +$BackupPath = $null; $ResultsPath = $null; $OutgoingNtlmMode = 'PreserveOrAudit' +foreach ($command in @('configure-sacl', 'update-rules')) { + $Cmd = $command; $DryRun = $true + $script:saclCalls = 0; $script:updateCalls = 0 + $caught = '' + try { & $dispatchOnly | Out-Null } catch { $caught = $_.ToString() } + Assert ($caught -match '-DryRun is supported only by configure') "Unsupported DryRun for $command is rejected" + Assert ($script:saclCalls -eq 0 -and $script:updateCalls -eq 0) "DryRun rejection occurs before $command mutator" + $DryRun = $false + & $dispatchOnly | Out-Null + Assert (($script:saclCalls + $script:updateCalls) -eq 1) "Safe fixture would detect dispatch to $command without the guard" +} +$Cmd = 'configure'; $DryRun = $true; $script:configureCalls = 0 +& $dispatchOnly | Out-Null +Assert ($script:configureCalls -eq 1) 'Supported configure DryRun still dispatches' +$Profile = 'wela-2.2.0'; $script:profileCalls = 0 +& $dispatchOnly | Out-Null +Assert ($script:profileCalls -eq 1) 'Supported profile DryRun still dispatches' + +. (Join-Path $repo 'scripts/Configuration.ps1') +$script:cleanup = New-Object 'System.Collections.Generic.List[string]' +function Reset-Race($Value = 0, [string]$Type = 'DWord') { + $script:value = $Value; $script:type = $Type + $script:writes = 0; $script:changeOnPrompt = $null; $script:changeAfterJournal = $null + $script:prewriteReadFails = $false; $script:journalWritten = $false +} +function New-RaceContext([switch]$Prompt) { + $path = Join-Path ([IO.Path]::GetTempPath()) ('wela-safety-' + [guid]::NewGuid().ToString('N')) + $script:cleanup.Add($path) + New-WelaConfigurationContext -Auto:(-not $Prompt) -BackupPath $path +} +function global: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' } +} +function global:Get-WelaRegistryState { + param($Path, $Name) + if ($script:journalWritten -and $script:prewriteReadFails) { throw 'Mock prewrite read failure' } + [pscustomobject]@{ KeyExists = $true; ValueExists = $true; Value = $script:value; Type = $script:type } +} +function global:New-WelaRegistryKey { param($Path) } +function global:Set-ItemProperty { + param($LiteralPath, $Name, $Value, $Type, $ErrorAction) + $script:value = $Value; $script:type = $Type; $script:writes++ +} +function global:Read-Host { + param($Prompt) + if ($null -ne $script:changeOnPrompt) { $script:value = $script:changeOnPrompt } + return 'Y' +} +function global:Add-Content { + param($LiteralPath, $Value, $Encoding, $ErrorAction) + process { + # Preserve real temporary recovery files; only the mocked policy state changes. + $text = if ($PSBoundParameters.ContainsKey('Value')) { $Value } else { $_ } + Microsoft.PowerShell.Management\Add-Content -LiteralPath $LiteralPath -Value $text -Encoding $Encoding -ErrorAction Stop + $script:journalWritten = $true + if ($null -ne $script:changeAfterJournal) { $script:value = $script:changeAfterJournal } + } +} +try { + foreach ($value in @(2, 42)) { + Reset-Race $value + $context = New-RaceContext + Set-WelaNtlmConfigurationControl -Context $context -Scope Outgoing -Mode PreserveOrAudit + Assert ($script:writes -eq 0 -and $script:value -eq $value) "Fresh Before value $value is preserved despite stale display" + Assert ($context.Results[0].Status -eq 'Skipped' -and $context.Results[0].Before.Value -eq $value) 'Preservation records actual fresh snapshot' + Assert (-not $script:journalWritten) 'Initially preserved value produces no mutation journal' + } + Reset-Race 0 String + $context = New-RaceContext + Set-WelaNtlmConfigurationControl -Context $context -Scope Outgoing + Assert ($script:writes -eq 0 -and $context.Results[0].Status -eq 'Skipped') 'Unknown registry type is preserved by default' + + foreach ($changed in @(2, 42)) { + Reset-Race + $script:changeOnPrompt = $changed + $context = New-RaceContext -Prompt + Set-WelaNtlmConfigurationControl -Context $context -Scope Outgoing + Assert ($script:writes -eq 0 -and $script:value -eq $changed) "New value $changed introduced while prompting is never overwritten" + Assert ($context.Results[0].Status -eq 'Failed' -and $context.Results[0].Diagnostic -match 'Refused registry write') 'Changed policy is refused and reported for operator review' + } + foreach ($changed in @(2, 42)) { + Reset-Race + $script:changeAfterJournal = $changed + $context = New-RaceContext + Set-WelaNtlmConfigurationControl -Context $context -Scope Outgoing + Assert ($script:writes -eq 0 -and $script:value -eq $changed) "New value $changed introduced after journaling is preserved" + Assert ($context.Results[0].Status -eq 'Failed') 'Prewrite refusal contributes to overall failure' + } + Reset-Race + $script:prewriteReadFails = $true + $context = New-RaceContext + Set-WelaNtlmConfigurationControl -Context $context -Scope Outgoing + Assert ($script:writes -eq 0 -and $context.Results[0].Status -eq 'Failed') 'Unreadable prewrite policy prevents mutation' + + Reset-Race 2 + $context = New-RaceContext + Set-WelaNtlmConfigurationControl -Context $context -Scope Outgoing -Mode Audit + Assert ($script:writes -eq 1 -and $script:value -eq 1) 'Explicit Audit still overrides fresh deny intentionally' + Assert ($context.Results[0].Status -eq 'Applied') 'Explicit override requires successful verification' + Write-Host "PASS: $script:assertions integration safety assertions (stub dispatcher and registry; no Windows changes)." +} finally { + foreach ($path in $script:cleanup) { + if (Microsoft.PowerShell.Management\Test-Path -LiteralPath $path) { Remove-Item -LiteralPath $path -Recurse -Force } + } +} From 4c2193964e11d16fd142ab4a1175e8163e47221e Mon Sep 17 00:00:00 2001 From: Shirofune-Security <43838376+Shirofune-Security@users.noreply.github.com> Date: Fri, 18 Sep 2026 22:13:58 +0900 Subject: [PATCH 18/21] Keep deferred configuration callbacks in script scope on PowerShell 5.1 --- .github/workflows/configuration-results.yml | 12 +++ scripts/Configuration.ps1 | 96 +++++++++++-------- .../Test-ConfigurationScriptScopeWindows.ps1 | 53 ++++++++++ 3 files changed, 120 insertions(+), 41 deletions(-) create mode 100644 tests/Test-ConfigurationScriptScopeWindows.ps1 diff --git a/.github/workflows/configuration-results.yml b/.github/workflows/configuration-results.yml index ef3aac70..76e1de6d 100644 --- a/.github/workflows/configuration-results.yml +++ b/.github/workflows/configuration-results.yml @@ -11,6 +11,12 @@ jobs: runs-on: windows-latest steps: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + - name: Actual script-scope helpers with disposable HKCU key in Windows PowerShell 5.1 + shell: powershell + run: ./tests/Test-ConfigurationScriptScopeWindows.ps1 + - name: Actual script-scope helpers with disposable HKCU key in PowerShell 7 + shell: pwsh + run: ./tests/Test-ConfigurationScriptScopeWindows.ps1 - name: Mocked regressions in Windows PowerShell 5.1 shell: powershell run: ./tests/Test-ConfigurationResults.ps1 @@ -35,3 +41,9 @@ jobs: - name: Profile integration in PowerShell 7 shell: pwsh run: ./tests/IntegrationProfileConfiguration.Tests.ps1 + - name: Dry-run and NTLM race safety in Windows PowerShell 5.1 + shell: powershell + run: ./tests/IntegrationSafety.Tests.ps1 + - name: Dry-run and NTLM race safety in PowerShell 7 + shell: pwsh + run: ./tests/IntegrationSafety.Tests.ps1 diff --git a/scripts/Configuration.ps1 b/scripts/Configuration.ps1 index 5816c294..eb49793f 100644 --- a/scripts/Configuration.ps1 +++ b/scripts/Configuration.ps1 @@ -36,17 +36,17 @@ function New-WelaConfigurationContext { function Invoke-WelaConfigurationControl { param($Context, [string]$Id, [string]$Kind, $Target, $Desired, [scriptblock]$Read, [scriptblock]$Compliant, [scriptblock]$Apply, - [string]$Description = '', [scriptblock]$PreserveWhen) + [string]$Description = '', [scriptblock]$PreserveWhen, $CallbackState) $result = [pscustomobject][ordered]@{ Id = $Id; Kind = $Kind; Target = $Target; Desired = $Desired Before = $null; After = $null; Status = 'Failed'; Diagnostic = '' } try { - $result.Before = & $Read + $result.Before = & $Read $CallbackState $preserveReason = if ($PreserveWhen) { & $PreserveWhen $result.Before } else { $null } if ($preserveReason) { $result.Status = 'Skipped'; $result.After = $result.Before; $result.Diagnostic = [string]$preserveReason - } elseif (& $Compliant $result.Before) { + } elseif (& $Compliant $result.Before $CallbackState) { $result.Status = 'AlreadyCompliant' $result.After = $result.Before } elseif ($Context.DryRun) { @@ -70,19 +70,19 @@ function Invoke-WelaConfigurationControl { } $entry | ConvertTo-Json -Depth 12 -Compress | Add-Content -LiteralPath (Join-Path $Context.BackupPath 'before.jsonl') -Encoding UTF8 -ErrorAction Stop - $applied = @(& $Apply) + $applied = @(& $Apply $CallbackState) $result.Diagnostic = ($applied | ForEach-Object { if ($_.PSObject.Properties['Diagnostic']) { $_.Diagnostic } else { $_.ToString() } }) -join [Environment]::NewLine - $result.After = & $Read - if (-not (& $Compliant $result.After)) { + $result.After = & $Read $CallbackState + if (-not (& $Compliant $result.After $CallbackState)) { throw "Post-apply verification did not match the requested state. $($result.Diagnostic)" } $result.Status = 'Applied' } } if ($result.Status -in @('Applied', 'AlreadyCompliant')) { - $Context.Checks.Add([pscustomobject]@{ Result = $result; Read = $Read; Compliant = $Compliant }) + $Context.Checks.Add([pscustomobject]@{ Result = $result; Read = $Read; Compliant = $Compliant; CallbackState = $CallbackState }) } } catch { $result.Status = 'Failed'; $result.Diagnostic = $_.ToString() @@ -100,8 +100,8 @@ function Complete-WelaConfiguration { # this run. It does not establish whether GPO or another writer caused drift. foreach ($check in $Context.Checks) { try { - $check.Result.After = & $check.Read - if (-not (& $check.Compliant $check.Result.After)) { + $check.Result.After = & $check.Read $check.CallbackState + if (-not (& $check.Compliant $check.Result.After $check.CallbackState)) { $check.Result.Status = 'Overridden' $check.Result.Diagnostic = 'State was compliant earlier but changed before the final check; cause unknown.' } @@ -139,14 +139,22 @@ function Complete-WelaConfiguration { function Set-WelaEventLogControl { param($Context, [string]$Log, [string]$Property, $Desired) - $read = { (Get-WinEvent -ListLog $Log -ErrorAction Stop).$Property }.GetNewClosure() - $test = if ($Property -eq 'MaximumSizeInBytes') { - { param($value) $value -ge $Desired }.GetNewClosure() - } else { { param($value) $value -eq $Desired }.GetNewClosure() } - $argument = if ($Property -eq 'MaximumSizeInBytes') { "/ms:$Desired" } else { '/e:true' } - $apply = { Invoke-WelaNative -FilePath 'wevtutil.exe' -Arguments @('sl', $Log, $argument) }.GetNewClosure() + $state = @{ Log = $Log; Property = $Property; Desired = $Desired } + # Explicit callback state preserves values for the final recheck without + # GetNewClosure's dynamic-module scope, which hides script-local helpers in 5.1. + $read = { param($state) (Get-WinEvent -ListLog $state.Log -ErrorAction Stop).($state.Property) } + $test = { + param($value, $state) + if ($state.Property -eq 'MaximumSizeInBytes') { return $value -ge $state.Desired } + return $value -eq $state.Desired + } + $apply = { + param($state) + $argument = if ($state.Property -eq 'MaximumSizeInBytes') { "/ms:$($state.Desired)" } else { '/e:true' } + Invoke-WelaNative -FilePath 'wevtutil.exe' -Arguments @('sl', $state.Log, $argument) + } Invoke-WelaConfigurationControl -Context $Context -Id "EventLog/$Log/$Property" -Kind EventLog ` - -Target @{ Log = $Log; Property = $Property } -Desired $Desired -Read $read -Compliant $test -Apply $apply + -Target @{ Log = $Log; Property = $Property } -Desired $Desired -Read $read -Compliant $test -Apply $apply -CallbackState $state } function Get-WelaRegistryState { @@ -179,21 +187,23 @@ function New-WelaRegistryKey { function Set-WelaRegistryControl { param($Context, [string]$Path, [string]$Name, $Value, [string]$Type = 'DWord', [scriptblock]$PreserveWhen) - $read = { Get-WelaRegistryState -Path $Path -Name $Name }.GetNewClosure() - $test = { param($state) $state.ValueExists -and $state.Value -eq $Value -and $state.Type -eq $Type }.GetNewClosure() + $state = @{ Path = $Path; Name = $Name; Value = $Value; Type = $Type; PreserveWhen = $PreserveWhen } + $read = { param($state) Get-WelaRegistryState -Path $state.Path -Name $state.Name } + $test = { param($value, $state) $value.ValueExists -and $value.Value -eq $state.Value -and $value.Type -eq $state.Type } $apply = { - New-WelaRegistryKey -Path $Path - if ($PreserveWhen) { + param($state) + New-WelaRegistryKey -Path $state.Path + if ($state.PreserveWhen) { # Recheck after the prompt and journal, immediately before the value write. - $fresh = Get-WelaRegistryState -Path $Path -Name $Name - $preserveReason = & $PreserveWhen $fresh + $fresh = Get-WelaRegistryState -Path $state.Path -Name $state.Name + $preserveReason = & $state.PreserveWhen $fresh if ($preserveReason) { throw "Refused registry write after state changed: $preserveReason" } } - Set-ItemProperty -LiteralPath $Path -Name $Name -Value $Value -Type $Type -ErrorAction Stop - }.GetNewClosure() + Set-ItemProperty -LiteralPath $state.Path -Name $state.Name -Value $state.Value -Type $state.Type -ErrorAction Stop + } Invoke-WelaConfigurationControl -Context $Context -Id "Registry/$Path/$Name" -Kind Registry ` -Target @{ Path = $Path; Name = $Name } -Desired @{ Value = $Value; Type = $Type } ` - -Read $read -Compliant $test -Apply $apply -PreserveWhen $PreserveWhen + -Read $read -Compliant $test -Apply $apply -PreserveWhen $PreserveWhen -CallbackState $state } function Initialize-WelaConfigurationAuditApi { @@ -255,27 +265,29 @@ function Set-WelaAuditPolicyControl { param($Context, $Policy, [ValidateRange(0, 3)][int]$Mask = 3, [ValidateSet('exact', 'minimum')][string]$Mode = 'exact') $guid = $Policy.GUID - $read = { Get-WelaAuditPolicyMask -Guid $guid }.GetNewClosure() + $state = @{ Guid = $guid; Mask = $Mask; Mode = $Mode } + $read = { param($state) Get-WelaAuditPolicyMask -Guid $state.Guid } $test = { - param($value) - if ($Mode -eq 'minimum') { return ($value -band $Mask) -eq $Mask } - return $value -eq $Mask - }.GetNewClosure() + param($value, $state) + if ($state.Mode -eq 'minimum') { return ($value -band $state.Mask) -eq $state.Mask } + return $value -eq $state.Mask + } $apply = { - $arguments = @('/set', "/subcategory:{$guid}") - if ($Mode -eq 'minimum') { + param($state) + $arguments = @('/set', "/subcategory:{$($state.Guid)}") + if ($state.Mode -eq 'minimum') { # Only enable required flags: never disable another writer's added flag. - if ($Mask -band 1) { $arguments += '/success:enable' } - if ($Mask -band 2) { $arguments += '/failure:enable' } + if ($state.Mask -band 1) { $arguments += '/success:enable' } + if ($state.Mask -band 2) { $arguments += '/failure:enable' } } else { - $success = if ($Mask -band 1) { 'enable' } else { 'disable' } - $failure = if ($Mask -band 2) { 'enable' } else { 'disable' } + $success = if ($state.Mask -band 1) { 'enable' } else { 'disable' } + $failure = if ($state.Mask -band 2) { 'enable' } else { 'disable' } $arguments += "/success:$success", "/failure:$failure" } Invoke-WelaNative -FilePath 'auditpol.exe' -Arguments $arguments - }.GetNewClosure() + } Invoke-WelaConfigurationControl -Context $Context -Id "AuditPolicy/$($Policy.Name)" -Kind AuditPolicy ` - -Target @{ Guid = $guid } -Desired @{ Mask = $Mask; Mode = $Mode } -Read $read -Compliant $test -Apply $apply + -Target @{ Guid = $guid } -Desired @{ Mask = $Mask; Mode = $Mode } -Read $read -Compliant $test -Apply $apply -CallbackState $state } function Set-WelaProfileAuditControls { @@ -310,12 +322,14 @@ function Set-WelaCertificateAuditControl { $caName = (Get-ItemProperty -LiteralPath $root -Name Active -ErrorAction Stop).Active if (-not $caName) { throw 'CA configuration has no active CA name.' } $path = Join-Path $root $caName + $state = @{ Path = $path } $read = { + param($state) [pscustomobject]@{ - Registry = Get-WelaRegistryState -Path $path -Name AuditFilter + Registry = Get-WelaRegistryState -Path $state.Path -Name AuditFilter ServiceStatus = (Get-Service -Name CertSvc -ErrorAction Stop).Status.ToString() } - }.GetNewClosure() + } $test = { param($value) $value.Registry.ValueExists -and $value.Registry.Value -eq 127 -and $value.Registry.Type -eq 'DWord' -and $value.ServiceStatus -eq 'Running' } $apply = { $state = Get-Service -Name CertSvc -ErrorAction Stop @@ -327,7 +341,7 @@ function Set-WelaCertificateAuditControl { } Invoke-WelaConfigurationControl -Context $Context -Id 'ADCS/AuditFilter' -Kind CertificateService ` -Target @{ Path = $path; Name = 'AuditFilter'; Service = 'CertSvc' } -Desired 127 ` - -Read $read -Compliant $test -Apply $apply -Description 'Set AuditFilter=127 and restart Certificate Services.' + -Read $read -Compliant $test -Apply $apply -Description 'Set AuditFilter=127 and restart Certificate Services.' -CallbackState $state } catch { $Context.Results.Add([pscustomobject]@{ Id = 'ADCS/AuditFilter'; Kind = 'CertificateService'; Target = $root; Desired = 127; Before = $null; After = $null; Status = 'Failed'; Diagnostic = $_.ToString() }) } diff --git a/tests/Test-ConfigurationScriptScopeWindows.ps1 b/tests/Test-ConfigurationScriptScopeWindows.ps1 new file mode 100644 index 00000000..24e22e93 --- /dev/null +++ b/tests/Test-ConfigurationScriptScopeWindows.ps1 @@ -0,0 +1,53 @@ +# Actual script-scope helpers and Windows registry provider. Only a unique test +# key under HKCU and a temporary journal are written; no Windows logging changes. +$ErrorActionPreference = 'Stop' +if ($env:OS -ne 'Windows_NT') { throw 'Run this test on Windows.' } +$repo = Split-Path $PSScriptRoot -Parent +$script:ScriptRoot = $repo +. (Join-Path $repo 'scripts/Configuration.ps1') +# Deliberately keep all helper functions script-local, as in WELA.ps1 -File. +# Do not promote helpers or replace their calls with global mocks. +$token = [guid]::NewGuid().ToString('N') +$key = "HKCU:\Software\WELA-Configuration-Scope-$token" +$backup = Join-Path ([IO.Path]::GetTempPath()) "wela-script-scope-$token" +$ownsTestArtifacts = $false +function Assert-Scope($Condition, [string]$Message) { + if (-not $Condition) { throw "FAIL: $Message" } +} +function Add-ScopeControls($Context, [string]$Root) { + Set-WelaRegistryControl -Context $Context -Path "$Root\ParentA\Child" -Name Counter -Value 42 + Set-WelaRegistryControl -Context $Context -Path "$Root\ParentB\Child" -Name Label -Value 'second control' -Type String +} +try { + if ((Test-Path -LiteralPath $key) -or (Test-Path -LiteralPath $backup)) { throw 'Unique test artifact unexpectedly exists; refusing to use it.' } + $ownsTestArtifacts = $true + $context = New-WelaConfigurationContext -Auto -BackupPath $backup + # These calls must resolve Get-WelaRegistryState and recursively resolve + # New-WelaRegistryKey from ordinary script scope, then use real provider APIs. + Add-ScopeControls -Context $context -Root $key + $report = Complete-WelaConfiguration -Context $context + Assert-Scope ($report.ExitCode -eq 0) 'Script-local registry helper chain resolves and verifies' + Assert-Scope (@($report.Results | Where-Object Status -eq Applied).Count -eq 2) 'Both distinct control states remain available after their calling function returns' + Assert-Scope ((Get-ItemProperty -LiteralPath "$key\ParentA\Child" -Name Counter).Counter -eq 42) 'Actual DWORD value was written and read back' + Assert-Scope ((Get-ItemProperty -LiteralPath "$key\ParentB\Child" -Name Label).Label -eq 'second control') 'Actual string value was written and read back' + $journal = @(Get-Content -LiteralPath (Join-Path $backup 'before.jsonl') | ConvertFrom-Json) + Assert-Scope ($journal.Count -eq 2 -and -not $journal[0].Before.KeyExists -and -not $journal[1].Before.KeyExists) 'Missing nested registry parents were journaled before creation' + Add-ScopeControls -Context $context -Root $key + $report = Complete-WelaConfiguration -Context $context + Assert-Scope ($report.ExitCode -eq 0 -and @($report.Results | Where-Object Status -eq AlreadyCompliant).Count -eq 2) 'Actual registry rerun is idempotent' + Assert-Scope (@(Get-Content -LiteralPath (Join-Path $backup 'before.jsonl')).Count -eq 2) 'Compliant rerun writes no additional mutation journal records' + + # Exercise deferred native-policy and event-log readers in the same script + # scope. DryRun prevents every native configuration or service mutation. + $dry = New-WelaConfigurationContext -Auto -DryRun + Set-WelaRegistryControl -Context $dry -Path "$key\ParentA\Child" -Name Counter -Value 99 + Set-WelaAuditPolicyControl -Context $dry -Policy @{ GUID = '0CCE922B-69AE-11D9-BED3-505054503030'; Name = 'Process Creation' } + Set-WelaEventLogControl -Context $dry -Log Security -Property MaximumSizeInBytes -Desired 1 + $dryReport = Complete-WelaConfiguration -Context $dry + Assert-Scope ($dryReport.ExitCode -eq 0) 'Deferred native API and event-log callbacks resolve script-local helpers' + Assert-Scope ((Get-ItemProperty -LiteralPath "$key\ParentA\Child" -Name Counter).Counter -eq 42) 'DryRun leaves the actual registry value unchanged' + Write-Host 'Script-scope Windows provider checks passed. Only a disposable HKCU test key and temp journal were changed.' +} finally { + if ($ownsTestArtifacts -and (Test-Path -LiteralPath $key)) { Remove-Item -LiteralPath $key -Recurse -Force -ErrorAction Stop } + if ($ownsTestArtifacts -and (Test-Path -LiteralPath $backup)) { Remove-Item -LiteralPath $backup -Recurse -Force -ErrorAction Stop } +} From 9bd56a0840a06c0d041e4d97f1d0932052bd5b0a Mon Sep 17 00:00:00 2001 From: Shirofune-Security <43838376+Shirofune-Security@users.noreply.github.com> Date: Fri, 18 Sep 2026 22:16:11 +0900 Subject: [PATCH 19/21] Scope integration test doubles alongside script-local helpers --- tests/IntegrationNtlmConfiguration.Tests.ps1 | 12 +++++++----- tests/IntegrationProfileConfiguration.Tests.ps1 | 14 ++++++++------ tests/IntegrationSafety.Tests.ps1 | 14 ++++++++------ 3 files changed, 23 insertions(+), 17 deletions(-) diff --git a/tests/IntegrationNtlmConfiguration.Tests.ps1 b/tests/IntegrationNtlmConfiguration.Tests.ps1 index f5e546e4..540f70d1 100644 --- a/tests/IntegrationNtlmConfiguration.Tests.ps1 +++ b/tests/IntegrationNtlmConfiguration.Tests.ps1 @@ -1,5 +1,7 @@ # Composed #362/#363/#365 behavior, using mock registry/CIM and temporary journals only. $ErrorActionPreference = 'Stop' +# Keep mocks in the same script scope as dot-sourced helpers/imported commands; +# Windows PowerShell 5.1 resolves script-local originals ahead of global mocks. $repo = Split-Path $PSScriptRoot -Parent $script:ScriptRoot = $repo . (Join-Path $repo 'scripts/Configuration.ps1') @@ -29,30 +31,30 @@ function Reset-Mocks($Outgoing = 0, $Domain = 2, $ProductType = 2) { $script:writes = 0; $script:readFails = $false; $script:writeFails = '' $script:roleFails = $false } -function global:Get-CimInstance { +function Get-CimInstance { param($ClassName, $Property, $Namespace, $ErrorAction) if ($ClassName -eq 'Win32_OperatingSystem') { if ($script:roleFails) { throw 'Mock role query failure' } return [pscustomobject]@{ ProductType = $script:productType } } } -function global:Test-Path { +function Test-Path { param($LiteralPath, $Path, $ErrorAction) $target = if ($LiteralPath) { $LiteralPath } else { $Path } if ($target -like 'HKLM:*') { return $true } Microsoft.PowerShell.Management\Test-Path -LiteralPath $target } -function global:Get-ItemProperty { +function Get-ItemProperty { param($LiteralPath, $ErrorAction) if ($script:readFails) { throw 'Mock registry read failure' } return [pscustomobject]$script:registry } -function global:Get-WelaRegistryState { +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' } } -function global:Set-ItemProperty { +function Set-ItemProperty { param($LiteralPath, $Name, $Value, $Type, $ErrorAction) # Assert the actual mutation cannot run before its matching journal entry. $journal = Join-Path $script:currentContext.BackupPath 'before.jsonl' diff --git a/tests/IntegrationProfileConfiguration.Tests.ps1 b/tests/IntegrationProfileConfiguration.Tests.ps1 index e4188cf2..9fb75430 100644 --- a/tests/IntegrationProfileConfiguration.Tests.ps1 +++ b/tests/IntegrationProfileConfiguration.Tests.ps1 @@ -1,5 +1,7 @@ # Profile command + verified configuration integration. No Windows policy is touched. $ErrorActionPreference = 'Stop' +# Keep mocks in the same script scope as dot-sourced helpers/imported commands; +# Windows PowerShell 5.1 resolves script-local originals ahead of global mocks. $repo = Split-Path $PSScriptRoot -Parent $script:ScriptRoot = $repo Import-Module (Join-Path $repo 'modules/AuditProfiles.psm1') -Force @@ -44,16 +46,16 @@ function Reset-Run([string]$Profile = 'cis-win11-v4-l1', [switch]$DryRun) { $script:cleanup.Add($script:BackupPath) $script:cleanup.Add($script:ResultsPath) } -function global:TestWindows { return $true } -function global:TestAdministrator { return $true } -function global:Get-WelaHostContext { [pscustomobject]@{ Role = 'Client'; Build = $script:hostBuild } } -function global:Get-WelaEffectiveAuditPolicy { return $script:state.Clone() } -function global:Get-WelaNativeAuditPolicy { +function TestWindows { return $true } +function TestAdministrator { return $true } +function Get-WelaHostContext { [pscustomobject]@{ Role = 'Client'; Build = $script:hostBuild } } +function Get-WelaEffectiveAuditPolicy { return $script:state.Clone() } +function Get-WelaNativeAuditPolicy { param($Guid) if (-not $script:state.ContainsKey($Guid)) { throw "Mock missing policy: $Guid" } return $script:state[$Guid] } -function global:Invoke-WelaNative { +function Invoke-WelaNative { param($FilePath, $Arguments) if ($FilePath -ne 'auditpol.exe' -or $Arguments[0] -ne '/set') { throw 'Unexpected native mutation' } $guid = ($Arguments | Where-Object { $_ -like '/subcategory:*' }) -replace '^/subcategory:\{([^}]+)\}$', '$1' diff --git a/tests/IntegrationSafety.Tests.ps1 b/tests/IntegrationSafety.Tests.ps1 index d655b9ac..a149dee4 100644 --- a/tests/IntegrationSafety.Tests.ps1 +++ b/tests/IntegrationSafety.Tests.ps1 @@ -1,6 +1,8 @@ # Safety regressions use extracted dispatcher statements with stub mutators and mocked registry APIs. # Never dot-source WELA or invoke configure-sacl/update-rules implementations from this test. $ErrorActionPreference = 'Stop' +# Keep mocks in the same script scope as dot-sourced helpers/imported commands; +# Windows PowerShell 5.1 resolves script-local originals ahead of global mocks. $repo = Split-Path $PSScriptRoot -Parent $tokens = $null; $errors = $null $ast = [Management.Automation.Language.Parser]::ParseFile((Join-Path $repo 'WELA.ps1'), [ref]$tokens, [ref]$errors) @@ -54,26 +56,26 @@ function New-RaceContext([switch]$Prompt) { $script:cleanup.Add($path) New-WelaConfigurationContext -Auto:(-not $Prompt) -BackupPath $path } -function global:Get-WelaOutgoingNtlmState { +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' } } -function global:Get-WelaRegistryState { +function Get-WelaRegistryState { param($Path, $Name) if ($script:journalWritten -and $script:prewriteReadFails) { throw 'Mock prewrite read failure' } [pscustomobject]@{ KeyExists = $true; ValueExists = $true; Value = $script:value; Type = $script:type } } -function global:New-WelaRegistryKey { param($Path) } -function global:Set-ItemProperty { +function New-WelaRegistryKey { param($Path) } +function Set-ItemProperty { param($LiteralPath, $Name, $Value, $Type, $ErrorAction) $script:value = $Value; $script:type = $Type; $script:writes++ } -function global:Read-Host { +function Read-Host { param($Prompt) if ($null -ne $script:changeOnPrompt) { $script:value = $script:changeOnPrompt } return 'Y' } -function global:Add-Content { +function Add-Content { param($LiteralPath, $Value, $Encoding, $ErrorAction) process { # Preserve real temporary recovery files; only the mocked policy state changes. From e574dcde60d5551b7567d0a04859f8c38913e3e4 Mon Sep 17 00:00:00 2001 From: Shirofune-Security <43838376+Shirofune-Security@users.noreply.github.com> Date: Sat, 19 Sep 2026 00:23:17 +0900 Subject: [PATCH 20/21] Document verified configuration and recovery features in changelogs --- CHANGELOG-Japanese.md | 5 ++++- CHANGELOG.md | 5 ++++- website/docs/resources/changelog.ja.md | 4 ++++ website/docs/resources/changelog.md | 6 ++++++ 4 files changed, 18 insertions(+), 2 deletions(-) diff --git a/CHANGELOG-Japanese.md b/CHANGELOG-Japanese.md index d4b501a2..2343b339 100644 --- a/CHANGELOG-Japanese.md +++ b/CHANGELOG-Japanese.md @@ -4,12 +4,15 @@ **改善:** +- `configure`と`configure -Profile`に`-DryRun`と`-ResultsPath`を追加し、Windows設定を変更せずに変更内容を確認し、設定項目ごとの結果をJSONで出力できるようにした。`-DryRun`に対応していないコマンドは、実行前にエラーで停止する。 (#392) (@Shirofune-Security) +- `-BackupPath`と、各設定項目の変更前の状態を記録する復旧用ジャーナルを追加し、手動での復旧手順を文書化した。 (#392) (@Shirofune-Security) - ベースライン定義を`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) **バグ修正:** +- 設定時に外部コマンドの終了コードと変更後の設定値を確認し、処理の終了前にも再確認するようにした。書き込み失敗、設定の未反映、CAサービスの再起動失敗、最終確認時の設定の不一致を明示的に報告し、一律に成功とせず、0以外の終了コードを返すようにした。 (#392) (@Shirofune-Security) - ルールのフィルタ条件が全て適用されず最後の条件のみが適用されていたため、ルール数が正確ではなかった。 (#358) (@fukusuket) - 依存するログが無効になっているルールも使用可能として報告されていた。 (#358) (@fukusuket) - 複数のカテゴリに属するルールが重複してカウントされ、CSVファイルにも重複して出力されていた。 (#358) (@fukusuket) @@ -53,4 +56,4 @@ - `audit-settings`: Windows Event Log audit policy settingsをチェックする - `audit-filesize`: Windows Event Logファイルサイズをチェックする -- `update-rules`: WELAのSigmaルール設定ファイルを更新する \ No newline at end of file +- `update-rules`: WELAのSigmaルール設定ファイルを更新する diff --git a/CHANGELOG.md b/CHANGELOG.md index 96f9ad77..176d5e9d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,8 @@ **Improvements:** +- Added `-DryRun` and `-ResultsPath` to `configure` and `configure -Profile` to preview changes without modifying Windows settings and export per-control results as JSON. Commands that do not support `-DryRun` reject it before running. (#392) (@Shirofune-Security) +- Added `-BackupPath` and a recovery journal that records each control's previous state before making changes, with a documented manual recovery procedure. (#392) (@Shirofune-Security) - 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) @@ -12,6 +14,7 @@ **Bug Fixes:** +- 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) - 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 +58,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. \ No newline at end of file +- `update-rules`: Update WELA's Sigma rules config files. diff --git a/website/docs/resources/changelog.ja.md b/website/docs/resources/changelog.ja.md index 161dfb31..086c0d71 100644 --- a/website/docs/resources/changelog.ja.md +++ b/website/docs/resources/changelog.ja.md @@ -7,11 +7,15 @@ **改善:** +- `configure`と`configure -Profile`に`-DryRun`と`-ResultsPath`を追加し、Windows設定を変更せずに変更内容を確認し、設定項目ごとの結果をJSONで出力できるようにした。`-DryRun`に対応していないコマンドは、実行前にエラーで停止する。 (#392) (@Shirofune-Security) +- `-BackupPath`と、各設定項目の変更前の状態を記録する復旧用ジャーナルを追加し、手動での復旧手順を文書化した。 (#392) (@Shirofune-Security) - ベースライン定義を`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) **バグ修正:** +- 設定時に外部コマンドの終了コードと変更後の設定値を確認し、処理の終了前にも再確認するようにした。書き込み失敗、設定の未反映、CAサービスの再起動失敗、最終確認時の設定の不一致を明示的に報告し、一律に成功とせず、0以外の終了コードを返すようにした。 (#392) (@Shirofune-Security) - ルールのフィルタ条件が全て適用されず最後の条件のみが適用されていたため、ルール数が正確ではなかった。 (#358) (@fukusuket) - 依存するログが無効になっているルールも使用可能として報告されていた。 (#358) (@fukusuket) - 複数のカテゴリに属するルールが重複してカウントされ、CSVファイルにも重複して出力されていた。 (#358) (@fukusuket) diff --git a/website/docs/resources/changelog.md b/website/docs/resources/changelog.md index f6da9eca..8f7b96f3 100644 --- a/website/docs/resources/changelog.md +++ b/website/docs/resources/changelog.md @@ -7,11 +7,17 @@ **Improvements:** +- Added `-DryRun` and `-ResultsPath` to `configure` and `configure -Profile` to preview changes without modifying Windows settings and export per-control results as JSON. Commands that do not support `-DryRun` reject it before running. (#392) (@Shirofune-Security) +- Added `-BackupPath` and a recovery journal that records each control's previous state before making changes, with a documented manual recovery procedure. (#392) (@Shirofune-Security) +- 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:** +- 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) - 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) From 7d2117ebbae520aea9d146ecf16aafe159ec237f Mon Sep 17 00:00:00 2001 From: Shirofune-Security <43838376+Shirofune-Security@users.noreply.github.com> Date: Sat, 19 Sep 2026 00:36:38 +0900 Subject: [PATCH 21/21] Fix audit applicability, NTLM value types, and native exit verification --- .github/workflows/audit-profiles.yml | 12 ++ CHANGELOG-Japanese.md | 1 + CHANGELOG.md | 1 + WELA.ps1 | 85 +++++++++----- modules/AuditProfiles.psm1 | 12 +- scripts/Configuration.ps1 | 6 +- tests/AuditProfileNativeWriter.Tests.ps1 | 79 +++++++++++++ tests/AuditProfileOutput.Tests.ps1 | 113 +++++++++++++++++++ tests/DomainNtlm.Tests.ps1 | 39 ++++++- tests/IntegrationNtlmConfiguration.Tests.ps1 | 56 ++++++++- tests/IntegrationSafety.Tests.ps1 | 2 +- tests/OutgoingNtlm.Tests.ps1 | 58 +++++++++- website/docs/resources/changelog.ja.md | 1 + website/docs/resources/changelog.md | 1 + 14 files changed, 426 insertions(+), 40 deletions(-) create mode 100644 tests/AuditProfileNativeWriter.Tests.ps1 create mode 100644 tests/AuditProfileOutput.Tests.ps1 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)