Merge branch 'feat/367-native-channel-access' into feat/371-ad-object-sacl

# Conflicts:
#	CHANGELOG-Japanese.md
#	CHANGELOG.md
#	WELA.ps1
#	scripts/Configuration.ps1
#	website/docs/resources/changelog.ja.md
#	website/docs/resources/changelog.md
This commit is contained in:
Shirofune-Security committed 2026-09-19 06:54:08 +09:00
commit 9d993a2a7e
26 files changed
+2286 -4

No files matched your search

+198
View File
@@ -0,0 +1,198 @@
$ErrorActionPreference = 'Stop'
. (Join-Path $PSScriptRoot '../scripts/Configuration.ps1')
. (Join-Path $PSScriptRoot '../scripts/AppLockerReadiness.ps1')
$count=0
function Assert($Condition,$Message) { if (-not $Condition) { throw $Message }; $script:count++ }
function Assert-Throws([scriptblock]$Action,$Pattern) { $message=''; try { & $Action | Out-Null } catch { $message=$_.Exception.Message }; Assert ($message -match $Pattern) "Expected '$Pattern', got '$message'." }
$xml='<AppLockerPolicy Version="1"><RuleCollection Type="Exe" EnforcementMode="AuditOnly"><FilePathRule Id="12345678-1234-1234-1234-123456789abc" Name="Test" Description="" UserOrGroupSid="S-1-1-0" Action="Allow"><Conditions><FilePathCondition Path="%WINDIR%\*" /></Conditions></FilePathRule></RuleCollection></AppLockerPolicy>'
$placeholderNodes = @('Exe','Dll','Msi','Script','Appx') | ForEach-Object { '<RuleCollection Type="' + $_ + '" EnforcementMode="NotConfigured" />' }
$placeholders = '<AppLockerPolicy Version="1">' + ($placeholderNodes -join '') + '</AppLockerPolicy>'
$unusedPlaceholders = '<AppLockerPolicy Version="1">' + (($placeholderNodes | Select-Object -Skip 1) -join '') + '</AppLockerPolicy>'
$readbackPlaceholders = $xml.Replace('</AppLockerPolicy>', (($placeholderNodes | Select-Object -Skip 1) -join '') + '</AppLockerPolicy>')
$desired=ConvertFrom-WelaAppLockerXml -Xml $xml -ForImport
Assert ($desired.TotalRules -eq 1 -and -not $desired.HasEnforcement) 'Audit-only rule must parse.'
Assert ((Get-WelaAppLockerXmlKey $xml) -ceq (Get-WelaAppLockerXmlKey ($xml.Replace('Type="Exe" EnforcementMode="AuditOnly"', 'EnforcementMode="AuditOnly" Type="Exe"')))) 'Attribute ordering cannot change compliance.'
Assert-Throws { ConvertFrom-WelaAppLockerXml -Xml ($xml.Replace('AuditOnly','Enabled')) -ForImport } 'AuditOnly'
Assert-Throws { ConvertFrom-WelaAppLockerXml -Xml ($xml.Replace('AuditOnly','NotConfigured')) -ForImport } 'AuditOnly'
Assert-Throws { ConvertFrom-WelaAppLockerXml -Xml '<AppLockerPolicy Version="1" />' -ForImport } 'empty'
Assert-Throws { ConvertFrom-WelaAppLockerXml -Xml ('<!DOCTYPE x [<!ENTITY a SYSTEM "file:///etc/passwd">]>'+ $xml) -ForImport } 'DTD'
Assert-Throws { ConvertFrom-WelaAppLockerXml -Xml ($xml.Replace('</RuleCollection>', '<RuleCollectionExtensions /></RuleCollection>')) -ForImport } 'extensions'
Assert-Throws { ConvertFrom-WelaAppLockerXml -Xml ($xml.Replace('12345678-1234-1234-1234-123456789abc','not-a-guid')) -ForImport } 'IDs'
$implicit=ConvertFrom-WelaAppLockerXml -Xml ($xml.Replace('AuditOnly','NotConfigured'))
Assert ($implicit.HasEnforcement) 'NotConfigured with rules may enforce; never call it disabled.'
$parsedPlaceholders=ConvertFrom-WelaAppLockerXml -Xml $placeholders
Assert ($parsedPlaceholders.Collections.Count -eq 5 -and $parsedPlaceholders.EmptyPlaceholderCount -eq 5) 'Raw placeholder collections remain in assessment XML/metadata while all five are recognized as empty.'
Assert ($parsedPlaceholders.Xml -match 'NotConfigured' -and -not $parsedPlaceholders.HasEnforcement) 'Normalization never deletes the original policy evidence or fabricates enforcement.'
$commented=ConvertFrom-WelaAppLockerXml -Xml '<AppLockerPolicy Version="1"><RuleCollection Type="Exe" EnforcementMode="NotConfigured"> <!-- native comment --> </RuleCollection></AppLockerPolicy>'
Assert ($commented.EmptyPlaceholderCount -eq 1) 'Whitespace and comments do not turn an otherwise empty collection into policy content.'
Assert-Throws { ConvertFrom-WelaAppLockerXml -Xml $placeholders -ForImport } 'AuditOnly'
function Reset-Fixture {
$script:localXml='<AppLockerPolicy Version="1" />'; $script:effectiveXml=$script:localXml
$script:serviceState='Running'; $script:serviceMode='Auto'; $script:channelEnabled=$true
$script:domain=$false; $script:managed=@(); $script:unknownPolicy=$false; $script:writes=0; $script:readCount=0
$script:race=$false; $script:reject=$false; $script:drift=$false; $script:tamper=$false; $script:validations=0
$script:withPlaceholders=$false
}
function Get-WelaAppLockerHost { [pscustomobject]@{Status='Candidate'; Is64BitProcess=$true; PartOfDomain=$script:domain} }
function Get-WelaAppLockerManagement { [pscustomobject]@{Status='Observed'; ManagementEntries=$script:managed; CspPolicyState='Unknown'} }
function Get-WelaAppLockerService { [pscustomobject]@{Status='Observed'; State=$script:serviceState; StartMode=$script:serviceMode} }
function Get-WelaAppLockerChannels { foreach ($name in @('EXE and DLL','MSI and Script','Packaged app-Execution','Packaged app-Deployment')) { [pscustomobject]@{Channel="Microsoft-Windows-AppLocker/$name"; Status='Observed'; Enabled=$script:channelEnabled} } }
function Get-WelaAppLockerPolicySnapshot {
param($Scope)
if ($Scope -eq 'Local') {
$script:readCount++
if ($script:race -and $script:readCount -eq 2) { $script:localXml=$xml.Replace('AuditOnly','Enabled') }
if ($script:drift -and $script:readCount -ge 5) { $script:localXml='<AppLockerPolicy Version="1" />' }
}
if ($script:unknownPolicy) { return [pscustomobject]@{Status='Unknown'; Policy=$null} }
$value=if ($Scope -eq 'Local') {$script:localXml} else {$script:effectiveXml}
[pscustomobject]@{Status='Observed'; Policy=(ConvertFrom-WelaAppLockerXml -Xml $value)}
}
$script:originalImportFile = ${function:New-WelaAppLockerImportReadLock}
function New-WelaAppLockerImportReadLock {
param($Path,$Xml)
if (-not $script:tamper) { return & $script:originalImportFile -Path $Path -Xml $Xml }
# Simulate a file replaced before the read lock, without races or native policy calls.
[IO.File]::WriteAllText($Path, $Xml.Replace('AuditOnly', 'Enabled').Replace('<Conditions>', '<Conditions> '), (New-Object Text.UTF8Encoding($false)))
return [IO.File]::Open($Path, [IO.FileMode]::Open, [IO.FileAccess]::Read, [IO.FileShare]::Read)
}
function Test-AppLockerPolicy { [CmdletBinding()]param($XmlPolicy,$Path,$User) $script:validations++; [pscustomobject]@{PolicyDecision='Allowed'} }
function Set-AppLockerPolicy {
[CmdletBinding()]param($XmlPolicy,[switch]$Merge)
if (-not $Merge) { throw 'Import must never replace a policy.' }
$script:writes++
if ($script:reject) { throw 'native rejected policy' }
$script:localXml=[IO.File]::ReadAllText($XmlPolicy); $script:effectiveXml=$script:localXml
if ($script:withPlaceholders) { $script:localXml=$readbackPlaceholders; $script:effectiveXml=$script:localXml }
}
Reset-Fixture
$empty=Get-WelaAppLockerReadiness
Assert (@($empty.Collections | Where-Object PrerequisiteState -ne MissingGpPolicy).Count -eq 0) 'Enabled channels without rules must retain a missing GP policy prerequisite.'
Assert ($empty.CspPolicyState -eq 'Unknown' -and $empty.UsableRuleCredit -eq 0) 'GP readback never establishes CSP or detection readiness.'
$script:localXml=$xml; $script:effectiveXml=$xml
$ready=Get-WelaAppLockerReadiness
Assert ($ready.Collections[0].PrerequisiteState -eq 'Conditional' -and $ready.Collections[0].GenerationReadiness -eq 'Unverified') 'Audit policy plus service/channel is only conditional.'
$script:serviceState='Stopped'; $script:serviceMode='Disabled'
Assert ((Get-WelaAppLockerReadiness).Collections[0].PrerequisiteState -eq 'ServiceNotRunning') 'Disabled service must be explicit.'
$script:serviceState='Running';$script:serviceMode='Auto';$script:channelEnabled=$false
Assert ((Get-WelaAppLockerReadiness).Collections[0].PrerequisiteState -eq 'ChannelDisabled') 'Disabled channel must be explicit.'
Reset-Fixture; $script:effectiveXml=$xml.Replace('AuditOnly','Enabled')
Assert-Throws { Assert-WelaAppLockerImportSafe (Get-WelaAppLockerReadiness) $desired } 'enforcement'
Reset-Fixture; $script:localXml=$xml.Replace('AuditOnly','NotConfigured')
Assert-Throws { Assert-WelaAppLockerImportSafe (Get-WelaAppLockerReadiness) $desired } 'enforcement'
Reset-Fixture; $script:domain=$true
Assert-Throws { Assert-WelaAppLockerImportSafe (Get-WelaAppLockerReadiness) $desired } 'domain-joined'
Reset-Fixture; $script:managed=@('MDM provider')
Assert-Throws { Assert-WelaAppLockerImportSafe (Get-WelaAppLockerReadiness) $desired } 'managed'
Reset-Fixture; $script:unknownPolicy=$true
Assert-Throws { Assert-WelaAppLockerImportSafe (Get-WelaAppLockerReadiness) $desired } 'readable'
Reset-Fixture; $script:localXml=$placeholders; $script:effectiveXml=$placeholders
Assert-Throws { Assert-WelaAppLockerImportSafe (Get-WelaAppLockerReadiness) $desired } 'NotConfigured.*merge may retain'
Assert (-not (Test-WelaAppLockerPolicyMatch (Get-WelaAppLockerReadiness) $desired)) 'Empty placeholders do not satisfy a requested policy with rules.'
Reset-Fixture; $script:localXml=$unusedPlaceholders; $script:effectiveXml=$unusedPlaceholders
Assert-WelaAppLockerImportSafe (Get-WelaAppLockerReadiness) $desired
Assert (-not (Test-WelaAppLockerPolicyMatch (Get-WelaAppLockerReadiness) $desired)) 'Untargeted empty placeholders permit initialization without pretending that requested rules already exist.'
foreach ($scope in @('Local','Effective')) {
Reset-Fixture
if ($scope -eq 'Local') { $script:localXml=$placeholders } else { $script:effectiveXml=$placeholders }
Assert-Throws { Assert-WelaAppLockerImportSafe (Get-WelaAppLockerReadiness) $desired } 'NotConfigured.*merge may retain'
}
Reset-Fixture; $script:localXml=$readbackPlaceholders; $script:effectiveXml=$readbackPlaceholders
Assert (Test-WelaAppLockerPolicyMatch (Get-WelaAppLockerReadiness) $desired) 'One imported collection plus four empty placeholders matches the requested one-collection policy.'
Assert-WelaAppLockerImportSafe (Get-WelaAppLockerReadiness) $desired
# A RuleCount == 0 filter would incorrectly ignore each of these. Test both the
# initial local/effective guards and the post-import comparison against real XML.
$nonPlaceholders=@(
'<RuleCollection Type="Dll" EnforcementMode="Enabled" />',
'<RuleCollection Type="Dll" EnforcementMode="AuditOnly" />',
'<RuleCollection Type="Dll" EnforcementMode="NotConfigured"><RuleCollectionExtensions /></RuleCollection>',
'<RuleCollection Type="Dll" EnforcementMode="NotConfigured"><FutureRule /></RuleCollection>',
'<RuleCollection Type="Dll" EnforcementMode="NotConfigured" Future="" />',
'<RuleCollection Type="Dll" EnforcementMode="NotConfigured" Description="" />',
'<RuleCollection Type="Dll" EnforcementMode="NotConfigured">unknown content</RuleCollection>',
'<RuleCollection Type="Dll" EnforcementMode="NotConfigured"><![CDATA[unknown content]]></RuleCollection>',
'<RuleCollection Type="Dll" EnforcementMode="NotConfigured"><?future data?></RuleCollection>',
'<RuleCollection xmlns="urn:unknown" Type="Dll" EnforcementMode="NotConfigured" />',
'<RuleCollection xmlns:x="urn:unknown" Type="Dll" EnforcementMode="NotConfigured" x:Future="" />',
($desired.Collections[0].Xml.Replace('Type="Exe"','Type="Dll"').Replace('AuditOnly','NotConfigured'))
)
foreach ($node in $nonPlaceholders) {
$policyXml='<AppLockerPolicy Version="1">' + $node + '</AppLockerPolicy>'
$parsed=ConvertFrom-WelaAppLockerXml -Xml $policyXml
Assert ($parsed.EmptyPlaceholderCount -eq 0 -and -not $parsed.Collections[0].IsEmptyPlaceholder) 'Configured/unknown collection content is never normalized away.'
foreach ($scope in @('Local','Effective')) {
Reset-Fixture
if ($scope -eq 'Local') { $script:localXml=$policyXml } else { $script:effectiveXml=$policyXml }
Assert-Throws { Assert-WelaAppLockerImportSafe (Get-WelaAppLockerReadiness) $desired } 'preserved'
}
Reset-Fixture; $script:localXml=$xml.Replace('</AppLockerPolicy>', $node + '</AppLockerPolicy>')
Assert (-not (Test-WelaAppLockerPolicyMatch (Get-WelaAppLockerReadiness) $desired)) 'Unexpected configured/unknown collection fails readback even when the requested Exe rule matches.'
}
foreach ($policyXml in @($placeholders.Replace('Version="1"','Version="1" Future=""'), $placeholders.Replace('</AppLockerPolicy>','unknown content</AppLockerPolicy>'))) {
Reset-Fixture; $script:localXml=$policyXml
Assert-Throws { Assert-WelaAppLockerImportSafe (Get-WelaAppLockerReadiness) $desired } 'Unknown policy'
}
Assert-Throws { ConvertFrom-WelaAppLockerXml -Xml ($xml.Replace('Version="1"','Version="1" Future=""')) -ForImport } 'Unknown policy'
$cleanup=@()
try {
foreach ($scenario in @('apply','dry','race','failure','drift','existing','tamper','placeholders','placeholder-dry','placeholder-drift','target-placeholder')) {
Reset-Fixture
if ($scenario -like 'placeholder*') { $script:localXml=$unusedPlaceholders; $script:effectiveXml=$unusedPlaceholders; $script:withPlaceholders=$true }
if ($scenario -eq 'target-placeholder') { $script:localXml=$placeholders; $script:effectiveXml=$placeholders }
if ($scenario -eq 'race') {$script:race=$true}
if ($scenario -eq 'tamper') {$script:tamper=$true}
if ($scenario -eq 'failure') {$script:reject=$true}
if ($scenario -eq 'drift') {$script:drift=$true}
if ($scenario -eq 'existing') {$script:localXml=$xml;$script:effectiveXml=$xml}
$path=Join-Path ([IO.Path]::GetTempPath()) ('wela-applocker-'+[guid]::NewGuid().ToString('N'));$cleanup+=$path
$context=New-WelaConfigurationContext -Auto -DryRun:($scenario -in @('dry','placeholder-dry')) -BackupPath $path
Set-WelaAppLockerAuditPolicy -Context $context -Desired $desired
if ($scenario -eq 'placeholder-drift') { $script:localXml=$readbackPlaceholders.Replace('Type="Dll" EnforcementMode="NotConfigured"','Type="Dll" EnforcementMode="AuditOnly"') }
$result=Complete-WelaConfiguration -Context $context
switch ($scenario) {
'apply' {
Assert ($script:writes -eq 1 -and $result.ExitCode -eq 0) 'Verified initial audit-only merge should pass.'
Assert (Test-Path (Join-Path $path 'before.jsonl')) 'Recovery journal must precede merge.'
Set-WelaAppLockerAuditPolicy -Context $context -Desired $desired
Assert ($script:writes -eq 1 -and $context.Results[1].Status -eq 'AlreadyCompliant') 'Reapplying same policy should not write.'
}
'dry' { Assert ($script:writes -eq 0 -and -not (Test-Path $path)) 'Dry-run must not write policy or recovery files.' }
'tamper' { Assert ($script:writes -eq 0 -and $script:validations -eq 0 -and $result.ExitCode -eq 1 -and $result.Results[0].Diagnostic -match 'changed before its read lock') 'Altered prepared XML must fail before native validation or import, including equal-length mode tampering.' }
'race' { Assert ($script:writes -eq 0 -and $result.ExitCode -eq 1) 'Concurrent enforcement must block merge.' }
'failure' { Assert ($result.ExitCode -eq 1) 'Native write failure must propagate.' }
'drift' { Assert ($result.ExitCode -eq 1) 'Final readback must detect policy drift.' }
'existing' { Assert ($script:writes -eq 0 -and $result.ExitCode -eq 0) 'Identical policy stays unchanged.' }
'placeholders' {
Assert ($script:writes -eq 1 -and $result.ExitCode -eq 0 -and $result.Results[0].Status -eq 'Applied') 'Public runner imports from empty placeholders and verifies populated readback with remaining placeholders.'
$journal=Get-Content (Join-Path $path 'before.jsonl') | ConvertFrom-Json
Assert ($journal.Before.LocalPolicy.Policy.Collections.Count -eq 4 -and $journal.Before.LocalPolicy.Policy.EmptyPlaceholderCount -eq 4) 'Recovery journal preserves all original unused placeholder collection metadata.'
$export=$result | ConvertTo-Json -Depth 20 | ConvertFrom-Json
Assert ($export.Results[0].After.LocalPolicy.Policy.Collections.Count -eq 5 -and $export.Results[0].After.LocalPolicy.Policy.EmptyPlaceholderCount -eq 4) 'Result JSON distinguishes the configured collection from four retained placeholders.'
Set-WelaAppLockerAuditPolicy -Context $context -Desired $desired
Assert ($script:writes -eq 1 -and $context.Results[1].Status -eq 'AlreadyCompliant') 'Repeated import with placeholders performs no duplicate merge.'
}
'placeholder-dry' { Assert ($script:writes -eq 0 -and -not (Test-Path $path) -and $result.Results[0].Status -eq 'Skipped') 'Placeholder normalization does not weaken dry-run guarantees.' }
'placeholder-drift' { Assert ($script:writes -eq 1 -and $result.ExitCode -eq 1 -and $result.Results[0].Status -in @('Failed','Overridden')) 'Final drift from an empty placeholder into a configured empty collection remains a failure.' }
'target-placeholder' { Assert ($script:writes -eq 0 -and $script:validations -eq 0 -and $result.ExitCode -eq 1 -and $result.Results[0].Diagnostic -match 'merge may retain NotConfigured') 'A targeted empty NotConfigured collection blocks before native import to avoid accidental enforcement.' }
}
}
$path=Join-Path ([IO.Path]::GetTempPath()) ('wela-applocker-existing-'+[guid]::NewGuid().ToString('N')+'.xml');$cleanup+=$path
[IO.File]::WriteAllText($path,'Existing unrelated file')
$rejected=$false
try { $stream=& $script:originalImportFile -Path $path -Xml $xml; $stream.Dispose() } catch { $rejected=$true }
Assert ($rejected -and [IO.File]::ReadAllText($path) -eq 'Existing unrelated file') 'Prepared import creation cannot overwrite a pre-existing file/link.'
# Execute only actual top-level option guards; no command dispatcher/mutator.
$tokens=$null;$parseErrors=$null
$ast=[System.Management.Automation.Language.Parser]::ParseFile((Join-Path $PSScriptRoot '../WELA.ps1'),[ref]$tokens,[ref]$parseErrors)
Assert ($parseErrors.Count -eq 0) 'CLI option guards parse.'
$guard=$ast.EndBlock.Statements | Where-Object { $_ -is [System.Management.Automation.Language.IfStatementAst] -and $_.Extent.Text.StartsWith("if ((`$PSBoundParameters.ContainsKey('AppLockerAction')") } | Select-Object -First 1
Assert ($null -ne $guard) 'Explicit AppLocker options must be guarded before dispatch.'
$exercise=[scriptblock]::Create('param($AppLockerAction,$AppLockerPolicyPath,$Cmd)' + [Environment]::NewLine + $guard.Extent.Text)
Assert-Throws { & $exercise -AppLockerAction Plan -Cmd configure } 'require applocker-readiness'
Assert-Throws { & $exercise -AppLockerPolicyPath 'operator.xml' -Cmd configure-sacl } 'require applocker-readiness'
& $exercise -AppLockerAction Plan -Cmd applocker-readiness
$guard=$ast.EndBlock.Statements | Where-Object { $_ -is [System.Management.Automation.Language.IfStatementAst] -and $_.Extent.Text.StartsWith("if (`$Cmd -eq 'applocker-readiness' -and (`$Profile") } | Select-Object -First 1
$Cmd='applocker-readiness';$Profile='wela-2.2.0';$Baseline=$null
Assert-Throws { & ([scriptblock]::Create($guard.Extent.Text)) } 'not -Profile or -Baseline'
} finally { foreach ($path in $cleanup) { Remove-Item -LiteralPath $path -Recurse -Force -ErrorAction SilentlyContinue } }
Write-Host "PASS: $count AppLocker readiness/import assertions; no Windows policies changed."
@@ -0,0 +1,37 @@
$ErrorActionPreference='Stop'
if ([Environment]::OSVersion.Platform -ne [PlatformID]::Win32NT) { throw 'Windows required.' }
. (Join-Path $PSScriptRoot '../scripts/AppLockerReadiness.ps1')
$report=Get-WelaAppLockerReadiness
if ($report.Collections.Count -ne 5 -or $report.CspPolicyState -ne 'Unknown' -or $report.UsableRuleCredit -ne 0) { throw 'Native report lost collection/CSP uncertainty.' }
if ($report.Host.Status -eq 'Unknown') { throw ($report.Host | ConvertTo-Json) }
foreach ($scope in @($report.LocalPolicy,$report.EffectiveGpPolicy)) {
if ($scope.Status -eq 'Observed' -and -not $scope.Policy.Xml) { throw 'Observed policy must retain XML evidence.' }
if ($scope.Status -ne 'Observed') { Write-Host "Policy read limitation: $($scope.Status) $($scope.Diagnostic)" }
}
# The native cmdlet parses the XML without installing it or executing the file.
if (Get-Command Test-AppLockerPolicy -ErrorAction SilentlyContinue) {
$path=Join-Path $env:TEMP ('wela-applocker-schema-'+[guid]::NewGuid().ToString('N')+'.xml')
$placeholderPath=$path.Replace('.xml','-placeholders.xml')
try {
$xml='<AppLockerPolicy Version="1"><RuleCollection Type="Exe" EnforcementMode="AuditOnly"><FilePathRule Id="12345678-1234-1234-1234-123456789abc" Name="Read-only test" Description="" UserOrGroupSid="S-1-1-0" Action="Allow"><Conditions><FilePathCondition Path="%WINDIR%\*" /></Conditions></FilePathRule></RuleCollection></AppLockerPolicy>'
$policy=ConvertFrom-WelaAppLockerXml -Xml $xml -ForImport
$lock=New-WelaAppLockerImportReadLock -Path $path -Xml $policy.Xml
try {
$validation=@(Test-AppLockerPolicy -XmlPolicy $path -Path "$env:SystemRoot\System32\cmd.exe" -User 'S-1-1-0' -ErrorAction Stop)
if (-not $validation.Count) { throw 'Native schema validation returned no decision.' }
} finally { $lock.Dispose() }
# In-memory native-readback representation: the one Exe collection plus
# empty NotConfigured shells for other types. Validate through the native
# reader only; this does not install or merge any policy.
$shells=(@('Dll','Msi','Script','Appx') | ForEach-Object { '<RuleCollection Type="' + $_ + '" EnforcementMode="NotConfigured" />' }) -join ''
$withPlaceholders=ConvertFrom-WelaAppLockerXml -Xml $policy.Xml.Replace('</AppLockerPolicy>',$shells+'</AppLockerPolicy>')
$snapshot=[pscustomobject]@{LocalPolicy=[pscustomobject]@{Status='Observed';Policy=$withPlaceholders}}
if (-not (Test-WelaAppLockerPolicyMatch $snapshot $policy) -or $withPlaceholders.EmptyPlaceholderCount -ne 4) { throw 'Placeholder readback representation did not match the requested collection.' }
$lock=New-WelaAppLockerImportReadLock -Path $placeholderPath -Xml $withPlaceholders.Xml
try {
$withShells=@(Test-AppLockerPolicy -XmlPolicy $placeholderPath -Path "$env:SystemRoot\System32\cmd.exe" -User 'S-1-1-0' -ErrorAction Stop)
if ($withShells.Count -ne $validation.Count -or (($withShells.PolicyDecision -join ',') -cne ($validation.PolicyDecision -join ','))) { throw 'Native XML reader changed its decision with empty NotConfigured placeholders.' }
} finally { $lock.Dispose() }
} finally { Remove-Item -LiteralPath $path,$placeholderPath -Force -ErrorAction SilentlyContinue }
} else { Write-Host 'Native policy validation unavailable in this PowerShell session; importer will refuse.' }
Write-Host 'PASS: native read-only AppLocker observations. No Set-AppLockerPolicy or service changes.'
+159
View File
@@ -0,0 +1,159 @@
# Safe fixtures through the public command/report and shared runner. No Windows writes.
$ErrorActionPreference = 'Stop'
$repo = Split-Path $PSScriptRoot -Parent
Import-Module (Join-Path $repo 'modules/EventLogSettings.psm1') -Force
Import-Module (Join-Path $repo 'modules/NativeProviders.psm1') -Force
Import-Module (Join-Path $repo 'modules/NativeChannelAccess.psm1') -Force
. (Join-Path $repo 'scripts/Configuration.ps1')
. (Join-Path $repo 'scripts/NativeChannelConfiguration.ps1')
$script:ScriptRoot = $repo
$script:assertions = 0
$script:cleanup = New-Object 'System.Collections.Generic.List[string]'
function Assert($Condition, [string]$Message) {
if (-not $Condition) { throw "FAIL: $Message" }; $script:assertions++
}
function New-FixtureState([string]$Name) {
[pscustomobject]@{ Name = $Name; State = 'Disabled'; IsEnabled = $false; LogMode = 'Retain'; SecurityDescriptor = 'fixture-original'; MaximumSizeInBytes = [long]1048576; MetadataErrors = @{}; Error = $null }
}
function Reset-Fixture {
$script:profile = Get-WelaNativeChannelProfile
$global:WelaChannelFixture = @{ States = @{}; Reads = @{}; Writes = (New-Object 'System.Collections.Generic.List[object]'); DriftRead = 0; Failure = ''; Prompt = 'Y' }
foreach ($control in $script:profile.controls) { $global:WelaChannelFixture.States[$control.channel] = New-FixtureState $control.channel }
$script:capi = $script:profile.controls[0].channel
$script:app = $script:profile.controls[1].channel
$script:driver = $script:profile.controls[2].channel
$script:backup = Join-Path ([IO.Path]::GetTempPath()) ('wela-channel-' + [guid]::NewGuid().ToString('N'))
$global:WelaChannelFixture.Backup = $script:backup
$script:cleanup.Add($script:backup)
}
function Get-WelaNativeChannel {
param($Name)
$f = $global:WelaChannelFixture
if (-not $f.States.ContainsKey($Name)) { return New-FixtureState $Name }
if (-not $f.Reads.ContainsKey($Name)) { $f.Reads[$Name] = 0 }
$f.Reads[$Name]++
if ($f.DriftRead -eq $f.Reads[$Name] -and $Name -eq $script:capi) { $f.States[$Name].SecurityDescriptor = 'fixture-concurrent' }
return $f.States[$Name].PSObject.Copy()
}
# Windows ACL serialization is tested separately against the real .NET APIs. These
# token descriptors let the command/runner fail-path tests execute safely on Linux.
function Test-WelaChannelDescriptorEqual { param($First, $Second) return $First -and $Second -and $First -ceq $Second }
function Get-WelaChannelAccessPlan {
param($SecurityDescriptor)
[pscustomobject]@{
State = $(if ($SecurityDescriptor -eq 'fixture-granted') { 'GrantPresent' } elseif ($SecurityDescriptor -eq 'fixture-original') { 'GrantRequired' } else { 'ManualReview' })
ProposedDescriptor = 'fixture-granted'; EffectiveReadAccess = 'Not tested'; Diagnostic = 'Fixture ACL planner'
}
}
function Read-Host { param($Prompt) return $global:WelaChannelFixture.Prompt }
function Invoke-WelaNative {
param($FilePath, $Arguments)
$f = $global:WelaChannelFixture
Assert ($FilePath -eq 'wevtutil.exe' -and $Arguments[0] -eq 'sl') 'Only wevtutil channel settings are written'
$journal = Join-Path $f.Backup 'before.jsonl'
Assert (Test-Path -LiteralPath $journal) 'Recovery journal exists before native write'
$record = @(Get-Content -LiteralPath $journal | ForEach-Object { $_ | ConvertFrom-Json })[-1]
Assert ($record.Target.Channel -eq $Arguments[1] -and $record.Before.SecurityDescriptor -eq $f.States[$Arguments[1]].SecurityDescriptor) 'Journal holds the fresh original descriptor for this channel'
$f.Writes.Add(@($Arguments))
if ($f.Failure -eq 'native') { throw 'fixture native failure' }
if ($f.Failure -eq 'false-success') { return }
foreach ($argument in $Arguments) {
if ($argument -eq '/e:true') { $f.States[$Arguments[1]].IsEnabled = $true; $f.States[$Arguments[1]].State = 'Enabled' }
if ($argument -like '/ms:*') { $f.States[$Arguments[1]].MaximumSizeInBytes = [long]$argument.Substring(4) }
if ($argument -like '/ca:*') { $f.States[$Arguments[1]].SecurityDescriptor = $argument.Substring(4) }
}
}
$module = Get-Module NativeChannelAccess
& $module {
function script:Get-WelaNativeChannel {
param($Name)
if ($global:WelaChannelFixture.States.ContainsKey($Name)) { return $global:WelaChannelFixture.States[$Name].PSObject.Copy() }
[pscustomobject]@{ Name = $Name; State = 'Not installed'; IsEnabled = $null; LogMode = $null; SecurityDescriptor = $null; MaximumSizeInBytes = $null; MetadataErrors = @{}; Error = @{ Message = 'fixture missing registration' } }
}
}
$savedOS = $env:OS
try {
$env:OS = 'Windows_NT' # Only mocked readers/setters are reachable in this suite.
Reset-Fixture
Assert ($script:profile.controls.Count -eq 3) 'Profile declares exactly the three Appendix C channel examples'
Assert ($script:profile.controls[0].sourceExampleBytes -eq 102432768 -and $script:profile.controls[1].sourceExampleBytes -eq 102432768) 'CAPI2/AppLocker preserve the exact source byte values'
Assert ($script:profile.controls[2].sourceExampleBytes -eq 52432896) 'DriverFrameworks source is not approximated as 50 MiB'
Assert ((ConvertTo-WelaEventLogBytes 52432896) -eq 52494336) 'Applied minimum rounds upward to Windows 64 KiB units'
$caught = $false; try { Get-WelaNativeChannelProfile -Id 'unknown' } catch { $caught = $true }
Assert $caught 'Unknown channel profile is rejected'
$out = $script:backup + '.json'; $script:cleanup.Add($out)
$report = Invoke-WelaNativeChannelCommand -Action Plan -GrantEventLogReaders -ResultsPath $out
$json = Get-Content -LiteralPath $out -Raw | ConvertFrom-Json
Assert ($json.Controls[0].Desired.AccessChangeRequested -and $json.Controls[0].Desired.SecurityDescriptor -eq 'fixture-granted') 'Public JSON contains explicit proposed CAPI2 ACL'
Assert ($json.QueryInventory.Count -eq 18 -and $json.ExcludedQueries.Count -eq 2) 'Both queries inventory 18 unique native channels and exclude EMET/Sysmon'
Assert (@($json.QueryInventory | Where-Object { $_.Channel.Name -like '*Sysmon*' }).Count -eq 0) 'Sysmon is outside native inventory'
Assert (($json.QueryInventory | Where-Object { $_.Channel.Name -eq 'Microsoft-Windows-CAPI2/Operational' }).Queries[0].QueryIds[0] -eq '2') 'Inventory preserves source query IDs'
Assert ($json.ForwardingReadiness -eq 'Not verified' -and @($json.QueryInventory | Where-Object EffectiveReadAccess -ne 'Not tested').Count -eq 0) 'Public export does not infer identity access or forwarding from ACEs'
Assert (($json.QueryInventory | Where-Object { $_.Channel.Name -eq 'Security' }).Channel.State -eq 'Not installed') 'Inventory retains absent channel evidence'
Assert ($global:WelaChannelFixture.Writes.Count -eq 0 -and -not (Test-Path $script:backup)) 'Plan performs no mutation or journal creation'
$report = Invoke-WelaNativeChannelCommand -Action Audit -QuerySet Baseline
Assert ($report.QueryInventory.Count -eq 12) 'Baseline query selection inventories its twelve native channels'
$report = Invoke-WelaNativeChannelCommand -Action Audit -QuerySet Suspect
Assert ($report.QueryInventory.Count -eq 8 -and $report.ExcludedQueries.Count -eq 0) 'Suspect selection remains distinct'
Reset-Fixture
$report = Invoke-WelaNativeChannelCommand -Action Configure -GrantEventLogReaders -Auto -BackupPath $script:backup
Assert ($report.ExitCode -eq 0 -and $report.Scope -eq 'native-channel-settings-only') 'Configure succeeds only for requested channel settings'
Assert ($global:WelaChannelFixture.Writes.Count -eq 3) 'Configure changes only three declared channels'
Assert ($global:WelaChannelFixture.States[$script:capi].IsEnabled -and $global:WelaChannelFixture.States[$script:capi].SecurityDescriptor -eq 'fixture-granted') 'CAPI2 enablement and ACL are read back'
Assert (-not $global:WelaChannelFixture.States[$script:app].IsEnabled -and $global:WelaChannelFixture.States[$script:app].SecurityDescriptor -eq 'fixture-original') 'AppLocker size control preserves disabled state and ACL'
Assert ($global:WelaChannelFixture.States[$script:driver].MaximumSizeInBytes -eq 52494336) 'DriverFrameworks applied size matches rounded source bytes'
Assert (@($global:WelaChannelFixture.Writes | Where-Object { ($_ -join ' ') -match '/[ar][bt]:' }).Count -eq 0) 'No retention settings are modified'
Assert ($report.Controls[0].Access.State -eq 'GrantPresent' -and $report.Controls[0].Access.EffectiveReadAccess -eq 'Not tested') 'Structural readback never becomes an effective-access claim'
$json = @(Get-Content (Join-Path $script:backup 'before.jsonl') | ForEach-Object { $_ | ConvertFrom-Json })
Assert ($json[0].Before.MaximumSizeInBytes -eq 1048576 -and $json[0].Before.SecurityDescriptor -eq 'fixture-original' -and $json[0].Before.LogMode -eq 'Retain') 'Journal includes original bytes, full descriptor and retention mode'
Reset-Fixture
$global:WelaChannelFixture.States[$script:capi].MaximumSizeInBytes = [long]4294967296
$report = Invoke-WelaNativeChannelCommand -Action Configure -Auto -BackupPath $script:backup
Assert ($global:WelaChannelFixture.States[$script:capi].MaximumSizeInBytes -eq 4294967296) 'Existing larger buffer is preserved'
Assert (@($global:WelaChannelFixture.Writes | Where-Object { ($_ -join ' ') -like '*/ca:*' }).Count -eq 0) 'No ACL change without separate opt-in'
Assert ($report.Controls[0].Access.State -eq 'GrantRequired' -and $report.Controls[0].Prerequisites.Count -ge 2) 'Omitted ACL opt-in remains an unmet profile prerequisite'
Reset-Fixture
$report = Invoke-WelaNativeChannelCommand -Action Configure -GrantEventLogReaders -DryRun -BackupPath $script:backup
Assert ($report.DryRun -and $report.Skipped -eq 3 -and $global:WelaChannelFixture.Writes.Count -eq 0 -and -not (Test-Path $script:backup)) 'Dry run does no native writes and creates no backup directory'
Reset-Fixture
$global:WelaChannelFixture.Prompt = 'n'
$report = Invoke-WelaNativeChannelCommand -Action Configure -GrantEventLogReaders -BackupPath $script:backup
Assert ($report.Skipped -eq 3 -and $global:WelaChannelFixture.Writes.Count -eq 0) 'Declining prompts preserves every channel'
foreach ($driftRead in @(2, 3, 5)) {
Reset-Fixture; $global:WelaChannelFixture.DriftRead = $driftRead
$report = Invoke-WelaNativeChannelCommand -Action Configure -GrantEventLogReaders -Auto -QuerySet Baseline -BackupPath $script:backup
Assert ($report.ExitCode -eq 1) "Drift at observation $driftRead cannot report success"
$writes = @($global:WelaChannelFixture.Writes | Where-Object { $_[1] -eq $script:capi })
Assert ($writes.Count -eq $(if ($driftRead -eq 5) { 1 } else { 0 })) "Plan-to-initial/prewrite drift rejects stale ACL; final drift is detected ($driftRead)"
}
foreach ($failure in @('native', 'false-success', 'denied', 'missing', 'acl')) {
Reset-Fixture; $global:WelaChannelFixture.Failure = $failure
if ($failure -eq 'denied') { $global:WelaChannelFixture.States[$script:capi].State = 'Unknown'; $global:WelaChannelFixture.States[$script:capi].SecurityDescriptor = $null }
if ($failure -eq 'missing') { $global:WelaChannelFixture.States[$script:capi].State = 'Not installed' }
if ($failure -eq 'acl') { $global:WelaChannelFixture.States[$script:capi].SecurityDescriptor = 'fixture-deny' }
$report = Invoke-WelaNativeChannelCommand -Action Configure -GrantEventLogReaders -Auto -BackupPath $script:backup
Assert ($report.ExitCode -eq 1 -and $report.Results[0].Status -eq 'Failed') "Failure $failure remains explicit and nonzero"
if ($failure -in @('denied', 'missing', 'acl')) { Assert (@($global:WelaChannelFixture.Writes | Where-Object { $_[1] -eq $script:capi }).Count -eq 0) "$failure never writes CAPI2" }
}
Reset-Fixture
$plan = @(Get-WelaNativeChannelPlan -Profile $script:profile -GrantEventLogReaders)
$context = New-WelaConfigurationContext -Auto -BackupPath $script:backup
New-Item -ItemType Directory -Path (Join-Path $script:backup 'before.jsonl') | Out-Null
Set-WelaNativeChannelControls -Context $context -Plan $plan -Profile $script:profile.id
Assert ($global:WelaChannelFixture.Writes.Count -eq 0 -and $context.Results[0].Status -eq 'Failed') 'Journal failure blocks all writes'
$caught = $false; try { Invoke-WelaNativeChannelCommand -Action Audit -DryRun } catch { $caught = $true }
Assert $caught 'Unsupported dry-run action is rejected before reads/writes'
Reset-Fixture
$report = Invoke-WelaNativeChannelCommand -Action Plan -ResultsPath (Join-Path $script:backup 'missing/results.json')
Assert ($report.ExitCode -eq 1) 'Failed report export has a nonzero result'
Write-Host "PASS: $script:assertions native channel command/runner assertions. No Windows settings were changed."
} finally {
$env:OS = $savedOS
& $module { Remove-Item Function:script:Get-WelaNativeChannel }
Remove-Variable -Name WelaChannelFixture -Scope Global -ErrorAction SilentlyContinue
foreach ($path in $script:cleanup) { if (Test-Path -LiteralPath $path) { Remove-Item -LiteralPath $path -Recurse -Force } }
}
@@ -0,0 +1,86 @@
# Real Windows descriptor API tests and read-only metadata/CLI smoke. No channel writes.
$ErrorActionPreference = 'Stop'
$repo = Split-Path $PSScriptRoot -Parent
Import-Module (Join-Path $repo 'modules/EventLogSettings.psm1') -Force
Import-Module (Join-Path $repo 'modules/NativeProviders.psm1') -Force
Import-Module (Join-Path $repo 'modules/NativeChannelAccess.psm1') -Force
$script:assertions = 0
function Assert($Condition, [string]$Message) {
if (-not $Condition) { throw "FAIL: $Message" }; $script:assertions++
}
function Binary($Value) {
$bytes = New-Object byte[] $Value.BinaryLength
$Value.GetBinaryForm($bytes, 0)
[Convert]::ToBase64String($bytes)
}
function Check-Preservation([string]$Sddl) {
$before = [System.Security.AccessControl.RawSecurityDescriptor]::new($Sddl)
$plan = Get-WelaChannelAccessPlan -SecurityDescriptor $Sddl
Assert ($plan.State -eq 'GrantRequired') "Descriptor supports lossless append: $($plan.Diagnostic)"
$after = [System.Security.AccessControl.RawSecurityDescriptor]::new($plan.ProposedDescriptor)
Assert ($before.Owner -eq $after.Owner -and $before.Group -eq $after.Group) 'Owner/group are retained'
Assert ($before.ControlFlags -eq $after.ControlFlags -and $before.ResourceManagerControl -eq $after.ResourceManagerControl) 'Control flags are retained'
Assert (($null -eq $before.SystemAcl -and $null -eq $after.SystemAcl) -or ((Binary $before.SystemAcl) -ceq (Binary $after.SystemAcl))) 'Complete SACL bytes are retained'
Assert ($after.DiscretionaryAcl.Count -eq $before.DiscretionaryAcl.Count + 1) 'Exactly one DACL ACE is added'
$j = 0
for ($i = 0; $i -lt $after.DiscretionaryAcl.Count; $i++) {
if ($i -eq $plan.AddedAceIndex) {
$ace = $after.DiscretionaryAcl[$i]
Assert ($ace.SecurityIdentifier.Value -eq 'S-1-5-32-573' -and $ace.AccessMask -eq 1 -and $ace.AceFlags -eq 0 -and -not $ace.IsCallback) 'New ACE is precisely unconditional Event Log Readers read (no write/clear)'
} else {
Assert ((Binary $before.DiscretionaryAcl[$j]) -ceq (Binary $after.DiscretionaryAcl[$i])) 'Every preexisting ACE stays byte-identical and in order'
$j++
}
}
Assert ($plan.EffectiveReadAccess -eq 'Not tested') 'Structural grant does not prove effective token access'
$second = Get-WelaChannelAccessPlan -SecurityDescriptor $plan.ProposedDescriptor
Assert ($second.State -eq 'GrantPresent' -and -not $second.ProposedDescriptor) 'Repeated planning does not duplicate the ACE'
Assert (Test-WelaChannelDescriptorEqual $plan.ProposedDescriptor $after.GetSddlForm('All')) 'Binary descriptor comparison handles Windows SDDL formatting'
Assert (-not (Test-WelaChannelDescriptorEqual $Sddl $plan.ProposedDescriptor)) 'Descriptor comparison detects the added ACE'
}
Check-Preservation 'O:BAG:SYD:PAI(A;;0x7;;;BA)(A;;0x2;;;AU)(A;ID;0x1;;;SY)S:AI(AU;SAFA;0x1;;;WD)'
Check-Preservation 'O:BAG:SYD:(OA;;0x2;00112233-4455-6677-8899-aabbccddeeff;;AU)(A;;0x7;;;BA)'
Check-Preservation 'O:BAG:SYD:(A;;0x2;;;S-1-5-32-573)(A;;0x7;;;BA)'
foreach ($sddl in @('O:BAG:SYD:(A;;0x1;;;S-1-5-32-573)', 'O:BAG:SYD:(A;;0x7;;;S-1-5-32-573)')) {
$result = Get-WelaChannelAccessPlan $sddl
Assert ($result.State -eq 'GrantPresent' -and -not $result.ProposedDescriptor -and $result.EffectiveReadAccess -eq 'Not tested') 'Existing read/superset permission is preserved without claiming event access'
}
foreach ($sddl in @('', 'invalid', 'O:BAG:SY', 'O:BAG:SYD:NO_ACCESS_CONTROL', 'O:BAG:SYD:(D;;0x1;;;WD)(A;;0x7;;;BA)', 'O:BAG:SYD:(D;;GR;;;WD)(A;;0x7;;;BA)')) {
$result = Get-WelaChannelAccessPlan $sddl
Assert ($result.State -eq 'ManualReview' -and -not $result.ProposedDescriptor) 'Missing, invalid, null and denied descriptors refuse automatic modification'
}
# The original unknown ACE bytes must never be discarded. SDDL has no representation
# for arbitrary custom ACEs; parsing/planning must refuse instead of replacing them.
$raw = [System.Security.AccessControl.RawSecurityDescriptor]::new('O:BAG:SYD:(A;;0x7;;;BA)')
$raw.DiscretionaryAcl.InsertAce(1, [System.Security.AccessControl.CustomAce]::new(([Enum]::ToObject([System.Security.AccessControl.AceType], 127)), [System.Security.AccessControl.AceFlags]::None, [byte[]]@(0, 0, 0, 0)))
$binaryBefore = Binary $raw
$refused = $false
try {
$sddl = $raw.GetSddlForm('All')
$refused = (Get-WelaChannelAccessPlan $sddl).State -eq 'ManualReview'
} catch { $refused = $true }
Assert ($refused -and (Binary $raw) -ceq $binaryBefore) 'Unsupported unknown ACEs are retained and mutation is refused'
$profile = Get-WelaNativeChannelProfile
$before = @{}
foreach ($control in $profile.controls) { $before[$control.channel] = Get-WelaNativeChannel -Name $control.channel }
# Exercise actual CLI dispatch and JSON export using live Windows read APIs.
$out = Join-Path ([IO.Path]::GetTempPath()) ('wela-native-channel-live-' + [guid]::NewGuid().ToString('N') + '.json')
$shell = (Get-Process -Id $PID).Path
try {
& $shell -NoProfile -File (Join-Path $repo 'WELA.ps1') channel-settings -ChannelAction Plan -GrantEventLogReaders -ResultsPath $out
$cliExit = $LASTEXITCODE
$report = Get-Content -LiteralPath $out -Raw -ErrorAction Stop | ConvertFrom-Json
Assert ($cliExit -eq $report.ExitCode -and $cliExit -in @(0, 1)) 'Read-only CLI exit code agrees with its report (missing/unknown channels may return 1)'
Assert ($report.Action -eq 'Plan' -and $report.QueryInventory.Count -eq 18 -and $report.ForwardingReadiness -eq 'Not verified') 'Real CLI plan exports channel inventory without a forwarding claim'
Assert ($report.ExcludedQueries.Count -eq 2 -and @($report.QueryInventory | Where-Object { $_.Channel.Name -like '*Sysmon*' }).Count -eq 0) 'Live public output excludes non-native queries'
foreach ($control in $profile.controls) {
$first = $before[$control.channel]; $last = Get-WelaNativeChannel -Name $control.channel
Assert ($first.State -eq $last.State -and $first.MaximumSizeInBytes -eq $last.MaximumSizeInBytes -and $first.LogMode -eq $last.LogMode -and $first.SecurityDescriptor -ceq $last.SecurityDescriptor) 'Live plan leaves channel metadata unchanged (or detects concurrent external drift)'
$row = @($report.Controls | Where-Object { $_.Definition.channel -eq $control.channel })[0]
Assert ($row.Before.MaximumSizeInBytes -eq $first.MaximumSizeInBytes -and $row.Before.SecurityDescriptor -ceq $first.SecurityDescriptor) 'Live exported metadata matches the actual native reader'
}
Write-Host "PASS: $script:assertions real Windows ACL and read-only CLI assertions. Identity access, event generation and forwarding were not tested."
$global:LASTEXITCODE = 0 # A reported missing/manual-review channel is valid smoke evidence.
} finally { Remove-Item -LiteralPath $out -Force -ErrorAction SilentlyContinue }
@@ -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 }
}
@@ -0,0 +1,76 @@
# Compile the production privilege lifecycle with in-memory native API substitutes.
# No process token or live WMI namespace is modified by this test.
$ErrorActionPreference = 'Stop'
$repo = Split-Path $PSScriptRoot -Parent
$source = Get-Content -LiteralPath (Join-Path $repo 'scripts/WmiNamespaceAuditing.ps1') -Raw
. (Join-Path $repo 'scripts/WmiNamespaceAuditing.ps1')
$script:assertions = 0
function Assert($Condition, [string]$Message) { if (-not $Condition) { throw "FAIL: $Message" }; $script:assertions++ }
$match = [regex]::Match($source, "(?s)Add-Type -TypeDefinition @'\r?\n(.*?)\r?\n'@ -ErrorAction Stop")
Assert $match.Success 'Production privilege helper located'
$csharp = $match.Groups[1].Value.Replace('namespace Wela {', 'namespace WelaPrivilegeFixture {')
# Replace only external API declarations/error reads, retaining constructor and
# Dispose control flow from the shipped helper rather than mirroring that logic.
$csharp = [regex]::Replace($csharp, '(?m)^ \[DllImport[^\r\n]+\r?\n', '')
$csharp = $csharp.Replace('Marshal.GetLastWin32Error()', 'TestError')
$native = @'
public static int TestError, EnableError, RestoreError, AdjustCalls, CloseCalls;
public static bool RestoreSuccess = true;
public static void Reset() { TestError=EnableError=RestoreError=AdjustCalls=CloseCalls=0; RestoreSuccess=true; }
static IntPtr GetCurrentProcess() { return (IntPtr)1; }
static bool CloseHandle(IntPtr handle) { CloseCalls++; return true; }
static bool OpenProcessToken(IntPtr process, uint access, out IntPtr token) { token=(IntPtr)2; return true; }
static bool LookupPrivilegeValue(string system, string name, out Luid luid) { luid=new Luid(); return true; }
static bool AdjustTokenPrivileges(IntPtr token, bool disable, ref TokenPrivileges current, uint size, out TokenPrivileges previous, out uint required) {
previous=current; previous.Attributes=0; required=16;
AdjustCalls++; TestError=AdjustCalls==1 ? EnableError : RestoreError;
return AdjustCalls==1 || RestoreSuccess;
}
'@
$csharp = $csharp.Replace(' IntPtr token;', $native + "`n IntPtr token;")
Assert ($csharp -notmatch '\[DllImport') 'All token API imports are replaced before compilation'
Add-Type -TypeDefinition $csharp -ErrorAction Stop
$type = [WelaPrivilegeFixture.WmiSecurityPrivilege]
$type::Reset()
$instance = [WelaPrivilegeFixture.WmiSecurityPrivilege]::new()
$instance.Dispose(); $instance.Dispose()
Assert ($type::AdjustCalls -eq 2 -and $type::CloseCalls -eq 1) 'Normal restoration executes once and closes the token once'
foreach ($restoreError in @(1300, 5)) {
$type::Reset(); $type::RestoreError = $restoreError
$instance = [WelaPrivilegeFixture.WmiSecurityPrivilege]::new()
$failed = $false
try { $instance.Dispose() } catch { $failed = $_.Exception.InnerException.NativeErrorCode -eq $restoreError }
Assert $failed 'A true AdjustTokenPrivileges return with nonzero last error is a restoration failure'
Assert ($type::CloseCalls -eq 1) 'Failed privilege restoration still closes the token handle'
}
$type::Reset(); $type::RestoreError = 5; $type::RestoreSuccess = $false
$instance = [WelaPrivilegeFixture.WmiSecurityPrivilege]::new()
$failed = $false; try { $instance.Dispose() } catch { $failed = $true }
Assert ($failed -and $type::CloseCalls -eq 1) 'False API restoration result is reported and handle is closed'
$type::Reset(); $type::EnableError = 1300
$failed = $false; try { [WelaPrivilegeFixture.WmiSecurityPrivilege]::new() } catch { $failed = $true }
Assert ($failed -and $type::AdjustCalls -eq 1 -and $type::CloseCalls -eq 1) 'Unavailable SeSecurityPrivilege refuses the operation and closes its handle'
# Exercise the production PowerShell cleanup paths with a throwing connection.
function Initialize-WelaWmiInterop { }
$script:disposed = 0
$script:privilegeFixture = [pscustomobject]@{}
$script:privilegeFixture | Add-Member ScriptMethod Dispose { $script:disposed++ }
function New-Object {
param([string]$TypeName, [object[]]$ArgumentList)
if ($TypeName -eq 'Wela.WmiSecurityPrivilege') { return $script:privilegeFixture }
throw "Unexpected construction in failure fixture: $TypeName"
}
$script:connectionFixture = [pscustomobject]@{}
$script:connectionFixture | Add-Member ScriptMethod Dispose { throw 'fixture COM cleanup failure' }
function New-WelaWmiConnection { param($Namespace) return $script:connectionFixture }
function Get-WelaWmiNativeDescriptor { param($Connection) throw 'fixture descriptor read failure' }
foreach ($operation in @('Get', 'Set')) {
$before = $script:disposed; $failed = $false
try {
if ($operation -eq 'Get') { Get-WelaWmiNamespaceSnapshot 'root\cimv2' }
else { Set-WelaWmiNamespaceDescriptor 'root\cimv2' '{}' @() }
} catch { $failed = $true }
Assert ($failed -and $script:disposed -eq $before + 1) "$operation restores privilege even when connection cleanup throws"
}
Write-Host "PASS: $script:assertions WMI privilege/cleanup assertions with in-memory APIs only."
+137
View File
@@ -0,0 +1,137 @@
# In-memory descriptors only. All native readers/writers are replaced before control execution.
$ErrorActionPreference = 'Stop'
$repo = Split-Path $PSScriptRoot -Parent
$script:ScriptRoot = $repo
. (Join-Path $repo 'scripts/Configuration.ps1')
. (Join-Path $repo 'scripts/WmiNamespaceAuditing.ps1')
$script:assertions = 0
$root = Join-Path ([IO.Path]::GetTempPath()) ('wela-wmi-' + [guid]::NewGuid().ToString('N'))
$null = New-Item -ItemType Directory -Path $root
$fixture = Get-Content -LiteralPath (Join-Path $PSScriptRoot 'fixtures/wmi-namespace-descriptor.json') -Raw
function Assert($Condition, [string]$Message) { if (-not $Condition) { throw "FAIL: $Message" }; $script:assertions++ }
function Throws([scriptblock]$Action, [string]$Message) { $caught = $false; try { & $Action } catch { $caught = $true }; Assert $caught $Message }
function Reset-Mocks {
$script:descriptor = $fixture | ConvertFrom-Json
$script:writes = 0; $script:reads = 0; $script:readFail = $false; $script:writeCode = 0
$script:race = $false; $script:alterOwner = $false; $script:dropUnknown = $false; $script:ineffective = $false
$script:decline = $false; $script:promptCallback = $null
}
function Get-WelaWmiNamespaceSnapshot {
param($Namespace)
$script:reads++
if ($script:readFail) { throw 'Access denied or namespace missing; no complete SACL available.' }
[pscustomobject]@{ Namespace = $Namespace; DescriptorJson = ConvertTo-WelaWmiJson $script:descriptor; DescriptorMof = 'mock full descriptor'; SaclReadPrivilege = 'mock assigned/enabled' }
}
function Set-WelaWmiNamespaceDescriptor {
param($Namespace, $ExpectedJson, $Definitions)
# Model the production immediate re-read and verify the generic runner journal.
$entry = @(Get-Content -LiteralPath (Join-Path $script:context.BackupPath 'before.jsonl') | ConvertFrom-Json)[-1]
Assert ($entry.Before.DescriptorJson -ceq $ExpectedJson -and $entry.Target.Namespace -eq $Namespace) 'Full recovery descriptor persisted before any setter'
Assert ($entry.Before.DescriptorMof -eq 'mock full descriptor') 'Journal includes native descriptor representation'
if ($script:race) { $script:descriptor.Owner.SIDString = 'S-1-5-18' }
if ((ConvertTo-WelaWmiJson $script:descriptor) -cne $ExpectedJson) { throw 'Namespace descriptor changed after its recovery snapshot; no SACL was written.' }
$script:writes++
Assert-WelaWmiReturnCode ([pscustomobject]@{ ReturnValue = $script:writeCode }) 'SetSecurityDescriptor'
if ($script:ineffective) { return }
foreach ($definition in @(Get-WelaWmiMissingAces $script:descriptor $Definitions)) {
$script:descriptor.SACL += [pscustomobject]@{ AccessMask = $definition.AccessMask; AceFlags = $definition.AceFlags; AceType = 2; Trustee = [pscustomobject]@{ SIDString = $definition.Sid } }
}
$script:descriptor.ControlFlags = [uint32]$script:descriptor.ControlFlags -bor 16
if ($script:alterOwner) { $script:descriptor.Owner.SIDString = 'S-1-5-18' }
if ($script:dropUnknown) { $script:descriptor.SACL = @($script:descriptor.SACL | Where-Object AceType -ne 19) }
}
function Read-Host { param($Prompt) if ($script:promptCallback) { & $script:promptCallback }; if ($script:decline) { 'n' } else { 'Y' } }
function New-TestContext([switch]$DryRun, [switch]$Prompt) {
$script:context = New-WelaConfigurationContext -Auto:(-not $Prompt) -DryRun:$DryRun -BackupPath (Join-Path $root ([guid]::NewGuid().ToString('N')))
return $script:context
}
function Run-Controls { param($Context, [string[]]$Namespace = @('root\cimv2'), [switch]$IncludeChildren)
Set-WelaWmiAuditControls -Context $Context -Plan @(Get-WelaWmiAuditPlan -Namespace $Namespace -IncludeChildren:$IncludeChildren)
}
try {
$source = @(Get-WelaWmiAuditDefinitions -IncludeChildren)
Assert ($source.Count -eq 8) 'All eight pinned ASD entries are represented'
Assert (@($source.Namespace | Select-Object -Unique).Count -eq 5) 'Exactly five supported namespaces'
$expected = @('root\cimv2|262146|64|S-1-1-0', 'root\cimv2|1|64|S-1-5-4', 'root\cimv2|1|64|S-1-5-2', 'root\cimv2|1|64|S-1-5-3', 'root\SecurityCenter|262145|66|S-1-1-0', 'root\SecurityCenter2|262145|66|S-1-1-0', 'root\subscription|262174|66|S-1-1-0', 'root\default|262175|66|S-1-1-0')
foreach ($i in 0..7) { Assert (("$($source[$i].Namespace)|$($source[$i].AccessMask)|$($source[$i].AceFlags)|$($source[$i].Sid)") -eq $expected[$i]) 'Masks, principals and inheritance match pinned ASD source' }
Assert (@(Get-WelaWmiAuditDefinitions | Where-Object AceFlags -ne 64).Count -eq 0) 'Default does not extend auditing into unselected descendants'
Assert (@(Get-WelaWmiAuditDefinitions -Namespace @('ROOT\CIMV2','root\cimv2')).Count -eq 4) 'Case-insensitive duplicate selections do not duplicate ACE definitions'
foreach ($invalid in @('root\*','\\server\root\cimv2','root/cimv2','root\cimv2\child','root\default ')) { Throws { Get-WelaWmiAuditDefinitions -Namespace $invalid } 'Wildcards, remote paths and unreviewed namespace targets rejected' }
Throws { Get-WelaWmiAuditPlan } 'Empty selection refuses implicit configuration'
foreach ($value in @(2,8,9,21,4294967295,$null,$false,'unknown')) {
Throws { Assert-WelaWmiReturnCode ([pscustomobject]@{ReturnValue=$value}) 'GetSecurityDescriptor' } 'Every nonzero/missing/invalid return fails'
Throws { Assert-WelaWmiReturnCode ([pscustomobject]@{ReturnValue=$value}) 'SetSecurityDescriptor' } 'Every setter error is checked'
}
Assert-WelaWmiReturnCode ([pscustomobject]@{ReturnValue=[uint32]0}) 'GetSecurityDescriptor'
$getter = [pscustomobject]@{ Code = 2; Descriptor = [pscustomobject]@{ControlFlags=4} }
$getter | Add-Member ScriptMethod InvokeMethod { param($Name,$Parameters,$Options) [pscustomobject]@{ReturnValue=$this.Code;Descriptor=$this.Descriptor} }
Throws { Get-WelaWmiNativeDescriptor $getter } 'Production getter checks provider return code even when descriptor is populated'
$getter.Code=0; $getter.Descriptor=$null
Throws { Get-WelaWmiNativeDescriptor $getter } 'Production getter refuses missing descriptor even with success code'
$getter.Descriptor=[pscustomobject]@{ControlFlags=4;SACL=$null}
Assert ((Get-WelaWmiNativeDescriptor $getter).ControlFlags -eq 4) 'Production getter accepts explicit success and descriptor'
Assert ((Get-WelaWmiSid ([pscustomobject]@{ SID = [byte[]]@(1,1,0,0,0,0,0,1,0,0,0,0) })) -eq 'S-1-1-0') 'Binary SID identity supported without localized names'
Reset-Mocks
$before = $script:descriptor | ConvertTo-Json -Depth 30 | ConvertFrom-Json
$context = New-TestContext -DryRun
Run-Controls $context
Assert ($script:writes -eq 0 -and -not (Test-Path $context.BackupPath) -and $context.Results[0].Status -eq 'Skipped') 'Dry-run has no setter or journal mutation'
$context = New-TestContext -Prompt; $script:decline = $true
Run-Controls $context
Assert ($script:writes -eq 0 -and $context.Results[0].Diagnostic -match 'Declined') 'Operator decline has no setter'
Reset-Mocks
$context = New-TestContext
Run-Controls $context
Assert ($script:writes -eq 1 -and $context.Results[0].Status -eq 'Applied') 'One namespace update appends four missing CIMV2 ACEs'
Assert ((Complete-WelaConfiguration $context -Scope wmi-namespace-sacl-only).ExitCode -eq 0) 'Applied namespace passes final verification'
Assert (Test-WelaWmiDescriptorPreserved $before $script:descriptor) 'Owner/group/DACL/control flags and duplicate unknown ACEs preserved'
Assert ($script:descriptor.ControlFlags -eq (36868 -bor 16)) 'Only SACL_PRESENT is added to descriptor control flags'
Assert ($script:descriptor.SACL.Count -eq 7 -and @($script:descriptor.SACL | Where-Object AceType -eq 19).Count -eq 2) 'Unknown ACE multiplicity preserved'
$context = New-TestContext
Run-Controls $context
Assert ($script:writes -eq 1 -and $context.Results[0].Status -eq 'AlreadyCompliant') 'Second run does not duplicate entries'
$script:descriptor.DACL[0].AccessMask = 1
Assert ((Complete-WelaConfiguration $context).ExitCode -eq 1 -and $context.Results[0].Status -eq 'Overridden') 'Final already-compliant DACL drift is detected'
Reset-Mocks; $context = New-TestContext; $script:race = $true
Run-Controls $context
Assert ($script:writes -eq 0 -and $context.Results[0].Status -eq 'Failed') 'Changed owner between journal and setter refuses update'
Reset-Mocks; $context = New-TestContext -Prompt
$script:promptCallback = { $script:descriptor.SACL[1].OpaqueFutureField = @(99) }
Run-Controls $context
Assert ($script:writes -eq 0 -and $context.Results[0].Status -eq 'Failed') 'Unknown ACE changes during operator prompt are preserved by refusing write'
foreach ($failure in @('alterOwner','dropUnknown','ineffective')) {
Reset-Mocks; Set-Variable -Scope Script -Name $failure -Value $true; $context = New-TestContext
Run-Controls $context
Assert ($context.Results[0].Status -eq 'Failed' -and (Complete-WelaConfiguration $context).ExitCode -eq 1) 'Read-back rejects permission damage, dropped unknown ACE or ineffective update'
}
Reset-Mocks; $script:writeCode = 9; $context = New-TestContext
Run-Controls $context
Assert ($context.Results[0].Status -eq 'Failed' -and $script:descriptor.SACL.Count -eq 3) 'Provider return-code error is a failed control'
Reset-Mocks; $script:readFail = $true; $context = New-TestContext
Run-Controls $context
Assert ($script:writes -eq 0 -and $context.Results[0].Status -eq 'Failed') 'Unreadable/missing selected namespace fails without partial descriptor writes'
Reset-Mocks; $context = New-TestContext
Remove-Item -LiteralPath $context.BackupPath -Recurse
Run-Controls $context
Assert ($script:writes -eq 0 -and $context.Results[0].Status -eq 'Failed') 'Journal failure prevents setter invocation'
Reset-Mocks; $context = New-TestContext
Run-Controls $context
$script:descriptor.SACL += [pscustomobject]@{ AceType=2; AceFlags=128; AccessMask=1; Trustee=[pscustomobject]@{SIDString='S-1-5-18'} }
Assert ((Complete-WelaConfiguration $context).ExitCode -eq 1 -and $context.Results[0].Status -eq 'Overridden') 'Extra SACL drift after successful write is detected at final check'
Reset-Mocks; $context = New-TestContext
Run-Controls $context -Namespace 'root\subscription' -IncludeChildren
Assert ($script:descriptor.SACL[-1].AceFlags -eq 66 -and $script:descriptor.SACL[-1].AccessMask -eq 262174) 'Explicit child option enables exactly ASD inheritance for subscription'
$definition = @(Get-WelaWmiAuditDefinitions -Namespace 'root\cimv2')[0]
$ace = [pscustomobject]@{AceType=2;AceFlags=64;AccessMask=262146;Trustee=[pscustomobject]@{SIDString='S-1-1-0'}}
Assert (Test-WelaWmiAceMatch $ace $definition) 'Exact ordinary success ACE satisfies requirement'
foreach ($flags in @(80,192,66,72)) { $ace.AceFlags=$flags; Assert (-not (Test-WelaWmiAceMatch $ace $definition)) 'Inherited/broader/different-scope ACE does not hide a missing exact request' }
$tokens=$null;$parseErrors=$null
$ast=[System.Management.Automation.Language.Parser]::ParseFile((Join-Path $repo 'WELA.ps1'),[ref]$tokens,[ref]$parseErrors)
Assert ($parseErrors.Count -eq 0) 'Combined CLI parses'
# Execute only the actual top-level DryRun guard, with no command dispatch.
$guard=$ast.EndBlock.Statements | Where-Object { $_ -is [System.Management.Automation.Language.IfStatementAst] -and $_.Extent.Text.StartsWith('if ($DryRun') } | Select-Object -First 1
$Cmd='wmi-auditing';$DryRun=$true;$WmiAction='Configure';$FirewallAction='Audit';$SmbAction='Audit'
& ([scriptblock]::Create($guard.Extent.Text));$WmiAction='Audit'
Throws { & ([scriptblock]::Create($guard.Extent.Text)) } 'WMI Audit rejects DryRun before dispatch'
Write-Host "PASS: $script:assertions WMI namespace assertions (mocked, no live namespace changes)."
} finally { Remove-Item -LiteralPath $root -Recurse -Force -ErrorAction SilentlyContinue }
@@ -0,0 +1,68 @@
# Actual reads and in-memory typed provider responses only. Never sends a native SetSecurityDescriptor.
$ErrorActionPreference = 'Stop'
if ($env:OS -ne 'Windows_NT') { Write-Host 'SKIP: Windows only'; return }
$repo = Split-Path $PSScriptRoot -Parent
. (Join-Path $repo 'scripts/Configuration.ps1')
. (Join-Path $repo 'scripts/WmiNamespaceAuditing.ps1')
$script:assertions = 0
function Assert($Condition, [string]$Message) { if (-not $Condition) { throw "FAIL: $Message" }; $script:assertions++ }
$before = Get-WelaWmiNamespaceSnapshot 'root\cimv2'
Assert ($before.DescriptorJson -and $before.DescriptorMof -and $before.SaclReadPrivilege -eq 'SeSecurityPrivilege enabled') 'Actual privileged native descriptor read and full export'
$inventory = @(Get-WelaWmiNamespaceInventory)
Assert ($inventory.Count -eq 5 -and @($inventory | Where-Object { $_.Namespace -eq 'root\cimv2' -and $_.State -eq 'Present' }).Count -eq 1) 'Supported namespace inventory reads actual local namespaces'
$plan = @(Get-WelaWmiAuditPlan -Namespace 'root\cimv2')
Assert ($plan[0].Status -in @('AlreadyCompliant','ChangeRequired')) 'Actual CIMV2 plan reads successfully'
$path = Join-Path ([IO.Path]::GetTempPath()) ('wela-wmi-readonly-' + [guid]::NewGuid().ToString('N'))
$context = New-WelaConfigurationContext -Auto -DryRun -BackupPath $path
Set-WelaWmiAuditControls -Context $context -Plan $plan
Assert (-not (Test-Path $path) -and $context.Results[0].Status -in @('AlreadyCompliant','Skipped')) 'Real dry-run neither writes SACL nor creates journal'
$after = Get-WelaWmiNamespaceSnapshot 'root\cimv2'
Assert ($before.DescriptorJson -ceq $after.DescriptorJson) 'Full descriptor unchanged by read-only planning/dry-run'
# Read actual native objects once; from here the native connection factory is
# replaced in the same script scope before invoking ANY setter code.
Initialize-WelaWmiInterop
$privilege = New-Object Wela.WmiSecurityPrivilege
$connection = $null
try {
$connection = New-WelaWmiConnection 'root\cimv2'
$script:fixtureDescriptor = (Get-WelaWmiNativeDescriptor $connection).Clone()
$script:fixtureParameters = $connection.GetMethodParameters('SetSecurityDescriptor')
} finally { if ($connection) { $connection.Dispose() }; $privilege.Dispose() }
# An empty in-memory SACL forces all requested additions without changing Windows.
$script:fixtureDescriptor.SACL = $null
$expected = ConvertTo-WelaWmiJson (ConvertTo-WelaWmiData $script:fixtureDescriptor)
$script:setCalls = 0; $script:captured = $null; $script:returnCode = [uint32]0
$script:fake = [pscustomobject]@{}
$script:fake | Add-Member ScriptMethod InvokeMethod {
param($Name, $Parameters, $Options)
if ($Name -eq 'GetSecurityDescriptor') { return [pscustomobject]@{ ReturnValue = [uint32]0; Descriptor = $script:fixtureDescriptor } }
if ($Name -ne 'SetSecurityDescriptor') { throw "Unexpected method: $Name" }
$script:setCalls++; $script:captured = $Parameters.Descriptor.Clone()
return [pscustomobject]@{ ReturnValue = $script:returnCode }
}
$script:fake | Add-Member ScriptMethod GetMethodParameters { param($Name) if ($Name -ne 'SetSecurityDescriptor') { throw 'Unexpected method parameters' }; return $script:fixtureParameters.Clone() }
$script:fake | Add-Member ScriptMethod Dispose { }
function New-WelaWmiConnection { param($Namespace) if ($Namespace -ne 'root\cimv2') { throw 'Unexpected fake target' }; return $script:fake }
$definitions = @(Get-WelaWmiAuditDefinitions -Namespace 'root\cimv2')
Set-WelaWmiNamespaceDescriptor -Namespace 'root\cimv2' -ExpectedJson $expected -Definitions $definitions
Assert ($script:setCalls -eq 1 -and $script:captured -is [System.Management.ManagementBaseObject]) 'Production writer builds typed descriptor against fake provider only'
$original = $expected | ConvertFrom-Json
$captured = ConvertTo-WelaWmiData $script:captured
Assert ($null -eq $captured.DACL -and $null -eq $captured.Owner -and $null -eq $captured.Group) 'Native request omits access-permission fields instead of requesting that they be rewritten'
Assert (([uint32]$captured.ControlFlags -band 4) -eq 0 -and ([uint32]$captured.ControlFlags -band 16) -eq 16) 'Native request uses only SACL-present mutation semantics, with DACL-present cleared'
Assert ((ConvertTo-WelaWmiJson (ConvertTo-WelaWmiData $script:fixtureDescriptor)) -ceq $expected) 'Building the SACL-only request leaves the complete original descriptor unchanged'
# Simulate the documented provider contract in memory: absent access fields and
# SE_DACL_PRESENT preserve the current access permissions.
$effective = $expected | ConvertFrom-Json
$effective.SACL = $captured.SACL
$effective.ControlFlags = [uint32]$effective.ControlFlags -bor 16
Assert (Test-WelaWmiDescriptorPreserved $original $effective) 'SACL-only provider semantics retain every original non-SACL field'
Assert (@(Get-WelaWmiMissingAces $captured $definitions).Count -eq 0 -and @($captured.SACL).Count -eq 4) 'Actual Win32_ACE/Trustee objects carry all four exact masks and binary SIDs'
$script:returnCode = [uint32]9
$failed = $false
try { Set-WelaWmiNamespaceDescriptor -Namespace 'root\cimv2' -ExpectedJson $expected -Definitions $definitions } catch { $failed = $_.Exception.Message -match 'ReturnValue=9' }
Assert $failed 'Production SetSecurityDescriptor wrapper rejects native nonzero return code'
$prior = $script:setCalls; $failed = $false
try { Set-WelaWmiNamespaceDescriptor -Namespace 'root\cimv2' -ExpectedJson '{}' -Definitions $definitions } catch { $failed = $_.Exception.Message -match 'changed after' }
Assert ($failed -and $script:setCalls -eq $prior) 'Production writer detects changed snapshot before fake setter'
Write-Host "PASS: $script:assertions Windows namespace read-only / in-memory native adapter assertions. No live SACL changes or event-generation claims."
+15
View File
@@ -0,0 +1,15 @@
{
"ControlFlags": 36868,
"DACL": [
{"AccessMask": 393279, "AceFlags": 2, "AceType": 0, "GuidObjectType": null, "GuidInheritedObjectType": null, "Trustee": {"SIDString": "S-1-5-32-544", "Name": "Administrators", "Domain": "BUILTIN"}},
{"AccessMask": 32, "AceFlags": 0, "AceType": 1, "Trustee": {"SIDString": "S-1-5-21-1-2-3-1001"}}
],
"Group": {"SIDString": "S-1-5-18"},
"Owner": {"SIDString": "S-1-5-32-544"},
"SACL": [
{"AccessMask": 2, "AceFlags": 128, "AceType": 2, "Trustee": {"SIDString": "S-1-1-0"}},
{"AccessMask": 8, "AceFlags": 16, "AceType": 19, "GuidObjectType": "unfamiliar-object", "OpaqueFutureField": [1, 7, 255], "Trustee": {"SIDString": "S-1-5-18"}},
{"AccessMask": 8, "AceFlags": 16, "AceType": 19, "GuidObjectType": "unfamiliar-object", "OpaqueFutureField": [1, 7, 255], "Trustee": {"SIDString": "S-1-5-18"}}
],
"ProviderExtension": {"Keep": "unchanged"}
}