mirror of
https://github.com/Yamato-Security/WELA.git
synced 2026-10-02 20:44:42 +02:00
112 lines
8.4 KiB
PowerShell
112 lines
8.4 KiB
PowerShell
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."
|