mirror of
https://github.com/Yamato-Security/WELA.git
synced 2026-10-25 06:28:12 +01:00
Add opt-in Windows PowerShell transcription for CIS Level 2
This commit is contained in:
1 parent
9d993a2a7e
commit
89253fa812
11 files changed
+630
-2
No files matched your search
@@ -0,0 +1,137 @@
|
||||
$ErrorActionPreference = 'Stop'
|
||||
$repo = Split-Path $PSScriptRoot -Parent
|
||||
$script:ScriptRoot = $repo
|
||||
. (Join-Path $repo 'scripts/Configuration.ps1')
|
||||
. (Join-Path $repo 'scripts/PowerShellTranscription.ps1')
|
||||
$script:checks = 0
|
||||
function Assert($Condition, [string]$Message) { if (-not $Condition) { throw "FAIL: $Message" }; $script:checks++ }
|
||||
function Assert-Throws([scriptblock]$Action, [string]$Message) { $thrown = $false; try { & $Action } catch { $thrown = $true }; Assert $thrown $Message }
|
||||
$root = Join-Path ([IO.Path]::GetTempPath()) ('wela-transcription-' + [guid]::NewGuid().ToString('N'))
|
||||
$null = New-Item -ItemType Directory -Path $root
|
||||
$originalCapability = ${function:Get-WelaTranscriptCapability}
|
||||
function New-Value($Value = $null, [string]$Type = '') {
|
||||
[pscustomobject]@{ KeyExists = [bool]$Type; ValueExists = [bool]$Type; Value = $Value; Type = $(if ($Type) { $Type } else { $null }) }
|
||||
}
|
||||
function Reset-Mocks {
|
||||
$script:machine = @{ EnableTranscripting = New-Value; OutputDirectory = New-Value; EnableInvocationHeader = New-Value }
|
||||
$script:user = @{ EnableTranscripting = New-Value; OutputDirectory = New-Value; EnableInvocationHeader = New-Value }
|
||||
$script:writes = @(); $script:reads = 0; $script:destinationReads = 0; $script:registryFailure = $false
|
||||
$script:viewMismatch = $false; $script:destinationFailure = $false; $script:destinationRisk = $false
|
||||
$script:race = $false; $script:drift = $false; $script:writeFailure = ''; $script:locationReadbackFailure = $false
|
||||
$script:headerDrift = $false; $script:onPrompt = $null; $script:version = '5.1.26100.0'
|
||||
}
|
||||
function Get-WelaTranscriptCapability {
|
||||
[pscustomobject]@{ Status = 'Supported'; TargetEngine = 'Windows PowerShell 5.1'; Views = @('Registry64', 'Registry32'); Diagnostic = 'Mock engine' }
|
||||
}
|
||||
function Get-WelaTranscriptRegistryValue {
|
||||
param($Hive = 'LocalMachine', $View, $SubKey, $Name)
|
||||
$script:reads++
|
||||
if ($script:registryFailure) { throw 'Registry read denied' }
|
||||
if ($Name -eq 'PowerShellVersion') { return New-Value $script:version String }
|
||||
$values = if ($Hive -eq 'CurrentUser') { $script:user } else { $script:machine }
|
||||
$result = $values[$Name] | ConvertTo-Json -Depth 8 | ConvertFrom-Json
|
||||
if ($script:viewMismatch -and $View -eq 'Registry32' -and $Name -eq 'EnableTranscripting' -and $Hive -eq 'LocalMachine') { $result = New-Value 2 DWord }
|
||||
if ($script:drift -and $script:destinationReads -ge 3 -and $Name -eq 'EnableTranscripting' -and $Hive -eq 'LocalMachine') { $result = New-Value 0 DWord }
|
||||
if ($script:headerDrift -and $script:writes.Count -gt 0 -and $Name -eq 'EnableInvocationHeader' -and $Hive -eq 'LocalMachine') { $result = New-Value 0 DWord }
|
||||
return $result
|
||||
}
|
||||
function Get-WelaTranscriptDestination {
|
||||
param($Path)
|
||||
$script:destinationReads++
|
||||
$sddl = if ($script:race -and $script:destinationReads -gt 1) { 'changed' } else { 'private-directory-acl' }
|
||||
[pscustomobject]@{ RequestedPath = $Path; Path = $Path; Status = $(if ($script:destinationRisk) { 'Blocked' } elseif ($script:destinationFailure) { 'Unknown' } else { 'Observed' });
|
||||
ConfigureAllowed = -not ($script:destinationFailure -or $script:destinationRisk); Acl = @{ Sddl = $sddl };
|
||||
ShareAuthorization = $(if ($Path.StartsWith('\\')) { 'Unknown' } else { 'NotApplicable' }); WriterAuthorization = 'Unknown'; CollectorAuthorization = 'Unknown';
|
||||
Diagnostic = 'Mock directory; effective access remains unverified' }
|
||||
}
|
||||
function Set-WelaTranscriptRegistryValue {
|
||||
param($Name, $Value, $Type)
|
||||
if ($Name -eq $script:writeFailure) { throw "Mock $Name write denied" }
|
||||
$script:writes += $Name
|
||||
if ($Name -eq 'OutputDirectory' -and $script:locationReadbackFailure) { return }
|
||||
$script:machine[$Name] = New-Value $Value $Type
|
||||
foreach ($entry in $script:machine.Values) { $entry.KeyExists = $true }
|
||||
}
|
||||
function Read-Host { param($Prompt) if ($script:onPrompt) { & $script:onPrompt }; return 'y' }
|
||||
function Configure([switch]$DryRun, [switch]$Prompt, [string]$Path = 'C:\ReviewedTranscripts') {
|
||||
Invoke-WelaTranscriptCommand -Action Configure -OutputDirectory $Path -Auto:(-not $Prompt) -DryRun:$DryRun `
|
||||
-BackupPath (Join-Path $root ([guid]::NewGuid().ToString('N')))
|
||||
}
|
||||
try {
|
||||
foreach ($path in @('C:\ReviewedTranscripts', '\\collector.example.test\Transcripts', '\\collector\Transcripts\Windows')) {
|
||||
Test-WelaTranscriptDirectoryPath $path; Assert $true "absolute path accepted: $path"
|
||||
}
|
||||
foreach ($path in @('', '.\transcripts', 'C:relative', 'C:\..\transcripts', '%TEMP%\transcripts', 'C:\foo*', '\\?\C:\transcripts', '\\.\pipe\name', 'C:\transcripts:stream', 'https://collector/logs')) {
|
||||
Assert-Throws { Test-WelaTranscriptDirectoryPath $path } "unsafe/ambiguous path rejected: $path"
|
||||
}
|
||||
Reset-Mocks
|
||||
Assert-Throws { Invoke-WelaTranscriptCommand -Action Plan } 'plan requires an explicit reviewed path'
|
||||
Assert-Throws { Invoke-WelaTranscriptCommand -Action Configure -Auto } 'configure cannot fall back to per-user Documents'
|
||||
Assert-Throws { Invoke-WelaTranscriptCommand -Action Audit -DryRun } 'dry-run only applies to configure'
|
||||
$report = Invoke-WelaTranscriptCommand -Action Audit
|
||||
Assert ($report.Results[0].Status -eq 'Unknown' -and $script:writes.Count -eq 0) 'missing output policy remains unknown during audit'
|
||||
$report = Invoke-WelaTranscriptCommand -Action Plan -OutputDirectory 'C:\ReviewedTranscripts'
|
||||
Assert ($report.Results[0].Status -eq 'ChangeRequired' -and $report.Benchmark -like '*Level 2 only*') 'opt-in plan explicitly identifies Level 2'
|
||||
Assert ($report.Telemetry.SigmaEvtxCredit -eq 0 -and $report.Telemetry.EventIds.Count -eq 0 -and $report.VerificationScope -like '*PowerShell 7*unverified*') 'transcript text gives no EVTX or PowerShell 7 session credit'
|
||||
Reset-Mocks; $report = Configure -DryRun
|
||||
Assert ($report.DryRun -and $report.Skipped -eq 1 -and $script:writes.Count -eq 0 -and -not (Test-Path $report.BackupPath)) 'dry-run makes no registry or recovery-directory writes'
|
||||
Reset-Mocks; $report = Configure
|
||||
Assert ($report.ExitCode -eq 0 -and $report.Results[0].Status -eq 'Applied') 'new policy applies and verifies'
|
||||
Assert (($script:writes -join ',') -eq 'OutputDirectory,EnableTranscripting') 'reviewed location is written and verified before enabling transcription'
|
||||
Assert ($script:machine.EnableTranscripting.Type -eq 'DWord' -and $script:machine.EnableTranscripting.Value -eq 1 -and $script:machine.OutputDirectory.Type -eq 'String') 'exact canonical registry types'
|
||||
Assert (-not $script:machine.EnableInvocationHeader.ValueExists) 'absent invocation-header preference remains absent even when key is created'
|
||||
$journal = Get-Content (Join-Path $report.BackupPath 'before.jsonl') -Raw | ConvertFrom-Json
|
||||
Assert ($journal.Before.Policy.Count -eq 2 -and -not $journal.Before.Policy[0].Machine.OutputDirectory.ValueExists -and $journal.Before.Destination.Acl.Sddl -eq 'private-directory-acl') 'journal records both views and original destination security observations'
|
||||
$script:destinationReads = 0; $again = Configure
|
||||
Assert ($again.Results[0].Status -eq 'AlreadyCompliant' -and $script:writes.Count -eq 2) 'repeat configuration is idempotent'
|
||||
Reset-Mocks; $script:machine.EnableInvocationHeader = New-Value 1 DWord
|
||||
$script:user.EnableTranscripting = New-Value 0 DWord
|
||||
$report = Configure
|
||||
Assert ($report.ExitCode -eq 0 -and $script:machine.EnableInvocationHeader.Value -eq 1 -and $script:user.EnableTranscripting.Value -eq 0) 'existing header and user policy are preserved'
|
||||
Reset-Mocks; $script:machine.EnableTranscripting = New-Value '1' String
|
||||
$script:machine.OutputDirectory = New-Value 'C:\ReviewedTranscripts' ExpandString
|
||||
$report = Configure
|
||||
Assert ($report.ExitCode -eq 0 -and $script:machine.EnableTranscripting.Type -eq 'DWord' -and $script:machine.OutputDirectory.Type -eq 'String') 'numeric strings and ExpandString are repaired by explicit configure'
|
||||
Reset-Mocks; $script:viewMismatch = $true; $report = Configure
|
||||
Assert ($report.ExitCode -eq 1 -and $script:writes.Count -eq 0) 'shared-view mismatch fails closed'
|
||||
Reset-Mocks; $script:registryFailure = $true; $report = Configure
|
||||
Assert ($report.ExitCode -eq 1 -and $script:writes.Count -eq 0) 'registry read failure cannot be mistaken for absent configuration'
|
||||
foreach ($risk in @('destinationFailure', 'destinationRisk')) {
|
||||
Reset-Mocks; Set-Variable -Scope Script -Name $risk -Value $true; $report = Configure
|
||||
Assert ($report.ExitCode -eq 1 -and $script:writes.Count -eq 0) 'unreadable or known-unsafe destination blocks writes'
|
||||
}
|
||||
Reset-Mocks; $report = Configure -Path '\\collector\Transcripts'
|
||||
Assert ($report.ExitCode -eq 0 -and $report.Results[0].After.Destination.ShareAuthorization -eq 'Unknown' -and $report.Results[0].After.Destination.WriterAuthorization -eq 'Unknown') 'explicit UNC configure retains unknown share and writer authorization'
|
||||
Reset-Mocks; $script:race = $true; $report = Configure
|
||||
Assert ($report.ExitCode -eq 1 -and $script:writes.Count -eq 0 -and $report.Results[0].Diagnostic -like '*changed after*') 'destination ACL race fails before writing'
|
||||
Reset-Mocks; $script:locationReadbackFailure = $true; $report = Configure
|
||||
Assert ($report.ExitCode -eq 1 -and ($script:writes -join ',') -eq 'OutputDirectory') 'bad location readback prevents enablement'
|
||||
Reset-Mocks; $script:writeFailure = 'EnableTranscripting'; $report = Configure
|
||||
Assert ($report.ExitCode -eq 1 -and (Test-Path (Join-Path $report.BackupPath 'before.jsonl')) -and $script:machine.OutputDirectory.ValueExists) 'partial write failure retains accurate recovery evidence'
|
||||
Reset-Mocks; $script:drift = $true; $report = Configure
|
||||
Assert ($report.ExitCode -eq 1 -and $report.Results[0].Status -eq 'Overridden') 'final policy drift is overridden'
|
||||
Reset-Mocks; $script:headerDrift = $true; $report = Configure
|
||||
Assert ($report.ExitCode -eq 1 -and $report.Results[0].Diagnostic -like '*Invocation-header*') 'unrequested header change fails verification'
|
||||
Reset-Mocks
|
||||
$script:onPrompt = { Get-ChildItem $root -Directory | Remove-Item -Recurse -Force }
|
||||
$report = Configure -Prompt
|
||||
Assert ($report.ExitCode -eq 1 -and $script:writes.Count -eq 0) 'journal write failure prevents policy changes'
|
||||
Reset-Mocks
|
||||
$path = Join-Path $root 'report.json'
|
||||
$null = Invoke-WelaTranscriptCommand -Action Plan -OutputDirectory 'C:\ReviewedTranscripts' -ResultsPath $path
|
||||
$export = Get-Content $path -Raw | ConvertFrom-Json
|
||||
Assert ($export.Telemetry.SigmaEvtxCredit -eq 0 -and $export.Results[0].Before.TranscriptGeneration -eq 'Unverified') 'JSON retains generation and coverage limits'
|
||||
# Test actual capability classification through stubbed OS filesystem/registry reads.
|
||||
Set-Item function:Get-WelaTranscriptCapability $originalCapability
|
||||
$savedOs = $env:OS; $savedWindir = $env:windir
|
||||
try {
|
||||
$env:OS = 'Windows_NT'; $env:windir = $root
|
||||
function Test-Path { param($LiteralPath, $PathType, $ErrorAction) return $true }
|
||||
$script:version = '5.1.26100.0'; Assert ((Get-WelaTranscriptCapability).Status -eq 'Supported') '5.1 engine recognized from PowerShell 7 host'
|
||||
$script:version = '4.0'; Assert ((Get-WelaTranscriptCapability).Status -eq 'Unknown') 'older engine is not assumed to support the target policy'
|
||||
} finally { $env:OS = $savedOs; $env:windir = $savedWindir }
|
||||
$tokens = $null; $errors = $null
|
||||
[void][Management.Automation.Language.Parser]::ParseFile((Join-Path $repo 'WELA.ps1'), [ref]$tokens, [ref]$errors)
|
||||
Assert ($errors.Count -eq 0) 'combined WELA entry point parses'
|
||||
Write-Host "Passed $script:checks transcription mock assertions. No Windows policy, ACL or share was changed."
|
||||
} finally { Remove-Item -LiteralPath $root -Recurse -Force -ErrorAction SilentlyContinue }
|
||||
@@ -0,0 +1,111 @@
|
||||
param([switch]$AllowDisposablePolicyWrite)
|
||||
$ErrorActionPreference = 'Stop'
|
||||
if ($env:OS -ne 'Windows_NT') { Write-Host 'Skipped: native Windows ACL/registry tests require Windows.'; exit 0 }
|
||||
if ($AllowDisposablePolicyWrite -and ($env:GITHUB_ACTIONS -ne 'true' -or $env:RUNNER_ENVIRONMENT -ne 'github-hosted')) {
|
||||
throw 'Native policy mutation is restricted to this explicitly opted-in disposable GitHub-hosted runner test.'
|
||||
}
|
||||
$repo = Split-Path $PSScriptRoot -Parent
|
||||
$script:ScriptRoot = $repo
|
||||
. (Join-Path $repo 'scripts/Configuration.ps1')
|
||||
. (Join-Path $repo 'scripts/PowerShellTranscription.ps1')
|
||||
$script:checks = 0
|
||||
function Assert($Condition, [string]$Message) { if (-not $Condition) { throw "FAIL: $Message" }; $script:checks++ }
|
||||
$root = Join-Path ([IO.Path]::GetTempPath()) ('wela-native-transcription-' + [guid]::NewGuid().ToString('N'))
|
||||
$null = New-Item -ItemType Directory -Path $root
|
||||
$policyTouched = $false; $restored = $true; $before = $null
|
||||
function Set-PrivateDirectoryAcl([string]$Path) {
|
||||
$acl = [Security.AccessControl.DirectorySecurity]::new()
|
||||
$acl.SetAccessRuleProtection($true, $false)
|
||||
$sid = [Security.Principal.WindowsIdentity]::GetCurrent().User
|
||||
$acl.SetOwner($sid)
|
||||
foreach ($identity in @($sid.Value, 'S-1-5-18', 'S-1-5-32-544') | Select-Object -Unique) {
|
||||
$rule = [Security.AccessControl.FileSystemAccessRule]::new([Security.Principal.SecurityIdentifier]::new($identity),
|
||||
[Security.AccessControl.FileSystemRights]::FullControl, [Security.AccessControl.InheritanceFlags]'ContainerInherit,ObjectInherit',
|
||||
[Security.AccessControl.PropagationFlags]::None, [Security.AccessControl.AccessControlType]::Allow)
|
||||
$acl.AddAccessRule($rule)
|
||||
}
|
||||
Set-Acl -LiteralPath $Path -AclObject $acl
|
||||
}
|
||||
function Restore-OriginalPolicy($Policy) {
|
||||
$base = $null; $key = $null
|
||||
try {
|
||||
$base = [Microsoft.Win32.RegistryKey]::OpenBaseKey([Microsoft.Win32.RegistryHive]::LocalMachine, [Microsoft.Win32.RegistryView]([string]$Policy[0].View))
|
||||
$key = $base.CreateSubKey('SOFTWARE\Policies\Microsoft\Windows\PowerShell\Transcription')
|
||||
# Stop new-session transcription while restoring the previous location.
|
||||
$key.SetValue('EnableTranscripting', 0, [Microsoft.Win32.RegistryValueKind]::DWord)
|
||||
foreach ($name in @('OutputDirectory', 'EnableTranscripting')) {
|
||||
$original = $Policy[0].Machine.$name
|
||||
if ($original.ValueExists) { $key.SetValue($name, $original.Value, [Microsoft.Win32.RegistryValueKind]([string]$original.Type)) }
|
||||
else { $key.DeleteValue($name, $false) }
|
||||
}
|
||||
$deleteEmptyKey = -not $Policy[0].Machine.EnableTranscripting.KeyExists -and $key.GetValueNames().Count -eq 0 -and $key.GetSubKeyNames().Count -eq 0
|
||||
$key.Dispose(); $key = $null
|
||||
if ($deleteEmptyKey) { $base.DeleteSubKey('SOFTWARE\Policies\Microsoft\Windows\PowerShell\Transcription', $false) }
|
||||
} finally { if ($key) { $key.Dispose() }; if ($base) { $base.Dispose() } }
|
||||
}
|
||||
try {
|
||||
Set-PrivateDirectoryAcl $root
|
||||
$destination = Get-WelaTranscriptDestination $root
|
||||
Assert ($destination.ConfigureAllowed -and $destination.Status -eq 'Observed') 'actual private directory passes conservative ACL observations'
|
||||
Assert ($destination.Acl.Sddl -and $destination.WriterAuthorization -eq 'Unknown') 'SDDL is captured without claiming all writers have access'
|
||||
$unsafe = Join-Path $root 'unsafe-fixture'; $null = New-Item -ItemType Directory -Path $unsafe
|
||||
$unsafeAcl = Get-Acl -LiteralPath $unsafe
|
||||
$unsafeAcl.AddAccessRule([Security.AccessControl.FileSystemAccessRule]::new([Security.Principal.SecurityIdentifier]::new('S-1-1-0'),
|
||||
[Security.AccessControl.FileSystemRights]::ReadAndExecute, [Security.AccessControl.InheritanceFlags]'ContainerInherit,ObjectInherit',
|
||||
[Security.AccessControl.PropagationFlags]::None, [Security.AccessControl.AccessControlType]::Allow))
|
||||
Set-Acl -LiteralPath $unsafe -AclObject $unsafeAcl
|
||||
$blocked = Get-WelaTranscriptDestination $unsafe
|
||||
Assert (-not $blocked.ConfigureAllowed -and $blocked.Status -eq 'Blocked' -and $blocked.Acl.Risks.Count -gt 0) 'actual broad-read ACL fixture is blocked'
|
||||
$missing = Get-WelaTranscriptDestination (Join-Path $root 'not-created')
|
||||
Assert (-not $missing.ConfigureAllowed -and $missing.Status -eq 'Unknown') 'missing destination never auto-created'
|
||||
$capability = Get-WelaTranscriptCapability
|
||||
Assert ($capability.Status -eq 'Supported') 'actual native Windows PowerShell 5.1 installation detected'
|
||||
$before = @(Get-WelaTranscriptPolicy $capability.Views)
|
||||
Test-WelaTranscriptSharedPolicy $before
|
||||
Assert ($before.Count -eq 2) 'actual Windows runner exposes shared 64/32 policy views'
|
||||
if ($AllowDisposablePolicyWrite) {
|
||||
$output = Join-Path $root 'transcripts'; $null = New-Item -ItemType Directory -Path $output
|
||||
$before | ConvertTo-Json -Depth 12 | Set-Content (Join-Path $root 'original-policy.json') -Encoding UTF8
|
||||
$policyTouched = $true; $restored = $false
|
||||
$report = Invoke-WelaTranscriptCommand -Action Configure -OutputDirectory $output -Auto -BackupPath (Join-Path $root 'backup')
|
||||
Assert ($report.ExitCode -eq 0 -and $report.Results[0].Status -eq 'Applied') 'actual policy writes and both-view readback succeed'
|
||||
$again = Invoke-WelaTranscriptCommand -Action Configure -OutputDirectory $output -Auto -BackupPath (Join-Path $root 'repeat-backup')
|
||||
Assert ($again.ExitCode -eq 0 -and $again.Results[0].Status -eq 'AlreadyCompliant') 'actual repeated policy configuration is idempotent'
|
||||
$executables = @((Join-Path $env:windir 'System32\WindowsPowerShell\v1.0\powershell.exe'))
|
||||
$x86 = Join-Path $env:windir 'SysWOW64\WindowsPowerShell\v1.0\powershell.exe'
|
||||
if (Test-Path -LiteralPath $x86 -PathType Leaf) { $executables += $x86 }
|
||||
foreach ($executable in $executables) {
|
||||
$marker = 'WELA-BENIGN-TRANSCRIPT-' + [guid]::NewGuid().ToString('N')
|
||||
$command = "if (`$PSVersionTable.PSVersion.Major -ne 5 -or `$PSVersionTable.PSVersion.Minor -ne 1) { exit 9 }; Write-Output '$marker'"
|
||||
$encoded = [Convert]::ToBase64String([Text.Encoding]::Unicode.GetBytes($command))
|
||||
$start = [Diagnostics.ProcessStartInfo]::new()
|
||||
$start.FileName = $executable; $start.Arguments = '-NoLogo -NoProfile -NonInteractive -EncodedCommand ' + $encoded
|
||||
$start.UseShellExecute = $false; $start.RedirectStandardOutput = $true; $start.RedirectStandardError = $true
|
||||
$process = [Diagnostics.Process]::Start($start)
|
||||
try {
|
||||
if (-not $process.WaitForExit(30000)) { $process.Kill(); throw 'Benign Windows PowerShell child timed out.' }
|
||||
$childOutput = $process.StandardOutput.ReadToEnd(); $childError = $process.StandardError.ReadToEnd()
|
||||
Assert ($process.ExitCode -eq 0 -and $childOutput.Contains($marker)) "benign native 5.1 session succeeded: $executable; $childError"
|
||||
} finally { $process.Dispose() }
|
||||
$found = $false
|
||||
foreach ($file in @(Get-ChildItem -LiteralPath $output -Filter 'PowerShell_transcript*.txt' -Recurse -File)) {
|
||||
if ((Get-Content -LiteralPath $file.FullName -Raw).Contains($marker)) { $found = $true }
|
||||
}
|
||||
Assert $found 'policy-created transcript contains the benign marker without Start-Transcript in the child'
|
||||
}
|
||||
Assert ($report.Telemetry.SigmaEvtxCredit -eq 0) 'native text generation still provides no automatic Sigma EVTX credit'
|
||||
} else { Write-Host 'Native policy mutation skipped. Use the explicit disposable CI switch only on GitHub-hosted runners.' }
|
||||
} finally {
|
||||
try {
|
||||
if ($policyTouched) {
|
||||
Restore-OriginalPolicy $before
|
||||
$after = @(Get-WelaTranscriptPolicy @($before.View))
|
||||
$restored = ($after | ConvertTo-Json -Depth 12 -Compress) -ceq ($before | ConvertTo-Json -Depth 12 -Compress)
|
||||
Assert $restored 'exact original machine/current-user values, types and policy-key presence restored'
|
||||
}
|
||||
} finally {
|
||||
if ($restored) { Remove-Item -LiteralPath $root -Recurse -Force -ErrorAction Stop }
|
||||
else { Write-Host "Policy restoration was not verified. Private evidence directory retained: $root" -ForegroundColor Red }
|
||||
}
|
||||
}
|
||||
Write-Host "Passed $script:checks Windows transcription assertions. Native policy was restored; fixtures were removed. UNC authorization and collection remain lab checks."
|
||||
Reference in new issue
Block a user