Files
WELA/tests/WecRuntime.Tests.ps1
田中ザック Isaac Mathis 913b1dfaee Add typed native WEC runtime observations (#424)
* Observe typed native WEC subscription runtime status

* Link typed WEC runtime changelog to PR 424

* Pass a native null source for subscription runtime queries

* Ignore unused count storage for native null WEC variants

* Keep unavailable WEC source inventories unknown and diagnose native XML reads

* Read native WEC subscription XML with bounded explicit Unicode pipes

* Use bounded Unicode subscription reads in runtime observations and cleanup

* Decode native WEC XML BOMs without unsupported Unicode switch

* Describe strict native WEC XML byte decoding

* Reference System.Xml explicitly when compiling under Windows PowerShell

* Regenerate website changelog snapshots with their proper headers
2026-09-20 22:48:32 +09:00

151 lines
14 KiB
PowerShell

$ErrorActionPreference='Stop'
$repo=Split-Path $PSScriptRoot -Parent
Import-Module (Join-Path $repo 'modules/WefSubscriptions.psm1') -Force
. (Join-Path $repo 'scripts/WecRuntime.ps1')
Add-Type -Path (Join-Path $repo 'scripts/WecRuntimeNative.cs')
$script:count=0
function Assert($Value,$Message) {if (-not $Value) {throw $Message};$script:count++}
function Throws($Action,$Pattern) {$message='';try {& $Action | Out-Null} catch {$message=$_.Exception.Message};Assert ($message -match $Pattern) "Expected $Pattern; received $message"}
function NativeValue([uint32]$Type,$Data,[uint32]$Count=0) {[pscustomobject]@{State='Observed';NativeType=$Type;Data=$Data;Count=$Count;ErrorCode=0;Diagnostic=''}}
# Decode synthetic native-memory buffers, not a mock of the ABI decoder.
$buffer=[Runtime.InteropServices.Marshal]::AllocHGlobal(256)
try {
$zero=New-Object byte[] 256;[Runtime.InteropServices.Marshal]::Copy($zero,0,$buffer,256)
[Runtime.InteropServices.Marshal]::WriteInt32($buffer,12,2)
[Runtime.InteropServices.Marshal]::WriteInt32($buffer,0,-1)
$decoded=[Wela.WecRuntime.Native]::Decode($buffer,16,1)
Assert ($decoded.Data -is [uint32] -and $decoded.Data -eq [uint32]::MaxValue) 'UInt32 error codes keep their unsigned width.'
Throws {[Wela.WecRuntime.Native]::Decode($buffer,16,2)} 'Unexpected EC_VARIANT'
Throws {[Wela.WecRuntime.Native]::Decode($buffer,15,1)} 'Invalid runtime buffer'
[Runtime.InteropServices.Marshal]::WriteInt32($buffer,12,3)
[Runtime.InteropServices.Marshal]::WriteInt64($buffer,0,[DateTime]::UtcNow.ToFileTimeUtc())
Assert (([Wela.WecRuntime.Native]::Decode($buffer,16,6)).Data -is [uint64]) 'FILETIME retains unsigned64 representation.'
[Runtime.InteropServices.Marshal]::WriteInt32($buffer,12,4)
$text=[Text.Encoding]::Unicode.GetBytes("日本語 Fehler`0");$textPointer=[IntPtr]::Add($buffer,64)
[Runtime.InteropServices.Marshal]::Copy($text,0,$textPointer,$text.Length)
[Runtime.InteropServices.Marshal]::WriteIntPtr($buffer,0,$textPointer)
Assert (([Wela.WecRuntime.Native]::Decode($buffer,256,2)).Data -ceq '日本語 Fehler') 'Native strings are Unicode without English parsing.'
[Runtime.InteropServices.Marshal]::WriteIntPtr($buffer,0,[IntPtr]::Add($buffer,256))
Throws {[Wela.WecRuntime.Native]::Decode($buffer,256,2)} 'outside'
[Runtime.InteropServices.Marshal]::WriteInt32($buffer,12,132)
[Runtime.InteropServices.Marshal]::WriteInt32($buffer,8,1)
[Runtime.InteropServices.Marshal]::WriteIntPtr($buffer,0,[IntPtr]::Add($buffer,24))
[Runtime.InteropServices.Marshal]::WriteIntPtr($buffer,24,$textPointer)
Assert (([Wela.WecRuntime.Native]::Decode($buffer,256,5)).Data[0] -ceq '日本語 Fehler') 'Source string-array pointer layout is decoded.'
[Runtime.InteropServices.Marshal]::WriteInt32($buffer,8,4097)
Throws {[Wela.WecRuntime.Native]::Decode($buffer,256,5)} '4096'
[Runtime.InteropServices.Marshal]::WriteInt32($buffer,8,0)
[Runtime.InteropServices.Marshal]::WriteInt32($buffer,12,0)
Assert (([Wela.WecRuntime.Native]::Decode($buffer,16,2)).State -eq 'NotAvailable') 'Null is distinct from a successful nonempty property.'
[Runtime.InteropServices.Marshal]::WriteInt32($buffer,8,12345)
$nativeNull=[Wela.WecRuntime.Native]::Decode($buffer,16,4)
Assert ($nativeNull.State -eq 'NotAvailable' -and $nativeNull.Count -eq 0) 'Native null ignores unused count/union storage, which Windows need not initialize.'
} finally {[Runtime.InteropServices.Marshal]::FreeHGlobal($buffer)}
foreach ($property in @(0,1,5)) {
if ($property -in @(0,1)) {Throws {ConvertTo-WelaWecRuntimeField (NativeValue 2 '2') $property} 'UInt32'}
$nullValue=[pscustomobject]@{State='NotAvailable';NativeType=0;ErrorCode=0;Diagnostic=''}
Assert ((ConvertTo-WelaWecRuntimeField $nullValue $property).Status -eq 'Unknown') 'Missing mandatory runtime values cannot make a complete observation.'
}
Assert ((ConvertTo-WelaWecRuntimeField (NativeValue 2 ([uint32]99)) 0).Status -eq 'Unknown') 'Unknown future native enum keeps uncertainty.'
Assert ((ConvertTo-WelaWecRuntimeField (NativeValue 3 ([uint64]0)) 6).Status -eq 'NotAvailable') 'Zero heartbeat is not a1601 observed timestamp.'
Assert ((ConvertTo-WelaWecRuntimeField (NativeValue 3 ([uint64]::MaxValue)) 6).Status -eq 'Unknown') 'Out-of-range FILETIME is not silently rounded.'
$utc=[DateTime]::SpecifyKind([DateTime]'2025-01-02T03:04:05',[DateTimeKind]::Utc)
$time=ConvertTo-WelaWecRuntimeField (NativeValue 3 ([uint64]$utc.ToFileTimeUtc())) 6
Assert ($time.Value -ceq '2025-01-02T03:04:05.0000000Z') 'FILETIME renders UTC without labeling local time Z.'
$script:scenario='';$script:contextReads=0;$script:definitionReads=0;$script:inventoryReads=0;$script:nativeReads=0
$definitionReader=(Get-Command Get-WelaWecRuntimeDefinition).ScriptBlock
function Reset-Fixture {$script:scenario='';$script:contextReads=0;$script:definitionReads=0;$script:inventoryReads=0;$script:nativeReads=0}
function Get-WelaWecRuntimeContext {
$script:contextReads++
if ($script:scenario -eq 'context-denied') {throw 'Reader context unavailable'}
[pscustomobject]@{Computer='FixtureCollector';ReaderSid=$(if ($script:scenario -eq 'context-drift' -and $script:contextReads -gt 1) {'OTHER'} else {'S-1-5-18'});Host=[pscustomobject]@{Build=26100;DomainRole=3}}
}
function Get-WelaWecRuntimeDefinition {
param($Id)
$script:definitionReads++
if ($script:scenario -eq 'definition-denied') {throw 'Definition access denied'}
[pscustomobject]@{Id=$Id;Type='SourceInitiated';Enabled=$false;Key=$(if ($script:scenario -eq 'definition-drift' -and $script:definitionReads -gt 1) {'changed'} else {'same'});RawXml='<Subscription/>'}
}
function Read-WelaWecRuntimeValue {
param($Id,$Source,$Property)
$script:nativeReads++
if (($script:scenario -eq 'field-denied' -and $Property -eq 1) -or ($script:scenario -eq 'one-source-denied' -and $Source -eq 'source2' -and $Property -eq 0)) {return [pscustomobject]@{State='Unknown';NativeType=$null;Count=0;ErrorCode=5;Data=$null;Diagnostic='Localized access denied'}}
switch ($Property) {
0 {NativeValue 2 ([uint32]$(if ($script:scenario -eq 'active') {2} elseif ($script:scenario -eq 'trying') {4} else {1}))}
1 {NativeValue 2 ([uint32]$(if ($script:scenario -eq 'trying') {1722} else {0}))}
2 {NativeValue 4 'Lokalisierte Nachricht <script>'}
5 {
$script:inventoryReads++
if ($script:scenario -eq 'null-inventory' -or ($script:scenario -eq 'null-after-inventory' -and $script:inventoryReads -gt 1)) {return [pscustomobject]@{State='NotAvailable';NativeType=0;ErrorCode=0;Count=0;Data=$null;Diagnostic=''}}
$sources=@('source1','source2')
if ($script:scenario -eq 'empty') {$sources=@()}
if ($script:scenario -eq 'duplicate') {$sources=@('source1','SOURCE1')}
if ($script:scenario -eq 'source-drift' -and $script:inventoryReads -gt 1) {$sources=@('source1')}
NativeValue 132 $sources $sources.Count
}
default {NativeValue 3 ([uint64]0)}
}
}
$temp=Join-Path ([IO.Path]::GetTempPath()) ('wela-wec-runtime-'+[guid]::NewGuid().ToString('N'));$null=New-Item -ItemType Directory -Path $temp
try {
foreach ($case in @('','active','trying','empty')) {
Reset-Fixture;$script:scenario=$case;$report=Get-WelaWecRuntime 'Fixture'
Assert ($report.Status -eq 'Observed') "$case yields a complete observation without claiming health."
Assert ($report.ReadyRuleCredit -eq 0 -and $report.EventArrival -eq 'Not tested' -and $report.Backlog -eq 'Unknown') 'Native status grants no arrival/backlog/Ready proof.'
Assert ($report.SourceInventory.Meaning -match '30 days' -and $report.SourceInventory.Meaning -match 'not a current connection') 'Source inventory retains documented historical meaning.'
if ($case -eq 'trying') {Assert ($report.Subscription.Activity -eq 'Trying' -and $report.Subscription.Fields.LastError.Value -eq 1722) 'Trying and numeric native failure remain visible despite successful reads.'}
if ($case -eq 'empty') {Assert ($report.SourceInventory.ReportedCount -eq 0 -and $report.Sources.Count -eq 0) 'Empty source array is an observed empty inventory, never active sources.'}
}
foreach ($case in @('field-denied','one-source-denied','definition-drift','context-drift','source-drift','duplicate','null-inventory','null-after-inventory')) {
Reset-Fixture;$script:scenario=$case;$report=Get-WelaWecRuntime 'Fixture'
Assert ($report.Status -eq 'Partial') "$case cannot return complete observation."
Assert ($null -ne $report.CollectorAfter) 'Partial observation retains actual final collector context.'
if ($case -eq 'null-inventory') {Assert ($null -eq $report.SourceInventory.ReportedCount -and $report.SourceInventory.Observation.Status -eq 'Unknown') 'Null inventory cannot be promoted to an observed zero-source result.'}
if ($case -eq 'null-after-inventory') {Assert ($report.SourceListChanged -and $report.SourceInventory.AfterObservation.Status -eq 'Unknown') 'Inventory becoming unavailable is explicit observation drift.'}
if ($case -eq 'field-denied') {Assert ($report.Subscription.Fields.LastError.ErrorCode -eq 5 -and $report.Subscription.Fields.LastError.Status -eq 'Unknown') 'Native read error code is separate from subscription LastError value.'}
}
foreach ($case in @('definition-denied','context-denied')) {Reset-Fixture;$script:scenario=$case;$report=Get-WelaWecRuntime 'Fixture';Assert ($report.Status -eq 'Unknown' -and $script:nativeReads -eq 0) 'Unverified context/definition stops runtime queries.'}
Reset-Fixture;$report=Get-WelaWecRuntime 'Fixture' -MaximumSources 1
Assert ($report.Status -eq 'Partial' -and $report.Capped -and $report.Sources.Count -eq 1 -and $report.SourceInventory.ReportedCount -eq 2) 'Source cap keeps actual denominator and explicit incompleteness.'
Assert ($script:nativeReads -eq 14) 'Source cap bounds actual per-source native calls.'
Reset-Fixture
Throws {Invoke-WelaWecRuntime @('Fixture','fixture')} 'duplicate'
Throws {Invoke-WelaWecRuntime @()} '1..32'
Throws {Invoke-WelaWecRuntime @('../other')} 'Invalid'
Push-Location $temp
try {$report=Invoke-WelaWecRuntime @('Fixture') -ResultsPath './runtime.json';Assert (Test-Path ./runtime.json) 'Relative output uses PowerShell location.';Throws {Invoke-WelaWecRuntime @('Fixture') -ResultsPath './runtime.json'} 'new ordinary'} finally {Pop-Location}
$json=Get-Content -LiteralPath (Join-Path $temp 'runtime.json') -Raw | ConvertFrom-Json
Assert ($json.Subscriptions[0].Subscription.Fields.LastErrorMessage.Value -ceq 'Lokalisierte Nachricht <script>') 'JSON preserves localized text and exact nested observations.'
# Exercise actual definition parser with only native XML acquisition replaced.
function Read-WelaWecSubscriptionXml {param($Id) $script:xml}
function Invoke-WelaNative {param($FilePath,$Arguments) [pscustomobject]@{ExitCode=0;Diagnostic=$script:xml}}
$script:xml='<Subscription xmlns="http://schemas.microsoft.com/2006/03/windows/events/subscription"><SubscriptionId>Fixture</SubscriptionId><SubscriptionType>SourceInitiated</SubscriptionType><Enabled>false</Enabled><Query>&lt;QueryList&gt;&lt;Query Id="0" Path="Security"&gt;&lt;Select&gt;*&lt;/Select&gt;&lt;/Query&gt;&lt;/QueryList&gt;</Query></Subscription>'
Assert ((& $definitionReader 'Fixture').Enabled -eq $false) 'Native definition parser retains disabled state.'
Throws {& $definitionReader 'Other'} 'identity'
$script:xml=$script:xml.Replace('Path="Security"','Path="Microsoft-Windows-Sysmon/Operational"')
Throws {& $definitionReader 'Fixture'} 'Sysmon'
# Both older inventories preserve raw evidence and add typed observations.
. (Join-Path $repo 'scripts/WefDeployment.ps1')
. (Join-Path $repo 'scripts/RetentionHealth.ps1')
function Get-WelaNativeChannel {param($Name) [pscustomobject]@{Name=$Name}}
function Get-WelaWefControlState {[pscustomobject]@{Exists=$true;Definition=[pscustomobject]@{Enabled=$false}}}
$script:xml='<Subscription xmlns="http://schemas.microsoft.com/2006/03/windows/events/subscription"><Enabled>false</Enabled><Query>&lt;QueryList&gt;&lt;Query Id="0" Path="Security"&gt;&lt;Select&gt;*&lt;/Select&gt;&lt;/Query&gt;&lt;/QueryList&gt;</Query></Subscription>'
Reset-Fixture
$input=[pscustomobject]@{Subscriptions=@([pscustomobject]@{Id='Fixture';SourceSids=@();Definition=[pscustomobject]@{Enabled=$false};Query=[pscustomobject]@{Channels=@('Security');Filters=@()}})}
$wef=Get-WelaWefInventory $input Collector
Assert ($wef.Runtime.Raw -ceq $script:xml -and $wef.TypedRuntime.Status -eq 'Observed') 'WEF inventory retains raw runtime evidence alongside typed result.'
$source=Get-WelaWefInventory $input Source
Assert ($null -eq $source.TypedRuntime -and $null -eq $source.Runtime) 'Source inventory does not claim collector runtime observations.'
$retention=Get-WelaRetentionSubscriptions @('Fixture')
Assert ($retention.Runtime.Raw -ceq $script:xml -and $retention.TypedRuntime.Status -eq 'Observed' -and $retention.DeliveryHealth -eq 'Unknown') 'Retention adds typed observations without promoting delivery health.'
$exe=(Get-Process -Id $PID).Path
foreach ($arguments in @(@('configure','-WecRuntimeId','Fixture'),@('wec-runtime','-Auto'),@('wec-runtime','-Profile','wela-2.2.0'))) {
$old=$ErrorActionPreference;$ErrorActionPreference='Continue'
try {$output=& $exe -NoProfile -File (Join-Path $repo 'WELA.ps1') @arguments 2>&1;$code=$LASTEXITCODE} finally {$ErrorActionPreference=$old}
Assert ($code -ne 0 -and ($output -join ' ') -match 'No command was run') 'Public early guard prevents unrelated options from dispatching.'
}
} finally {Remove-Item -LiteralPath $temp -Recurse -Force}
$global:LASTEXITCODE=0
Write-Host "WEC runtime: $script:count assertions passed. Synthetic native buffers and observations; no subscriptions or services changed."