From 072bcdf6afc1f3d87715b8c7141c5ba195fdc142 Mon Sep 17 00:00:00 2001 From: Shirofune-Security <43838376+Shirofune-Security@users.noreply.github.com> Date: Sat, 19 Sep 2026 06:20:32 +0900 Subject: [PATCH] Verify native WMI SACL flags on disposable Windows namespaces --- .github/workflows/wmi-namespace-auditing.yml | 25 ++++++ ...paceAuditing.DisposableNamespace.Tests.ps1 | 85 +++++++++++++++++++ 2 files changed, 110 insertions(+) create mode 100644 tests/WmiNamespaceAuditing.DisposableNamespace.Tests.ps1 diff --git a/.github/workflows/wmi-namespace-auditing.yml b/.github/workflows/wmi-namespace-auditing.yml index cad32532..5c086ac2 100644 --- a/.github/workflows/wmi-namespace-auditing.yml +++ b/.github/workflows/wmi-namespace-auditing.yml @@ -36,3 +36,28 @@ jobs: - name: In-memory privilege restoration failure paths (PowerShell 7) shell: pwsh run: ./tests/WmiNamespaceAuditing.Privilege.Tests.ps1 + disposable-namespace: + # Mutations are restricted to newly created namespaces on hosted throwaway VMs. + strategy: + fail-fast: false + matrix: + os: [windows-2022, windows-2025] + runs-on: ${{ matrix.os }} + steps: + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + - name: Real temporary-namespace SACL write/readback (Windows PowerShell 5.1) + shell: powershell + run: ./tests/WmiNamespaceAuditing.DisposableNamespace.Tests.ps1 -AllowDisposableNamespaceWrite -EvidencePath wmi-native-ps51.json + - name: Real temporary-namespace SACL write/readback (PowerShell 7) + shell: pwsh + run: ./tests/WmiNamespaceAuditing.DisposableNamespace.Tests.ps1 -AllowDisposableNamespaceWrite -EvidencePath wmi-native-ps7.json + - name: Record native descriptor evidence + if: always() + shell: pwsh + run: | + foreach ($path in @('wmi-native-ps51.json', 'wmi-native-ps7.json')) { + if (Test-Path -LiteralPath $path) { + Write-Host "Evidence: $path" + Get-Content -LiteralPath $path -Raw + } + } diff --git a/tests/WmiNamespaceAuditing.DisposableNamespace.Tests.ps1 b/tests/WmiNamespaceAuditing.DisposableNamespace.Tests.ps1 new file mode 100644 index 00000000..2ecabed1 --- /dev/null +++ b/tests/WmiNamespaceAuditing.DisposableNamespace.Tests.ps1 @@ -0,0 +1,85 @@ +# Actual SACL writes, confined to fresh temporary namespaces on a disposable VM. +# Existing namespaces are read only as the parent/factory; never passed to a setter. +param([switch]$AllowDisposableNamespaceWrite, [string]$EvidencePath) +$ErrorActionPreference = 'Stop' +if (-not $AllowDisposableNamespaceWrite) { throw 'This integration test requires -AllowDisposableNamespaceWrite on a disposable Windows VM.' } +if ($env:OS -ne 'Windows_NT') { throw 'Disposable-namespace integration requires Windows.' } +$repo = Split-Path $PSScriptRoot -Parent +. (Join-Path $repo 'scripts/Configuration.ps1') +. (Join-Path $repo 'scripts/WmiNamespaceAuditing.ps1') +Initialize-WelaWmiInterop +$script:assertions = 0 +function Assert($Condition, [string]$Message) { if (-not $Condition) { throw "FAIL: $Message" }; $script:assertions++ } +$evidence = [pscustomobject]@{ + SchemaVersion = 1; Computer = $env:COMPUTERNAME; OperatingSystem = [Environment]::OSVersion.VersionString + PowerShell = $PSVersionTable.PSVersion.ToString(); StartedUtc = [DateTime]::UtcNow.ToString('o') + Scope = 'Real SACL write/readback on uniquely created root child namespaces only' + EventGeneration = 'Not tested'; Forwarding = 'Not tested'; Cases = @(); Complete = $false +} +$backup = Join-Path ([IO.Path]::GetTempPath()) ('wela-wmi-integration-' + [guid]::NewGuid().ToString('N')) +try { + foreach ($flags in @(64, 66)) { + $name = 'WelaSaclTest_' + [guid]::NewGuid().ToString('N') + $namespace = 'root\' + $name + Assert ($namespace -match '^root\\WelaSaclTest_[0-9a-f]{32}$') 'Only the generated test namespace can receive writes' + $created = $false; $factory = $null; $instance = $null + $case = [pscustomobject]@{ Namespace=$namespace; AceFlags=$flags; Before=$null; After=$null; Result=$null; RepeatResult=$null; BeforeControlFlags=$null; ExpectedControlFlags=$null; AfterControlFlags=$null; Removed=$false } + $evidence.Cases += $case + try { + # CreateOnly is essential: never adopt or delete an existing namespace. + $factory = New-Object System.Management.ManagementClass -ArgumentList '\\.\root:__Namespace' + $instance = $factory.CreateInstance(); $instance.Name = $name + $options = New-Object System.Management.PutOptions + $options.Type = [System.Management.PutType]::CreateOnly + $createdPath = $instance.Put($options) + $created = $true + Assert ($createdPath.RelativePath -eq ('__NAMESPACE.Name="' + $name + '"')) 'Created namespace identity matches the generated name' + $before = Get-WelaWmiNamespaceSnapshot $namespace + $case.Before = $before + $beforeData = $before.DescriptorJson | ConvertFrom-Json + $case.BeforeControlFlags = [uint32]$beforeData.ControlFlags + $case.ExpectedControlFlags = [uint32]$beforeData.ControlFlags -bor 16 + Assert (@($beforeData.SACL | Where-Object { $null -ne $_ }).Count -eq 0) 'Fixture exercises first SACL creation on a namespace with no existing audit ACEs' + # Reuse the real ASD root-default mask/SID, with the test target and + # explicit inheritance mode. Production profile scope is unchanged. + $definitions = @(Get-WelaWmiAuditDefinitions -Namespace 'root\default' -IncludeChildren) + $definitions[0].Namespace = $namespace; $definitions[0].AceFlags = [uint32]$flags + $entry = [pscustomobject]@{ Namespace=$namespace; Definitions=$definitions } + $context = New-WelaConfigurationContext -Auto -BackupPath (Join-Path $backup ('first-' + $flags)) + Set-WelaWmiAuditControls -Context $context -Plan @($entry) + $case.Result = Complete-WelaConfiguration -Context $context -Scope 'wmi-namespace-sacl-only' + $after = Get-WelaWmiNamespaceSnapshot $namespace + $case.After = $after + $afterData = $after.DescriptorJson | ConvertFrom-Json + $case.AfterControlFlags = [uint32]$afterData.ControlFlags + Write-Host "Native flags: mode=$flags before=$($case.BeforeControlFlags) expected=$($case.ExpectedControlFlags) after=$($case.AfterControlFlags)" + Assert ($case.Result.ExitCode -eq 0 -and $case.Result.Results[0].Status -eq 'Applied') 'Actual production runner accepts the provider readback after first SACL creation' + Assert ($case.AfterControlFlags -eq $case.ExpectedControlFlags) 'Provider control flags match the exact preservation contract for this tested host/mode' + Assert (Test-WelaWmiDescriptorPreserved $beforeData $afterData) 'Original access fields and existing ACEs survive the real SACL-only write' + Assert (@(Get-WelaWmiMissingAces $afterData $definitions).Count -eq 0) 'Native provider stores the requested SID/mask/outcome/inheritance' + $repeat = New-WelaConfigurationContext -Auto -BackupPath (Join-Path $backup ('repeat-' + $flags)) + Set-WelaWmiAuditControls -Context $repeat -Plan @($entry) + $case.RepeatResult = Complete-WelaConfiguration -Context $repeat -Scope 'wmi-namespace-sacl-only' + Assert ($case.RepeatResult.ExitCode -eq 0 -and $case.RepeatResult.Results[0].Status -eq 'AlreadyCompliant') 'Repeated real configuration is idempotent' + Assert ((Get-WelaWmiNamespaceSnapshot $namespace).DescriptorJson -ceq $after.DescriptorJson) 'Repeat leaves the full descriptor unchanged' + } finally { + try { + if ($created) { + # The only deletion target is the instance this run created. + $instance.Delete() + $remaining = @(Get-CimInstance -Namespace root -ClassName __Namespace -Filter ("Name='$name'") -ErrorAction Stop) + Assert ($remaining.Count -eq 0) 'Owned temporary namespace was removed' + $case.Removed = $true + } + } finally { + if ($instance) { $instance.Dispose() } + if ($factory) { $factory.Dispose() } + } + } + } + $evidence.Complete = $true + Write-Host "PASS: $script:assertions disposable-namespace native SACL assertions. Event generation and forwarding were not tested." +} finally { + if ($EvidencePath) { $evidence | ConvertTo-Json -Depth 25 | Set-Content -LiteralPath $EvidencePath -Encoding UTF8 -ErrorAction Stop } + if (Test-Path -LiteralPath $backup) { Remove-Item -LiteralPath $backup -Recurse -Force -ErrorAction Stop } +}