mirror of
https://github.com/Yamato-Security/WELA.git
synced 2026-10-01 12:04:46 +02:00
* Verify reviewed descendant SACL propagation and preservation * Reference descendant SACL PR429 in release notes * Prepare protected disposable SACL fixtures through native handles * Use read-control handles for disposable native SACL protection
123 lines
10 KiB
PowerShell
123 lines
10 KiB
PowerShell
# Bounded observations only. Descendants are never supplied to the native writer.
|
|
function Get-WelaSelectedSaclChildNames {
|
|
param($Definition,$Snapshot,[int]$Maximum)
|
|
$path=Resolve-WelaSelectedSaclNativePath $Definition
|
|
Initialize-WelaSelectedSaclNative
|
|
$privilege=New-Object Wela.SelectedSacl.Privilege;$target=$null
|
|
try {
|
|
$target=New-Object Wela.SelectedSacl.Target($Definition.Kind,$path,$true)
|
|
$before=$target.Read()
|
|
if((Get-WelaSelectedSaclSnapshotKey $before) -cne (Get-WelaSelectedSaclSnapshotKey $Snapshot)){throw 'Container changed before enumeration.'}
|
|
$children=$target.Enumerate($Maximum)
|
|
if((Get-WelaSelectedSaclSnapshotKey ($target.Read())) -cne (Get-WelaSelectedSaclSnapshotKey $before)){throw 'Container changed during enumeration.'}
|
|
$children
|
|
} finally {if($target){$target.Dispose()};$privilege.Dispose()}
|
|
}
|
|
function New-WelaSelectedSaclChildDefinition {
|
|
param([string]$Kind,[string]$Path)
|
|
[pscustomobject]@{Kind=$Kind;Path=$(if($Kind -eq 'Registry'){'Registry::'+$Path}else{$Path});Resolution='Resolved'}
|
|
}
|
|
function Get-WelaSelectedSaclDescendantKey {
|
|
param($Inventory)
|
|
if($null -eq $Inventory -or $Inventory.Status -cne 'Complete' -or $Inventory.Maximum -ne 128 -or $Inventory.MaximumDepth -ne 16 -or $Inventory.Entries -isnot [array] -or $Inventory.Entries.Count -gt 128){throw 'Descendant capture is incomplete or has unknown limits; review a new plan.'}
|
|
$fields=@('128','16',(Get-WelaSelectedSaclSnapshotKey $Inventory.Root))
|
|
foreach($entry in $Inventory.Entries){
|
|
if($entry.ProtectedBarrier -isnot [bool] -or $entry.Depth -lt 1 -or $entry.Depth -gt 16 -or $entry.Path -cne $entry.Snapshot.Path){throw 'Malformed descendant evidence.'}
|
|
$fields+=@($entry.Path,$entry.ParentPath,[string]$entry.Depth,[string]$entry.ProtectedBarrier,(Get-WelaSelectedSaclSnapshotKey $entry.Snapshot))
|
|
}
|
|
Get-WelaSelectedSaclHash $fields
|
|
}
|
|
function Get-WelaSelectedSaclDescendants {
|
|
param($Definition,$RootSnapshot)
|
|
$entries=New-Object 'System.Collections.Generic.List[object]'
|
|
$diagnostics=New-Object 'System.Collections.Generic.List[string]'
|
|
$queue=New-Object 'System.Collections.Generic.Queue[object]'
|
|
$queue.Enqueue([pscustomobject]@{Definition=$Definition;Snapshot=$RootSnapshot;Depth=0;ProtectedBarrier=$false})
|
|
$seen=New-Object 'System.Collections.Generic.HashSet[string]' ([StringComparer]::OrdinalIgnoreCase)
|
|
$null=$seen.Add($RootSnapshot.Path)
|
|
$identities=New-Object 'System.Collections.Generic.HashSet[string]' ([StringComparer]::Ordinal)
|
|
if($RootSnapshot.Kind -eq 'FileSystem'){$null=$identities.Add($RootSnapshot.Identity)}
|
|
$started=[DateTime]::UtcNow;$bytes=0
|
|
try {
|
|
while($queue.Count){
|
|
if(([DateTime]::UtcNow-$started).TotalSeconds -gt 30){throw 'Descendant scan time budget exceeded (individual native reads are not cancellable).'}
|
|
$parent=$queue.Dequeue()
|
|
if($parent.Snapshot.Kind -ne 'Registry' -and -not $parent.Snapshot.IsDirectory){continue}
|
|
$remaining=128-$entries.Count
|
|
$children=Get-WelaSelectedSaclChildNames $parent.Definition $parent.Snapshot ([Math]::Max(1,$remaining))
|
|
if($children.Truncated -or @($children.Names).Count -gt $remaining){throw 'Descendant count exceeds the reviewed maximum of 128.'}
|
|
if($parent.Depth -ge 16 -and @($children.Names).Count){throw 'Descendant depth exceeds the reviewed maximum of 16.'}
|
|
foreach($name in $children.Names){
|
|
if([string]::IsNullOrEmpty($name) -or $name -in @('.','..') -or $name -match '[\\/\x00-\x1f]' -or ($parent.Snapshot.Kind -eq 'FileSystem' -and $name -match '[:*?<>|]|[ .]$')){throw 'Ambiguous native descendant name.'}
|
|
$path=$parent.Snapshot.Path.TrimEnd('\')+'\'+$name
|
|
if(-not $seen.Add($path)){throw 'Duplicate descendant path during enumeration.'}
|
|
$childDefinition=New-WelaSelectedSaclChildDefinition $Definition.Kind $path
|
|
$snapshot=Get-WelaSelectedSaclSnapshot $childDefinition
|
|
if($snapshot.Path -ine $path -or $snapshot.Kind -cne $Definition.Kind){throw 'Descendant snapshot does not identify the enumerated child.'}
|
|
$null=Get-WelaSelectedSaclSnapshotKey $snapshot
|
|
if($snapshot.Kind -eq 'FileSystem' -and -not $identities.Add($snapshot.Identity)){throw 'Repeated file identity (hard link/alias) prevents unique descendant attribution.'}
|
|
$bytes+=[Text.Encoding]::UTF8.GetByteCount(($snapshot|ConvertTo-Json -Depth 12 -Compress))
|
|
if($bytes -gt 2097152){throw 'Descendant snapshot evidence exceeds 2 MiB.'}
|
|
$barrier=$parent.ProtectedBarrier -or (($snapshot.ControlFlags -band 8192) -ne 0)
|
|
$entry=[pscustomobject]@{Path=$path;ParentPath=$parent.Snapshot.Path;Depth=$parent.Depth+1;ProtectedBarrier=[bool]$barrier;Snapshot=$snapshot}
|
|
$entries.Add($entry)
|
|
$queue.Enqueue([pscustomobject]@{Definition=$childDefinition;Snapshot=$snapshot;Depth=$entry.Depth;ProtectedBarrier=[bool]$barrier})
|
|
}
|
|
}
|
|
# A second pass by the caller verifies membership and descriptor stability.
|
|
}catch{$diagnostics.Add($_.Exception.Message)}
|
|
[pscustomobject]@{Status=$(if($diagnostics.Count){'Incomplete'}else{'Complete'});Maximum=128;MaximumDepth=16;StartedUtc=$started.ToString('o');CompletedUtc=[DateTime]::UtcNow.ToString('o');Root=$RootSnapshot;Entries=@($entries.ToArray());Diagnostics=@($diagnostics.ToArray())}
|
|
}
|
|
function Get-WelaSelectedSaclStableDescendants {
|
|
param($Definition,$Snapshot)
|
|
$first=Get-WelaSelectedSaclDescendants $Definition $Snapshot
|
|
if($first.Status -ne 'Complete'){return $first}
|
|
$fresh=Get-WelaSelectedSaclSnapshot $Definition
|
|
$second=Get-WelaSelectedSaclDescendants $Definition $fresh
|
|
if($second.Status -eq 'Complete' -and (Get-WelaSelectedSaclDescendantKey $first) -cne (Get-WelaSelectedSaclDescendantKey $second)){
|
|
$second.Status='Incomplete';$second.Diagnostics=@('Descendant membership, identity or descriptor changed between captures.')
|
|
}
|
|
$second
|
|
}
|
|
function Assert-WelaSelectedSaclDescendantPreservation {
|
|
param($Before,$After,[bool]$Protected)
|
|
if($Before.Kind -cne $After.Kind -or $Before.Path -cne $After.Path -or $Before.IsDirectory -ne $After.IsDirectory -or $Before.SecurityInformation -ne $After.SecurityInformation -or $Before.DescriptorScope -cne $After.DescriptorScope){throw 'Child identity/type or descriptor scope changed.'}
|
|
# Registry identity incorporates last-write time and therefore can change as part of an ACL update.
|
|
if($Before.Kind -eq 'FileSystem' -and $Before.Identity -cne $After.Identity){throw 'Child file identity changed.'}
|
|
if($Before.Owner -cne $After.Owner -or $Before.Group -cne $After.Group -or $Before.DaclBase64 -cne $After.DaclBase64 -or ($Before.ControlFlags -band (-bnot 2576)) -ne ($After.ControlFlags -band (-bnot 2576))){throw 'Child owner/group/DACL/protection or non-SACL controls changed.'}
|
|
if($Protected -and $Before.DescriptorBase64 -cne $After.DescriptorBase64){throw 'Protected child or protected subtree descriptor changed.'}
|
|
$counts=New-Object 'System.Collections.Generic.Dictionary[string,int]' ([StringComparer]::Ordinal)
|
|
foreach($entry in $After.Aces){if(-not $counts.ContainsKey($entry.Binary)){$counts[$entry.Binary]=0};$counts[$entry.Binary]++}
|
|
foreach($entry in $Before.Aces){if(-not $counts.ContainsKey($entry.Binary) -or $counts[$entry.Binary] -lt 1){throw 'Original child audit/unknown ACE changed or disappeared.'};$counts[$entry.Binary]--}
|
|
# Arbitrary new explicit/unknown ACEs cannot be attributed to inheritance.
|
|
foreach($entry in $After.Aces){if($counts[$entry.Binary] -gt 0 -and (-not $entry.Ordinary -or $entry.Type -ne 2 -or ($entry.Flags -band 16) -eq 0)){throw 'Unexplained explicit or unknown child ACE appeared.'}}
|
|
}
|
|
function Test-WelaSelectedSaclDescendantOutcomes {
|
|
param($Before,$After,$Ace)
|
|
$outcomes=New-Object 'System.Collections.Generic.List[object]'
|
|
$diagnostics=New-Object 'System.Collections.Generic.List[string]'
|
|
if($After.Status -ne 'Complete'){$diagnostics.Add('After-state inventory is incomplete: '+($After.Diagnostics -join '; '))}
|
|
$map=@{};foreach($entry in $After.Entries){$map[$entry.Path]=$entry}
|
|
foreach($entry in $Before.Entries){
|
|
$status='Unverified';$message='';$actual=$null
|
|
try {
|
|
if(-not $map.ContainsKey($entry.Path)){throw 'Reviewed descendant disappeared or could not be observed.'}
|
|
$actual=$map[$entry.Path];$map.Remove($entry.Path)
|
|
if($actual.ParentPath -cne $entry.ParentPath -or $actual.Depth -ne $entry.Depth -or $actual.ProtectedBarrier -ne $entry.ProtectedBarrier){throw 'Child topology or inheritance protection changed.'}
|
|
Assert-WelaSelectedSaclDescendantPreservation $entry.Snapshot $actual.Snapshot $entry.ProtectedBarrier
|
|
if($entry.ProtectedBarrier){$status='ProtectedUnchanged'}
|
|
elseif(($Ace.Flags -band 3) -eq 0){$status='PreservedWithoutRequestedInheritance'}
|
|
else {
|
|
$flags=($Ace.Flags -band 192) -bor 16
|
|
if($actual.Snapshot.Kind -eq 'Registry' -or $actual.Snapshot.IsDirectory){$flags=$flags -bor ($Ace.Flags -band 3)}
|
|
$expected=[pscustomobject]@{Sid=$Ace.Sid;Mask=$Ace.Mask;Flags=$flags}
|
|
if(-not (Test-WelaSelectedSaclAce $actual.Snapshot $expected)){throw 'Requested inherited audit ACE was not observed; propagation may be incomplete or blocked.'}
|
|
$status='InheritedAceObserved'
|
|
}
|
|
}catch{$message=$_.Exception.Message;$diagnostics.Add($entry.Path+': '+$message)}
|
|
$outcomes.Add([pscustomobject]@{Path=$entry.Path;Status=$status;Diagnostic=$message;Before=$entry.Snapshot;After=$(if($actual){$actual.Snapshot}else{$null})})
|
|
}
|
|
foreach($entry in $map.Values){$outcomes.Add([pscustomobject]@{Path=$entry.Path;Status='NewUnreviewedChild';Diagnostic='Child appeared after the reviewed snapshot; no pre-write backup or ownership established.';Before=$null;After=$entry.Snapshot});$diagnostics.Add('New unreviewed descendant: '+$entry.Path)}
|
|
[pscustomobject]@{Status=$(if($diagnostics.Count){'Unverified'}else{'Observed'});Scope='Reviewed existing descendants at the recorded observations only; propagation is non-atomic. Registry recreation between post-write observations cannot be excluded by last-write metadata.';Ownership='No descendant ACE ownership or automatic rollback authority.';Outcomes=@($outcomes.ToArray());Diagnostics=@($diagnostics.ToArray())}
|
|
}
|