mirror of
https://github.com/Yamato-Security/WELA.git
synced 2026-11-01 10:48:19 +01:00
Add guarded recovery for one selected leaf-file audit ACE
This commit is contained in:
1 parent
fd7a7924aa
commit
2b1a3bce82
14 files changed
+685
-1
No files matched your search
@@ -0,0 +1,24 @@
|
||||
$ErrorActionPreference='Stop'
|
||||
$repo=Split-Path $PSScriptRoot -Parent;$engine=(Get-Process -Id $PID).Path
|
||||
$cases=@(
|
||||
@{Args=@('file-sacl-recovery','-Help');Exit=0;Pattern='Usage: file-sacl-recovery'},
|
||||
@{Args=@('file-sacl-recovery','-FileSaclRecoveryAction','Restore','-DryRun','-Help');Exit=0;Pattern='Usage:'},
|
||||
@{Args=@('file-sacl-recovery','-FileSaclRecoveryAction','Restore','-Auto','-Help');Exit=0;Pattern='Usage:'},
|
||||
@{Args=@('file-sacl-recovery','-DryRun','-Help');Exit=1;Pattern='DryRun'},
|
||||
@{Args=@('configure','-FileSaclRecoveryAction','Restore','-Help');Exit=1;Pattern='require file-sacl-recovery'},
|
||||
@{Args=@('targeted-sacl','-FileSaclRecoveryPlanPath','unread.json','-Help');Exit=1;Pattern='require file-sacl-recovery|targeted-sacl accepts only'},
|
||||
@{Args=@('file-sacl-recovery','-TargetSaclIncludeChildren','-Help');Exit=1;Pattern='require targeted-sacl|dedicated'},
|
||||
@{Args=@('file-sacl-recovery','-Role','Client','-Help');Exit=1;Pattern='dedicated'},
|
||||
@{Args=@('file-sacl-recovery','-ResultsPath','unwritten.json','-Help');Exit=1;Pattern='dedicated'},
|
||||
@{Args=@('file-sacl-recovery','-RecoveryPlanPath','unread.json','-Help');Exit=1;Pattern='dedicated'},
|
||||
@{Args=@('file-sacl-recovery','-Auto');Exit=1;Pattern='Plan requires four original'},
|
||||
@{Args=@('file-sacl-recovery','-FileSaclRecoveryAction','Restore','-Auto');Exit=1;Pattern='Restore requires PlanPath'},
|
||||
@{Args=@('file-sacl-recovery','-FileSaclRecoveryAction','Restore','-DryRun','-FileSaclRecoveryOutputPath','unwritten-directory');Exit=1;Pattern='Restore requires PlanPath'},
|
||||
@{Args=@('audit-recovery','-RecoveryAction','Restore','-DryRun','-Help');Exit=0;Pattern='Usage: audit-recovery'}
|
||||
)
|
||||
foreach($case in $cases){
|
||||
$ErrorActionPreference='Continue';try{$text=@(& $engine -NoProfile -File (Join-Path $repo 'WELA.ps1') @($case.Args) 2>&1);$code=$LASTEXITCODE}finally{$ErrorActionPreference='Stop'}
|
||||
if($code -ne $case.Exit -or ($text -join "`n") -notmatch $case.Pattern){throw "Unexpected CLI result for $($case.Args -join ' '): $code / $text"}
|
||||
}
|
||||
Write-Host "File SACL recovery public CLI: $($cases.Count) checks passed."
|
||||
$global:LASTEXITCODE=0
|
||||
@@ -0,0 +1,98 @@
|
||||
# Actual Windows security-descriptor parsing, without file or policy mutation.
|
||||
$ErrorActionPreference='Stop'
|
||||
if ($env:OS -ne 'Windows_NT') {Write-Host 'Skipped: Windows security descriptor runtime required.';exit 0}
|
||||
$repo=Split-Path $PSScriptRoot -Parent
|
||||
. (Join-Path $repo 'scripts/WefArrival.ps1')
|
||||
. (Join-Path $repo 'scripts/FileSaclRecovery.ps1')
|
||||
Initialize-WelaFileSaclRecoveryNative
|
||||
$script:n=0
|
||||
function Assert($Value,$Message) {if (-not $Value) {throw $Message};$script:n++}
|
||||
function Throws($Action,$Pattern) {$message='';try {& $Action | Out-Null} catch {$message=$_.Exception.Message};Assert ($message -match $Pattern) "Expected $Pattern, received $message"}
|
||||
function Encode($Descriptor) {$bytes=New-Object byte[] $Descriptor.BinaryLength;$Descriptor.GetBinaryForm($bytes,0);[Convert]::ToBase64String($bytes)}
|
||||
function Clone($Descriptor) {[Security.AccessControl.RawSecurityDescriptor]::new([Convert]::FromBase64String((Encode $Descriptor)),0)}
|
||||
function New-AuditAce([int]$Mask=1,[int]$Flags=64,[string]$Sid='S-1-1-0') {
|
||||
[Security.AccessControl.CommonAce]::new([Security.AccessControl.AceFlags]$Flags,[Security.AccessControl.AceQualifier]::SystemAudit,$Mask,[Security.Principal.SecurityIdentifier]::new($Sid),$false,$null)
|
||||
}
|
||||
function Add-AuditAce($Descriptor,$Ace) {
|
||||
$copy=Clone $Descriptor
|
||||
if ($null -eq $copy.SystemAcl) {$copy.SystemAcl=[Security.AccessControl.RawAcl]::new(2,1);$copy.SetFlags($copy.ControlFlags -bor [Security.AccessControl.ControlFlags]::SystemAclPresent)}
|
||||
$copy.SystemAcl.InsertAce($copy.SystemAcl.Count,$Ace)
|
||||
$copy
|
||||
}
|
||||
$base=[Security.AccessControl.RawSecurityDescriptor]::new('O:SYG:SYD:(A;;FA;;;SY)')
|
||||
$before=Encode $base
|
||||
foreach ($flags in @(64,128,192)) {
|
||||
foreach ($sid in @('S-1-1-0','S-1-5-11')) {
|
||||
$after=Add-AuditAce $base (New-AuditAce 1 $flags $sid)
|
||||
$added=[Wela.FileSaclRecovery.Descriptor]::AddedAce($before,(Encode $after),$sid,1,$flags)
|
||||
Assert ($added -ceq [Wela.FileSaclRecovery.Descriptor]::Bytes($after.SystemAcl[0])) 'Exactly the ordinary selected ACE is identified.'
|
||||
$empty=Clone $after;$empty.SystemAcl.RemoveAce(0)
|
||||
[Wela.FileSaclRecovery.Descriptor]::Removed((Encode $after),(Encode $empty),$added)
|
||||
Assert ($empty.SystemAcl.Count -eq 0 -and ($empty.ControlFlags -band 16) -ne 0 -and (Encode $empty) -cne $before) 'ACE removal preserves an empty present SACL without claiming historical representation equality.'
|
||||
Throws {[Wela.FileSaclRecovery.Descriptor]::Removed((Encode $after),$before,$added)} 'control'
|
||||
$duplicate=Add-AuditAce $after (New-AuditAce 1 $flags $sid)
|
||||
Throws {[Wela.FileSaclRecovery.Descriptor]::AddedAce($before,(Encode $duplicate),$sid,1,$flags)} 'exactly one'
|
||||
Throws {[Wela.FileSaclRecovery.Descriptor]::Removed((Encode $duplicate),(Encode $after),$added)} 'no longer unique'
|
||||
$unrelated=Add-AuditAce $after (New-AuditAce 2 128 'S-1-5-11')
|
||||
Throws {[Wela.FileSaclRecovery.Descriptor]::AddedAce($before,(Encode $unrelated),$sid,1,$flags)} 'more than one|exactly one'
|
||||
Throws {[Wela.FileSaclRecovery.Descriptor]::Removed((Encode $after),(Encode $unrelated),$added)} 'Unrelated|Unexpected'
|
||||
}
|
||||
}
|
||||
$old=Add-AuditAce $base (New-AuditAce 2 128 'S-1-5-11')
|
||||
$after=Add-AuditAce $old (New-AuditAce)
|
||||
$added=[Wela.FileSaclRecovery.Descriptor]::AddedAce((Encode $old),(Encode $after),'S-1-1-0',1,64)
|
||||
[Wela.FileSaclRecovery.Descriptor]::Removed((Encode $after),(Encode $old),$added)
|
||||
Assert ($old.SystemAcl.Count -eq 1) 'The original unrelated audit ACE remains after a valid removal.'
|
||||
$lost=Add-AuditAce $base (New-AuditAce)
|
||||
Throws {[Wela.FileSaclRecovery.Descriptor]::AddedAce((Encode $old),(Encode $lost),'S-1-1-0',1,64)} 'original ACE'
|
||||
$missing=Clone $after;$missing.SystemAcl.RemoveAce(0);$missing.SystemAcl.RemoveAce(0)
|
||||
Throws {[Wela.FileSaclRecovery.Descriptor]::Removed((Encode $after),(Encode $missing),$added)} 'Unrelated audit ACEs'
|
||||
$covering=Add-AuditAce $base (New-AuditAce 3 64)
|
||||
$redundant=Add-AuditAce $covering (New-AuditAce)
|
||||
Throws {[Wela.FileSaclRecovery.Descriptor]::AddedAce((Encode $covering),(Encode $redundant),'S-1-1-0',1,64)} 'already covered'
|
||||
foreach ($flags in @(0,16,65,80,129,208)) {Throws {[Wela.FileSaclRecovery.Descriptor]::AddedAce($before,(Encode $after),'S-1-1-0',1,$flags)} 'explicit ordinary'}
|
||||
Throws {[Wela.FileSaclRecovery.Descriptor]::AddedAce($before,(Encode $after),'S-1-5-18',1,64)} 'explicit ordinary'
|
||||
Throws {[Wela.FileSaclRecovery.Descriptor]::AddedAce($before,(Encode $after),'S-1-1-0',0,64)} 'explicit ordinary'
|
||||
# Both historical addition and removal must preserve non-audit descriptor fields.
|
||||
foreach ($mutation in @('Owner','Group','Dacl','ControlFlags')) {
|
||||
$changed=Clone $old
|
||||
switch ($mutation) {
|
||||
Owner {$changed.Owner=[Security.Principal.SecurityIdentifier]::new('S-1-5-32-544')}
|
||||
Group {$changed.Group=[Security.Principal.SecurityIdentifier]::new('S-1-5-32-544')}
|
||||
Dacl {$changed.DiscretionaryAcl.RemoveAce(0)}
|
||||
ControlFlags {$changed.SetFlags($changed.ControlFlags -bor [Security.AccessControl.ControlFlags]::DiscretionaryAclProtected)}
|
||||
}
|
||||
Throws {[Wela.FileSaclRecovery.Descriptor]::Removed((Encode $after),(Encode $changed),$added)} 'Owner|control|header|manager'
|
||||
$withAddition=Add-AuditAce $changed (New-AuditAce)
|
||||
Throws {[Wela.FileSaclRecovery.Descriptor]::AddedAce((Encode $old),(Encode $withAddition),'S-1-1-0',1,64)} 'Owner|control|header|manager'
|
||||
}
|
||||
# Resource-manager control is serialized only when its valid flag is present.
|
||||
$rmBefore=Clone $old;$rmBefore.SetFlags($rmBefore.ControlFlags -bor [Security.AccessControl.ControlFlags]::RMControlValid);$rmBefore.ResourceManagerControl=1
|
||||
$rmAfter=Add-AuditAce $rmBefore (New-AuditAce)
|
||||
$rmChanged=Clone $rmBefore;$rmChanged.ResourceManagerControl=2
|
||||
Assert ((Encode $rmChanged) -cne (Encode $rmBefore)) 'RMControl fixture changes actual serialized bytes with flags unchanged.'
|
||||
Throws {[Wela.FileSaclRecovery.Descriptor]::Removed((Encode $rmAfter),(Encode $rmChanged),$added)} 'control|header'
|
||||
$rmChangedAddition=Add-AuditAce $rmChanged (New-AuditAce)
|
||||
Throws {[Wela.FileSaclRecovery.Descriptor]::AddedAce((Encode $rmBefore),(Encode $rmChangedAddition),'S-1-1-0',1,64)} 'control|header'
|
||||
# ACL revision changes cannot hide behind unchanged ACE bytes.
|
||||
$revised=Clone $old;$acl4=[Security.AccessControl.RawAcl]::new(4,$revised.SystemAcl.Count)
|
||||
foreach ($entry in $revised.SystemAcl) {$acl4.InsertAce($acl4.Count,$entry)}
|
||||
$revised.SystemAcl=$acl4;$revisedAddition=Add-AuditAce $revised (New-AuditAce)
|
||||
Throws {[Wela.FileSaclRecovery.Descriptor]::AddedAce((Encode $old),(Encode $revisedAddition),'S-1-1-0',1,64)} 'revision'
|
||||
Throws {[Wela.FileSaclRecovery.Descriptor]::Removed((Encode $after),(Encode $revised),$added)} 'revision'
|
||||
# Duplicate unrelated entries retain their exact counts.
|
||||
$duplicateOld=Add-AuditAce $old (New-AuditAce 2 128 'S-1-5-11')
|
||||
$duplicateAfter=Add-AuditAce $duplicateOld (New-AuditAce)
|
||||
$duplicateAdded=[Wela.FileSaclRecovery.Descriptor]::AddedAce((Encode $duplicateOld),(Encode $duplicateAfter),'S-1-1-0',1,64)
|
||||
[Wela.FileSaclRecovery.Descriptor]::Removed((Encode $duplicateAfter),(Encode $duplicateOld),$duplicateAdded)
|
||||
Assert ($duplicateOld.SystemAcl.Count -eq 2) 'Duplicate unrelated ACEs are preserved.'
|
||||
Throws {[Wela.FileSaclRecovery.Descriptor]::Removed((Encode $duplicateAfter),(Encode $old),$duplicateAdded)} 'Unrelated audit ACEs'
|
||||
# Native object audit ACEs never qualify as the ordinary selected addition.
|
||||
$objectBase=Clone $base;$objectBase.SystemAcl=[Security.AccessControl.RawAcl]::new(4,0);$objectBase.SetFlags($objectBase.ControlFlags -bor [Security.AccessControl.ControlFlags]::SystemAclPresent)
|
||||
$objectAfter=Clone $objectBase
|
||||
$objectAce=[Security.AccessControl.ObjectAce]::new([Security.AccessControl.AceFlags]64,[Security.AccessControl.AceQualifier]::SystemAudit,1,[Security.Principal.SecurityIdentifier]::new('S-1-1-0'),[Security.AccessControl.ObjectAceFlags]::ObjectAceTypePresent,[guid]::NewGuid(),[guid]::Empty,$false,$null)
|
||||
$objectAfter.SystemAcl.InsertAce(0,$objectAce)
|
||||
Throws {[Wela.FileSaclRecovery.Descriptor]::AddedAce((Encode $objectBase),(Encode $objectAfter),'S-1-1-0',1,64)} 'exactly one'
|
||||
Throws {[Wela.FileSaclRecovery.Descriptor]::Parse('not base64')} '.'
|
||||
$global:LASTEXITCODE=0
|
||||
Write-Host "File SACL recovery native descriptor guards: $script:n assertions passed."
|
||||
@@ -0,0 +1,37 @@
|
||||
$ErrorActionPreference='Stop'
|
||||
$root=Split-Path $PSScriptRoot -Parent
|
||||
Import-Module (Join-Path $root 'modules/AuditProfiles.psm1') -Force
|
||||
. (Join-Path $root 'scripts/WefArrival.ps1')
|
||||
. (Join-Path $root 'scripts/EvtxRecovery.ps1')
|
||||
. (Join-Path $root 'scripts/FileSaclRecovery.ps1')
|
||||
$script:count=0
|
||||
function Assert($Value,$Message){if(-not $Value){throw $Message};$script:count++}
|
||||
function Throws($Action,$Pattern){$message='';try{& $Action|Out-Null}catch{$message=$_.Exception.Message};Assert ($message -match $Pattern) "Expected $Pattern, got $message"}
|
||||
Initialize-WelaFileSaclRecoveryNative
|
||||
Assert ([Wela.FileSaclRecovery.Descriptor]::SourceSha256 -ceq (Get-FileHash (Join-Path $root 'scripts/FileSaclRecoveryNative.cs')).Hash.ToLowerInvariant()) 'Compiled helper is bound to actual source bytes.'
|
||||
$temp=Join-Path ([IO.Path]::GetTempPath()) ('wela-file-recovery-json-'+[guid]::NewGuid().ToString('N'));$null=New-Item -ItemType Directory $temp
|
||||
try {
|
||||
$path=Join-Path $temp 'input.json'
|
||||
foreach($value in @('{"ExitCode":0}','{"text":"東京","SchemaVersion":1}')){
|
||||
[IO.File]::WriteAllText($path,$value,[Text.UTF8Encoding]::new($false))
|
||||
$input=Read-WelaFileSaclRecoveryInput $path
|
||||
Assert ($input.Sha256 -ceq (Get-FileHash $path).Hash.ToLowerInvariant() -and $input.Bytes -eq ([IO.File]::ReadAllBytes($path)).Length) 'Strict evidence reader hashes actual UTF-8 bytes.'
|
||||
}
|
||||
$zero=ConvertFrom-WelaEvtxJson '{"ExitCode":0}'
|
||||
Assert (($zero.ExitCode -is [int] -or $zero.ExitCode -is [long]) -and $zero.ExitCode -eq 0) 'Real JSON integer zero is accepted across engines.'
|
||||
foreach($invalid in @('{"a":1,"a":2}','{"x":NaN}','{"x":1,}','{"x":true} trailing','')){
|
||||
[IO.File]::WriteAllText($path,$invalid)
|
||||
Throws {Read-WelaFileSaclRecoveryInput $path} 'JSON|json|byte|Unexpected|Invalid|Duplicate|custom-profile'
|
||||
}
|
||||
[IO.File]::WriteAllBytes($path,[byte[]]@(0xc3,0x28));Throws {Read-WelaFileSaclRecoveryInput $path} 'translate|valid|Unable'
|
||||
$oversize=New-Object byte[] 4194305;[IO.File]::WriteAllBytes($path,$oversize);Throws {Read-WelaFileSaclRecoveryInput $path} 'four MiB'
|
||||
$artifact=Write-WelaFileSaclRecoveryArtifact $temp 'pending.json' '{"state":"Pending"}'
|
||||
Assert ($artifact.Bytes -gt 0 -and $artifact.Sha256 -ceq (Get-FileHash (Join-Path $temp 'pending.json')).Hash.ToLowerInvariant()) 'Durably flushed pending artifact is reopened and hashed.'
|
||||
Throws {Write-WelaFileSaclRecoveryArtifact $temp 'pending.json' '{}'} 'exists'
|
||||
foreach($arguments in @(@{},@{Action='Plan';PlanPath='x'},@{Action='Plan';Auto=$true},@{Action='Plan';DryRun=$true},@{Action='Restore'},@{Action='Restore';PlanPath='x';PlanHash=('a'*64);DryRun=$true;Auto=$true},@{Action='Restore';PlanPath='x';PlanHash=('a'*64);DryRun=$true;OutputPath='out'},@{Action='Restore';PlanPath='x';PlanHash=('a'*64);OutputPath='out'})) {
|
||||
Throws {Invoke-WelaFileSaclRecovery @arguments} 'requires'
|
||||
}
|
||||
if($env:OS -ne 'Windows_NT'){Throws {Get-WelaFileSaclRecoveryOperator} 'Windows'}
|
||||
Write-Host "PASS: $script:count file recovery source, strict input, durable output and argument assertions. Native descriptor semantics run separately on Windows."
|
||||
} finally {Remove-Item -LiteralPath $temp -Recurse -Force}
|
||||
$global:LASTEXITCODE=0
|
||||
@@ -0,0 +1,103 @@
|
||||
param([switch]$AllowDisposableSaclWrite)
|
||||
$ErrorActionPreference='Stop'
|
||||
if(-not $AllowDisposableSaclWrite -or $env:GITHUB_ACTIONS -ne 'true' -or $env:RUNNER_ENVIRONMENT -ne 'github-hosted'){throw 'Explicit opt-in on a disposable GitHub-hosted Windows runner is required.'}
|
||||
$root=Split-Path $PSScriptRoot -Parent
|
||||
Import-Module (Join-Path $root 'modules/AuditProfiles.psm1') -Force
|
||||
. (Join-Path $root 'scripts/Configuration.ps1')
|
||||
$script:count=0
|
||||
function Assert($Value,$Message){if(-not $Value){throw $Message};$script:count++}
|
||||
function Fingerprint($Map){(@($Map.Keys|Sort-Object|ForEach-Object{"$_=$($Map[$_])"}) -join ';')}
|
||||
function Json($Path){Get-Content -LiteralPath $Path -Raw|ConvertFrom-Json}
|
||||
function Save($Path,$Value){[IO.File]::WriteAllText($Path,($Value|ConvertTo-Json -Depth 30),[Text.UTF8Encoding]::new($false))}
|
||||
$policyBefore=Get-WelaEffectiveAuditPolicy
|
||||
$precedencePath='HKLM:\SYSTEM\CurrentControlSet\Control\Lsa';$precedenceBefore=Get-WelaRegistryState $precedencePath SCENoApplyLegacyAuditPolicy
|
||||
$nonce=[guid]::NewGuid().ToString('N');$temp=Join-Path ([IO.Path]::GetTempPath()) ('wela-file-recovery-'+$nonce);$copy=Join-Path $temp 'checkout'
|
||||
$engine=(Get-Process -Id $PID).Path
|
||||
$script:call=0
|
||||
function Run-Wela {
|
||||
param([string[]]$Arguments,[int]$Expected=0,[string]$Pattern='')
|
||||
$script:call++;$log=Join-Path $temp ('call-'+$script:call+'.log')
|
||||
$start=[Diagnostics.ProcessStartInfo]::new();$start.FileName=$engine;$start.UseShellExecute=$false;$start.RedirectStandardOutput=$true;$start.RedirectStandardError=$true
|
||||
$all=@('-NoProfile','-NonInteractive','-ExecutionPolicy','Bypass','-File',(Join-Path $copy 'WELA.ps1'))+$Arguments
|
||||
$start.Arguments=(@($all|ForEach-Object {'"'+$_.Replace('"','\"')+'"'}) -join ' ')
|
||||
$process=[Diagnostics.Process]::new();$process.StartInfo=$start
|
||||
try {$null=$process.Start();$out=$process.StandardOutput.ReadToEndAsync();$err=$process.StandardError.ReadToEndAsync();if(-not $process.WaitForExit(180000)){$process.Kill();throw 'Public recovery fixture command timed out.'};if(-not [Threading.Tasks.Task]::WaitAll([Threading.Tasks.Task[]]@($out,$err),10000)){throw 'Public fixture output capture timed out.'};$text=$out.Result+$err.Result;[IO.File]::WriteAllText($log,$text);Assert ($process.ExitCode -eq $Expected) "Public command failed with $($process.ExitCode), expected $Expected. $text";if($Pattern){Assert ($text -match $Pattern) "Expected diagnostic $Pattern. $text"}}finally{if($process.Id -and -not $process.HasExited){$process.Kill();$null=$process.WaitForExit(10000)};$process.Dispose()}
|
||||
}
|
||||
$completed=$false
|
||||
try {
|
||||
$null=New-Item -ItemType Directory $copy -Force
|
||||
foreach($name in @('WELA.ps1','config','scripts','modules')){Copy-Item -LiteralPath (Join-Path $root $name) -Destination $copy -Recurse}
|
||||
# Only the owned disposable checkout gets this installed one-file catalog.
|
||||
# Production command and receipt validation expose no arbitrary-target override.
|
||||
$file=Join-Path $temp 'owned.txt';[IO.File]::WriteAllText($file,'owned recovery fixture')
|
||||
$catalog=[pscustomobject]@{description='Owned disposable installed catalog';registry=@();files=@([pscustomobject]@{path=$file;inherit=$false;rights=@('ReadData');note='Owned leaf'});user_registry=@();user_files=@()}
|
||||
Save (Join-Path $copy 'config/audit_sacl_targets.json') $catalog
|
||||
Import-Module (Join-Path $copy 'modules/AuditProfiles.psm1') -Force
|
||||
. (Join-Path $copy 'scripts/ControlApplicability.ps1')
|
||||
. (Join-Path $copy 'scripts/TargetedSaclPlanning.ps1')
|
||||
. (Join-Path $copy 'scripts/SelectedSaclConfiguration.ps1')
|
||||
. (Join-Path $copy 'scripts/WefArrival.ps1')
|
||||
. (Join-Path $copy 'scripts/EvtxRecovery.ps1')
|
||||
. (Join-Path $copy 'scripts/FileSaclRecovery.ps1')
|
||||
Initialize-WelaFileSaclRecoveryNative
|
||||
Set-ItemProperty -LiteralPath $precedencePath -Name SCENoApplyLegacyAuditPolicy -Value 1 -Type DWord
|
||||
Set-WelaEffectiveAuditPolicy -Guid '0CCE921D-69AE-11D9-BED3-505054503030' -Mask 3 -Mode minimum
|
||||
$context=Get-WelaSelectedSaclContext
|
||||
$target=@((Get-WelaSelectedSaclCatalog -Profile wela-2.2.0 -IncludeOptional -Context $context).Rows)
|
||||
Assert ($target.Count -eq 1 -and $target[0].Definition.Path -ceq $file) 'Installed fixture catalog selects only the owned leaf.'
|
||||
$id=$target[0].Id;$definition=$target[0].Definition
|
||||
foreach($case in @('empty','unrelated')){
|
||||
$caseDir=Join-Path $temp $case;$null=New-Item -ItemType Directory $caseDir
|
||||
if($case -eq 'unrelated'){
|
||||
$beforeUnrelated=Get-WelaSelectedSaclSnapshot $definition
|
||||
$other=[pscustomobject]@{Sid='S-1-5-11';Mask=2;Flags=64;RequiredPolicyMask=1}
|
||||
$null=Write-WelaSelectedSaclNative $definition $beforeUnrelated $other
|
||||
}
|
||||
$before=Get-WelaSelectedSaclSnapshot $definition
|
||||
$original=Join-Path $caseDir 'original.json';$backup=Join-Path $caseDir 'receipts';$configured=Join-Path $caseDir 'configured.json'
|
||||
Run-Wela @('targeted-sacl','-TargetSaclAction','Plan','-TargetSaclProfile','wela-2.2.0','-TargetSaclId',$id,'-IncludeOptional','-ResultsPath',$original)
|
||||
Run-Wela @('targeted-sacl','-TargetSaclAction','Configure','-TargetSaclPlanPath',$original,'-TargetSaclId',$id,'-IncludeOptional','-Auto','-BackupPath',$backup,'-ResultsPath',$configured)
|
||||
$completedAddition=Json $configured
|
||||
Assert ($completedAddition.Results[0].Status -ceq 'Applied' -and $completedAddition.ExitCode -eq 0) 'Original public Configure supplied genuine Applied result and receipt pair.'
|
||||
$pending=Join-Path $backup ($id+'.pending.json');$confirmed=Join-Path $backup ($id+'.confirmed.json')
|
||||
$afterAddition=Get-WelaSelectedSaclSnapshot $definition
|
||||
$planDir=Join-Path $caseDir 'recovery-plan'
|
||||
$planArgs=@('file-sacl-recovery','-FileSaclRecoveryOriginalPlanPath',$original,'-FileSaclRecoveryPendingPath',$pending,'-FileSaclRecoveryConfirmedPath',$confirmed,'-FileSaclRecoveryResultsPath',$configured)
|
||||
Run-Wela ($planArgs+@('-FileSaclRecoveryOutputPath',$planDir))
|
||||
$planPath=Join-Path $planDir 'plan.json';$hash=(Get-FileHash $planPath).Hash.ToLowerInvariant();$plan=Json $planPath
|
||||
Assert ($plan.Kind -ceq 'WelaFileSaclRecoveryPlan' -and $plan.Expected.Identity -ceq $before.Identity -and $plan.ReadyRuleCredit -eq 0) 'Recovery plan binds the original actual file identity without telemetry credit.'
|
||||
$restore=@('file-sacl-recovery','-FileSaclRecoveryAction','Restore','-FileSaclRecoveryPlanPath',$planPath,'-FileSaclRecoveryPlanHash',$hash)
|
||||
Run-Wela ($restore+@('-DryRun'))
|
||||
Assert ((Get-WelaSelectedSaclSnapshotKey (Get-WelaSelectedSaclSnapshot $definition)) -ceq (Get-WelaSelectedSaclSnapshotKey $afterAddition)) 'Public dry run preserves the exact current full descriptor.'
|
||||
if($case -eq 'empty'){
|
||||
$saved=[IO.File]::ReadAllBytes($confirmed);$broken=Json $confirmed;$broken.State='Pending';Save $confirmed $broken
|
||||
Run-Wela ($restore+@('-DryRun')) 1 'pending and confirmed'
|
||||
[IO.File]::WriteAllBytes($confirmed,$saved)
|
||||
$nativePath=Join-Path $copy 'scripts/FileSaclRecoveryNative.cs';$nativeBytes=[IO.File]::ReadAllBytes($nativePath);[IO.File]::AppendAllText($nativePath,"`n// owned source mismatch fixture`n")
|
||||
Run-Wela ($restore+@('-DryRun')) 1 'stale or modified'
|
||||
[IO.File]::WriteAllBytes($nativePath,$nativeBytes)
|
||||
# A different file at the identical path must not inherit recovery authority.
|
||||
$held=Join-Path $caseDir 'original-held.txt';Move-Item -LiteralPath $file -Destination $held;[IO.File]::WriteAllText($file,'replacement')
|
||||
Run-Wela ($restore+@('-DryRun')) 1 'identity or descriptor differs'
|
||||
Remove-Item -LiteralPath $file;Move-Item -LiteralPath $held -Destination $file
|
||||
Assert ((Get-WelaSelectedSaclSnapshotKey (Get-WelaSelectedSaclSnapshot $definition)) -ceq (Get-WelaSelectedSaclSnapshotKey $afterAddition)) 'Refused receipt/source/replacement cases did not alter the original descriptor.'
|
||||
}
|
||||
$out=Join-Path $caseDir 'restored'
|
||||
Run-Wela ($restore+@('-Auto','-FileSaclRecoveryOutputPath',$out))
|
||||
$result=Json (Join-Path $out 'result.json');$after=Get-WelaSelectedSaclSnapshot $definition
|
||||
Assert ($result.Status -ceq 'AddedAceRemoved' -and $result.WriteAttempted -and $result.ExitCode -eq 0 -and $result.PolicyChanges -eq 0) 'Public recovery performs and verifies only the proven added ACE removal.'
|
||||
[Wela.FileSaclRecovery.Descriptor]::Removed($afterAddition.DescriptorBase64,$after.DescriptorBase64,$plan.AddedAce)
|
||||
Assert ($before.Identity -ceq $after.Identity -and $before.Owner -ceq $after.Owner -and $before.Group -ceq $after.Group -and $before.DaclBase64 -ceq $after.DaclBase64 -and $before.Aces.Count -eq $after.Aces.Count) 'Actual reopened leaf preserves identity, owner/group/DACL and unrelated ACE counts.'
|
||||
foreach($artifact in $result.Artifacts){Assert ($artifact.Sha256 -ceq (Get-FileHash (Join-Path $out $artifact.Name)).Hash.ToLowerInvariant()) 'Durable review and pre-write intent artifacts retain their recorded hashes.'}
|
||||
Assert ((Json (Join-Path $out 'pending.json')).Before.DescriptorBase64 -ceq $afterAddition.DescriptorBase64) 'Pending receipt records the exact descriptor reviewed before removal.'
|
||||
Run-Wela ($restore+@('-DryRun')) 1 'identity or descriptor differs'
|
||||
Write-Host "PASS: actual public leaf recovery $case, original identity $($before.Identity), $($before.Aces.Count) unrelated ACEs preserved."
|
||||
}
|
||||
$completed=$true
|
||||
} finally {
|
||||
Set-WelaEffectiveAuditPolicy -Guid '0CCE921D-69AE-11D9-BED3-505054503030' -Mask $policyBefore['0CCE921D-69AE-11D9-BED3-505054503030'] -Mode exact
|
||||
if($precedenceBefore.ValueExists){Set-ItemProperty -LiteralPath $precedencePath -Name SCENoApplyLegacyAuditPolicy -Type $precedenceBefore.Type -Value $precedenceBefore.Value}else{Remove-ItemProperty -LiteralPath $precedencePath -Name SCENoApplyLegacyAuditPolicy -ErrorAction SilentlyContinue}
|
||||
Assert ((Fingerprint (Get-WelaEffectiveAuditPolicy)) -ceq (Fingerprint $policyBefore) -and ((Get-WelaRegistryState $precedencePath SCENoApplyLegacyAuditPolicy)|ConvertTo-Json -Compress) -ceq ($precedenceBefore|ConvertTo-Json -Compress)) 'All 59 original policy masks and typed precedence restored.'
|
||||
if($completed){Remove-Item -LiteralPath $temp -Recurse -Force;Write-Host "PASS: $script:count actual public file recovery assertions; only owned files and checkout removed."}else{Write-Host "Failed fixture evidence retained at $temp"}
|
||||
}
|
||||
$global:LASTEXITCODE=0
|
||||
Reference in new issue
Block a user