From 8546d319ebf767e5874d9110cdce2abe58a06a68 Mon Sep 17 00:00:00 2001 From: Shirofune-Security <43838376+Shirofune-Security@users.noreply.github.com> Date: Tue, 22 Sep 2026 14:45:06 +0900 Subject: [PATCH] Reject unexplained descendant changes and unrelated WMI command arguments --- .github/workflows/wmi-descendants.yml | 2 ++ CHANGELOG-Japanese.md | 2 +- CHANGELOG.md | 2 +- WELA.ps1 | 5 ++++- docs/wmi-descendants.md | 2 +- scripts/WmiNamespaceDescendants.ps1 | 2 ++ tests/WmiNamespaceDescendants.Cli.Tests.ps1 | 16 ++++++++++++++++ tests/WmiNamespaceDescendants.Tests.ps1 | 14 +++++++++++++- website/docs/resources/changelog.ja.md | 2 +- website/docs/resources/changelog.md | 2 +- 10 files changed, 42 insertions(+), 7 deletions(-) create mode 100644 tests/WmiNamespaceDescendants.Cli.Tests.ps1 diff --git a/.github/workflows/wmi-descendants.yml b/.github/workflows/wmi-descendants.yml index b188c510..ec11f62c 100644 --- a/.github/workflows/wmi-descendants.yml +++ b/.github/workflows/wmi-descendants.yml @@ -28,6 +28,7 @@ jobs: run: | ./tests/WmiNamespaceAuditing.Tests.ps1 ./tests/WmiNamespaceDescendants.Tests.ps1 + ./tests/WmiNamespaceDescendants.Cli.Tests.ps1 ./tests/WmiNamespaceDescendants.Windows.Tests.ps1 -AllowDisposableNamespaceWrite -EvidencePath wmi-descendants-native.json - name: Native tree validation in PowerShell 7 if: matrix.shell == 'pwsh' @@ -35,6 +36,7 @@ jobs: run: | ./tests/WmiNamespaceAuditing.Tests.ps1 ./tests/WmiNamespaceDescendants.Tests.ps1 + ./tests/WmiNamespaceDescendants.Cli.Tests.ps1 ./tests/WmiNamespaceDescendants.Windows.Tests.ps1 -AllowDisposableNamespaceWrite -EvidencePath wmi-descendants-native.json - name: Retain complete native observations if: always() diff --git a/CHANGELOG-Japanese.md b/CHANGELOG-Japanese.md index f100ea6d..16da333a 100644 --- a/CHANGELOG-Japanese.md +++ b/CHANGELOG-Japanese.md @@ -4,7 +4,7 @@ **改善:** -- WMI の明示的な継承設定に、既存の子名前空間の上限付き調査、完全な変更前記録、ツリー変更の拒否を追加しました。親の SACL のみを書き込み、継承・保護状態と最終状態を確認します。不完全な伝播は失敗として扱い、子への直接書き込み、自動復元、イベントや Sigma 対応を保証しません。(Related #372) (@Shirofune-Security) +- WMI の明示的な継承設定に、既存の子名前空間の上限付き調査、完全な変更前記録、ツリー変更の拒否を追加しました。親の SACL のみを書き込み、継承・保護状態と最終状態を確認します。不完全な伝播は失敗として扱い、子への直接書き込み、自動復元、イベントや Sigma 対応を保証しません。無関係な引数や余分な位置引数も拒否します。(Related #372) (@Shirofune-Security) - Server2022/2025 と PowerShell5.1/7 で、Token Right Adjusted の正規GUIDに対する Security4703 の実機検証を追加しました。所有する子プロセスの既存権限を固定手順で無効化・復元し、候補となる2つの監査マスクを比較して、実イベント・実行条件・ハッシュとポリシー/トークンの復元を記録します。過去の候補は条件付きのまま維持し、製品用プローブ・全OS共通の対応関係・Sigma加点は追加しません。(関連 #380) (@Shirofune-Security) diff --git a/CHANGELOG.md b/CHANGELOG.md index 349a153d..f4526790 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,7 +4,7 @@ **Improvements:** -- Added bounded existing-descendant snapshots, full recovery evidence and stale-tree guards for explicit WMI inheritance. Parent-only SACL writes now require native inherited/protected readbacks and final drift checks; incomplete propagation fails without child setters, rollback ownership, event or Sigma credit. (Related #372) (@Shirofune-Security) +- Added bounded existing-descendant snapshots, full recovery evidence and stale-tree guards for explicit WMI inheritance. Parent-only SACL writes now require native inherited/protected readbacks and final drift checks; incomplete propagation fails without child setters, rollback ownership, event or Sigma credit. Unrelated or extra positional WMI command arguments are now refused. (Related #372) (@Shirofune-Security) - Added disposable native Security4703 attribution for the canonical Token Right Adjusted GUID on Server2022/2025 and PowerShell5.1/7. A fixed owned-child privilege disable/restore compares the two historical audit-mask candidates, retains exact event/context/hash evidence and verifies policy/token cleanup. Historical candidates remain conditional; no production probe, universal mapping or Sigma credit. (Related #380) (@Shirofune-Security) diff --git a/WELA.ps1 b/WELA.ps1 index 7ece9a25..af810ac9 100644 --- a/WELA.ps1 +++ b/WELA.ps1 @@ -2310,6 +2310,9 @@ if ($DryRun -and -not ($Cmd -eq 'ntlm-auditing' -and $NtlmAuditAction -eq 'Confi if (($WmiNamespace -or $WmiIncludeChildren -or $PSBoundParameters.ContainsKey('WmiAction')) -and $Cmd -ne 'wmi-auditing') { throw '-WmiAction, -WmiNamespace and -WmiIncludeChildren require wmi-auditing. No command was run.' } +if ($Cmd -eq 'wmi-auditing' -and ($args.Count -or @($PSBoundParameters.Keys | Where-Object {$_ -notin @('Cmd','WmiAction','WmiNamespace','WmiIncludeChildren','Auto','DryRun','BackupPath','ResultsPath','Help')}).Count)) { + throw 'wmi-auditing accepts only its dedicated namespace options, Auto, DryRun, BackupPath, ResultsPath and Help. Unexpected positional or unrelated arguments are refused.' +} if ($Profile -and $Cmd -in @('eventlog-profiles', 'audit-filesize', 'configure-eventlogs')) { throw '-Profile selects advanced audit policy only. Use -LogProfile for event-log size/mode settings.' } @@ -2695,7 +2698,7 @@ switch ($Cmd.ToLower()) { 'wmi-auditing' { if ($Help) { Write-Host 'Usage: ./WELA.ps1 wmi-auditing -WmiAction List|Audit|Plan|Configure [-WmiNamespace root\cimv2,root\subscription] [-WmiIncludeChildren] [-Auto] [-DryRun] [-BackupPath new-directory] [-ResultsPath file.json]' - Write-Host 'Select exact local namespaces explicitly. Default action List is read-only. Configure appends ASD success audit ACEs; descendant inheritance requires an explicit switch. No access permissions, audit policy or forwarding changes.' + Write-Host 'Select exact local namespaces explicitly. Default action List is read-only. Configure appends ASD success audit ACEs; WmiIncludeChildren requires complete stable descendant snapshots and inherited/protected readbacks. Unverified propagation fails even if the parent write succeeded; no child setter or automatic rollback. See docs/wmi-descendants.md.' return } if ($Profile -or $Baseline) { throw 'wmi-auditing uses its own namespace selections, not -Profile or -Baseline.' } diff --git a/docs/wmi-descendants.md b/docs/wmi-descendants.md index d837478f..1f11972f 100644 --- a/docs/wmi-descendants.md +++ b/docs/wmi-descendants.md @@ -8,7 +8,7 @@ ./WELA.ps1 wmi-auditing -WmiAction Configure -WmiNamespace 'root\default' -WmiIncludeChildren -BackupPath C:\Evidence\new-wmi-backup -ResultsPath tree-result.json ``` -Review `Controls[].Descendants` and the full descriptor strings before configuration. Configure builds a fresh in-memory plan, shows the descendant count in its confirmation, and checks that same tree again before writing. The earlier Plan export is documentation of its observation, not a persisted authorization token consumed by Configure. `-Auto` skips the confirmation only; it does not skip the tree checks. +Review `Controls[].Descendants` and the full descriptor strings before configuration. Configure builds a fresh in-memory plan, shows the descendant count in its confirmation, and checks that same tree again before writing. The earlier Plan export is documentation of its observation, not a persisted authorization token consumed by Configure. `-Auto` skips the confirmation only; it does not skip the tree checks. The CLI refuses unrelated parameters and extra positional arguments instead of silently binding them to an unused output-format parameter. The inventory records every existing child and grandchild within 64 descendants, eight levels and two MiB of descriptor evidence. Two complete passes must agree on names, parent relationships and every full descriptor. Unknown names, duplicates, access failures, caps, incomplete reads and drift fail closed. The scan checks a 30-second budget between namespaces, and native enumeration requests a ten-second timeout. Individual synchronous provider calls cannot be forcibly cancelled, so this is not a hard total runtime limit. Winmgmt must already be running. Host, implementation fingerprints and the full observed caller SID, logon, groups and privilege attributes must remain unchanged. diff --git a/scripts/WmiNamespaceDescendants.ps1 b/scripts/WmiNamespaceDescendants.ps1 index c975b77e..394807fe 100644 --- a/scripts/WmiNamespaceDescendants.ps1 +++ b/scripts/WmiNamespaceDescendants.ps1 @@ -111,6 +111,7 @@ function Test-WelaWmiDescendantOutcomes { } else { # Allow only SACL_PRESENT to appear. Every other control and full # owner/group/DACL/unknown descriptor property remains identical. + if((ConvertTo-WelaWmiJson @($a.PSObject.Properties.Name|Sort-Object)) -cne (ConvertTo-WelaWmiJson @($b.PSObject.Properties.Name|Sort-Object))){throw 'Child descriptor property inventory changed.'} foreach($property in $a.PSObject.Properties){ if($property.Name -eq 'SACL'){continue} if($property.Name -eq 'ControlFlags'){ @@ -126,6 +127,7 @@ function Test-WelaWmiDescendantOutcomes { } $expected=@($Definitions|Where-Object {($_.AceFlags -band 2) -ne 0}|ForEach-Object {[pscustomobject]@{Sid=$_.Sid;AccessMask=$_.AccessMask;AceFlags=([uint32]$_.AceFlags -bor 16)}}) foreach($ace in $remaining){if(-not @($expected|Where-Object {Test-WelaWmiAceMatch $ace $_}).Count){throw 'Unexplained child audit entry appeared.'}} + foreach($definition in $expected){if(@($remaining|Where-Object {Test-WelaWmiAceMatch $_ $definition}).Count -gt 1){throw 'Unexplained duplicate inherited child entry appeared.'}} foreach($definition in $expected){if(-not @($b.SACL|Where-Object {Test-WelaWmiAceMatch $_ $definition}).Count){throw 'Requested inherited ACE was not observed; existing-child propagation is unverified.'}} $status='InheritedAceObserved' } diff --git a/tests/WmiNamespaceDescendants.Cli.Tests.ps1 b/tests/WmiNamespaceDescendants.Cli.Tests.ps1 new file mode 100644 index 00000000..932e7aa9 --- /dev/null +++ b/tests/WmiNamespaceDescendants.Cli.Tests.ps1 @@ -0,0 +1,16 @@ +$ErrorActionPreference='Stop';$repo=Split-Path $PSScriptRoot -Parent;$engine=(Get-Process -Id $PID).Path +$cases=@( + @{Args=@('wmi-auditing','-Help');Exit=0;Pattern='WmiIncludeChildren'}, + @{Args=@('wmi-auditing','-WmiAction','Plan','-WmiNamespace','root\default','-WmiIncludeChildren','-Help');Exit=0;Pattern='Usage:'}, + @{Args=@('wmi-auditing','-WmiAction','Configure','-WmiIncludeChildren','-DryRun','-Help');Exit=0;Pattern='Usage:'}, + @{Args=@('wmi-auditing','-WmiAction','Audit','-WmiIncludeChildren','-DryRun','-Help');Exit=1;Pattern='DryRun'}, + @{Args=@('configure','-WmiIncludeChildren','-Help');Exit=1;Pattern='require wmi-auditing'}, + @{Args=@('wmi-auditing','unexpected','-WmiIncludeChildren','-Help');Exit=1;Pattern='positional|argument|Unrecognized'}, + @{Args=@('wmi-auditing','-WmiNamespace','root\default','-FileProbeAction','Run','-Help');Exit=1;Pattern='require file-access-probe|dedicated'} +) +foreach($c in $cases){ + $ErrorActionPreference='Continue';try{$text=@(& $engine -NoProfile -File (Join-Path $repo 'WELA.ps1') @($c.Args) 2>&1);$code=$LASTEXITCODE}finally{$ErrorActionPreference='Stop'} + if($code -ne $c.Exit -or ($text -join "`n") -notmatch $c.Pattern){throw "CLI mismatch: $($c.Args -join ' ') : $code / $text"} +} +Write-Host "PASS: $($cases.Count) WMI inheritance public CLI assertions." +$global:LASTEXITCODE=0 diff --git a/tests/WmiNamespaceDescendants.Tests.ps1 b/tests/WmiNamespaceDescendants.Tests.ps1 index 52a9c4f9..e123c08d 100644 --- a/tests/WmiNamespaceDescendants.Tests.ps1 +++ b/tests/WmiNamespaceDescendants.Tests.ps1 @@ -58,6 +58,10 @@ try{ $c=New-WelaConfigurationContext -Auto -DryRun -BackupPath $temp Set-WelaWmiAuditControls $c $p Assert ($script:writes -eq 0 -and $c.Results[0].Status -eq 'Skipped' -and -not (Test-Path $temp)) 'DryRun no state or journal mutation.' + $script:context='changed token';$c=New-WelaConfigurationContext -Auto -BackupPath (Join-Path $temp token) + Set-WelaWmiAuditControls $c $p + Assert ($script:writes -eq 0 -and $c.Results[0].Status -eq 'Failed') 'Full context drift invalidates planned subtree.' + $script:context='caller/host/source' $script:tree['root\default\New']=Descriptor $c=New-WelaConfigurationContext -Auto -BackupPath (Join-Path $temp stale) Set-WelaWmiAuditControls $c $p @@ -80,17 +84,25 @@ try{ Assert ((Get-WelaWmiDescendantKey $journal.Before.Descendants) -ceq (Get-WelaWmiDescendantKey $p[0].Descendants)) 'Journal serialization does not truncate original child snapshots.' $script:tree['root\default\A'].Opaque='drift' Assert ((Complete-WelaConfiguration $c).ExitCode -eq 1) 'Final child drift propagates failure.' + Reset;$script:tree['root\default'].SACL+=Ace 66;$script:tree['root\default'].ControlFlags=32788 + $unverified=@(Get-WelaWmiAuditPlan -Namespace 'root\default' -IncludeChildren) + Assert ($unverified[0].Status -eq 'Unknown' -and $unverified[0].Diagnostic -match 'descendants are unverified') 'Already-compliant parent cannot imply descendant compliance.' + $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.' # Each unrelated mutation invalidates observed propagation, even when required ACE still exists. - foreach($kind in @('Owner','Dacl','Control','Unknown','Protected','Removed','Extra','Missing','New')){ + 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 switch($kind){ Owner {$script:tree['root\default\A'].Owner='other'} Dacl {$script:tree['root\default\A'].DACL=@('other')} Control {$script:tree['root\default\A'].ControlFlags=$script:tree['root\default\A'].ControlFlags -bor 256} Unknown {$script:tree['root\default\A'].Opaque='other'} + NewProperty {$script:tree['root\default\A']|Add-Member NoteProperty NewOpaque 1} Protected {$script:tree['root\default\Protected\B'].SACL+=Ace} Removed {$script:tree.Remove('root\default\A\B')} Extra {$script:tree['root\default\A'].SACL+=Ace 64} + Duplicate {$script:tree['root\default\A'].SACL+=Ace} Missing {$script:tree['root\default\A'].SACL=@()} New {$script:tree['root\default\Unreviewed']=Descriptor} } diff --git a/website/docs/resources/changelog.ja.md b/website/docs/resources/changelog.ja.md index 620336df..ee8d8e5e 100644 --- a/website/docs/resources/changelog.ja.md +++ b/website/docs/resources/changelog.ja.md @@ -7,7 +7,7 @@ **改善:** -- WMI の明示的な継承設定に、既存の子名前空間の上限付き調査、完全な変更前記録、ツリー変更の拒否を追加しました。親の SACL のみを書き込み、継承・保護状態と最終状態を確認します。不完全な伝播は失敗として扱い、子への直接書き込み、自動復元、イベントや Sigma 対応を保証しません。(Related #372) (@Shirofune-Security) +- WMI の明示的な継承設定に、既存の子名前空間の上限付き調査、完全な変更前記録、ツリー変更の拒否を追加しました。親の SACL のみを書き込み、継承・保護状態と最終状態を確認します。不完全な伝播は失敗として扱い、子への直接書き込み、自動復元、イベントや Sigma 対応を保証しません。無関係な引数や余分な位置引数も拒否します。(Related #372) (@Shirofune-Security) - Server2022/2025 と PowerShell5.1/7 で、Token Right Adjusted の正規GUIDに対する Security4703 の実機検証を追加しました。所有する子プロセスの既存権限を固定手順で無効化・復元し、候補となる2つの監査マスクを比較して、実イベント・実行条件・ハッシュとポリシー/トークンの復元を記録します。過去の候補は条件付きのまま維持し、製品用プローブ・全OS共通の対応関係・Sigma加点は追加しません。(関連 #380) (@Shirofune-Security) diff --git a/website/docs/resources/changelog.md b/website/docs/resources/changelog.md index 4e364ff8..d11eeb37 100644 --- a/website/docs/resources/changelog.md +++ b/website/docs/resources/changelog.md @@ -7,7 +7,7 @@ **Improvements:** -- Added bounded existing-descendant snapshots, full recovery evidence and stale-tree guards for explicit WMI inheritance. Parent-only SACL writes now require native inherited/protected readbacks and final drift checks; incomplete propagation fails without child setters, rollback ownership, event or Sigma credit. (Related #372) (@Shirofune-Security) +- Added bounded existing-descendant snapshots, full recovery evidence and stale-tree guards for explicit WMI inheritance. Parent-only SACL writes now require native inherited/protected readbacks and final drift checks; incomplete propagation fails without child setters, rollback ownership, event or Sigma credit. Unrelated or extra positional WMI command arguments are now refused. (Related #372) (@Shirofune-Security) - Added disposable native Security4703 attribution for the canonical Token Right Adjusted GUID on Server2022/2025 and PowerShell5.1/7. A fixed owned-child privilege disable/restore compares the two historical audit-mask candidates, retains exact event/context/hash evidence and verifies policy/token cleanup. Historical candidates remain conditional; no production probe, universal mapping or Sigma credit. (Related #380) (@Shirofune-Security)