Files
WELA/tests/WecUpdate.Windows.Tests.ps1
田中ザック Isaac Mathis 610d27e8ff Review and apply query updates to existing disabled WEC subscriptions (#426)
* Add reviewed existing-only updates for disabled WEC subscriptions

* Pin loaded updater and flush locked recovery artifacts; reference PR 426

* Compare formatted WEC queries semantically before pinning raw native state

* Read native WEC subscription XML with bounded explicit Unicode pipes

* Use explicit Unicode reads for reviewed update and native cleanup

* Keep timestamp strings exact in inherited native evidence fixtures

* Reject XML-invalid descriptions before any native save

* Decode native WEC XML BOMs without unsupported Unicode switch

* Describe native XML byte decoding accurately

* Reference System.Xml explicitly when compiling under Windows PowerShell
2026-09-20 22:51:01 +09:00

73 lines
7.6 KiB
PowerShell

param([switch]$AllowDisposableSubscription)
$ErrorActionPreference='Stop'
if([Environment]::OSVersion.Platform -ne [PlatformID]::Win32NT -or -not $AllowDisposableSubscription -or $env:GITHUB_ACTIONS -ne 'true' -or $env:RUNNER_ENVIRONMENT -ne 'github-hosted'){throw 'Explicit disposable GitHub-hosted Windows subscription opt-in required.'}
$repo=Split-Path $PSScriptRoot -Parent
Import-Module "$repo/modules/WefSubscriptions.psm1" -Force
Import-Module "$repo/modules/AuditProfiles.psm1" -Force
. "$repo/scripts/Configuration.ps1"
. "$repo/scripts/ControlApplicability.ps1"
. "$repo/scripts/WefArrival.ps1"
. "$repo/scripts/WecUpdate.ps1"
$count=0
function Assert($Value,$Message){if(-not $Value){throw $Message};$script:count++}
function Key($Value){ConvertTo-Json -InputObject $Value -Depth 16 -Compress}
function ServiceState {Get-CimInstance Win32_Service -Filter "Name='Wecsvc'"|Select-Object Name,State,StartMode}
function Subscriptions {@((Invoke-WelaNative 'wecutil.exe' @('es')).Output|ForEach-Object {$_.ToString().Trim()}|Where-Object {$_})}
$beforeService=ServiceState;$serviceKey='HKLM:\SYSTEM\CurrentControlSet\Services\Wecsvc';$beforeDelayed=Get-WelaRegistryState $serviceKey DelayedAutoStart
if($beforeService.State -notin @('Running','Stopped') -or $beforeService.StartMode -notin @('Auto','Manual','Disabled')){throw 'Stable Wecsvc state required.'}
$nonce=[guid]::NewGuid().ToString('N');$id='WELA-Update-Test-'+$nonce;$description='Owned original '+$nonce;$changedDescription='Owned reviewed '+([string][char]0x65e5)+([string][char]0x672c)+([string][char]0x8a9e)+' '+$nonce
$sid='S-1-5-21-111111111-222222222-333333333-1234'
$root=Join-Path $env:RUNNER_TEMP ('wela-wec-update-'+$nonce);$null=New-Item -ItemType Directory $root
$created=$false;$beforeIds=$null;$primary=$null
try {
if($beforeService.StartMode -eq 'Disabled'){Set-Service Wecsvc -StartupType Manual}
if($beforeService.State -eq 'Stopped'){Start-Service Wecsvc}
$beforeIds=@(Subscriptions);if($beforeIds -contains $id){throw 'Unique ID already exists.'}
$query='<QueryList><Query Id="0" Path="Application"><Select Path="Application">*[System[(EventID=1)]]</Select></Query></QueryList>'
$xml=@"
<Subscription xmlns="http://schemas.microsoft.com/2006/03/windows/events/subscription"><SubscriptionId>$id</SubscriptionId><SubscriptionType>SourceInitiated</SubscriptionType><Description>$description</Description><Enabled>false</Enabled><Uri>http://schemas.microsoft.com/wbem/wsman/1/windows/EventLog</Uri><ConfigurationMode>Normal</ConfigurationMode><Query><![CDATA[$query]]></Query><ReadExistingEvents>false</ReadExistingEvents><TransportName>HTTP</TransportName><ContentFormat>Events</ContentFormat><Locale Language="en-US"/><LogFile>ForwardedEvents</LogFile><AllowedSourceDomainComputers>$(Get-WelaWefAuthorization @($sid))</AllowedSourceDomainComputers></Subscription>
"@
$xmlPath=Join-Path $root 'owned.xml';[IO.File]::WriteAllText($xmlPath,$xml);$created=$true;$null=Invoke-WelaNative 'wecutil.exe' @('cs',$xmlPath)
$before=Read-WelaWecUpdateDefinition $id @($sid);$duringService=ServiceState
$queryPath=Join-Path $root 'query.xml';[IO.File]::WriteAllText($queryPath,$query.Replace('EventID=1','EventID=2'))
$plan=Invoke-WelaWecUpdate Plan -Id $id -SourceSids @($sid) -QueryPath $queryPath -Description $changedDescription -OutputPath (Join-Path $root 'plan')
Assert ($plan.ExitCode -eq 0) "Native plan: $($plan.Diagnostic)"
Assert ((Read-WelaWecUpdateDefinition $id @($sid)).WholeKey -ceq $before.WholeKey) 'Planning is read only'
$planPath=Join-Path $plan.OutputPath 'plan.json'
$apply=Invoke-WelaWecUpdate Apply -PlanPath $planPath -PlanHash $plan.PlanHash -OutputPath (Join-Path $root 'apply')
if($apply.ExitCode){throw ($apply|ConvertTo-Json -Depth 24)}
Assert ($apply.Status -eq 'UpdatedAndVerified' -and $apply.NativeSaveAttempted) 'Native existing-only save succeeded'
$after=Read-WelaWecUpdateDefinition $id @($sid)
Assert ($after.PreservedKey -ceq $before.PreservedKey -and $after.Description -ceq $changedDescription -and $after.QueryKey -ceq (ConvertFrom-WelaWefQuery ([IO.File]::ReadAllText($queryPath))).Key) 'Only query/description changed; complete remaining XML preserved'
Assert ($apply.ReadyRuleCredit -eq 0) 'No delivery/Sigma credit'
$stale=Invoke-WelaWecUpdate Apply -PlanPath $planPath -PlanHash $plan.PlanHash -OutputPath (Join-Path $root 'stale')
Assert ($stale.ExitCode -eq 1 -and -not $stale.NativeSaveAttempted -and $stale.Diagnostic -match 'differs') 'Stale actual definition refuses another native save'
Assert ((Read-WelaWecUpdateDefinition $id @($sid)).WholeKey -ceq $after.WholeKey) 'Stale refusal preserves native definition'
$samePlan=Invoke-WelaWecUpdate Plan -Id $id -SourceSids @($sid) -QueryPath $queryPath -Description $changedDescription -OutputPath (Join-Path $root 'same-plan')
$same=Invoke-WelaWecUpdate Apply -PlanPath (Join-Path $samePlan.OutputPath 'plan.json') -PlanHash $samePlan.PlanHash -OutputPath (Join-Path $root 'same')
Assert ($same.ExitCode -eq 0 -and $same.Status -eq 'AlreadyMatches' -and -not $same.NativeSaveAttempted) 'Native idempotence does not save'
[IO.File]::WriteAllText($queryPath,$before.QueryXml)
$restorePlan=Invoke-WelaWecUpdate Plan -Id $id -SourceSids @($sid) -QueryPath $queryPath -Description $description -OutputPath (Join-Path $root 'restore-plan')
$restore=Invoke-WelaWecUpdate Apply -PlanPath (Join-Path $restorePlan.OutputPath 'plan.json') -PlanHash $restorePlan.PlanHash -OutputPath (Join-Path $root 'restore')
Assert ($restore.ExitCode -eq 0 -and (Read-WelaWecUpdateDefinition $id @($sid)).WholeKey -ceq $before.WholeKey) "Original query/description restored: $($restore.Diagnostic)"
Assert ((Key (ServiceState)) -ceq (Key $duringService)) 'Production updater did not change Wecsvc'
Write-Host "Native WEC update passed $count assertions on $([Environment]::OSVersion.Version), PowerShell $($PSVersionTable.PSVersion). Disabled subscription only; no delivery/bookmark claim."
}catch{$primary=$_}
finally {
$errors=@()
try {
if($created -and @(Subscriptions) -contains $id){$raw=Read-WelaWecSubscriptionXml $id;$doc=Read-WelaWefXml $raw;$ns=New-Object Xml.XmlNamespaceManager($doc.NameTable);$ns.AddNamespace('s',$doc.DocumentElement.NamespaceURI);$observed=$doc.SelectSingleNode('/s:Subscription/s:Description',$ns).InnerText;if($observed -cnotin @($description,$changedDescription)){throw 'Fixture ownership changed; refusing deletion.'};$null=Invoke-WelaNative 'wecutil.exe' @('ds',$id)}
if($null -ne $beforeIds -and (Key @($beforeIds|Sort-Object)) -cne (Key @(Subscriptions|Sort-Object))){throw 'Subscription inventory differs after cleanup.'}
}catch{$errors+=$_.Exception.Message}
try {
if($beforeService.State -eq 'Stopped' -and (Get-Service Wecsvc).Status -ne 'Stopped'){Stop-Service Wecsvc}
if($beforeService.StartMode -eq 'Disabled'){Set-Service Wecsvc -StartupType Disabled}
if((Key (Get-WelaRegistryState $serviceKey DelayedAutoStart)) -cne (Key $beforeDelayed)){if($beforeDelayed.ValueExists){$null=New-ItemProperty -LiteralPath $serviceKey -Name DelayedAutoStart -Value $beforeDelayed.Value -PropertyType $beforeDelayed.Type -Force}else{Remove-ItemProperty -LiteralPath $serviceKey -Name DelayedAutoStart -ErrorAction Stop}}
if((Key (ServiceState)) -cne (Key $beforeService) -or (Key (Get-WelaRegistryState $serviceKey DelayedAutoStart)) -cne (Key $beforeDelayed)){throw 'Original Wecsvc state/startup differs.'}
}catch{$errors+=$_.Exception.Message}
if($errors.Count){throw "Fixture cleanup failed; retained $root : $($errors -join '; '); primary failure: $primary"}
Write-Host 'Original subscription inventory and Wecsvc state/startup restored.'
}
if($primary){throw $primary}
$global:LASTEXITCODE=0