From d5de556f74d467c4d1379e793261e3d9fafa5671 Mon Sep 17 00:00:00 2001 From: Shirofune-Security <43838376+Shirofune-Security@users.noreply.github.com> Date: Tue, 22 Sep 2026 14:58:07 +0900 Subject: [PATCH] Retain partial WMI tree observations after a parent write --- scripts/WmiNamespaceAuditing.ps1 | 9 ++++++--- scripts/WmiNamespaceDescendants.ps1 | 4 +++- tests/WmiNamespaceDescendants.Tests.ps1 | 10 +++++++++- 3 files changed, 18 insertions(+), 5 deletions(-) diff --git a/scripts/WmiNamespaceAuditing.ps1 b/scripts/WmiNamespaceAuditing.ps1 index 3240c370..11afcffd 100644 --- a/scripts/WmiNamespaceAuditing.ps1 +++ b/scripts/WmiNamespaceAuditing.ps1 @@ -278,11 +278,11 @@ function Set-WelaWmiAuditControls { param($Context, [array]$Plan) foreach ($entry in $Plan) { $inherit=@($entry.Definitions|Where-Object {($_.AceFlags -band 2) -ne 0}).Count -gt 0 - $callback = @{ Namespace = $entry.Namespace; Definitions = $entry.Definitions; Original = $null; ExpectedJson = $null; Applied = $false; VerifiedJson = $null; Inherit=$inherit; PlannedTree=$entry.Descendants; OriginalTree=$null; VerifiedTree=$null; DescendantVerification=[pscustomobject]@{Observation=$null} } + $callback = @{ Namespace = $entry.Namespace; Definitions = $entry.Definitions; Original = $null; ExpectedJson = $null; Applied = $false; VerifiedJson = $null; Inherit=$inherit; PlannedTree=$entry.Descendants; OriginalTree=$null; VerifiedTree=$null; DescendantVerification=[pscustomobject]@{ParentSetterAttempted=$false;ParentSetterAccepted=$false;Observation=$null;LastTree=$null} } $read = { param($state) if($state.Inherit){ - $tree=Get-WelaWmiStableDescendants $state.Namespace + $tree=Get-WelaWmiStableDescendants $state.Namespace $state.DescendantVerification if($null -eq $state.OriginalTree){ if((Get-WelaWmiDescendantKey $tree) -cne (Get-WelaWmiDescendantKey $state.PlannedTree)){throw 'WMI descendant tree changed after planning; no SACL was written.'} $state.OriginalTree=$tree @@ -318,10 +318,13 @@ function Set-WelaWmiAuditControls { $apply = { param($state) if($state.Inherit){ - $fresh=Get-WelaWmiStableDescendants $state.Namespace + $fresh=Get-WelaWmiStableDescendants $state.Namespace $state.DescendantVerification if((Get-WelaWmiDescendantKey $fresh) -cne (Get-WelaWmiDescendantKey $state.OriginalTree)){throw 'WMI descendant topology or descriptor changed before the parent setter; no SACL was written.'} + $state.DescendantVerification.ParentSetterAttempted=$true + $state.DescendantVerification.Observation=$null } Set-WelaWmiNamespaceDescriptor -Namespace $state.Namespace -ExpectedJson $state.ExpectedJson -Definitions $state.Definitions + if($state.Inherit){$state.DescendantVerification.ParentSetterAccepted=$true} $state.Applied = $true } Invoke-WelaConfigurationControl -Context $Context -Id "WmiNamespace/$($entry.Namespace)/SACL" -Kind WmiNamespaceSacl ` diff --git a/scripts/WmiNamespaceDescendants.ps1 b/scripts/WmiNamespaceDescendants.ps1 index bc8a7601..f7efeda1 100644 --- a/scripts/WmiNamespaceDescendants.ps1 +++ b/scripts/WmiNamespaceDescendants.ps1 @@ -82,12 +82,14 @@ function Get-WelaWmiDescendants { [pscustomobject]@{Status=$(if($diagnostics.Count){'Incomplete'}else{'Complete'});Maximum=64;MaximumDepth=8;StartedUtc=$started.ToString('o');CompletedUtc=[DateTime]::UtcNow.ToString('o');Root=$root;Entries=@($entries.ToArray());Diagnostics=@($diagnostics.ToArray())} } function Get-WelaWmiStableDescendants { - param([string]$Namespace) + param([string]$Namespace,$Observation) $context=Get-WelaWmiDescendantContext try { $first=Get-WelaWmiDescendants $Namespace + if($Observation){$Observation.LastTree=$first} if($first.Status -cne 'Complete'){throw ('Incomplete WMI descendant inventory: '+($first.Diagnostics -join '; '))} $second=Get-WelaWmiDescendants $Namespace + if($Observation){$Observation.LastTree=$second} if((Get-WelaWmiDescendantKey $first) -cne (Get-WelaWmiDescendantKey $second)){throw 'WMI descendant topology or full descriptor changed between observations.'} $second|Add-Member NoteProperty Context $context $second diff --git a/tests/WmiNamespaceDescendants.Tests.ps1 b/tests/WmiNamespaceDescendants.Tests.ps1 index 532c7d3e..db65c77d 100644 --- a/tests/WmiNamespaceDescendants.Tests.ps1 +++ b/tests/WmiNamespaceDescendants.Tests.ps1 @@ -11,10 +11,11 @@ function Ace([uint32]$flags=82){[pscustomobject]@{AceType=2;AceFlags=$flags;Acce $script:tree=@{};$script:reads=0;$script:changeAt=0;$script:context='caller/host/source';$script:writes=0 function Reset { $script:tree=@{'root\default'=(Descriptor);'root\default\A'=(Descriptor);'root\default\A\B'=(Descriptor);'root\default\Protected'=(Descriptor 40964);'root\default\Protected\B'=(Descriptor)} - $script:reads=0;$script:changeAt=0;$script:context='caller/host/source';$script:writes=0;$script:prompt=$null + $script:reads=0;$script:changeAt=0;$script:context='caller/host/source';$script:writes=0;$script:prompt=$null;$script:failChildrenAfterWrite=$false } function Get-WelaWmiChildNames { param($Namespace,$Maximum) + if($script:failChildrenAfterWrite -and $script:writes -gt 0){throw "Injected post-write child-read refusal."} @($script:tree.Keys|Where-Object {$_ -clike ($Namespace+'\*') -and $_.Substring($Namespace.Length+1) -notmatch '\\'}|ForEach-Object {$_.Substring($Namespace.Length+1)}|Sort-Object) } function Get-WelaWmiDescendantContext {$script:context} @@ -92,6 +93,13 @@ try{ $c=New-WelaConfigurationContext -Auto -BackupPath (Join-Path $temp unverified) Set-WelaWmiAuditControls $c $unverified Assert ($script:writes -eq 0 -and (Complete-WelaConfiguration $c).ExitCode -eq 1) 'Missing existing-child inheritance fails without an unnecessary parent rewrite.' + Reset;$p=@(Get-WelaWmiAuditPlan -Namespace 'root\default' -IncludeChildren);$script:failChildrenAfterWrite=$true + $c=New-WelaConfigurationContext -Auto -BackupPath (Join-Path $temp partial) + Set-WelaWmiAuditControls $c $p + $r=Complete-WelaConfiguration $c + Assert ($r.ExitCode -eq 1 -and $script:writes -eq 1 -and $r.Results[0].Status -eq 'Failed') 'Post-write enumeration failure propagates a nonzero result.' + $v=$r.Results[0].DescendantVerification + Assert ($v.ParentSetterAttempted -and $v.ParentSetterAccepted -and $null -eq $v.Observation -and $v.LastTree.Status -eq 'Incomplete') 'Partial read failure retains parent-write flags and incomplete native observations without claiming verified outcomes.' # Each unrelated mutation invalidates observed propagation, even when required ACE still exists. foreach($kind in @('Owner','Dacl','Control','Unknown','NewProperty','Protected','Removed','Extra','Duplicate','Missing','New')){ Reset;$a=Get-WelaWmiStableDescendants 'root\default';$null=Set-WelaWmiNamespaceDescriptor 'root\default' $a.Root.DescriptorJson $defs