mirror of
https://github.com/Yamato-Security/WELA.git
synced 2026-10-08 07:15:25 +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
@@ -41,6 +41,7 @@ function Get-WelaSelectedSaclSnapshot {
|
||||
if($script:scenario -eq 'read-denied'){throw 'Selected descriptor access denied'}
|
||||
Clone $script:states[$Definition.Path]
|
||||
}
|
||||
function Get-WelaSelectedSaclChildNames {param($Definition,$Snapshot,$Maximum) [pscustomobject]@{Names=@();Truncated=$false}}
|
||||
function Write-WelaSelectedSaclNative {
|
||||
param($Definition,$Before,$Ace)
|
||||
$script:writes++
|
||||
|
||||
@@ -0,0 +1,142 @@
|
||||
$ErrorActionPreference='Stop'
|
||||
$root=Split-Path $PSScriptRoot -Parent
|
||||
Import-Module (Join-Path $root 'modules/AuditProfiles.psm1') -Force
|
||||
. (Join-Path $root 'scripts/SelectedSaclConfiguration.ps1')
|
||||
$script:count=0
|
||||
function Assert($Condition,$Message){if(-not $Condition){throw $Message};$script:count++}
|
||||
function Clone($Value){$Value|ConvertTo-Json -Depth 24|ConvertFrom-Json}
|
||||
function Throws($Action,$Pattern){$message='';try{& $Action|Out-Null}catch{$message=$_.Exception.Message};Assert ($message -match $Pattern) "Expected $Pattern, got $message"}
|
||||
function Snapshot($Path,[bool]$Directory=$false){
|
||||
[pscustomobject]@{SecurityInformation=511;DescriptorScope='WinSDK-defined sections 0x1ff; future sections unobserved';Path=$Path;Kind='FileSystem';Identity=$Path;IsDirectory=$Directory;DescriptorBase64=('before-'+$Path);Owner='S-1-5-18';Group='S-1-5-18';DaclBase64='dacl';ControlFlags=32788;Aces=@([pscustomobject]@{Binary='original';Type=17;Flags=0;Mask=0;Sid=$null;Ordinary=$false})}
|
||||
}
|
||||
$script:definition=[pscustomobject]@{Origin='fixture';Scope='files';UserSid=$null;Path='C:\owned';Kind='FileSystem';Resolution='Resolved';PrincipalSid='S-1-1-0';Propagation='None';Inheritance='ContainerInherit,ObjectInherit';Rights=@('ReadData');AuditFlags=@('Success');Policy='fixture';PolicyMode='minimum';PolicySelected=$true;RequiredPolicyMask=1}
|
||||
$key=Get-WelaSelectedSaclDefinitionKey $script:definition;$script:id='sacl-'+$key.Substring(0,24)
|
||||
function Get-WelaSelectedSaclContext {[pscustomobject]@{Computer='fixture';Role='MemberServer';Build=26100;Key='same-host'}}
|
||||
function Get-WelaSelectedSaclCatalog {param($Profile,$IncludeOptional,$Context) [pscustomobject]@{Profile=$Profile;Rows=@([pscustomobject]@{Id=$script:id;DefinitionKey=(Get-WelaSelectedSaclDefinitionKey $script:definition);Definition=$script:definition});UserInventory=@()}}
|
||||
function Assert-WelaSelectedSaclPrerequisites {}
|
||||
$script:states=@{};$script:names=@{};$script:scenario='';$script:writes=0;$script:enumerations=0
|
||||
function Reset {
|
||||
$script:states=@{};$script:names=@{};$script:scenario='';$script:writes=0;$script:enumerations=0
|
||||
$script:states['C:\owned']=Snapshot 'C:\owned' $true
|
||||
$script:states['C:\owned\open']=Snapshot 'C:\owned\open' $true
|
||||
$script:states['C:\owned\open\leaf']=Snapshot 'C:\owned\open\leaf'
|
||||
$script:states['C:\owned\protected']=Snapshot 'C:\owned\protected' $true
|
||||
$script:states['C:\owned\protected'].ControlFlags=32788 -bor 8192
|
||||
$script:states['C:\owned\protected\leaf']=Snapshot 'C:\owned\protected\leaf'
|
||||
$script:names['C:\owned']=@('open','protected');$script:names['C:\owned\open']=@('leaf');$script:names['C:\owned\protected']=@('leaf')
|
||||
}
|
||||
function Get-WelaSelectedSaclSnapshot {
|
||||
param($Definition)
|
||||
if($Definition.Path -eq 'C:\owned\open\leaf' -and $script:scenario -eq 'after-pending-drift' -and (Test-Path (Join-Path $script:backup ($script:id+'.pending.json')))){$script:states[$Definition.Path].DescriptorBase64='changed';$script:scenario=''}
|
||||
if($Definition.Path -eq 'C:\owned\open\leaf' -and $script:scenario -eq 'final-drift' -and (Test-Path (Join-Path $script:backup ($script:id+'.confirmed.json')))){$script:states[$Definition.Path].DescriptorBase64='changed';$script:scenario=''}
|
||||
if($script:scenario -eq 'denied' -and $Definition.Path -eq 'C:\owned\open\leaf'){throw 'Native descendant access denied'}
|
||||
if(-not $script:states.ContainsKey($Definition.Path)){throw 'Missing child'}
|
||||
Clone $script:states[$Definition.Path]
|
||||
}
|
||||
function Get-WelaSelectedSaclChildNames {
|
||||
param($Definition,$Snapshot,$Maximum)
|
||||
$script:enumerations++
|
||||
if($script:scenario -eq 'reparse'){throw 'Reparse-point target refused'}
|
||||
if($script:scenario -eq 'registry-link'){throw 'Registry symbolic-link component refused'}
|
||||
if($script:scenario -eq 'capture-drift' -and $script:enumerations -eq 4){$script:states['C:\owned\open\leaf'].DescriptorBase64='changed'}
|
||||
$children=@($script:names[$Definition.Path] | Where-Object {$null -ne $_})
|
||||
[pscustomobject]@{Names=@($children|Select-Object -First $Maximum);Truncated=($children.Count -gt $Maximum)}
|
||||
}
|
||||
function Add-Ace($Snapshot,$Ace,[bool]$Inherited){
|
||||
$flags=$Ace.Flags
|
||||
if($Inherited){$flags=($Ace.Flags -band 192) -bor 16;if($Snapshot.IsDirectory -or $Snapshot.Kind -eq 'Registry'){$flags=$flags -bor ($Ace.Flags -band 3)}}
|
||||
$Snapshot.Aces+=@([pscustomobject]@{Binary=('added-'+$flags);Type=2;Flags=$flags;Mask=$Ace.Mask;Sid=$Ace.Sid;Ordinary=$true})
|
||||
$Snapshot.DescriptorBase64='after-'+$Snapshot.DescriptorBase64
|
||||
}
|
||||
function Write-WelaSelectedSaclNative {
|
||||
param($Definition,$Before,$Ace)
|
||||
$script:writes++
|
||||
$pending=Get-Content -LiteralPath (Join-Path $script:backup ($script:id+'.pending.json')) -Raw|ConvertFrom-Json
|
||||
Assert ($pending.State -eq 'Pending' -and $pending.DescendantsBefore.Entries.Count -eq 4) 'Every reviewed child snapshot is durably recorded before root write.'
|
||||
if($script:scenario -eq 'native-failure'){throw 'Native root write failed'}
|
||||
Add-Ace $script:states['C:\owned'] $Ace $false
|
||||
if($script:scenario -ne 'missing-inheritance'){
|
||||
Add-Ace $script:states['C:\owned\open'] $Ace $true
|
||||
Add-Ace $script:states['C:\owned\open\leaf'] $Ace $true
|
||||
}
|
||||
switch($script:scenario){
|
||||
child-dacl {$script:states['C:\owned\open\leaf'].DaclBase64='changed'}
|
||||
child-identity {$script:states['C:\owned\open\leaf'].Identity='replacement'}
|
||||
child-ace-loss {$script:states['C:\owned\open\leaf'].Aces=@($script:states['C:\owned\open\leaf'].Aces|Where-Object Binary -ne 'original')}
|
||||
protected-drift {$script:states['C:\owned\protected\leaf'].DescriptorBase64='changed'}
|
||||
child-new {$script:names['C:\owned\open']+=@('new');$script:states['C:\owned\open\new']=Snapshot 'C:\owned\open\new'}
|
||||
child-disappeared {$script:names['C:\owned\open']=@()}
|
||||
after-denied {$script:scenario='denied'}
|
||||
}
|
||||
Clone $script:states['C:\owned']
|
||||
}
|
||||
function Read-Host {
|
||||
if($script:scenario -eq 'prompt-child-drift'){$script:states['C:\owned\open\leaf'].DescriptorBase64='changed'}
|
||||
'y'
|
||||
}
|
||||
$temp=Join-Path ([IO.Path]::GetTempPath()) ('wela-descendants-'+[guid]::NewGuid().ToString('N'));$null=New-Item -ItemType Directory $temp
|
||||
function Review {
|
||||
Reset
|
||||
$script:planPath=Join-Path $temp ([guid]::NewGuid().ToString('N')+'.json');$script:backup=Join-Path $temp ([guid]::NewGuid().ToString('N'))
|
||||
Invoke-WelaSelectedSacl -Action Plan -Profile fixture -Ids $script:id -IncludeChildren -ResultsPath $script:planPath
|
||||
}
|
||||
function Apply([switch]$DryRun){Invoke-WelaSelectedSacl -Action Configure -PlanPath $script:planPath -Ids $script:id -IncludeChildren -BackupPath $script:backup -DryRun:$DryRun}
|
||||
try {
|
||||
$plan=Review
|
||||
Assert ($plan.Rows[0].Status -eq 'ChangeRequired' -and $plan.Rows[0].DescendantsBefore.Entries.Count -eq 4 -and $script:writes -eq 0) 'Complete plan captures populated tree without native writes.'
|
||||
Assert (@($plan.Rows[0].DescendantsBefore.Entries|Where-Object ProtectedBarrier).Count -eq 2) 'Protection propagates as an observation barrier to the protected subtree.'
|
||||
$result=Apply -DryRun
|
||||
Assert ($result.Results[0].Status -eq 'Skipped' -and -not(Test-Path $script:backup)) 'Descendant review does not weaken DryRun.'
|
||||
$result=Apply
|
||||
Assert ($result.ExitCode -eq 0 -and $result.Results[0].Status -eq 'Applied') 'Root addition with observed inheritance and preserved protected children succeeds.'
|
||||
Assert (@($result.Results[0].DescendantVerification.Outcomes|Where-Object Status -eq 'InheritedAceObserved').Count -eq 2) 'Directory and leaf inheritance are separately observed.'
|
||||
Assert (@($result.Results[0].DescendantVerification.Outcomes|Where-Object Status -eq 'ProtectedUnchanged').Count -eq 2) 'Protected descendants remain unchanged and are never called inherited coverage.'
|
||||
$receipt=Get-Content (Join-Path $script:backup ($script:id+'.confirmed.json')) -Raw|ConvertFrom-Json
|
||||
Assert ($receipt.DescendantVerification.Status -eq 'Observed' -and $receipt.Ownership -match 'never descendant') 'Confirmed root receipt explicitly excludes child ownership.'
|
||||
$script:planPath=Join-Path $temp 'again.json';$script:backup=Join-Path $temp 'again-backup'
|
||||
$null=Invoke-WelaSelectedSacl -Action Plan -Profile fixture -Ids $script:id -IncludeChildren -ResultsPath $script:planPath
|
||||
$result=Apply
|
||||
Assert ($result.Results[0].Status -eq 'AlreadyCompliant' -and $script:writes -eq 1) 'Reviewed populated-tree rerun adds no duplicate root or child ACE.'
|
||||
foreach($case in @('denied','reparse','registry-link','capture-drift')){
|
||||
Reset;$script:scenario=$case
|
||||
$capture=Get-WelaSelectedSaclStableDescendants $script:definition (Get-WelaSelectedSaclSnapshot $script:definition)
|
||||
Assert ($capture.Status -eq 'Incomplete') "$case cannot be a complete descendant inventory."
|
||||
}
|
||||
Reset;$script:names['C:\owned']=@(1..129|ForEach-Object{"child$_"})
|
||||
$capture=Get-WelaSelectedSaclStableDescendants $script:definition (Get-WelaSelectedSaclSnapshot $script:definition)
|
||||
Assert ($capture.Status -eq 'Incomplete' -and ($capture.Diagnostics -join '') -match '128') 'Count cap blocks rather than silently truncating coverage.'
|
||||
Reset;$path='C:\owned';$script:names=@{}
|
||||
foreach($i in 1..17){$script:names[$path]=@('deep');$path+='\deep';$script:states[$path]=Snapshot $path $true}
|
||||
$capture=Get-WelaSelectedSaclStableDescendants $script:definition (Get-WelaSelectedSaclSnapshot $script:definition)
|
||||
Assert ($capture.Status -eq 'Incomplete' -and ($capture.Diagnostics -join '') -match 'depth') 'Depth cap is explicit and blocks writes.'
|
||||
Reset;$script:states['C:\owned\open\leaf'].Identity=$script:states['C:\owned\protected\leaf'].Identity
|
||||
$capture=Get-WelaSelectedSaclStableDescendants $script:definition (Get-WelaSelectedSaclSnapshot $script:definition)
|
||||
Assert ($capture.Status -eq 'Incomplete' -and ($capture.Diagnostics -join '') -match 'identity') 'Hard-link aliases cannot be called unique verified descendants.'
|
||||
Reset;$script:states['C:\owned\open\leaf'].DescriptorBase64='x'*2097153
|
||||
$capture=Get-WelaSelectedSaclStableDescendants $script:definition (Get-WelaSelectedSaclSnapshot $script:definition)
|
||||
Assert ($capture.Status -eq 'Incomplete' -and ($capture.Diagnostics -join '') -match '2 MiB') 'Snapshot evidence cap cannot truncate backups silently.'
|
||||
$child=New-WelaSelectedSaclChildDefinition Registry 'HKEY_USERS\S-1-5-21-1\owned\child'
|
||||
Assert ($child.Path -ceq 'Registry::HKEY_USERS\S-1-5-21-1\owned\child') 'Enumerated native registry paths keep an explicit provider boundary.'
|
||||
Throws {Resolve-WelaSelectedSaclNativePath (New-WelaSelectedSaclChildDefinition Registry 'HKEY_USERS')} 'canonical existing HKLM/HKU'
|
||||
Assert ((Resolve-WelaSelectedSaclNativePath $child) -ceq 'HKEY_USERS\S-1-5-21-1\owned\child') 'Native registry path validation does not perform a provider lookup that could follow a registry link.'
|
||||
foreach($case in @('child-dacl','child-identity','child-ace-loss','protected-drift','child-new','child-disappeared','missing-inheritance','after-denied','native-failure')){
|
||||
$null=Review;$script:scenario=$case;$result=Apply
|
||||
Assert ($result.ExitCode -eq 1 -and $result.Results[0].Status -eq 'Failed') "$case fails without claiming complete propagation."
|
||||
Assert ((Test-Path (Join-Path $script:backup ($script:id+'.pending.json'))) -and -not(Test-Path (Join-Path $script:backup ($script:id+'.confirmed.json')))) "$case retains Pending evidence without confirmed root/child ownership."
|
||||
}
|
||||
$null=Review;$script:scenario='prompt-child-drift';$result=Apply
|
||||
Assert ($result.ExitCode -eq 1 -and $script:writes -eq 0 -and -not(Test-Path (Join-Path $script:backup ($script:id+'.pending.json')))) 'Fresh child race after review/prompt refuses root mutation.'
|
||||
$null=Review;$script:scenario='after-pending-drift';$result=Apply
|
||||
Assert ($result.ExitCode -eq 1 -and $script:writes -eq 0 -and (Test-Path (Join-Path $script:backup ($script:id+'.pending.json'))) -and -not(Test-Path (Join-Path $script:backup ($script:id+'.confirmed.json')))) 'Race after Pending backup cannot authorize a native write or Confirmed ownership.'
|
||||
$null=Review;$script:scenario='final-drift';$result=Apply
|
||||
Assert ($result.ExitCode -eq 1 -and $result.Results[0].Diagnostic -match 'Final descendant' -and (Test-Path (Join-Path $script:backup ($script:id+'.confirmed.json')))) 'Final child drift fails the run despite an earlier confirmed observation.'
|
||||
$null=Review;$script:states['C:\owned\open\leaf'].Identity='replaced'
|
||||
Throws {Apply} 'preflight failed'
|
||||
Assert ($script:writes -eq 0 -and -not(Test-Path $script:backup)) 'Changed child identity blocks the whole preflight before journal creation.'
|
||||
$null=Review;$before=Get-WelaSelectedSaclSnapshot $script:definition;$ace=Get-WelaSelectedSaclAce $script:definition $before -IncludeChildren
|
||||
Add-Ace $script:states['C:\owned'] $ace $false
|
||||
$incomplete=Invoke-WelaSelectedSacl -Action Plan -Profile fixture -Ids $script:id -IncludeChildren
|
||||
Assert ($incomplete.Rows[0].Status -eq 'Blocked' -and $incomplete.Rows[0].Diagnostic -match 'already has') 'Compliant root with missing child inheritance never produces a false AlreadyCompliant claim or duplicate write.'
|
||||
Write-Host "PASS: $script:count descendant SACL fixture assertions; all native reads and writes mocked."
|
||||
}finally{Remove-Item -LiteralPath $temp -Recurse -Force}
|
||||
$global:LASTEXITCODE=0
|
||||
@@ -0,0 +1,95 @@
|
||||
param([switch]$AllowDisposableSaclWrite)
|
||||
$ErrorActionPreference='Stop'
|
||||
if(-not $AllowDisposableSaclWrite -or $env:GITHUB_ACTIONS -ne 'true' -or $env:RUNNER_ENVIRONMENT -ne 'github-hosted'){throw 'This mutating fixture requires explicit opt-in on a disposable GitHub-hosted Windows runner.'}
|
||||
$root=Split-Path $PSScriptRoot -Parent
|
||||
Import-Module (Join-Path $root 'modules/AuditProfiles.psm1') -Force
|
||||
. (Join-Path $root 'scripts/Configuration.ps1')
|
||||
. (Join-Path $root 'scripts/TargetedSaclPlanning.ps1')
|
||||
. (Join-Path $root 'scripts/SelectedSaclConfiguration.ps1')
|
||||
$script:count=0
|
||||
function Assert($Condition,$Message){if(-not $Condition){throw $Message};$script:count++}
|
||||
function Fingerprint($Map){(@($Map.Keys|Sort-Object|ForEach-Object{"$_=$($Map[$_])"}) -join ';')}
|
||||
$beforePolicy=Get-WelaEffectiveAuditPolicy
|
||||
$precedencePath='HKLM:\SYSTEM\CurrentControlSet\Control\Lsa'
|
||||
$beforePrecedence=Get-WelaRegistryState $precedencePath SCENoApplyLegacyAuditPolicy
|
||||
$privilegeBefore=(Invoke-WelaNative whoami.exe @('/priv','/fo','csv')).Diagnostic
|
||||
$nonce=[guid]::NewGuid().ToString('N');$temp=Join-Path ([IO.Path]::GetTempPath()) ('wela-sacl-'+$nonce)
|
||||
$regSub='Software\WELASelectedSacl_'+$nonce;$regProvider='HKCU:\'+$regSub
|
||||
$file=Join-Path $temp 'probe.txt';$sid=[Security.Principal.WindowsIdentity]::GetCurrent().User.Value
|
||||
$policyGuids=@('0CCE921D-69AE-11D9-BED3-505054503030','0CCE921E-69AE-11D9-BED3-505054503030')
|
||||
$restored=$false
|
||||
try {
|
||||
$null=New-Item -ItemType Directory -Path $temp
|
||||
$null=New-Item -Path $regProvider
|
||||
Set-ItemProperty -LiteralPath $precedencePath -Name SCENoApplyLegacyAuditPolicy -Value 1 -Type DWord
|
||||
foreach($guid in $policyGuids){Set-WelaEffectiveAuditPolicy -Guid $guid -Mask 1 -Mode minimum}
|
||||
$fileTree=Join-Path $temp 'tree';$null=New-Item -ItemType Directory $fileTree
|
||||
$regTree=Join-Path $regProvider 'Tree';$null=New-Item -Path $regTree
|
||||
Add-Type -Path (Join-Path $PSScriptRoot 'SelectedSaclFixtureProtection.cs') -ErrorAction Stop
|
||||
foreach($tree in @($fileTree,$regTree)){
|
||||
if($tree -eq $fileTree){$null=New-Item -ItemType Directory (Join-Path $tree 'open');$null=New-Item -ItemType Directory (Join-Path $tree 'protected')}
|
||||
else{$null=New-Item -Path (Join-Path $tree 'open');$null=New-Item -Path (Join-Path $tree 'protected')}
|
||||
# Fixture setup changes protection only on an owned object. Production never changes it.
|
||||
$protected=Join-Path $tree 'protected'
|
||||
$protectedDefinition=if($tree -eq $fileTree){[pscustomobject]@{Kind='FileSystem';Path=$protected;Resolution='Resolved'}}else{[pscustomobject]@{Kind='Registry';Path=('Registry::HKEY_USERS\'+$sid+'\'+$regSub+'\Tree\protected');Resolution='Resolved'}}
|
||||
Write-Host ("Preparing owned native SACL protection for "+$protectedDefinition.Kind+": "+$protectedDefinition.Path)
|
||||
$protectedBefore=Get-WelaSelectedSaclSnapshot $protectedDefinition
|
||||
Initialize-WelaSelectedSaclNative;$privilege=New-Object Wela.SelectedSacl.Privilege
|
||||
try {[Wela.SelectedSaclFixture.Protection]::Protect($protectedBefore.Kind,$protectedBefore.Path,$protectedBefore.DescriptorBase64,$nonce)}finally{$privilege.Dispose()}
|
||||
$protectedAfter=Get-WelaSelectedSaclSnapshot $protectedDefinition
|
||||
Assert (($protectedAfter.ControlFlags -band 8192) -ne 0 -and $protectedBefore.Owner -ceq $protectedAfter.Owner -and $protectedBefore.Group -ceq $protectedAfter.Group -and $protectedBefore.DaclBase64 -ceq $protectedAfter.DaclBase64) 'Native owned fixture setup sets SACL protection while preserving owner/group/DACL.'
|
||||
foreach($branch in @('open','protected')){
|
||||
if($tree -eq $fileTree){[IO.File]::WriteAllText((Join-Path (Join-Path $tree $branch) 'leaf.txt'),'owned descendant fixture')}
|
||||
else{$null=New-Item -Path (Join-Path (Join-Path $tree $branch) 'Leaf')}
|
||||
}
|
||||
}
|
||||
$definitions=@(
|
||||
[pscustomobject]@{Path=$fileTree;Kind='FileSystem';Resolution='Resolved';PrincipalSid='S-1-1-0';Propagation='None';Inheritance='ContainerInherit,ObjectInherit';Rights=@('ReadData');AuditFlags=@('Success');PolicyMode='minimum';PolicySelected=$true;RequiredPolicyMask=1},
|
||||
[pscustomobject]@{Path=('Registry::HKEY_USERS\'+$sid+'\'+$regSub+'\Tree');Kind='Registry';Resolution='Resolved';PrincipalSid='S-1-1-0';Propagation='None';Inheritance='ContainerInherit';Rights=@('SetValue');AuditFlags=@('Success');PolicyMode='minimum';PolicySelected=$true;RequiredPolicyMask=1}
|
||||
)
|
||||
foreach($definition in $definitions){
|
||||
$before=Get-WelaSelectedSaclSnapshot $definition
|
||||
$ace=Get-WelaSelectedSaclAce $definition $before -IncludeChildren
|
||||
Assert-WelaSelectedSaclPrerequisites $definition $ace
|
||||
$children=Get-WelaSelectedSaclStableDescendants $definition $before
|
||||
Assert ($children.Status -eq 'Complete' -and $children.Entries.Count -eq 4) ('Native populated '+$definition.Kind+' enumeration captures all four existing children: '+($children.Diagnostics -join '; '))
|
||||
Assert (@($children.Entries|Where-Object ProtectedBarrier).Count -eq 2) 'Native SACL protection marks the protected object and its subtree.'
|
||||
$journal=Join-Path $temp ($definition.Kind+'.pending.json')
|
||||
Write-WelaSelectedSaclJson $journal ([pscustomobject]@{State='Pending';Before=$before;DescendantsBefore=$children;Ace=$ace})
|
||||
$saved=Get-Content -LiteralPath $journal -Raw|ConvertFrom-Json
|
||||
Assert ($saved.DescendantsBefore.Entries.Count -eq 4 -and $saved.Before.DescriptorBase64 -ceq $before.DescriptorBase64) 'Actual complete parent/child backup exists before native root mutation.'
|
||||
$fresh=Get-WelaSelectedSaclStableDescendants $definition (Get-WelaSelectedSaclSnapshot $definition)
|
||||
Assert ((Get-WelaSelectedSaclDescendantKey $fresh) -ceq (Get-WelaSelectedSaclDescendantKey $children)) 'Native child pre-write snapshots remain stable.'
|
||||
$after=Write-WelaSelectedSaclNative $definition $before $ace
|
||||
Assert-WelaSelectedSaclPreserved $before $after $ace
|
||||
$afterChildren=Get-WelaSelectedSaclStableDescendants $definition $after
|
||||
$outcomes=Test-WelaSelectedSaclDescendantOutcomes $children $afterChildren $ace
|
||||
if($outcomes.Status -ne 'Observed'){Write-Host ($outcomes|ConvertTo-Json -Depth 20)}
|
||||
Assert ($outcomes.Status -eq 'Observed') 'Real native inheritance preserves all reviewed child owner/group/DACL/original ACEs/protection.'
|
||||
Assert (@($outcomes.Outcomes|Where-Object Status -eq 'InheritedAceObserved').Count -eq 2) 'Actual inherited requested audit ACE appears on unprotected child container and leaf.'
|
||||
Assert (@($outcomes.Outcomes|Where-Object Status -eq 'ProtectedUnchanged').Count -eq 2) 'Protected child and its descendant retain exact descriptors without inherited coverage claims.'
|
||||
$again=Get-WelaSelectedSaclStableDescendants $definition (Get-WelaSelectedSaclSnapshot $definition)
|
||||
Assert ((Get-WelaSelectedSaclDescendantKey $again) -ceq (Get-WelaSelectedSaclDescendantKey $afterChildren)) 'Actual final descendant membership and descriptor state is stable.'
|
||||
Assert ((Test-WelaSelectedSaclAce $again.Root $ace) -and (Test-WelaSelectedSaclDescendantOutcomes $again $again $ace).Status -eq 'Observed') 'Native idempotence inputs verify root and all reviewed inheritance without another write.'
|
||||
if($definition.Kind -eq 'FileSystem'){[IO.File]::WriteAllText((Join-Path $fileTree 'appeared.txt'),'owned new child')}
|
||||
else{$null=New-Item -Path (Join-Path $regTree 'Appeared')}
|
||||
$appeared=Get-WelaSelectedSaclStableDescendants $definition (Get-WelaSelectedSaclSnapshot $definition)
|
||||
$changed=Test-WelaSelectedSaclDescendantOutcomes $again $appeared $ace
|
||||
Assert ($changed.Status -eq 'Unverified' -and @($changed.Outcomes|Where-Object Status -eq 'NewUnreviewedChild').Count -eq 1) 'New actual child is unreviewed even when Windows inherited a matching audit ACE.'
|
||||
Write-Host ('PASS: actual '+$definition.Kind+' populated-tree inheritance, protected-subtree preservation and final snapshots; no child ownership or future coverage claim.')
|
||||
}
|
||||
Assert ((Invoke-WelaNative whoami.exe @('/priv','/fo','csv')).Diagnostic -ceq $privilegeBefore) 'All native enumeration, snapshot, setup and writer operations restore process privilege state.'
|
||||
Write-Host "PASS: $script:count actual descendant SACL assertions on owned disposable populated trees."
|
||||
} finally {
|
||||
foreach($guid in $policyGuids){Set-WelaEffectiveAuditPolicy -Guid $guid -Mask $beforePolicy[$guid] -Mode exact}
|
||||
if($beforePrecedence.ValueExists){Set-ItemProperty -LiteralPath $precedencePath -Name SCENoApplyLegacyAuditPolicy -Type $beforePrecedence.Type -Value $beforePrecedence.Value}
|
||||
else{Remove-ItemProperty -LiteralPath $precedencePath -Name SCENoApplyLegacyAuditPolicy -ErrorAction SilentlyContinue}
|
||||
$afterPolicy=Get-WelaEffectiveAuditPolicy;$afterPrecedence=Get-WelaRegistryState $precedencePath SCENoApplyLegacyAuditPolicy
|
||||
if((Fingerprint $beforePolicy) -cne (Fingerprint $afterPolicy) -or ($beforePrecedence|ConvertTo-Json -Compress) -cne ($afterPrecedence|ConvertTo-Json -Compress)){throw "Fixture policy restoration failed; retain owned evidence at $temp and $regProvider."}
|
||||
if(Test-Path -LiteralPath $regProvider){Remove-Item -LiteralPath $regProvider -Recurse -Force}
|
||||
if(Test-Path -LiteralPath $temp){Remove-Item -LiteralPath $temp -Recurse -Force}
|
||||
if((Test-Path -LiteralPath $regProvider) -or (Test-Path -LiteralPath $temp)){throw 'Owned fixture objects remain after cleanup.'}
|
||||
$restored=$true
|
||||
Write-Host 'PASS: all59 native audit masks and typed precedence restored; only owned disposable targets removed.'
|
||||
}
|
||||
$global:LASTEXITCODE=0
|
||||
@@ -0,0 +1,33 @@
|
||||
// Disposable fixture setup only. Never loaded by WELA production commands.
|
||||
using System;
|
||||
using System.ComponentModel;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Security.AccessControl;
|
||||
namespace Wela.SelectedSaclFixture {
|
||||
public static class Protection {
|
||||
[DllImport("kernel32.dll",CharSet=CharSet.Unicode,SetLastError=true)] static extern IntPtr CreateFile(string name,uint access,uint share,IntPtr security,uint disposition,uint flags,IntPtr template);
|
||||
[DllImport("kernel32.dll")] static extern bool CloseHandle(IntPtr handle);
|
||||
[DllImport("advapi32.dll",CharSet=CharSet.Unicode)] static extern int RegOpenKeyEx(IntPtr parent,string name,uint options,uint access,out IntPtr handle);
|
||||
[DllImport("advapi32.dll")] static extern int RegCloseKey(IntPtr key);
|
||||
[DllImport("advapi32.dll")] static extern uint SetSecurityInfo(IntPtr handle,uint kind,uint flags,IntPtr owner,IntPtr group,IntPtr dacl,IntPtr sacl);
|
||||
public static void Protect(string kind,string path,string descriptor,string nonce) {
|
||||
if(Environment.GetEnvironmentVariable("GITHUB_ACTIONS")!="true"||Environment.GetEnvironmentVariable("RUNNER_ENVIRONMENT")!="github-hosted"||String.IsNullOrEmpty(nonce)||nonce.Length!=32||!path.Contains(nonce)||!path.EndsWith("\\protected",StringComparison.OrdinalIgnoreCase))throw new InvalidOperationException("Only explicitly owned disposable protected fixture objects are accepted.");
|
||||
IntPtr handle=IntPtr.Zero,buffer=IntPtr.Zero;bool registry=kind=="Registry";
|
||||
try {
|
||||
if(registry) {
|
||||
if(!path.StartsWith("HKEY_USERS\\",StringComparison.Ordinal))throw new InvalidOperationException("Fixture must use its current HKU identity.");
|
||||
int error=RegOpenKeyEx(new IntPtr(unchecked((int)0x80000003)),path.Substring(11),8,0x01020101,out handle);
|
||||
if(error!=0)throw new Win32Exception(error,"Owned registry protection handle open failed.");
|
||||
} else {
|
||||
if(kind!="FileSystem")throw new InvalidOperationException("Unknown fixture kind.");
|
||||
handle=CreateFile(path,0x01020000,3,IntPtr.Zero,3,0x02200000,IntPtr.Zero);
|
||||
if(handle==new IntPtr(-1)){handle=IntPtr.Zero;throw new Win32Exception(Marshal.GetLastWin32Error());}
|
||||
}
|
||||
RawSecurityDescriptor sd=new RawSecurityDescriptor(Convert.FromBase64String(descriptor),0);
|
||||
if(sd.SystemAcl!=null){byte[] bytes=new byte[sd.SystemAcl.BinaryLength];sd.SystemAcl.GetBinaryForm(bytes,0);buffer=Marshal.AllocHGlobal(bytes.Length);Marshal.Copy(bytes,0,buffer,bytes.Length);}
|
||||
uint result=SetSecurityInfo(handle,registry?4U:1U,0x40000008,IntPtr.Zero,IntPtr.Zero,IntPtr.Zero,buffer);
|
||||
if(result!=0)throw new Win32Exception((int)result,"Owned "+kind+" SetSecurityInfo(SACL|PROTECTED_SACL) failed.");
|
||||
} finally {if(buffer!=IntPtr.Zero)Marshal.FreeHGlobal(buffer);if(handle!=IntPtr.Zero){if(registry)RegCloseKey(handle);else CloseHandle(handle);}}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user