Files
WELA/tests/TranscriptionRecovery.Windows.Tests.ps1

117 lines
10 KiB
PowerShell

param([switch]$AllowDisposablePolicyWrite)
$ErrorActionPreference='Stop'
if($env:OS -ne 'Windows_NT'){Write-Host 'Skipped: actual Windows transcription recovery requires Windows.';exit 0}
if(-not $AllowDisposablePolicyWrite -or $env:GITHUB_ACTIONS -ne 'true' -or $env:RUNNER_ENVIRONMENT -ne 'github-hosted'){throw 'This native mutation fixture requires explicit consent on a disposable GitHub-hosted runner.'}
$script:ScriptRoot=Split-Path $PSScriptRoot -Parent
foreach($file in @('Configuration','AuditRecovery','PowerShellTranscription','TranscriptionRecovery')){. (Join-Path $script:ScriptRoot ('scripts/'+$file+'.ps1'))}
$script:checks=0
function Assert($Value,[string]$Message){if(-not $Value){throw "FAIL: $Message"};$script:checks++}
$root=New-WelaRecoveryOutput (Join-Path $env:RUNNER_TEMP ('wela-transcription-recovery-'+[guid]::NewGuid().ToString('N')))
$before=@(Get-WelaTranscriptPolicy @('Registry64','Registry32'))
$protectedBefore=Get-WelaTranscriptRecoveryProtectedPolicy
Write-WelaRecoveryArtifact (Join-Path $root 'original-policy.json') $before
Write-WelaRecoveryArtifact (Join-Path $root 'original-protected-policy.json') $protectedBefore
$base=[Microsoft.Win32.RegistryKey]::OpenBaseKey([Microsoft.Win32.RegistryHive]::LocalMachine,[Microsoft.Win32.RegistryView]::Registry64)
$policyRoot='SOFTWARE\Policies\Microsoft\Windows\PowerShell'
$originalParents=@{}
foreach($path in @($policyRoot,($policyRoot+'\Transcription'))){$key=$base.OpenSubKey($path);$originalParents[$path]=($null -ne $key);if($key){$key.Dispose()}}
$base.Dispose()
$hostExe=Join-Path $PSHOME $(if($PSVersionTable.PSEdition -eq 'Desktop'){'powershell.exe'}else{'pwsh.exe'})
$native51=Join-Path $env:windir 'System32\WindowsPowerShell\v1.0\powershell.exe'
$restored=$false;$touched=$false
function Set-FixtureValue([string]$Name,$Value,[string]$Type='DWord') {
$base=[Microsoft.Win32.RegistryKey]::OpenBaseKey([Microsoft.Win32.RegistryHive]::LocalMachine,[Microsoft.Win32.RegistryView]::Registry64)
$key=$base.CreateSubKey($policyRoot+'\Transcription')
try{if($null -eq $Value){$key.DeleteValue($Name,$false)}else{$key.SetValue($Name,$Value,[Microsoft.Win32.RegistryValueKind]$Type)};$key.Flush()}finally{$key.Dispose();$base.Dispose()}
}
function Invoke-WelaTranscriptFixtureCli {
param([string[]]$Parameters,[string]$Log,[switch]$ExpectFailure)
$global:LASTEXITCODE=$null
$priorPreference=$ErrorActionPreference
try {
$ErrorActionPreference='Continue'
& $hostExe -NoLogo -NoProfile -ExecutionPolicy Bypass -File (Join-Path $script:ScriptRoot 'WELA.ps1') @Parameters *> $Log
$code=$global:LASTEXITCODE
} finally {$ErrorActionPreference=$priorPreference}
if($ExpectFailure){Assert ($null -ne $code -and $code -ne 0) 'native public CLI refuses unsupported or stale recovery'}
elseif($code -ne 0){throw "Public CLI failed ($code): $(Get-Content $Log -Raw)"}
$global:LASTEXITCODE=0
}
try {
foreach($scenario in @('Enabled','DisabledAbsentDirectory','AbsentEnablement','Drift')) {
$case=New-WelaRecoveryOutput (Join-Path $root $scenario)
$old=New-WelaRecoveryOutput (Join-Path $case 'old-transcripts')
$new=New-WelaRecoveryOutput (Join-Path $case 'new-transcripts')
$touched=$true
Set-FixtureValue EnableTranscripting 0
Set-FixtureValue OutputDirectory $(if($scenario -eq 'DisabledAbsentDirectory'){$null}else{$old}) String
Set-FixtureValue EnableTranscripting $(if($scenario -eq 'AbsentEnablement'){$null}elseif($scenario -eq 'DisabledAbsentDirectory'){0}else{1})
$caseBefore=@(Get-WelaTranscriptPolicy @('Registry64','Registry32'))
$preserved=Get-WelaTranscriptRecoveryProtectedPolicy
Write-WelaRecoveryArtifact (Join-Path $case 'fixture-before.json') $caseBefore
$backup=Join-Path $case 'configure-backup';$original=Join-Path $case 'configure-result.json'
Invoke-WelaTranscriptFixtureCli @('powershell-transcription','-TranscriptionAction','Configure','-TranscriptDirectory',$new,'-Auto','-BackupPath',$backup,'-ResultsPath',$original) (Join-Path $case 'configure.log')
$configured=ConvertFrom-WelaRecoveryJson (Get-Content $original -Raw)
Assert ($configured.Results.Count -eq 1 -and $configured.Results[0].Status -eq 'Applied') 'actual public Configure creates the exact completed composite history'
$planDirectory=Join-Path $case 'plan';$planPath=Join-Path $planDirectory 'plan.json'
Invoke-WelaTranscriptFixtureCli @('transcription-recovery','-TranscriptRecoveryJournalPath',(Join-Path $backup 'before.jsonl'),'-TranscriptRecoveryOriginalResultsPath',$original,'-TranscriptRecoveryOutputPath',$planDirectory) (Join-Path $case 'plan.log')
$plan=ConvertFrom-WelaRecoveryJson (Get-Content $planPath -Raw)
$planHash=(Get-FileHash $planPath -Algorithm SHA256).Hash.ToLowerInvariant()
Assert ($plan.Context.Reader.UserSid -and $plan.Sources.'scripts/TranscriptionRecovery.ps1' -and $plan.SigmaEvtxCredit -eq 0) 'native plan binds reader/code and grants no EVTX credit'
$restoreDirectory=Join-Path $case 'restore'
$restoreArguments=@('transcription-recovery','-TranscriptRecoveryAction','Restore','-TranscriptRecoveryPlanPath',$planPath,'-TranscriptRecoveryPlanHash',$planHash,'-TranscriptRecoveryOutputPath',$restoreDirectory,'-Auto')
if($scenario -eq 'Drift') {
Set-FixtureValue EnableTranscripting 0
Invoke-WelaTranscriptFixtureCli ($restoreArguments+@('-TranscriptRecoveryAllowTemporarySuspension')) (Join-Path $case 'drift-refusal.log') -ExpectFailure
Assert (-not (Test-Path $restoreDirectory) -and (Get-WelaTranscriptRegistryValue -Name EnableTranscripting).Value -eq 0) 'actual changed native policy is preserved before any output/write'
continue
}
if($plan.RequiresTemporarySuspension) {
Invoke-WelaTranscriptFixtureCli $restoreArguments (Join-Path $case 'consent-refusal.log') -ExpectFailure
Assert (-not (Test-Path $restoreDirectory) -and (Get-WelaTranscriptRegistryValue -Name EnableTranscripting).Value -eq 1) 'no suspension consent preserves the enabled policy'
$restoreArguments += '-TranscriptRecoveryAllowTemporarySuspension'
}
$previewArguments=@('transcription-recovery','-TranscriptRecoveryAction','Restore','-TranscriptRecoveryPlanPath',$planPath,'-TranscriptRecoveryPlanHash',$planHash,'-DryRun')
if($plan.RequiresTemporarySuspension){$previewArguments += '-TranscriptRecoveryAllowTemporarySuspension'}
Invoke-WelaTranscriptFixtureCli $previewArguments (Join-Path $case 'preview.log')
Assert ((Get-WelaRecoveryKey @(Get-WelaTranscriptPolicy @('Registry64','Registry32'))) -ceq (Get-WelaRecoveryKey $plan.ExpectedPolicy)) 'actual public preview leaves both native registry views unchanged'
Invoke-WelaTranscriptFixtureCli $restoreArguments (Join-Path $case 'restore.log')
$report=ConvertFrom-WelaRecoveryJson (Get-Content (Join-Path $restoreDirectory 'result.json') -Raw)
Assert ($report.Status -eq 'Restored' -and $report.ExitCode -eq 0) 'actual public Restore completes'
Assert ((Get-WelaRecoveryKey @(Get-WelaTranscriptPolicy @('Registry64','Registry32'))) -ceq (Get-WelaRecoveryKey $caseBefore)) 'native restore matches original typed policy including value absence in both views'
Assert ((Get-WelaRecoveryKey (Get-WelaTranscriptRecoveryProtectedPolicy)) -ceq (Get-WelaRecoveryKey $preserved)) 'all other machine/user PowerShell policy remains exact'
$pending=@(Get-ChildItem $restoreDirectory '*-pending.json');$confirmed=@(Get-ChildItem $restoreDirectory '*-confirmed.json')
Assert ($pending.Count -eq $plan.Steps.Count -and $confirmed.Count -eq $plan.Steps.Count) 'every actual native write has separate durable pending and confirmed receipts'
if($scenario -eq 'Enabled') {
$marker='WELA_RECOVERED_TRANSCRIPT_'+[guid]::NewGuid().ToString('N')
& $native51 -NoLogo -NoProfile -Command "Write-Output '$marker'" *> (Join-Path $case 'benign-session.log')
Assert ($LASTEXITCODE -eq 0) 'fresh built-in Windows PowerShell session completes after recovery'
$matching=@(Get-ChildItem -LiteralPath $old -Recurse -File -Filter '*.txt'|Where-Object {(Get-Content $_.FullName -Raw).Contains($marker)})
Assert ($matching.Count -eq 1) 'one real fresh Windows PowerShell transcript contains the benign marker at the restored destination'
Write-WelaRecoveryArtifact (Join-Path $case 'transcript-marker.json') ([pscustomobject]@{Marker=$marker;Path=$matching[0].FullName;Sha256=(Get-FileHash $matching[0].FullName).Hash;Scope='Disposable local fixture only; no production/central assertion'})
}
}
} finally {
if($touched) {
Set-FixtureValue EnableTranscripting 0
foreach($name in @('OutputDirectory','EnableInvocationHeader','EnableTranscripting')) {
$value=$before[0].Machine.$name
Set-FixtureValue $name $(if($value.ValueExists){$value.Value}else{$null}) $(if($value.ValueExists){$value.Type}else{'DWord'})
}
$base=[Microsoft.Win32.RegistryKey]::OpenBaseKey([Microsoft.Win32.RegistryHive]::LocalMachine,[Microsoft.Win32.RegistryView]::Registry64)
try {
foreach($path in @(($policyRoot+'\Transcription'),$policyRoot)) {
if($originalParents[$path]){continue}
$key=$base.OpenSubKey($path)
$empty=$null -ne $key -and $key.GetValueNames().Count -eq 0 -and $key.GetSubKeyNames().Count -eq 0
if($key){$key.Dispose()};if($empty){$base.DeleteSubKey($path,$false)}
}
} finally {$base.Dispose()}
}
$after=@(Get-WelaTranscriptPolicy @('Registry64','Registry32'))
$restored=(Get-WelaRecoveryKey $after) -ceq (Get-WelaRecoveryKey $before) -and (Get-WelaRecoveryKey (Get-WelaTranscriptRecoveryProtectedPolicy)) -ceq (Get-WelaRecoveryKey $protectedBefore)
Write-WelaRecoveryArtifact (Join-Path $root 'cleanup.json') ([pscustomobject]@{CleanupVerified=$restored;Checks=$script:checks;Engine=$PSVersionTable.PSVersion.ToString();Computer=$env:COMPUTERNAME;After=$after})
if(-not $restored){throw "Exact native policy cleanup failed; retained private evidence at $root"}
}
Write-Host "Passed $script:checks actual native transcription recovery assertions; exact policy cleanup verified. Evidence: $root"