mirror of
https://github.com/Yamato-Security/WELA.git
synced 2026-11-09 22:49:58 +01:00
Fix audit applicability, NTLM value types, and native exit verification
This commit is contained in:
1 parent
71215ab498
commit
7d2117ebba
14 files changed
+426
-40
No files matched your search
@@ -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)."
|
||||
@@ -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
|
||||
}
|
||||
@@ -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)."
|
||||
@@ -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) {
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)."
|
||||
Reference in new issue
Block a user