mirror of
https://github.com/Yamato-Security/WELA.git
synced 2026-09-29 02:24:53 +02:00
135 lines
7.7 KiB
PowerShell
135 lines
7.7 KiB
PowerShell
# 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)
|
|
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 Get-WelaOutgoingNtlmState {
|
|
# The first display is deliberately stale; the shared runner must trust its own fresh read.
|
|
[pscustomobject]@{ Readable = $true; Value = 0; Type = 'DWord'; Description = 'Allow all (initial read)'; PolicySource = 'mock' }
|
|
}
|
|
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 New-WelaRegistryKey { param($Path) }
|
|
function Set-ItemProperty {
|
|
param($LiteralPath, $Name, $Value, $Type, $ErrorAction)
|
|
$script:value = $Value; $script:type = $Type; $script:writes++
|
|
}
|
|
function Read-Host {
|
|
param($Prompt)
|
|
if ($null -ne $script:changeOnPrompt) { $script:value = $script:changeOnPrompt }
|
|
return 'Y'
|
|
}
|
|
function 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 }
|
|
}
|
|
}
|