mirror of
https://github.com/Yamato-Security/WELA.git
synced 2026-09-29 18:44:31 +02:00
87 lines
6.7 KiB
PowerShell
87 lines
6.7 KiB
PowerShell
# Real Windows descriptor API tests and read-only metadata/CLI smoke. No channel writes.
|
|
$ErrorActionPreference = 'Stop'
|
|
$repo = Split-Path $PSScriptRoot -Parent
|
|
Import-Module (Join-Path $repo 'modules/EventLogSettings.psm1') -Force
|
|
Import-Module (Join-Path $repo 'modules/NativeProviders.psm1') -Force
|
|
Import-Module (Join-Path $repo 'modules/NativeChannelAccess.psm1') -Force
|
|
$script:assertions = 0
|
|
function Assert($Condition, [string]$Message) {
|
|
if (-not $Condition) { throw "FAIL: $Message" }; $script:assertions++
|
|
}
|
|
function Binary($Value) {
|
|
$bytes = New-Object byte[] $Value.BinaryLength
|
|
$Value.GetBinaryForm($bytes, 0)
|
|
[Convert]::ToBase64String($bytes)
|
|
}
|
|
function Check-Preservation([string]$Sddl) {
|
|
$before = [System.Security.AccessControl.RawSecurityDescriptor]::new($Sddl)
|
|
$plan = Get-WelaChannelAccessPlan -SecurityDescriptor $Sddl
|
|
Assert ($plan.State -eq 'GrantRequired') "Descriptor supports lossless append: $($plan.Diagnostic)"
|
|
$after = [System.Security.AccessControl.RawSecurityDescriptor]::new($plan.ProposedDescriptor)
|
|
Assert ($before.Owner -eq $after.Owner -and $before.Group -eq $after.Group) 'Owner/group are retained'
|
|
Assert ($before.ControlFlags -eq $after.ControlFlags -and $before.ResourceManagerControl -eq $after.ResourceManagerControl) 'Control flags are retained'
|
|
Assert (($null -eq $before.SystemAcl -and $null -eq $after.SystemAcl) -or ((Binary $before.SystemAcl) -ceq (Binary $after.SystemAcl))) 'Complete SACL bytes are retained'
|
|
Assert ($after.DiscretionaryAcl.Count -eq $before.DiscretionaryAcl.Count + 1) 'Exactly one DACL ACE is added'
|
|
$j = 0
|
|
for ($i = 0; $i -lt $after.DiscretionaryAcl.Count; $i++) {
|
|
if ($i -eq $plan.AddedAceIndex) {
|
|
$ace = $after.DiscretionaryAcl[$i]
|
|
Assert ($ace.SecurityIdentifier.Value -eq 'S-1-5-32-573' -and $ace.AccessMask -eq 1 -and $ace.AceFlags -eq 0 -and -not $ace.IsCallback) 'New ACE is precisely unconditional Event Log Readers read (no write/clear)'
|
|
} else {
|
|
Assert ((Binary $before.DiscretionaryAcl[$j]) -ceq (Binary $after.DiscretionaryAcl[$i])) 'Every preexisting ACE stays byte-identical and in order'
|
|
$j++
|
|
}
|
|
}
|
|
Assert ($plan.EffectiveReadAccess -eq 'Not tested') 'Structural grant does not prove effective token access'
|
|
$second = Get-WelaChannelAccessPlan -SecurityDescriptor $plan.ProposedDescriptor
|
|
Assert ($second.State -eq 'GrantPresent' -and -not $second.ProposedDescriptor) 'Repeated planning does not duplicate the ACE'
|
|
Assert (Test-WelaChannelDescriptorEqual $plan.ProposedDescriptor $after.GetSddlForm('All')) 'Binary descriptor comparison handles Windows SDDL formatting'
|
|
Assert (-not (Test-WelaChannelDescriptorEqual $Sddl $plan.ProposedDescriptor)) 'Descriptor comparison detects the added ACE'
|
|
}
|
|
|
|
Check-Preservation 'O:BAG:SYD:PAI(A;;0x7;;;BA)(A;;0x2;;;AU)(A;ID;0x1;;;SY)S:AI(AU;SAFA;0x1;;;WD)'
|
|
Check-Preservation 'O:BAG:SYD:(OA;;0x2;00112233-4455-6677-8899-aabbccddeeff;;AU)(A;;0x7;;;BA)'
|
|
Check-Preservation 'O:BAG:SYD:(A;;0x2;;;S-1-5-32-573)(A;;0x7;;;BA)'
|
|
foreach ($sddl in @('O:BAG:SYD:(A;;0x1;;;S-1-5-32-573)', 'O:BAG:SYD:(A;;0x7;;;S-1-5-32-573)')) {
|
|
$result = Get-WelaChannelAccessPlan $sddl
|
|
Assert ($result.State -eq 'GrantPresent' -and -not $result.ProposedDescriptor -and $result.EffectiveReadAccess -eq 'Not tested') 'Existing read/superset permission is preserved without claiming event access'
|
|
}
|
|
foreach ($sddl in @('', 'invalid', 'O:BAG:SY', 'O:BAG:SYD:NO_ACCESS_CONTROL', 'O:BAG:SYD:(D;;0x1;;;WD)(A;;0x7;;;BA)', 'O:BAG:SYD:(D;;GR;;;WD)(A;;0x7;;;BA)')) {
|
|
$result = Get-WelaChannelAccessPlan $sddl
|
|
Assert ($result.State -eq 'ManualReview' -and -not $result.ProposedDescriptor) 'Missing, invalid, null and denied descriptors refuse automatic modification'
|
|
}
|
|
# The original unknown ACE bytes must never be discarded. SDDL has no representation
|
|
# for arbitrary custom ACEs; parsing/planning must refuse instead of replacing them.
|
|
$raw = [System.Security.AccessControl.RawSecurityDescriptor]::new('O:BAG:SYD:(A;;0x7;;;BA)')
|
|
$raw.DiscretionaryAcl.InsertAce(1, [System.Security.AccessControl.CustomAce]::new(([Enum]::ToObject([System.Security.AccessControl.AceType], 127)), [System.Security.AccessControl.AceFlags]::None, [byte[]]@(0, 0, 0, 0)))
|
|
$binaryBefore = Binary $raw
|
|
$refused = $false
|
|
try {
|
|
$sddl = $raw.GetSddlForm('All')
|
|
$refused = (Get-WelaChannelAccessPlan $sddl).State -eq 'ManualReview'
|
|
} catch { $refused = $true }
|
|
Assert ($refused -and (Binary $raw) -ceq $binaryBefore) 'Unsupported unknown ACEs are retained and mutation is refused'
|
|
|
|
$profile = Get-WelaNativeChannelProfile
|
|
$before = @{}
|
|
foreach ($control in $profile.controls) { $before[$control.channel] = Get-WelaNativeChannel -Name $control.channel }
|
|
# Exercise actual CLI dispatch and JSON export using live Windows read APIs.
|
|
$out = Join-Path ([IO.Path]::GetTempPath()) ('wela-native-channel-live-' + [guid]::NewGuid().ToString('N') + '.json')
|
|
$shell = (Get-Process -Id $PID).Path
|
|
try {
|
|
& $shell -NoProfile -File (Join-Path $repo 'WELA.ps1') channel-settings -ChannelAction Plan -GrantEventLogReaders -ResultsPath $out
|
|
$cliExit = $LASTEXITCODE
|
|
$report = Get-Content -LiteralPath $out -Raw -ErrorAction Stop | ConvertFrom-Json
|
|
Assert ($cliExit -eq $report.ExitCode -and $cliExit -in @(0, 1)) 'Read-only CLI exit code agrees with its report (missing/unknown channels may return 1)'
|
|
Assert ($report.Action -eq 'Plan' -and $report.QueryInventory.Count -eq 18 -and $report.ForwardingReadiness -eq 'Not verified') 'Real CLI plan exports channel inventory without a forwarding claim'
|
|
Assert ($report.ExcludedQueries.Count -eq 2 -and @($report.QueryInventory | Where-Object { $_.Channel.Name -like '*Sysmon*' }).Count -eq 0) 'Live public output excludes non-native queries'
|
|
foreach ($control in $profile.controls) {
|
|
$first = $before[$control.channel]; $last = Get-WelaNativeChannel -Name $control.channel
|
|
Assert ($first.State -eq $last.State -and $first.MaximumSizeInBytes -eq $last.MaximumSizeInBytes -and $first.LogMode -eq $last.LogMode -and $first.SecurityDescriptor -ceq $last.SecurityDescriptor) 'Live plan leaves channel metadata unchanged (or detects concurrent external drift)'
|
|
$row = @($report.Controls | Where-Object { $_.Definition.channel -eq $control.channel })[0]
|
|
Assert ($row.Before.MaximumSizeInBytes -eq $first.MaximumSizeInBytes -and $row.Before.SecurityDescriptor -ceq $first.SecurityDescriptor) 'Live exported metadata matches the actual native reader'
|
|
}
|
|
Write-Host "PASS: $script:assertions real Windows ACL and read-only CLI assertions. Identity access, event generation and forwarding were not tested."
|
|
$global:LASTEXITCODE = 0 # A reported missing/manual-review channel is valid smoke evidence.
|
|
} finally { Remove-Item -LiteralPath $out -Force -ErrorAction SilentlyContinue }
|