Fix audit applicability, NTLM value types, and native exit verification

This commit is contained in:
Shirofune-Security committed 2026-09-19 00:36:38 +09:00
1 parent 71215ab498
commit 7d2117ebba
14 files changed
+426 -40

No files matched your search

+79
View File
@@ -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)."
+113
View File
@@ -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
}
+37 -2
View File
@@ -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)."
+55 -1
View File
@@ -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) {
+1 -1
View File
@@ -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)
+56 -2
View File
@@ -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)."