mirror of
https://github.com/Yamato-Security/WELA.git
synced 2026-10-04 21:44:43 +02:00
Verify reviewed descendant SACL propagation and preservation (#429)
* 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
This commit is contained in:
1 parent
b84b97b358
commit
bcd4e9717e
15 files changed
+522
-17
No files matched your search
@@ -1,4 +1,5 @@
|
||||
# Explicit selected, existing local targets. No audit-policy writes or hive loading.
|
||||
. (Join-Path $PSScriptRoot 'SelectedSaclDescendants.ps1')
|
||||
function Get-WelaSelectedSaclHash {
|
||||
param([string[]]$Values)
|
||||
$encoding=New-Object Text.UTF8Encoding($false,$true)
|
||||
@@ -7,7 +8,7 @@ function Get-WelaSelectedSaclHash {
|
||||
try {([BitConverter]::ToString($sha.ComputeHash($encoding.GetBytes($text)))).Replace('-','').ToLowerInvariant()} finally {$sha.Dispose()}
|
||||
}
|
||||
function Get-WelaSelectedSaclSources {
|
||||
foreach($path in @('config/audit_sacl_targets.json','config/audit_profiles.json','modules/AuditProfiles.psm1','modules/AuditCatalog.psm1','scripts/TargetedSaclPlanning.ps1','scripts/SelectedSaclConfiguration.ps1','scripts/SelectedSaclNative.cs')) {
|
||||
foreach($path in @('config/audit_sacl_targets.json','config/audit_profiles.json','modules/AuditProfiles.psm1','modules/AuditCatalog.psm1','scripts/TargetedSaclPlanning.ps1','scripts/SelectedSaclConfiguration.ps1','scripts/SelectedSaclNative.cs','scripts/SelectedSaclDescendants.ps1')) {
|
||||
[pscustomobject]@{Path=$path;Sha256=(Get-FileHash -LiteralPath (Join-Path $PSScriptRoot "../$path") -Algorithm SHA256 -ErrorAction Stop).Hash.ToLowerInvariant()}
|
||||
}
|
||||
}
|
||||
@@ -58,9 +59,9 @@ function Initialize-WelaSelectedSaclNative {
|
||||
function Resolve-WelaSelectedSaclNativePath {
|
||||
param($Definition)
|
||||
if($Definition.Resolution -notin @('Resolved','Redirected')){throw "Target path is unresolved: $($Definition.Resolution). No hive is loaded."}
|
||||
$observation=Get-WelaSaclTargetObservation -Path $Definition.Path -Kind $Definition.Kind -SkipSaclRead
|
||||
if($observation.PathState -ne 'Exists'){throw "Selected existing local target is unavailable: $($observation.PathState). $($observation.Diagnostic)"}
|
||||
if($Definition.Kind -eq 'FileSystem') {
|
||||
$observation=Get-WelaSaclTargetObservation -Path $Definition.Path -Kind $Definition.Kind -SkipSaclRead
|
||||
if($observation.PathState -ne 'Exists'){throw "Selected existing local target is unavailable: $($observation.PathState). $($observation.Diagnostic)"}
|
||||
if($Definition.Path -notmatch '^[A-Za-z]:\\'){throw 'Only absolute local filesystem targets are supported.'}
|
||||
if($Definition.Path.Substring(2).Contains(':') -or $Definition.Path -match '[*?<>|]|[ .](\\|$)'){throw 'Ambiguous filesystem target path.'}
|
||||
$full=[IO.Path]::GetFullPath($Definition.Path)
|
||||
@@ -68,6 +69,8 @@ function Resolve-WelaSelectedSaclNativePath {
|
||||
return $full
|
||||
}
|
||||
if($Definition.Kind -ne 'Registry'){throw 'Unsupported target kind.'}
|
||||
# Do not let a registry provider preflight follow a link before the native
|
||||
# component-by-component OPEN_LINK validation. Missing keys fail native open.
|
||||
$path=$Definition.Path -replace '^HKLM:\\','HKEY_LOCAL_MACHINE\' -replace '^Registry::',''
|
||||
if($path -notmatch '^HKEY_(LOCAL_MACHINE|USERS)\\[^\\]+' -or $path -match '\\\\|(^|\\)\.\.?($|\\)|[*?%/\x00-\x1f]'){throw 'Only canonical existing HKLM/HKU keys may be selected.'}
|
||||
return $path
|
||||
@@ -133,6 +136,7 @@ function Write-WelaSelectedSaclJson {
|
||||
param([string]$Path,$Value)
|
||||
$text=($Value | ConvertTo-Json -Depth 24 -Compress)+[Environment]::NewLine
|
||||
$bytes=[Text.UTF8Encoding]::new($false).GetBytes($text)
|
||||
if($Value.Kind -eq 'WelaSelectedSaclPlan' -and $bytes.Length -gt 4194304){throw 'Reviewed plan exceeds the 4 MiB import limit; select fewer roots.'}
|
||||
$stream=[IO.File]::Open($Path,[IO.FileMode]::CreateNew,[IO.FileAccess]::Write,[IO.FileShare]::Read)
|
||||
try {$stream.Write($bytes,0,$bytes.Length);$stream.Flush($true)} finally {$stream.Dispose()}
|
||||
}
|
||||
@@ -160,6 +164,7 @@ function Read-WelaSelectedSaclPlan {
|
||||
if($row.Id -isnot [string] -or $row.Id -cnotmatch '^sacl-[0-9a-f]{24}$' -or $seen.ContainsKey($row.Id)){throw 'Invalid or duplicate reviewed target ID.'};$seen[$row.Id]=$true
|
||||
if((Get-WelaSelectedSaclDefinitionKey $row.Definition) -cne $row.DefinitionKey){throw 'Reviewed target definition was modified.'}
|
||||
$null=Get-WelaSelectedSaclSnapshotKey $row.Before
|
||||
if($plan.IncludeChildren -and ($row.Before.Kind -eq 'Registry' -or $row.Before.IsDirectory)){$null=Get-WelaSelectedSaclDescendantKey $row.DescendantsBefore}
|
||||
}
|
||||
[pscustomobject]@{Path=$full;Hash=(Get-WelaSelectedSaclHash @([Convert]::ToBase64String($bytes)));Plan=$plan}
|
||||
}
|
||||
@@ -198,17 +203,26 @@ function Invoke-WelaSelectedSacl {
|
||||
Assert-WelaSelectedSaclSources $sources
|
||||
foreach($id in $Ids){if(@($catalog.Rows | Where-Object Id -ceq $id).Count -ne 1){throw "Unknown/stale target ID: $id"}}
|
||||
$rows=@(foreach($item in $catalog.Rows){if(-not $selected.ContainsKey($item.Id)){continue}
|
||||
$row=[pscustomobject]@{Id=$item.Id;DefinitionKey=$item.DefinitionKey;Definition=$item.Definition;Before=$null;Ace=$null;Status='Blocked';Diagnostic='';After=$null}
|
||||
$row=[pscustomobject]@{Id=$item.Id;DefinitionKey=$item.DefinitionKey;Definition=$item.Definition;Before=$null;Ace=$null;Status='Blocked';Diagnostic='';After=$null;DescendantsBefore=$null;DescendantsAfter=$null;DescendantVerification=$null}
|
||||
try {
|
||||
$row.Before=Get-WelaSelectedSaclSnapshot $item.Definition
|
||||
$row.Ace=Get-WelaSelectedSaclAce $item.Definition $row.Before -IncludeChildren:$IncludeChildren
|
||||
Assert-WelaSelectedSaclPrerequisites $item.Definition $row.Ace
|
||||
if($IncludeChildren -and ($row.Before.Kind -eq 'Registry' -or $row.Before.IsDirectory)){
|
||||
$row.DescendantsBefore=Get-WelaSelectedSaclStableDescendants $item.Definition $row.Before
|
||||
if($row.DescendantsBefore.Status -ne 'Complete'){throw ('Descendant capture incomplete: '+($row.DescendantsBefore.Diagnostics -join '; '))}
|
||||
}
|
||||
if($imported){
|
||||
$old=@($prior.Rows | Where-Object Id -ceq $item.Id)[0]
|
||||
if($row.DescendantsBefore -and (Get-WelaSelectedSaclDescendantKey $old.DescendantsBefore) -cne (Get-WelaSelectedSaclDescendantKey $row.DescendantsBefore)){throw 'Reviewed descendants changed; review a new plan.'}
|
||||
if($old.DefinitionKey -cne $item.DefinitionKey -or (Get-WelaSelectedSaclSnapshotKey $old.Before) -cne (Get-WelaSelectedSaclSnapshotKey $row.Before) -or
|
||||
$old.Ace.Sid -cne $row.Ace.Sid -or $old.Ace.Mask -ne $row.Ace.Mask -or $old.Ace.Flags -ne $row.Ace.Flags -or $old.Ace.RequiredPolicyMask -ne $row.Ace.RequiredPolicyMask){throw 'Reviewed target definition/identity/descriptor changed; review a new plan.'}
|
||||
}
|
||||
$row.Status=if(Test-WelaSelectedSaclAce $row.Before $row.Ace){'AlreadyCompliant'}else{'ChangeRequired'}
|
||||
if($row.DescendantsBefore -and $row.Status -eq 'AlreadyCompliant'){
|
||||
$row.DescendantVerification=Test-WelaSelectedSaclDescendantOutcomes $row.DescendantsBefore $row.DescendantsBefore $row.Ace
|
||||
if($row.DescendantVerification.Status -ne 'Observed'){$row.Status='Blocked';throw 'Selected root already has its ACE, but reviewed descendant inheritance is unverified. No duplicate root ACE is added.'}
|
||||
}
|
||||
} catch {$row.Diagnostic=$_.Exception.Message}
|
||||
$row
|
||||
})
|
||||
@@ -223,6 +237,16 @@ function Invoke-WelaSelectedSacl {
|
||||
$physical[$key].Status='Blocked';$physical[$key].Diagnostic=$row.Diagnostic
|
||||
}else{$physical[$key]=$row}
|
||||
}
|
||||
foreach($ancestor in $rows){
|
||||
if(-not $ancestor.DescendantsBefore){continue}
|
||||
foreach($child in $rows){
|
||||
if($child -eq $ancestor -or -not $child.Before -or $child.Before.Kind -cne $ancestor.Before.Kind){continue}
|
||||
if($child.Before.Path.StartsWith($ancestor.Before.Path.TrimEnd('\')+'\',[StringComparison]::OrdinalIgnoreCase)){
|
||||
$ancestor.Status='Blocked';$child.Status='Blocked'
|
||||
$ancestor.Diagnostic='Selected ancestor and descendant overlap. Configure one root and review a fresh plan before selecting another.';$child.Diagnostic=$ancestor.Diagnostic
|
||||
}
|
||||
}
|
||||
}
|
||||
$plan=[pscustomobject]@{SchemaVersion=1;Kind='WelaSelectedSaclPlan';CapturedUtc=[DateTime]::UtcNow.ToString('o');Profile=$Profile;IncludeOptional=[bool]$IncludeOptional;IncludeChildren=[bool]$IncludeChildren;Context=$context;Sources=$sources;Rows=$rows;GenerationReadiness='Conditional';UsableRuleCredit=0;Catalog=$(if(-not $Ids){$catalog.Rows}else{@()});UserInventory=$catalog.UserInventory}
|
||||
Assert-WelaSelectedSaclRun $plan $imported
|
||||
if($Action -ne 'Configure'){if($output){Write-WelaSelectedSaclJson $output $plan};return $plan}
|
||||
@@ -237,7 +261,7 @@ function Invoke-WelaSelectedSacl {
|
||||
$null=New-Item -ItemType Directory -Path $backup -ErrorAction Stop
|
||||
}
|
||||
foreach($row in $rows){
|
||||
if($row.Status -eq 'AlreadyCompliant'){$row.After=$row.Before;continue}
|
||||
if($row.Status -eq 'AlreadyCompliant'){$row.After=$row.Before;$row.DescendantsAfter=$row.DescendantsBefore;continue}
|
||||
if($DryRun){$row.Status='Skipped';$row.Diagnostic='Dry run; no SACL or recovery file written.';continue}
|
||||
if(-not $Auto -and (Read-Host "Add the selected audit ACE to $($row.Definition.Path)? (y/N)") -cnotin @('y','Y')){$row.Status='Skipped';$row.Diagnostic='Declined.';continue}
|
||||
try {
|
||||
@@ -245,13 +269,32 @@ function Invoke-WelaSelectedSacl {
|
||||
Assert-WelaSelectedSaclPrerequisites $row.Definition $row.Ace
|
||||
$fresh=Get-WelaSelectedSaclSnapshot $row.Definition
|
||||
if($fresh.Identity -cne $row.Before.Identity -or $fresh.DescriptorBase64 -cne $row.Before.DescriptorBase64){throw 'Target changed before journal/write.'}
|
||||
$receipt=[pscustomobject]@{SchemaVersion=1;Kind='WelaSelectedSaclReceipt';State='Pending';RecordedUtc=[DateTime]::UtcNow.ToString('o');Computer=$context.Computer;ContextKey=$context.Key;Id=$row.Id;Sources=$sources;Definition=$row.Definition;Before=$fresh;Ace=$row.Ace;After=$null}
|
||||
if($row.DescendantsBefore){
|
||||
$freshChildren=Get-WelaSelectedSaclStableDescendants $row.Definition $fresh
|
||||
if((Get-WelaSelectedSaclDescendantKey $freshChildren) -cne (Get-WelaSelectedSaclDescendantKey $row.DescendantsBefore)){throw 'Descendants changed before journal/write.'}
|
||||
}
|
||||
$receipt=[pscustomobject]@{SchemaVersion=1;Kind='WelaSelectedSaclReceipt';State='Pending';RecordedUtc=[DateTime]::UtcNow.ToString('o');Computer=$context.Computer;ContextKey=$context.Key;Id=$row.Id;Sources=$sources;Definition=$row.Definition;Before=$fresh;Ace=$row.Ace;After=$null;DescendantsBefore=$row.DescendantsBefore;DescendantsAfter=$null;DescendantVerification=$null;Ownership='Only the verified explicit selected-root addition; never descendant ACE ownership or bulk rollback authority.'}
|
||||
Write-WelaSelectedSaclJson (Join-Path $backup ($row.Id+'.pending.json')) $receipt
|
||||
Assert-WelaSelectedSaclRun $plan $imported
|
||||
Assert-WelaSelectedSaclPrerequisites $row.Definition $row.Ace
|
||||
$row.After=Write-WelaSelectedSaclNative $row.Definition $fresh $row.Ace
|
||||
Assert-WelaSelectedSaclPreserved $fresh $row.After $row.Ace
|
||||
$receipt.State='Confirmed';$receipt.After=$row.After
|
||||
if($row.DescendantsBefore){
|
||||
$lastChildren=Get-WelaSelectedSaclStableDescendants $row.Definition (Get-WelaSelectedSaclSnapshot $row.Definition)
|
||||
if((Get-WelaSelectedSaclDescendantKey $lastChildren) -cne (Get-WelaSelectedSaclDescendantKey $row.DescendantsBefore)){throw 'Descendants changed after pending receipt; native write refused.'}
|
||||
}
|
||||
try {
|
||||
$row.After=Write-WelaSelectedSaclNative $row.Definition $fresh $row.Ace
|
||||
Assert-WelaSelectedSaclPreserved $fresh $row.After $row.Ace
|
||||
} finally {
|
||||
if($row.DescendantsBefore){
|
||||
try {
|
||||
$row.DescendantsAfter=Get-WelaSelectedSaclStableDescendants $row.Definition (Get-WelaSelectedSaclSnapshot $row.Definition)
|
||||
$row.DescendantVerification=Test-WelaSelectedSaclDescendantOutcomes $row.DescendantsBefore $row.DescendantsAfter $row.Ace
|
||||
}catch{$row.DescendantVerification=[pscustomobject]@{Status='Unverified';Diagnostics=@($_.Exception.Message);Ownership='No descendant ownership or automatic rollback authority.'}}
|
||||
Write-WelaSelectedSaclJson (Join-Path $backup ($row.Id+'.descendants-observed.json')) ([pscustomobject]@{Kind='WelaSelectedSaclDescendantObservation';RecordedUtc=[DateTime]::UtcNow.ToString('o');Id=$row.Id;After=$row.DescendantsAfter;Verification=$row.DescendantVerification})
|
||||
}
|
||||
}
|
||||
if($row.DescendantVerification -and $row.DescendantVerification.Status -ne 'Observed'){throw ('Descendant preservation/propagation unverified: '+($row.DescendantVerification.Diagnostics -join '; '))}
|
||||
$receipt.State='Confirmed';$receipt.After=$row.After;$receipt.DescendantsAfter=$row.DescendantsAfter;$receipt.DescendantVerification=$row.DescendantVerification
|
||||
Write-WelaSelectedSaclJson (Join-Path $backup ($row.Id+'.confirmed.json')) $receipt
|
||||
$row.Status='Applied'
|
||||
}catch{$row.Status='Failed';$row.Diagnostic=$_.Exception.Message}
|
||||
@@ -261,6 +304,10 @@ function Invoke-WelaSelectedSacl {
|
||||
Assert-WelaSelectedSaclRun $plan $imported;Assert-WelaSelectedSaclPrerequisites $row.Definition $row.Ace
|
||||
$fresh=Get-WelaSelectedSaclSnapshot $row.Definition
|
||||
if($fresh.Identity -cne $row.After.Identity -or $fresh.DescriptorBase64 -cne $row.After.DescriptorBase64){throw 'Final selected target state drifted.'}
|
||||
if($row.DescendantsAfter){
|
||||
$finalChildren=Get-WelaSelectedSaclStableDescendants $row.Definition $fresh
|
||||
if((Get-WelaSelectedSaclDescendantKey $finalChildren) -cne (Get-WelaSelectedSaclDescendantKey $row.DescendantsAfter)){throw 'Final descendant membership, identity or descriptor drifted; earlier receipts describe an earlier moment only.'}
|
||||
}
|
||||
}catch{$row.Status='Failed';$row.Diagnostic=$_.Exception.Message}
|
||||
}
|
||||
$report=[pscustomobject]@{SchemaVersion=1;Kind='WelaSelectedSaclResult';ExitCode=$(if(@($rows | Where-Object Status -eq 'Failed').Count){1}else{0});DryRun=[bool]$DryRun;BackupPath=$backup;Plan=$plan;Results=$rows;GenerationReadiness='Conditional';UsableRuleCredit=0}
|
||||
|
||||
@@ -0,0 +1,122 @@
|
||||
# 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())}
|
||||
}
|
||||
@@ -2,6 +2,7 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel;
|
||||
using System.IO;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Security.AccessControl;
|
||||
using System.Security.Principal;
|
||||
@@ -15,6 +16,7 @@ namespace Wela.SelectedSacl {
|
||||
public string DescriptorBase64; public string Owner; public string Group; public string DaclBase64;
|
||||
public int ControlFlags; public int SecurityInformation; public string DescriptorScope; public Ace[] Aces;
|
||||
}
|
||||
public sealed class Children { public string[] Names; public bool Truncated; }
|
||||
public sealed class Privilege : IDisposable {
|
||||
[StructLayout(LayoutKind.Sequential)] struct Luid { public uint Low; public int High; }
|
||||
[StructLayout(LayoutKind.Sequential)] struct TokenPrivileges { public uint Count; public Luid Luid; public uint Attributes; }
|
||||
@@ -54,12 +56,14 @@ namespace Wela.SelectedSacl {
|
||||
[DllImport("advapi32.dll",CharSet=CharSet.Unicode)] static extern int RegOpenKeyEx(IntPtr key,string path,uint options,uint access,out IntPtr opened);
|
||||
[DllImport("advapi32.dll",CharSet=CharSet.Unicode)] static extern int RegQueryValueEx(IntPtr key,string name,IntPtr reserved,out uint type,IntPtr data,ref uint size);
|
||||
[DllImport("advapi32.dll",CharSet=CharSet.Unicode)] static extern int RegQueryInfoKey(IntPtr key,IntPtr cls,IntPtr clsSize,IntPtr reserved,IntPtr subKeys,IntPtr maxSubKey,IntPtr maxClass,IntPtr values,IntPtr maxValueName,IntPtr maxValue,IntPtr securitySize,out long written);
|
||||
[DllImport("advapi32.dll",CharSet=CharSet.Unicode)] static extern int RegEnumKeyEx(IntPtr key,uint index,StringBuilder name,ref uint length,IntPtr reserved,IntPtr cls,IntPtr clsLength,IntPtr written);
|
||||
[DllImport("advapi32.dll")] static extern int RegCloseKey(IntPtr key);
|
||||
[DllImport("advapi32.dll")] static extern uint GetSecurityInfo(IntPtr handle,uint kind,uint flags,out IntPtr owner,out IntPtr group,out IntPtr dacl,out IntPtr sacl,out IntPtr descriptor);
|
||||
[DllImport("advapi32.dll")] static extern uint GetSecurityDescriptorLength(IntPtr descriptor);
|
||||
[DllImport("advapi32.dll")] static extern uint SetSecurityInfo(IntPtr handle,uint kind,uint flags,IntPtr owner,IntPtr group,IntPtr dacl,IntPtr sacl);
|
||||
IntPtr handle;readonly List<IntPtr> keys=new List<IntPtr>();readonly string path;readonly string kind;readonly uint objectType;
|
||||
public Target(string kind,string path) {
|
||||
public Target(string kind,string path) : this(kind,path,false) {}
|
||||
public Target(string kind,string path,bool enumerate) {
|
||||
this.kind=kind;this.path=path;objectType=kind=="FileSystem"?1U:4U;
|
||||
try {
|
||||
if(kind=="FileSystem") {
|
||||
@@ -78,7 +82,7 @@ namespace Wela.SelectedSacl {
|
||||
else throw new InvalidOperationException("Only explicitly selected HKLM/HKU keys are supported.");
|
||||
if(parts.Length<2)throw new InvalidOperationException("A registry hive root cannot be selected.");
|
||||
for(int i=1;i<parts.Length;i++) {
|
||||
IntPtr opened;int error=RegOpenKeyEx(current,parts[i],8,0x01020101,out opened); // OPEN_LINK, 64-bit view, query/read-control/SACL.
|
||||
IntPtr opened;int error=RegOpenKeyEx(current,parts[i],8,0x01020101U|((enumerate&&i==parts.Length-1)?8U:0U),out opened); // OPEN_LINK, 64-bit view, query/read-control/SACL.
|
||||
if(error!=0)throw new Win32Exception(error);keys.Add(opened);current=opened;
|
||||
uint type;uint size=0;error=RegQueryValueEx(current,"SymbolicLinkValue",IntPtr.Zero,out type,IntPtr.Zero,ref size);
|
||||
if(error==0&&type==6)throw new InvalidOperationException("Registry symbolic-link component refused.");
|
||||
@@ -88,6 +92,27 @@ namespace Wela.SelectedSacl {
|
||||
} else throw new InvalidOperationException("Unsupported selected target kind.");
|
||||
}catch {Dispose();throw;}
|
||||
}
|
||||
public Children Enumerate(int maximum) {
|
||||
if(maximum<1||maximum>129)throw new ArgumentOutOfRangeException("maximum");
|
||||
if(handle==IntPtr.Zero)throw new ObjectDisposedException("Target");
|
||||
List<string> names=new List<string>();bool truncated=false;
|
||||
if(kind=="Registry") {
|
||||
for(uint index=0;;index++) {
|
||||
StringBuilder name=new StringBuilder(256);uint length=256;
|
||||
int error=RegEnumKeyEx(handle,index,name,ref length,IntPtr.Zero,IntPtr.Zero,IntPtr.Zero,IntPtr.Zero);
|
||||
if(error==259)break;if(error!=0)throw new Win32Exception(error,"Registry child enumeration failed.");
|
||||
if(names.Count==maximum){truncated=true;break;}names.Add(name.ToString());
|
||||
}
|
||||
} else {
|
||||
// The verified parent handle remains open without DELETE sharing during enumeration.
|
||||
foreach(string entry in Directory.EnumerateFileSystemEntries(path)) {
|
||||
if(names.Count==maximum){truncated=true;break;}names.Add(System.IO.Path.GetFileName(entry));
|
||||
}
|
||||
}
|
||||
HashSet<string> seen=new HashSet<string>(StringComparer.OrdinalIgnoreCase);
|
||||
foreach(string name in names)if(String.IsNullOrEmpty(name)||name=="."||name==".."||name.IndexOfAny(new char[]{'\\','/','\0'})>=0||!seen.Add(name))throw new InvalidOperationException("Ambiguous or duplicate child name.");
|
||||
names.Sort(StringComparer.OrdinalIgnoreCase);return new Children {Names=names.ToArray(),Truncated=truncated};
|
||||
}
|
||||
static string Bytes(GenericAcl acl){if(acl==null)return null;byte[] bytes=new byte[acl.BinaryLength];acl.GetBinaryForm(bytes,0);return Convert.ToBase64String(bytes);}
|
||||
static string Bytes(GenericAce ace){byte[] bytes=new byte[ace.BinaryLength];ace.GetBinaryForm(bytes,0);return Convert.ToBase64String(bytes);}
|
||||
public Snapshot Read() {
|
||||
|
||||
Reference in new issue
Block a user