From 3e9ba8c4038806af6618ca62d858f9ecdc4ec084 Mon Sep 17 00:00:00 2001
From: Shirofune-Security
<43838376+Shirofune-Security@users.noreply.github.com>
Date: Mon, 21 Sep 2026 22:58:08 +0900
Subject: [PATCH] Create one reviewed exact-IP collector listener through a
bounded native adapter
---
.gitattributes | 2 +
.github/workflows/wec-listener.yml | 16 +-
scripts/WecListener.ps1 | 271 ++++++++++++++++++++++++++++
scripts/WecListenerPipeNative.cs | 19 ++
scripts/WecListenerWorker.ps1 | 13 ++
tests/WecListener.Tests.ps1 | 105 +++++++++++
tests/WecListener.Windows.Tests.ps1 | 124 +++++++++++++
7 files changed, 545 insertions(+), 5 deletions(-)
create mode 100644 scripts/WecListener.ps1
create mode 100644 scripts/WecListenerPipeNative.cs
create mode 100644 scripts/WecListenerWorker.ps1
create mode 100644 tests/WecListener.Tests.ps1
create mode 100644 tests/WecListener.Windows.Tests.ps1
diff --git a/.gitattributes b/.gitattributes
index 190bc45a..eac7b035 100644
--- a/.gitattributes
+++ b/.gitattributes
@@ -68,3 +68,5 @@ tests/SelectedSaclFixtureProtection.cs text eol=lf
/scripts/WecState* text eol=lf
/scripts/WecRuntime* text eol=lf
/tests/WecState* text eol=lf
+
+/scripts/WecListener* text eol=lf
diff --git a/.github/workflows/wec-listener.yml b/.github/workflows/wec-listener.yml
index 957ce27e..f4f209cf 100644
--- a/.github/workflows/wec-listener.yml
+++ b/.github/workflows/wec-listener.yml
@@ -8,7 +8,7 @@ permissions:
contents: read
jobs:
wec-listener:
- timeout-minutes: 15
+ timeout-minutes: 20
strategy:
fail-fast: false
matrix:
@@ -17,14 +17,20 @@ jobs:
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd
- - name: Native listener checkpoint in Windows PowerShell 5.1
+ - name: Actual public listener in Windows PowerShell 5.1
if: matrix.engine == 'powershell'
shell: powershell
- run: ./tests/WecListener.Checkpoint.Windows.Tests.ps1 -AllowDisposableListenerReplacement
- - name: Native listener checkpoint in PowerShell 7
+ run: |
+ ./tests/WecListener.Tests.ps1
+ ./tests/WecListener.Cli.Tests.ps1
+ ./tests/WecListener.Windows.Tests.ps1 -AllowDisposableListenerReplacement
+ - name: Actual public listener in PowerShell 7
if: matrix.engine == 'pwsh'
shell: pwsh
- run: ./tests/WecListener.Checkpoint.Windows.Tests.ps1 -AllowDisposableListenerReplacement
+ run: |
+ ./tests/WecListener.Tests.ps1
+ ./tests/WecListener.Cli.Tests.ps1
+ ./tests/WecListener.Windows.Tests.ps1 -AllowDisposableListenerReplacement
- name: Retain native listener and cleanup evidence
if: always()
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02
diff --git a/scripts/WecListener.ps1 b/scripts/WecListener.ps1
new file mode 100644
index 00000000..14fc989a
--- /dev/null
+++ b/scripts/WecListener.ps1
@@ -0,0 +1,271 @@
+# One reviewed exact-IP HTTP listener; native creation always runs in Windows PowerShell 5.1.
+function Get-WelaListenerKey {param($Value) ConvertTo-Json -InputObject $Value -Depth 24 -Compress}
+function Get-WelaListenerSelection {
+ param($ComputerName,$LocalAddress)
+ if($ComputerName -isnot [string] -or $ComputerName -cnotmatch '^[A-Za-z0-9][A-Za-z0-9-]{0,62}$'){throw 'Select the actual local computer name.'}
+ if($LocalAddress -isnot [string] -or $LocalAddress -cnotmatch '^([0-9]{1,3}\.){3}[0-9]{1,3}$'){throw 'Select one canonical assigned IPv4 address.'}
+ $pieces=$LocalAddress.Split('.')
+ foreach($part in $pieces){if([int]$part -gt 255 -or ([int]$part).ToString() -cne $part){throw 'Select one canonical assigned IPv4 address.'}}
+ if([int]$pieces[0] -in @(0,127) -or [int]$pieces[0] -ge 224 -or ($pieces[0] -eq '169' -and $pieces[1] -eq '254')){throw 'Unspecified, loopback, link-local and multicast/reserved addresses are unsupported.'}
+ [pscustomobject][ordered]@{ComputerName=$ComputerName.ToUpperInvariant();LocalAddress=$LocalAddress}
+}
+function ConvertFrom-WelaListenerXml {
+ param([string]$Xml)
+ if(-not $Xml -or $Xml.Length -gt 131072){throw 'Listener XML exceeds its bound or is absent.'}
+ $doc=Read-WelaWefXml $Xml;$root=$doc.DocumentElement;$ns='http://schemas.microsoft.com/wbem/wsman/1/config/listener'
+ if($root.LocalName -cne 'Listener' -or $root.NamespaceURI -cne $ns){throw 'Unexpected native listener root.'}
+ $fields=@('Address','Transport','Port','Hostname','Enabled','URLPrefix','CertificateThumbprint');$result=[ordered]@{};$policy=$false
+ foreach($node in @($root)+@($root.ChildNodes|Where-Object NodeType -eq Element)){
+ foreach($attr in @($node.Attributes)){
+ if($attr.NamespaceURI -eq 'http://www.w3.org/2000/xmlns/' -or ($node -eq $root -and $attr.NamespaceURI -eq 'http://www.w3.org/XML/1998/namespace' -and $attr.LocalName -eq 'lang')){continue}
+ if($attr.Name -cne 'Source' -or -not $attr.Value){throw 'Unsupported listener provenance attribute.'};$policy=$true
+ }
+ }
+ foreach($child in $root.ChildNodes){if($child.NodeType -eq 'ProcessingInstruction' -or ($child.NodeType -in @('Text','CDATA') -and -not [string]::IsNullOrWhiteSpace($child.Value))){throw 'Unsupported listener container text.'}}
+ foreach($child in @($root.ChildNodes|Where-Object NodeType -eq Element)){
+ if($child.NamespaceURI -cne $ns -or $child.LocalName -cnotin ($fields+@('ListeningOn')) -or @($child.ChildNodes|Where-Object NodeType -in @('Element','ProcessingInstruction')).Count){throw 'Unsupported native listener field.'}
+ }
+ foreach($name in $fields){$nodes=@($root.ChildNodes|Where-Object {$_.NodeType -eq 'Element' -and $_.LocalName -ceq $name});if($nodes.Count -ne 1){throw "Listener field is absent or duplicated: $name"};$result[$name]=[string]$nodes[0].InnerText}
+ if(-not $result.Address -or $result.Address.Length -gt 256 -or $result.Transport -cnotin @('HTTP','HTTPS') -or $result.Port -cnotmatch '^[1-9][0-9]{0,4}$' -or [int]$result.Port -gt 65535 -or $result.Enabled -cnotin @('true','false') -or $result.Hostname.Length -gt 255 -or $result.URLPrefix -cnotmatch '^[A-Za-z0-9_]+(?:/[A-Za-z0-9_]+)*$' -or $result.CertificateThumbprint -cnotmatch '^(|[0-9A-Fa-f]{40})$'){throw 'Unsupported native listener values.'}
+ $listening=@($root.ChildNodes|Where-Object {$_.NodeType -eq 'Element' -and $_.LocalName -ceq 'ListeningOn'}|ForEach-Object InnerText|Sort-Object)
+ if($listening.Count -gt 64 -or @($listening|Sort-Object -Unique).Count -ne $listening.Count){throw 'Ambiguous or excessive ListeningOn addresses.'}
+ foreach($value in $listening){$ip=$null;if(-not [Net.IPAddress]::TryParse($value,[ref]$ip)){throw 'Invalid native ListeningOn address.'}}
+ $result.ListeningOn=$listening;$result.PolicyOwned=$policy;$result.XmlKey=Get-WelaWefXmlKey $root;$result.RawXml=$Xml
+ [pscustomobject]$result
+}
+function Read-WelaListenerInventory {
+ $values=@(Microsoft.WSMan.Management\Get-WSManInstance -ResourceURI 'http://schemas.microsoft.com/wbem/wsman/1/config/listener' -Enumerate -ErrorAction Stop|Select-Object -First 33)
+ if($values.Count -gt 32){throw 'Listener inventory exceeds 32 entries.'}
+ $seen=@{};$bytes=0
+ $rows=@(foreach($value in $values){$row=ConvertFrom-WelaListenerXml ([string]$value.OuterXml);$bytes+=$row.RawXml.Length;$id=$row.Address+'|'+$row.Transport;if($seen.ContainsKey($id) -or $bytes -gt 1048576){throw 'Duplicate or oversized listener inventory.'};$seen[$id]=$true;$row})
+ @($rows|Sort-Object Address,Transport)
+}
+function Assert-WelaListenerAbsent {
+ param([object[]]$Listeners,$Selection)
+ foreach($row in $Listeners){if($row.Transport -ceq 'HTTP' -and ($row.Address -ceq '*' -or $row.Port -ceq '5985' -or $row.Address -ieq ('IP:'+$Selection.LocalAddress))){throw 'Existing HTTP5985, wildcard or selected listener conflicts; existing listeners are never changed.'}}
+}
+function Assert-WelaListenerCreated {
+ param($Listener,$Selection)
+ $expected=@{Address=('IP:'+$Selection.LocalAddress);Transport='HTTP';Port='5985';Hostname='';Enabled='true';URLPrefix='wsman';CertificateThumbprint=''}
+ foreach($name in $expected.Keys){if($Listener.$name -isnot [string] -or $Listener.$name -cne $expected[$name]){throw "Created listener $name differs from the fixed selection."}}
+ if($Listener.PolicyOwned -isnot [bool] -or $Listener.PolicyOwned -or $Listener.ListeningOn.Count -ne 1 -or $Listener.ListeningOn[0] -cne $Selection.LocalAddress){throw 'Created listener must be local and listen on exactly the selected IPv4 address.'}
+}
+function Get-WelaListenerSources {
+ $sources=[ordered]@{}
+ foreach($name in @('WELA.ps1','scripts/WecListener.ps1','scripts/WecListenerWorker.ps1','scripts/WecListenerPipeNative.cs','scripts/WefArrival.ps1','scripts/WecUpdate.ps1','scripts/ChannelRead.ps1','scripts/ChannelReadNative.cs','scripts/FirewallLoggingRecovery.ps1','modules/WefSubscriptions.psm1','modules/AuditProfiles.psm1','scripts/CustomAuditProfiles.ps1')){$sources[$name]=(Get-FileHash -LiteralPath (Join-Path $script:ScriptRoot $name) -Algorithm SHA256 -ErrorAction Stop).Hash.ToLowerInvariant()}
+ Get-WelaListenerKey $sources
+}
+function Get-WelaListenerReaderKey {
+ param($Reader)
+ Get-WelaListenerKey ([ordered]@{Computer=$Reader.Computer;UserSid=$Reader.UserSid;UserName=$Reader.UserName;AuthenticationId=$Reader.AuthenticationId;GroupSids=$Reader.GroupSids;GroupCount=$Reader.GroupCount;PrivilegeCount=$Reader.PrivilegeCount;ElevatedAdministrator=$Reader.ElevatedAdministrator;TokenType=$Reader.TokenType;Impersonation=$Reader.Impersonation})
+}
+function Read-WelaListenerPolicy {
+ # Refuse policy-owned WinRM settings; observe both native registry views without writing keys.
+ $observations=@()
+ foreach($view in @([Microsoft.Win32.RegistryView]::Registry64,[Microsoft.Win32.RegistryView]::Registry32)){
+ $base=[Microsoft.Win32.RegistryKey]::OpenBaseKey([Microsoft.Win32.RegistryHive]::LocalMachine,$view)
+ try {
+ $queue=@('SOFTWARE\Policies\Microsoft\Windows\WinRM');$visited=0
+ while($queue.Count){$path=$queue[0];$queue=@($queue|Select-Object -Skip 1);$visited++;if($visited -gt 32){throw 'WinRM policy key bound exceeded.'};$key=$base.OpenSubKey($path,$false)
+ try{if($null -eq $key){$observations+=[pscustomobject]@{View=[string]$view;Path=$path;Exists=$false};continue};if($key.ValueCount){throw 'Policy-owned WinRM settings require manual review; no policy is overwritten.'};$children=@($key.GetSubKeyNames()|Sort-Object);$observations+=[pscustomobject]@{View=[string]$view;Path=$path;Exists=$true;Children=$children};foreach($child in $children){$queue+=($path+'\'+$child)}}finally{if($key){$key.Dispose()}}
+ }
+ }finally{$base.Dispose()}
+ }
+ Get-WelaListenerKey $observations
+}
+function Read-WelaListenerWinrm {
+ $values=@(Microsoft.WSMan.Management\Get-WSManInstance -ResourceURI 'http://schemas.microsoft.com/wbem/wsman/1/config' -ErrorAction Stop)
+ if($values.Count -ne 1 -or -not $values[0].OuterXml -or $values[0].OuterXml.Length -gt 262144){throw 'WinRM configuration is missing, ambiguous or oversized.'}
+ $doc=Read-WelaWefXml ([string]$values[0].OuterXml)
+ if($doc.DocumentElement.LocalName -cne 'Config' -or $doc.DocumentElement.NamespaceURI -cne 'http://schemas.microsoft.com/wbem/wsman/1/config'){throw 'Unexpected native WinRM configuration.'}
+ [string]$doc.OuterXml
+}
+function Get-WelaListenerLocalState {
+ $reader=Get-WelaChannelReader
+ if(-not $reader.ElevatedAdministrator){throw 'The actual non-impersonated elevated administrator is required.'}
+ $services=@(Get-Service -Name WinRM,Winmgmt,BFE,MpsSvc -ErrorAction Stop|Sort-Object Name|ForEach-Object {[pscustomobject]@{Name=$_.Name;Status=[string]$_.Status}})
+ if($services.Count -ne 4 -or @($services|Where-Object Status -cne 'Running').Count){throw 'WinRM, Winmgmt, BFE and MpsSvc must already be running; no service is started.'}
+ $hostState=Get-WelaChannelReadHost
+ if($hostState.ProductType -ne 3 -or $hostState.DomainRole -notin @(2,3) -or $hostState.Build -notin @(20348,26100) -or $null -eq $hostState.UBR -or $hostState.UBR -lt 1){throw 'A reviewed patched native Server 2022/2025 standalone or member collector is required.'}
+ $guid=(Get-ItemProperty -LiteralPath 'HKLM:\SOFTWARE\Microsoft\Cryptography' -Name MachineGuid -ErrorAction Stop).MachineGuid;$parsed=[guid]::Empty
+ if($guid -isnot [string] -or -not [guid]::TryParse($guid,[ref]$parsed) -or $parsed -eq [guid]::Empty){throw 'Actual machine identity is unavailable.'}
+ $nativeServices=@(Get-CimInstance Win32_Service -Filter "Name='WinRM' OR Name='Wecsvc' OR Name='Winmgmt' OR Name='BFE' OR Name='MpsSvc'" -ErrorAction Stop|Sort-Object Name|Select-Object Name,State,StartMode)
+ if($nativeServices.Count -ne 5 -or @($nativeServices|Where-Object {$_.State -notin @('Running','Stopped') -or $_.StartMode -notin @('Auto','Manual','Disabled')}).Count){throw 'Complete stable collector service observations are required.'}
+ $addresses=@(NetTCPIP\Get-NetIPAddress -AddressFamily IPv4 -ErrorAction Stop|Sort-Object InterfaceIndex,IPAddress|Select-Object IPAddress,InterfaceIndex,PrefixLength,PrefixOrigin,SuffixOrigin,AddressState,SkipAsSource)
+ if($addresses.Count -lt 1 -or $addresses.Count -gt 128){throw 'Assigned address inventory is incomplete or excessive.'}
+ $state=[pscustomobject][ordered]@{Host=$hostState;MachineGuid=$parsed.ToString();Reader=$reader;Services=$nativeServices;Addresses=$addresses;Policy=Read-WelaListenerPolicy;WinrmXml=Read-WelaListenerWinrm;Listeners=@(Read-WelaListenerInventory)}
+ if((Get-WelaListenerKey (Get-WelaChannelReader)) -cne (Get-WelaListenerKey $reader)){throw 'Actual token changed during native observations.'}
+ $state
+}
+function Get-WelaListenerState {
+ $local=Get-WelaListenerLocalState;$native=Get-WelaFirewallRecoveryNativeSources;$profiles=@();$digests=@()
+ foreach($store in @('PersistentStore','ActiveStore')){
+ $rows=@(NetSecurity\Get-NetFirewallProfile -PolicyStore $store -ErrorAction Stop|Sort-Object Name)
+ if($rows.Count -ne 3){throw 'All three firewall profiles must be observed in both stores.'}
+ foreach($row in $rows){$profiles+=[pscustomobject]@{Store=$store;Profile=ConvertTo-WelaFirewallRecoveryCim $row @('Status','StatusCode','PrimaryStatus','OperationalStatus','InstanceID','InstanceId')}}
+ $digests+=@(Get-WelaFirewallRecoveryRuleDigest $store)
+ }
+ $engine=Join-Path ([Environment]::SystemDirectory) 'WindowsPowerShell/v1.0/powershell.exe';$worker=Join-Path $PSScriptRoot 'WecListenerWorker.ps1'
+ $cmd=Get-Command 'Microsoft.WSMan.Management\Get-WSManInstance' -CommandType Cmdlet -ErrorAction Stop;$assembly=$cmd.ImplementingType.Assembly.Location
+ if(-not $assembly -or $cmd.ModuleName -cne 'Microsoft.WSMan.Management'){throw 'Native WSMan reader source is unavailable.'}
+ [pscustomobject][ordered]@{Local=$local;Profiles=$profiles;Rules=$digests;NativeFirewall=$native;NativeReader=[ordered]@{Path=$assembly;Sha256=(Get-FileHash $assembly -Algorithm SHA256).Hash};Adapter=[ordered]@{Engine=$engine;EngineSha256=(Get-FileHash $engine -Algorithm SHA256).Hash;Worker=$worker;WorkerSha256=(Get-FileHash $worker -Algorithm SHA256).Hash};Sources=Get-WelaListenerSources}
+}
+function Get-WelaListenerReviewKey {
+ param($State,[switch]$ExcludeSelected,$Selection)
+ $copy=Get-WelaListenerKey $State|ConvertFrom-Json
+ $copy.Local.Reader.ProcessId=$null;$copy.Local.Reader.TokenId=$null;$copy.Local.Reader.ModifiedId=$null
+ if($ExcludeSelected){$copy.Local.Listeners=@($copy.Local.Listeners|Where-Object {-not($_.Address -ceq ('IP:'+$Selection.LocalAddress) -and $_.Transport -ceq 'HTTP')})}
+ Get-WelaListenerKey $copy
+}
+function Assert-WelaListenerSelectedHost {
+ param($State,$Selection)
+ if($State.Host.Computer.ToUpperInvariant() -cne $Selection.ComputerName -or @($State.Addresses|Where-Object {$_.IPAddress -ceq $Selection.LocalAddress -and [string]$_.AddressState -ceq 'Preferred'}).Count -ne 1){throw 'Selection must identify this actual computer and exactly one currently assigned Preferred IPv4 address.'}
+}
+function New-WelaListenerPayload {
+ '5985truewsman'
+}
+function Assert-WelaListenerPlan {
+ param($Plan)
+ Assert-WelaArrivalObject $Plan @('SchemaVersion','Kind','Selection','StateKey','RecordedUtc')
+ if(($Plan.SchemaVersion -isnot [int] -and $Plan.SchemaVersion -isnot [long]) -or $Plan.SchemaVersion -ne 1 -or $Plan.Kind -isnot [string] -or $Plan.Kind -cne 'WelaExactIpListenerPlan' -or $Plan.StateKey -isnot [string] -or -not $Plan.StateKey -or $Plan.StateKey.Length -gt 2097152){throw 'Unknown or mistyped listener plan.'}
+ Assert-WelaArrivalObject $Plan.Selection @('ComputerName','LocalAddress');$selection=Get-WelaListenerSelection $Plan.Selection.ComputerName $Plan.Selection.LocalAddress
+ if((Get-WelaListenerKey $selection) -cne (Get-WelaListenerKey $Plan.Selection)){throw 'Listener plan selection is not canonical.'};$null=ConvertTo-WelaArrivalUtc $Plan.RecordedUtc
+}
+function Get-WelaListenerWorkerContextKey {
+ param($Local)
+ $copy=Get-WelaListenerKey $Local|ConvertFrom-Json
+ $copy.Reader=Get-WelaListenerReaderKey $Local.Reader
+ Get-WelaListenerKey $copy
+}
+function Invoke-WelaListenerWorkerRequest {
+ param([string]$RequestPath,[string]$RequestHash)
+ $report=[pscustomobject][ordered]@{SchemaVersion=1;Kind='WelaNative51ListenerCreate';Status='Refused';NativeCreateAttempted=$false;ProcessId=$PID;Engine=[Diagnostics.Process]::GetCurrentProcess().MainModule.FileName;EngineVersion=$PSVersionTable.PSVersion.ToString();Reader=$null;Selection=$null;CreatedXml=$null;After=@();Diagnostic='';NativeHResult=$null}
+ $held=$null
+ try {
+ if($PSVersionTable.PSVersion.Major -ne 5 -or $RequestHash -cnotmatch '^[a-f0-9]{64}$'){throw 'A hashed fixed native Windows PowerShell5.1 request is required.'}
+ $file=Read-WelaWecUpdateFile $RequestPath;if($file.Hash -cne $RequestHash){throw 'Native request hash differs.'}
+ $request=ConvertFrom-WelaArrivalJson $file.Text
+ Assert-WelaArrivalObject $request @('SchemaVersion','Kind','Selection','ContextKey','Sources','EngineSha256','PayloadHash')
+ if(($request.SchemaVersion -isnot [int] -and $request.SchemaVersion -isnot [long]) -or $request.SchemaVersion -ne 1 -or $request.Kind -isnot [string] -or $request.Kind -cne 'WelaNative51ListenerRequest' -or $request.ContextKey -isnot [string] -or $request.Sources -isnot [string] -or $request.EngineSha256 -isnot [string] -or $request.PayloadHash -isnot [string] -or $request.PayloadHash -cnotmatch '^[a-f0-9]{64}$'){throw 'Unknown or mistyped native request.'}
+ Assert-WelaArrivalObject $request.Selection @('ComputerName','LocalAddress');$selection=Get-WelaListenerSelection $request.Selection.ComputerName $request.Selection.LocalAddress;$report.Selection=$selection
+ $expectedEngine=Join-Path ([Environment]::SystemDirectory) 'WindowsPowerShell/v1.0/powershell.exe'
+ if($report.Engine -ine $expectedEngine -or (Get-FileHash $expectedEngine -Algorithm SHA256).Hash -cne $request.EngineSha256 -or (Get-WelaListenerSources) -cne $request.Sources){throw 'Native adapter engine or installed sources differ.'}
+ $payloadPath=Join-Path (Split-Path $file.Path -Parent) 'native-payload.xml';$payload=Read-WelaWecUpdateFile $payloadPath 4096
+ if($payload.Hash -cne $request.PayloadHash -or $payload.Text -cne (New-WelaListenerPayload)){throw 'Native listener payload is not the exact fixed XML.'}
+ $held=[IO.File]::Open($payload.Path,[IO.FileMode]::Open,[IO.FileAccess]::Read,[IO.FileShare]::Read)
+ $local=Get-WelaListenerLocalState;$report.Reader=$local.Reader
+ Assert-WelaListenerSelectedHost $local $selection;Assert-WelaListenerAbsent $local.Listeners $selection
+ if((Get-WelaListenerWorkerContextKey $local) -cne $request.ContextKey -or (Read-WelaWecUpdateFile $RequestPath).Hash -cne $RequestHash -or (Read-WelaWecUpdateFile $payloadPath 4096).Hash -cne $request.PayloadHash -or (Get-WelaListenerSources) -cne $request.Sources){throw 'Fresh native adapter context, input or code differs.'}
+ $report.NativeCreateAttempted=$true
+ $created=@(Microsoft.WSMan.Management\New-WSManInstance -ResourceURI 'http://schemas.microsoft.com/wbem/wsman/1/config/listener' -SelectorSet @{Address=('IP:'+$selection.LocalAddress);Transport='HTTP'} -FilePath $payload.Path -ErrorAction Stop)
+ if($created.Count -ne 1 -or -not $created[0].OuterXml -or $created[0].OuterXml.Length -gt 32768){throw 'Native create response is incomplete or excessive.'};$report.CreatedXml=[string]$created[0].OuterXml
+ $after=Get-WelaListenerLocalState;$report.After=$after.Listeners
+ $chosen=@($after.Listeners|Where-Object {$_.Address -ceq ('IP:'+$selection.LocalAddress) -and $_.Transport -ceq 'HTTP'})
+ if($chosen.Count -ne 1){throw 'Native creation did not produce exactly one selected listener.'};Assert-WelaListenerCreated $chosen[0] $selection
+ $after.Listeners=@($after.Listeners|Where-Object {-not($_.Address -ceq ('IP:'+$selection.LocalAddress) -and $_.Transport -ceq 'HTTP')})
+ if((Get-WelaListenerWorkerContextKey $after) -cne $request.ContextKey -or (Get-WelaListenerKey $after.Reader) -cne (Get-WelaListenerKey $local.Reader) -or (Get-WelaListenerSources) -cne $request.Sources){throw 'Native adapter context, token, other listeners or source changed during creation.'}
+ $report.Status='Created'
+ }catch{$report.Status=if($report.NativeCreateAttempted){'CreateAttemptedUnverified'}else{'Refused'};$report.Diagnostic=$_.Exception.Message;$report.NativeHResult=$_.Exception.HResult;try{$report.After=@(Read-WelaListenerInventory)}catch{}}
+ finally{if($held){$held.Dispose()}}
+ $report
+}
+function Initialize-WelaListenerPipe {
+ $path=Join-Path $PSScriptRoot 'WecListenerPipeNative.cs';$bytes=[IO.File]::ReadAllBytes($path)
+ if($bytes.Length -gt 65536){throw 'Listener pipe source exceeds its bound.'};$hash=Get-WelaArrivalHash $bytes
+ if(-not('Wela.ListenerPipe.Bounded' -as [type])){
+ $source=[Text.UTF8Encoding]::new($false,$true).GetString($bytes).TrimStart([char]0xfeff)
+ if([regex]::Matches($source,'__WELA_SOURCE_SHA256__').Count -ne 1){throw 'Listener pipe source marker is missing or ambiguous.'}
+ Add-Type -TypeDefinition $source.Replace('__WELA_SOURCE_SHA256__',$hash) -ErrorAction Stop
+ }
+ if([Wela.ListenerPipe.Bounded]::SourceSha256 -cne $hash){throw 'Loaded listener pipe helper differs from its source.'}
+}
+function Close-WelaListenerAdapterProcess {
+ param($Process,$Result)
+ # Cleanup must never discard Started=true after a possibly mutating child ran.
+ if($Result.Started){
+ $exited=$false
+ try{$exited=$Process.HasExited}catch{$Result.Diagnostic+=' Adapter exit observation failed: '+$_.Exception.Message}
+ if(-not $exited){
+ try{$Process.Kill()}catch{$Result.Diagnostic+=' Adapter termination request failed: '+$_.Exception.Message}
+ try{$exited=$Process.WaitForExit(5000)}catch{$Result.Diagnostic+=' Adapter termination wait failed: '+$_.Exception.Message}
+ }
+ $Result.TerminationConfirmed=[bool]$exited
+ if(-not $exited){$Result.Diagnostic+=' Adapter termination is unconfirmed.'}
+ }
+ try{$Process.Dispose()}catch{$Result.Diagnostic+=' Adapter resource cleanup failed: '+$_.Exception.Message}
+}
+function Assert-WelaListenerAdapterReceipt {
+ param($Receipt,$State,[int]$ProcessId,[int]$ExitCode)
+ Assert-WelaArrivalObject $receipt @('SchemaVersion','Kind','Status','NativeCreateAttempted','ProcessId','Engine','EngineVersion','Reader','Selection','CreatedXml','After','Diagnostic','NativeHResult')
+ if(($receipt.SchemaVersion -isnot [int] -and $receipt.SchemaVersion -isnot [long]) -or $receipt.SchemaVersion -ne 1 -or $receipt.Kind -isnot [string] -or $receipt.Kind -cne 'WelaNative51ListenerCreate' -or $receipt.NativeCreateAttempted -isnot [bool] -or ($receipt.ProcessId -isnot [int] -and $receipt.ProcessId -isnot [long]) -or $receipt.ProcessId -ne $ProcessId -or $receipt.Engine -isnot [string] -or $receipt.Engine -ine $State.Adapter.Engine -or $receipt.EngineVersion -isnot [string] -or $receipt.EngineVersion -cnotmatch '^5\.1\.[0-9]+\.[0-9]+$' -or $receipt.Status -isnot [string] -or $receipt.Status -cnotin @('Created','Refused','CreateAttemptedUnverified') -or $receipt.Diagnostic -isnot [string]){throw 'Native adapter receipt has inconsistent identity or status.'}
+ if($receipt.Reader -and (Get-WelaListenerReaderKey $receipt.Reader) -cne (Get-WelaListenerReaderKey $State.Local.Reader)){throw 'Native adapter did not run under the reviewed actual account/logon.'}
+ if($receipt.Status -ceq 'Created' -and (-not $receipt.Reader -or -not $receipt.NativeCreateAttempted -or $ExitCode -ne 0 -or $receipt.Diagnostic)){throw 'Native adapter success receipt is incomplete.'}
+}
+function Start-WelaListenerAdapter {
+ param($State,[string]$RequestPath,[string]$RequestHash)
+ foreach($path in @($State.Adapter.Engine,$State.Adapter.Worker,$RequestPath)){if($path.Contains('"') -or $path.EndsWith('\') -or $path -match '[\x00-\x1f]'){throw 'Unsupported native adapter path.'}}
+ $info=[Diagnostics.ProcessStartInfo]::new();$info.FileName=$State.Adapter.Engine
+ $info.Arguments='-NoLogo -NoProfile -NonInteractive -File "'+$State.Adapter.Worker+'" -RequestPath "'+$RequestPath+'" -RequestHash '+$RequestHash
+ $info.UseShellExecute=$false;$info.CreateNoWindow=$true;$info.RedirectStandardOutput=$true;$info.RedirectStandardError=$true;$info.StandardOutputEncoding=[Text.UTF8Encoding]::new($false);$info.StandardErrorEncoding=[Text.UTF8Encoding]::new($false)
+ Initialize-WelaListenerPipe
+ $result=[pscustomobject][ordered]@{Started=$false;ProcessId=$null;ExitCode=$null;TimedOut=$false;TerminationConfirmed=$false;Receipt=$null;Diagnostic=''};$process=[Diagnostics.Process]::new();$process.StartInfo=$info
+ try {
+ if(-not $process.Start()){throw 'Native listener adapter did not start.'};$result.Started=$true;$result.ProcessId=$process.Id
+ $stdout=[Wela.ListenerPipe.Bounded]::Read($process.StandardOutput,524288);$stderr=[Wela.ListenerPipe.Bounded]::Read($process.StandardError,65536)
+ if(-not $process.WaitForExit(45000)){$result.TimedOut=$true;throw 'Native listener adapter timed out; creation may have been attempted.'}
+ $result.ExitCode=$process.ExitCode
+ if(-not [Threading.Tasks.Task]::WaitAll([Threading.Tasks.Task[]]@($stdout,$stderr),5000)){throw 'Native listener adapter output drain timed out.'}
+ $text=$stdout.Result;$errorText=$stderr.Result
+ if($errorText -or $text.Length -gt 524288){throw 'Native adapter output is incomplete, excessive or contains errors.'}
+ $receipt=ConvertFrom-WelaArrivalJson $text
+ Assert-WelaListenerAdapterReceipt $receipt $State $result.ProcessId $result.ExitCode
+ $result.Receipt=$receipt
+ }catch{$result.Diagnostic=$_.Exception.Message}
+ finally{Close-WelaListenerAdapterProcess $process $result}
+ $result
+}
+function Invoke-WelaWecListener {
+ param([ValidateSet('Plan','Apply')][string]$Action='Plan',[string]$ComputerName,[string]$LocalAddress,[string]$PlanPath,[string]$PlanHash,[string]$OutputPath)
+ if($args.Count){throw 'Unknown listener arguments are not supported.'}
+ if($Action -eq 'Plan'){
+ if(-not $ComputerName -or -not $LocalAddress -or -not $OutputPath -or $PSBoundParameters.ContainsKey('PlanPath') -or $PSBoundParameters.ContainsKey('PlanHash')){throw 'Plan requires the actual computer, assigned IPv4 and new output only.'}
+ $selection=Get-WelaListenerSelection $ComputerName $LocalAddress;$reviewedFile=$null
+ }else{
+ if(-not $PlanPath -or $PlanHash -cnotmatch '^[a-fA-F0-9]{64}$' -or -not $OutputPath -or $PSBoundParameters.ContainsKey('ComputerName') -or $PSBoundParameters.ContainsKey('LocalAddress')){throw 'Apply requires only a reviewed plan, SHA256 and new output.'}
+ $PlanHash=$PlanHash.ToLowerInvariant();$reviewedFile=Read-WelaWecUpdateFile $PlanPath
+ }
+ $source=if($reviewedFile){$reviewedFile.Path}else{Join-Path $script:ScriptRoot 'WELA.ps1'};$output=New-WelaArrivalOutput $OutputPath $source
+ $report=[pscustomobject][ordered]@{SchemaVersion=1;Kind='WelaExactIpListener';Action=$Action;Status='Refused';ExitCode=1;OutputPath=$output;PlanHash=$null;AdapterStarted=$false;NativeCreateAttempted=$null;Adapter=$null;Artifacts=@();Diagnostic='';ReadyRuleCredit=0;ServiceChanges=0;AuthenticationChanges=0;FirewallChanges=0;Scope='One new exact assigned-IPv4 HTTP5985/wsman listener through a fixed native Windows PowerShell5.1 adapter. Existing WinRM endpoints may use it. No remote connection, WEF delivery, packet acceptance, retention or Sigma proof. Sysmon excluded.'}
+ try {
+ $state=Get-WelaListenerState;$key=Get-WelaListenerReviewKey $state;$tokenKey=Get-WelaListenerKey $state.Local.Reader
+ if($Action -eq 'Apply'){
+ if($reviewedFile.Hash -cne $PlanHash){throw 'Reviewed listener plan hash differs.'};$plan=ConvertFrom-WelaArrivalJson $reviewedFile.Text;Assert-WelaListenerPlan $plan
+ if($plan.StateKey -cne $key){throw 'Reviewed host/operator/code/listener/WinRM/firewall state differs.'};$selection=Get-WelaListenerSelection $plan.Selection.ComputerName $plan.Selection.LocalAddress;$report.PlanHash=$PlanHash
+ }
+ Assert-WelaListenerSelectedHost $state.Local $selection;Assert-WelaListenerAbsent $state.Local.Listeners $selection
+ if($Action -eq 'Plan'){
+ $plan=[pscustomobject][ordered]@{SchemaVersion=1;Kind='WelaExactIpListenerPlan';Selection=$selection;StateKey=$key;RecordedUtc=[DateTime]::UtcNow.ToString('o')};Assert-WelaListenerPlan $plan
+ $fresh=Get-WelaListenerState;if((Get-WelaListenerReviewKey $fresh) -cne $key -or (Get-WelaListenerKey $fresh.Local.Reader) -cne $tokenKey){throw 'Context changed during listener planning.'};Assert-WelaListenerAbsent $fresh.Local.Listeners $selection
+ $artifact=Write-WelaWecUpdateArtifact $output 'plan.json' ($plan|ConvertTo-Json -Depth 24);$report.Artifacts+=$artifact;$report.PlanHash=$artifact.Sha256;$report.Status='ReviewRequired';$report.ExitCode=0
+ }else{
+ $report.Artifacts+=Write-WelaWecUpdateArtifact $output 'reviewed-plan.json' $reviewedFile.Text
+ $report.Artifacts+=Write-WelaWecUpdateArtifact $output 'before-create.json' ([ordered]@{Status='Pending';PlanHash=$PlanHash;Selection=$selection;State=$state;RecordedUtc=[DateTime]::UtcNow.ToString('o')}|ConvertTo-Json -Depth 24)
+ $payload=Write-WelaWecUpdateArtifact $output 'native-payload.xml' (New-WelaListenerPayload);$report.Artifacts+=$payload
+ $request=[ordered]@{SchemaVersion=1;Kind='WelaNative51ListenerRequest';Selection=$selection;ContextKey=(Get-WelaListenerWorkerContextKey $state.Local);Sources=$state.Sources;EngineSha256=$state.Adapter.EngineSha256;PayloadHash=$payload.Sha256}
+ $artifact=Write-WelaWecUpdateArtifact $output 'native-request.json' ($request|ConvertTo-Json -Depth 24);$report.Artifacts+=$artifact
+ $fresh=Get-WelaListenerState;if((Get-WelaListenerReviewKey $fresh) -cne $key -or (Get-WelaListenerKey $fresh.Local.Reader) -cne $tokenKey -or (Read-WelaWecUpdateFile $PlanPath).Hash -cne $PlanHash){throw 'Plan or actual state changed immediately before creation.'};Assert-WelaListenerAbsent $fresh.Local.Listeners $selection
+ $adapter=Start-WelaListenerAdapter $state (Join-Path $output 'native-request.json') $artifact.Sha256;$report.Adapter=$adapter;$report.AdapterStarted=$adapter.Started
+ if($adapter.Receipt){$report.NativeCreateAttempted=$adapter.Receipt.NativeCreateAttempted}
+ $report.Artifacts+=Write-WelaWecUpdateArtifact $output 'adapter-receipt.json' ($adapter|ConvertTo-Json -Depth 24)
+ $after=Get-WelaListenerState;$report.Artifacts+=Write-WelaWecUpdateArtifact $output 'after-state.json' ($after|ConvertTo-Json -Depth 24)
+ if($adapter.Diagnostic -or -not $adapter.Receipt -or $adapter.Receipt.Status -cne 'Created' -or -not $adapter.Receipt.NativeCreateAttempted -or (Get-WelaListenerKey $adapter.Receipt.Selection) -cne (Get-WelaListenerKey $selection)){throw ('Native creation is unverified: '+$adapter.Diagnostic+' '+$adapter.Receipt.Diagnostic)}
+ $selected=@($after.Local.Listeners|Where-Object {$_.Address -ceq ('IP:'+$selection.LocalAddress) -and $_.Transport -ceq 'HTTP'});if($selected.Count -ne 1){throw 'Expected exactly one created listener.'};Assert-WelaListenerCreated $selected[0] $selection
+ if((Get-WelaListenerReviewKey $after -ExcludeSelected -Selection $selection) -cne $key -or (Get-WelaListenerKey $after.Local.Reader) -cne $tokenKey -or (Read-WelaWecUpdateFile $PlanPath).Hash -cne $PlanHash){throw 'Host/token/code/plan, other listeners, WinRM or firewall configuration changed during creation.'}
+ $report.Status='CreatedAndVerified';$report.ExitCode=0
+ }
+ }catch{
+ $report.Status=if($report.AdapterStarted -and ($null -eq $report.NativeCreateAttempted -or $report.NativeCreateAttempted)){'CreateAttemptedUnverified'}else{'Refused'};$report.Diagnostic=$_.Exception.Message
+ if($report.AdapterStarted -and -not @($report.Artifacts|Where-Object Name -eq 'after-state.json').Count){try{$report.Artifacts+=Write-WelaWecUpdateArtifact $output 'after-state.json' ((Get-WelaListenerState)|ConvertTo-Json -Depth 24)}catch{}}
+ }
+ $null=Write-WelaWecUpdateArtifact $output 'manifest.json' ($report|ConvertTo-Json -Depth 24);$report
+}
diff --git a/scripts/WecListenerPipeNative.cs b/scripts/WecListenerPipeNative.cs
new file mode 100644
index 00000000..6bcbc6d5
--- /dev/null
+++ b/scripts/WecListenerPipeNative.cs
@@ -0,0 +1,19 @@
+using System;
+using System.IO;
+using System.Text;
+using System.Threading.Tasks;
+namespace Wela.ListenerPipe {
+ public static class Bounded {
+ public const string SourceSha256 = "__WELA_SOURCE_SHA256__";
+ public static async Task Read(TextReader reader, int maximumCharacters) {
+ if (reader == null || maximumCharacters < 1 || maximumCharacters > 1048576) throw new ArgumentException("Invalid bounded reader.");
+ var text = new StringBuilder(); var buffer = new char[1024];
+ while (true) {
+ int count = await reader.ReadAsync(buffer, 0, buffer.Length).ConfigureAwait(false);
+ if (count == 0) return text.ToString();
+ if (count > maximumCharacters - text.Length) throw new InvalidDataException("Native listener adapter output exceeded its character bound.");
+ text.Append(buffer, 0, count);
+ }
+ }
+ }
+}
diff --git a/scripts/WecListenerWorker.ps1 b/scripts/WecListenerWorker.ps1
new file mode 100644
index 00000000..74dc2b43
--- /dev/null
+++ b/scripts/WecListenerWorker.ps1
@@ -0,0 +1,13 @@
+param([string]$RequestPath,[string]$RequestHash)
+$ErrorActionPreference='Stop';[Console]::OutputEncoding=[Text.UTF8Encoding]::new($false)
+if($args.Count -or $PSVersionTable.PSVersion.Major -ne 5 -or -not [Environment]::Is64BitProcess){throw 'Only the fixed native Windows PowerShell 5.1 listener adapter is supported.'}
+$script:ScriptRoot=Split-Path $PSScriptRoot -Parent
+Import-Module (Join-Path $script:ScriptRoot 'modules/AuditProfiles.psm1') -Force
+Import-Module (Join-Path $script:ScriptRoot 'modules/WefSubscriptions.psm1') -Force
+. (Join-Path $PSScriptRoot 'WefArrival.ps1')
+. (Join-Path $PSScriptRoot 'WecUpdate.ps1')
+. (Join-Path $PSScriptRoot 'ChannelRead.ps1')
+. (Join-Path $PSScriptRoot 'WecListener.ps1')
+$report=Invoke-WelaListenerWorkerRequest $RequestPath $RequestHash
+$report|ConvertTo-Json -Depth 24 -Compress
+if($report.Status -cne 'Created'){exit 1}
diff --git a/tests/WecListener.Tests.ps1 b/tests/WecListener.Tests.ps1
new file mode 100644
index 00000000..239d30c6
--- /dev/null
+++ b/tests/WecListener.Tests.ps1
@@ -0,0 +1,105 @@
+$ErrorActionPreference='Stop';$repo=Split-Path $PSScriptRoot -Parent;$script:ScriptRoot=$repo
+Import-Module "$repo/modules/AuditProfiles.psm1" -Force
+Import-Module "$repo/modules/WefSubscriptions.psm1" -Force
+. "$repo/scripts/WefArrival.ps1"
+. "$repo/scripts/WecUpdate.ps1"
+. "$repo/scripts/WecListener.ps1"
+$count=0
+function Assert($Value,$Message){if(-not $Value){throw $Message};$script:count++}
+function Reject([scriptblock]$Action,[string]$Pattern='.'){$message='';try{&$Action|Out-Null}catch{$message=$_.Exception.Message};Assert ($message -match $Pattern) "Expected refusal $Pattern, got: $message; input: $bad; action: $Action"}
+function Copy-TestListener($Value){Get-WelaListenerKey $Value|ConvertFrom-Json}
+$selection=Get-WelaListenerSelection 'test-host' '192.0.2.10'
+$xml='IP:192.0.2.10HTTP5985truewsman192.0.2.10'
+Assert ($selection.ComputerName -ceq 'TEST-HOST') 'Actual computer selection is canonical.'
+foreach($bad in @('*','IP:192.0.2.10','192.0.2.10/32','192.0.2.0/24','192.0.2.01','010.1.2.3','127.0.0.1','0.0.0.0','169.254.1.2','224.0.0.1','255.255.255.255','256.1.2.3','1.2.3','example.test','::1','',' 192.0.2.10')){Reject {Get-WelaListenerSelection 'TEST' $bad}}
+foreach($bad in @('','test.example','*','-TEST','TEST HOST','TEST/OTHER')){Reject {Get-WelaListenerSelection $bad '192.0.2.10'}}
+Reject {Get-WelaListenerSelection $true '192.0.2.10'};Reject {Get-WelaListenerSelection 'TEST' $true}
+$listener=ConvertFrom-WelaListenerXml $xml;Assert-WelaListenerCreated $listener $selection;$count++
+Assert ($listener.ListeningOn.Count -eq 1 -and -not $listener.PolicyOwned) 'Actual native shape has exact address and local provenance.'
+foreach($bad in @($xml.Replace('5985',''),$xml.Replace('','true'),$xml.Replace('cfg:Port','cfg:Unknown'),$xml.Replace('http://schemas.microsoft.com/wbem/wsman/1/config/listener','urn:wrong'),$xml.Replace('',''),$xml.Replace('',''),$xml.Replace('',''),$xml.Replace('>true<','>True<'),$xml.Replace('>5985<','>05985<'),$xml.Replace('','192.0.2.10'),$xml.Replace('>192.0.2.10<','>not-an-address<'),(']>'+$xml))){Reject {ConvertFrom-WelaListenerXml $bad}}
+foreach($name in @('Address','Transport','Port','Hostname','Enabled','URLPrefix','CertificateThumbprint')){$copy=Copy-TestListener $listener;$copy.$name='unexpected';Reject {Assert-WelaListenerCreated $copy $selection} 'differs'}
+$copy=Copy-TestListener $listener;$copy.ListeningOn=@('192.0.2.10','192.0.2.11');Reject {Assert-WelaListenerCreated $copy $selection} 'exactly'
+$copy=Copy-TestListener $listener;$copy.PolicyOwned=$true;Reject {Assert-WelaListenerCreated $copy $selection} 'local'
+$copy=Copy-TestListener $listener;$copy.PolicyOwned='False';Reject {Assert-WelaListenerCreated $copy $selection} 'local'
+$owned=ConvertFrom-WelaListenerXml ($xml.Replace('',''));Assert $owned.PolicyOwned 'Native GPO provenance remains explicit.'
+Reject {Assert-WelaListenerAbsent @($listener) $selection} 'Existing'
+$copy=Copy-TestListener $listener;$copy.Address='*';$copy.Port='6000';Reject {Assert-WelaListenerAbsent @($copy) $selection} 'Existing'
+$copy=Copy-TestListener $listener;$copy.Address='IP:192.0.2.11';Reject {Assert-WelaListenerAbsent @($copy) $selection} 'Existing'
+$copy=Copy-TestListener $listener;$copy.Port='6000';Reject {Assert-WelaListenerAbsent @($copy) $selection} 'Existing'
+$other=Copy-TestListener $listener;$other.Address='*';$other.Transport='HTTPS';$other.Port='5986';$other.ListeningOn=@('192.0.2.10');Assert-WelaListenerAbsent @($other) $selection;$count++
+$reader=[pscustomobject][ordered]@{Computer='TEST-HOST';ProcessId=100;UserSid='S-1-5-21-1-2-3-1001';UserName='TEST-HOST\operator';TokenId='111';ModifiedId='222';AuthenticationId='333';GroupSids=@('S-1-5-32-544');GroupCount=1;PrivilegeCount=20;ElevatedAdministrator=$true;TokenType='Primary';Impersonation='Absent'}
+$baseline=[pscustomobject][ordered]@{Local=[pscustomobject][ordered]@{Host=@{Computer='TEST-HOST';Build=26100;UBR=123;ProductType=3;DomainRole=2};MachineGuid='00000000-0000-0000-0000-000000000001';Reader=$reader;Services=@(@{Name='WinRM';State='Running';StartMode='Auto'});Addresses=@(@{IPAddress='192.0.2.10';AddressState='Preferred'});Policy='empty';WinrmXml='';Listeners=@($other)};Profiles=@('protected');Rules=@('digest');NativeFirewall='native';NativeReader='native-reader';Adapter=@{Engine='native51';EngineSha256='a'*64;Worker='fixed-worker';WorkerSha256='b'*64};Sources='sources'}
+$changed=Copy-TestListener $baseline;$changed.Local.Reader.ProcessId=101;$changed.Local.Reader.TokenId='different';$changed.Local.Reader.ModifiedId='other';Assert ((Get-WelaListenerReviewKey $changed) -ceq (Get-WelaListenerReviewKey $baseline)) 'Plans permit separate processes in the same actual logon.'
+$changed.Local.Reader.AuthenticationId='444';Assert ((Get-WelaListenerReviewKey $changed) -cne (Get-WelaListenerReviewKey $baseline)) 'Different logon requires a new plan.'
+Assert-WelaListenerSelectedHost $baseline.Local $selection;$count++
+$changed=Copy-TestListener $baseline;$changed.Local.Addresses[0].AddressState='Tentative';Reject {Assert-WelaListenerSelectedHost $changed.Local $selection} 'Preferred'
+$changed=Copy-TestListener $baseline;$changed.Local.Host.Computer='OTHER';Reject {Assert-WelaListenerSelectedHost $changed.Local $selection} 'actual'
+$plan=[pscustomobject]@{SchemaVersion=1;Kind='WelaExactIpListenerPlan';Selection=$selection;StateKey=(Get-WelaListenerReviewKey $baseline);RecordedUtc='2026-09-21T00:00:00Z'};Assert-WelaListenerPlan $plan;$count++
+foreach($field in @('SchemaVersion','Kind','StateKey')){$bad=Copy-TestListener $plan;$bad.$field=$true;Reject {Assert-WelaListenerPlan $bad} 'mistyped'}
+$bad=Copy-TestListener $plan;$bad.Selection.ComputerName='test-host';Reject {Assert-WelaListenerPlan $bad} 'canonical'
+$bad=Copy-TestListener $plan;$bad|Add-Member NoteProperty Extra true;Reject {Assert-WelaListenerPlan $bad}
+Initialize-WelaListenerPipe
+$textReader=[IO.StringReader]::new('bounded');try{$task=[Wela.ListenerPipe.Bounded]::Read($textReader,7);Assert ($task.GetAwaiter().GetResult() -ceq 'bounded') 'Bounded stream reads complete content.'}finally{$textReader.Dispose()}
+$textReader=[IO.StringReader]::new('x'*65536);try{Reject {$task=[Wela.ListenerPipe.Bounded]::Read($textReader,1024);$task.GetAwaiter().GetResult()} 'bound'}finally{$textReader.Dispose()}
+foreach($failure in @('kill','wait','dispose')){
+ $fake=[pscustomobject]@{HasExited=$false;Mode=$failure}
+ $fake|Add-Member ScriptMethod Kill {if($this.Mode -eq 'kill'){throw 'Natural-exit race'}}
+ $fake|Add-Member ScriptMethod WaitForExit {param($Timeout);if($this.Mode -eq 'wait'){throw 'Wait failed'};return $true}
+ $fake|Add-Member ScriptMethod Dispose {if($this.Mode -eq 'dispose'){throw 'Dispose failed'}}
+ $result=[pscustomobject]@{Started=$true;TerminationConfirmed=$false;Diagnostic=''};Close-WelaListenerAdapterProcess $fake $result
+ Assert ($result.Started -and $result.Diagnostic) "Possible creation remains recorded after $failure cleanup failure."
+ Assert ($result.TerminationConfirmed -eq ($failure -ne 'wait')) 'Termination certainty is separately retained.'
+}
+$receipt=[pscustomobject]@{SchemaVersion=1;Kind='WelaNative51ListenerCreate';Status='Created';NativeCreateAttempted=$true;ProcessId=123;Engine='native51';EngineVersion='5.1.26100.1';Reader=$reader;Selection=$selection;CreatedXml='';After=@($listener);Diagnostic='';NativeHResult=$null}
+Assert-WelaListenerAdapterReceipt $receipt $baseline 123 0;$count++
+foreach($field in @('Kind','Engine','EngineVersion','Status','ProcessId','SchemaVersion')){$bad=Copy-TestListener $receipt;$bad.$field=$true;Reject {Assert-WelaListenerAdapterReceipt $bad $baseline 123 0} 'identity|status'}
+$bad=Copy-TestListener $receipt;$bad.Reader.AuthenticationId='OTHER';Reject {Assert-WelaListenerAdapterReceipt $bad $baseline 123 0} 'logon'
+Reject {Assert-WelaListenerAdapterReceipt $receipt $baseline 124 0} 'identity'
+Reject {Assert-WelaListenerAdapterReceipt $receipt $baseline 123 1} 'success'
+$root=Join-Path ([IO.Path]::GetTempPath()) ('wela-listener-tests-'+[guid]::NewGuid().ToString('N'));$null=New-Item -ItemType Directory $root
+$script:mode='ok';$script:reads=0;$script:starts=0;$script:created=$false
+function Get-WelaListenerState {
+ $script:reads++;$state=Copy-TestListener $baseline
+ if($script:created){$state.Local.Listeners=@((Copy-TestListener $other),(Copy-TestListener $listener))}
+ if($script:mode -eq 'race' -and $script:reads -eq 2){$state.Local.WinrmXml=''}
+ if($script:created -and $script:mode -eq 'firewall-drift'){$state.Rules=@('changed')}
+ if($script:created -and $script:mode -eq 'token-drift'){$state.Local.Reader.ModifiedId='changed'}
+ if($script:created -and $script:mode -eq 'broader'){$state.Local.Listeners[1].ListeningOn=@('192.0.2.10','192.0.2.11')}
+ $state
+}
+function Start-WelaListenerAdapter {
+ param($State,$RequestPath,$RequestHash)
+ $script:starts++;$dir=Split-Path $RequestPath -Parent
+ $intent=Get-Content (Join-Path $dir 'before-create.json') -Raw|ConvertFrom-Json
+ Assert ($intent.Status -ceq 'Pending' -and (Read-WelaWecUpdateFile $RequestPath).Hash -ceq $RequestHash -and (Get-Content (Join-Path $dir 'native-payload.xml') -Raw) -ceq (New-WelaListenerPayload)) 'Durable intent and fixed payload precede adapter startup.'
+ if($script:mode -eq 'timeout'){return [pscustomobject]@{Started=$true;Receipt=$null;Diagnostic='timeout';TerminationConfirmed=$false}}
+ $reply=Copy-TestListener $receipt
+ if($script:mode -eq 'refused'){$reply.Status='Refused';$reply.NativeCreateAttempted=$false;$reply.Diagnostic='fresh worker context differs'}else{$script:created=$true}
+ if($script:mode -eq 'native-error'){$reply.Status='CreateAttemptedUnverified';$reply.Diagnostic='native failed'}
+ [pscustomobject]@{Started=$true;Receipt=$reply;Diagnostic=$(if($script:mode -eq 'cleanup-error'){'cleanup failed'}else{''});TerminationConfirmed=$true}
+}
+try {
+ foreach($scenario in @('ok','hash','schema','duplicate-json','context','race','refused','timeout','native-error','firewall-drift','token-drift','broader','cleanup-error','replay')){
+ $script:mode='ok';$script:reads=0;$script:starts=0;$script:created=$false
+ $planned=Invoke-WelaWecListener -ComputerName 'TEST-HOST' -LocalAddress '192.0.2.10' -OutputPath (Join-Path $root ($scenario+'-plan'))
+ Assert ($planned.Status -ceq 'ReviewRequired' -and $planned.ExitCode -eq 0 -and $script:starts -eq 0) "Plan reads only: $($planned.Diagnostic)"
+ $path=Join-Path $planned.OutputPath 'plan.json';$hash=$planned.PlanHash
+ if($scenario -eq 'hash'){$hash='f'*64}
+ if($scenario -in @('schema','duplicate-json','context')){
+ $text=[IO.File]::ReadAllText($path)
+ if($scenario -eq 'schema'){$text=$text.Replace('"SchemaVersion": 1','"SchemaVersion": true')}
+ if($scenario -eq 'duplicate-json'){$text=$text.Replace('"SchemaVersion":','"SchemaVersion":1,"SchemaVersion":')}
+ if($scenario -eq 'context'){$text=$text.Replace('TEST-HOST','OTHER-HOST')}
+ [IO.File]::WriteAllText($path,$text);$hash=(Get-FileHash $path).Hash.ToLowerInvariant()
+ }
+ $script:mode=$scenario;$script:reads=0;$applied=Invoke-WelaWecListener Apply -PlanPath $path -PlanHash $hash -OutputPath (Join-Path $root ($scenario+'-apply'))
+ Assert (($applied.ExitCode -eq 0) -eq ($scenario -in @('ok','replay'))) "Outcome $scenario : $($applied.Diagnostic)"
+ Assert ($applied.ReadyRuleCredit -eq 0 -and $applied.ServiceChanges -eq 0 -and $applied.FirewallChanges -eq 0 -and (Test-Path (Join-Path $applied.OutputPath 'manifest.json'))) 'No unrelated changes or detection credit; final receipt retained.'
+ foreach($artifact in $applied.Artifacts){Assert ((Get-FileHash (Join-Path $applied.OutputPath $artifact.Name)).Hash.ToLowerInvariant() -ceq $artifact.Sha256) 'Receipt artifact hash matches actual bytes.'}
+ if($scenario -in @('hash','schema','duplicate-json','context','race')){Assert ($script:starts -eq 0 -and $applied.Status -ceq 'Refused') 'Refusal precedes any adapter start.'}
+ if($scenario -in @('timeout','native-error','firewall-drift','token-drift','broader','cleanup-error')){Assert ($applied.AdapterStarted -and $applied.Status -ceq 'CreateAttemptedUnverified') 'Possible native creation is never mislabeled Refused.'}
+ if($scenario -eq 'refused'){Assert ($applied.Status -ceq 'Refused' -and $applied.NativeCreateAttempted -eq $false) 'Authenticated native refusal before create stays distinct.'}
+ if($scenario -eq 'replay'){$again=Invoke-WelaWecListener Apply -PlanPath $path -PlanHash $hash -OutputPath (Join-Path $root 'replay-again');Assert ($again.Status -ceq 'Refused' -and $script:starts -eq 1) 'Applied plan cannot be replayed.'}
+ }
+}finally{Remove-Item -LiteralPath $root -Recurse -Force}
+Write-Host "PASS: $count focused WEC listener assertions. No native listener proof is claimed."
diff --git a/tests/WecListener.Windows.Tests.ps1 b/tests/WecListener.Windows.Tests.ps1
new file mode 100644
index 00000000..639670c4
--- /dev/null
+++ b/tests/WecListener.Windows.Tests.ps1
@@ -0,0 +1,124 @@
+param([switch]$AllowDisposableListenerReplacement)
+$ErrorActionPreference='Stop'
+if([Environment]::OSVersion.Platform -ne [PlatformID]::Win32NT -or -not $AllowDisposableListenerReplacement -or $env:GITHUB_ACTIONS -ne 'true' -or $env:RUNNER_ENVIRONMENT -ne 'github-hosted'){throw 'Explicit disposable GitHub-hosted Windows listener replacement opt-in required.'}
+$repo=Split-Path $PSScriptRoot -Parent;$script:ScriptRoot=$repo
+Import-Module "$repo/modules/AuditProfiles.psm1" -Force
+Import-Module "$repo/modules/WefSubscriptions.psm1" -Force
+. "$repo/scripts/Configuration.ps1"
+. "$repo/scripts/NativeChannelConfiguration.ps1"
+. "$repo/scripts/WefDeployment.ps1"
+. "$repo/scripts/WefArrival.ps1"
+. "$repo/scripts/WecUpdate.ps1"
+. "$repo/scripts/ChannelRead.ps1"
+. "$repo/scripts/FirewallLoggingRecovery.ps1"
+. "$repo/scripts/WecListener.ps1"
+$engine=(Get-Process -Id $PID).Path
+function Public([string[]]$Arguments,[int]$Code=0){
+ $prior=$ErrorActionPreference;try{$ErrorActionPreference='Continue';$text=&$engine -NoLogo -NoProfile -NonInteractive -File "$repo/WELA.ps1" @Arguments 2>&1|Out-String;$actual=$LASTEXITCODE}finally{$ErrorActionPreference=$prior}
+ if($actual -ne $Code){Write-Host $text;Get-ChildItem $root -Filter manifest.json -Recurse|ForEach-Object {Write-Host (Get-Content $_.FullName -Raw)};throw "Public command exit $actual differs from expected $Code"}
+}
+
+$root=Join-Path $env:RUNNER_TEMP ('wela-listener-native-'+[guid]::NewGuid().ToString('N'));$null=New-Item -ItemType Directory $root
+function Save($Name,$Value){$Value|ConvertTo-Json -Depth 20|Set-Content -LiteralPath (Join-Path $root $Name) -Encoding UTF8}
+function ReadListeners {
+ @(Microsoft.WSMan.Management\Get-WSManInstance -ResourceURI 'http://schemas.microsoft.com/wbem/wsman/1/config/listener' -Enumerate -ErrorAction Stop|ForEach-Object {
+ [pscustomobject][ordered]@{Address=[string]$_.Address;Transport=[string]$_.Transport;Port=[string]$_.Port;Hostname=[string]$_.Hostname;Enabled=[string]$_.Enabled;URLPrefix=[string]$_.URLPrefix;CertificateThumbprint=[string]$_.CertificateThumbprint;ListeningOn=@($_.ListeningOn|ForEach-Object {[string]$_}|Sort-Object);RawXml=$_.OuterXml}
+ }|Sort-Object Address,Transport)
+}
+function Key($Value){ConvertTo-Json -InputObject @($Value|Select-Object Address,Transport,Port,Hostname,Enabled,URLPrefix,CertificateThumbprint,ListeningOn) -Depth 10 -Compress}
+function ReadServices {@(Get-CimInstance Win32_Service -Filter "Name='WinRM' OR Name='Wecsvc' OR Name='MpsSvc' OR Name='BFE'"|Sort-Object Name|Select-Object Name,StartMode,State)}
+function ReadFirewall {@(NetSecurity\Get-NetFirewallRule -PolicyStore ActiveStore|Sort-Object Name|Select-Object Name,Enabled,Profile,Direction,Action,PolicyStoreSourceType)}
+$adapter=Join-Path $root 'checkpoint-native51.ps1'
+@'
+param([string]$ListenerAddress,[string]$PayloadPath)
+$ErrorActionPreference='Stop';[Console]::OutputEncoding=[Text.UTF8Encoding]::new($false)
+$identity=[Security.Principal.WindowsIdentity]::GetCurrent();try{$sid=$identity.User.Value}finally{$identity.Dispose()}
+$r=[ordered]@{EngineMajor=$PSVersionTable.PSVersion.Major;Engine=$PSVersionTable.PSVersion.ToString();ProcessId=$PID;UserSid=$sid;Status='Failed';Xml='';Diagnostic=''}
+try {
+ if($PSVersionTable.PSVersion.Major -ne 5 -or $ListenerAddress -notmatch '^(\*|IP:[0-9.]+)$'){throw 'Only fixture native5.1 HTTP selectors are supported.'}
+ $held=[IO.File]::Open($PayloadPath,[IO.FileMode]::Open,[IO.FileAccess]::Read,[IO.FileShare]::Read)
+ try {$v=Microsoft.WSMan.Management\New-WSManInstance -ResourceURI 'http://schemas.microsoft.com/wbem/wsman/1/config/listener' -SelectorSet @{Address=$ListenerAddress;Transport='HTTP'} -FilePath $PayloadPath -ErrorAction Stop;$r.Xml=[string]$v.OuterXml;$r.Status='Created'}finally{$held.Dispose()}
+}catch{$r.Diagnostic=$_.ToString()}
+$r|ConvertTo-Json -Compress
+if($r.Status -ne 'Created'){exit 1}
+'@|Set-Content -LiteralPath $adapter -Encoding UTF8
+function NewCheckpointListener($Selector,$Values) {
+ $doc=[Xml.XmlDocument]::new();$element=$doc.CreateElement('cfg','Listener','http://schemas.microsoft.com/wbem/wsman/1/config/listener');$null=$doc.AppendChild($element)
+ foreach($name in @('Port','Hostname','Enabled','URLPrefix','CertificateThumbprint')){$child=$doc.CreateElement('cfg',$name,$element.NamespaceURI);$child.InnerText=[string]$Values[$name];$null=$element.AppendChild($child)}
+ $payload=Join-Path $root ('native-listener-'+[guid]::NewGuid().ToString('N')+'.xml');[IO.File]::WriteAllText($payload,$doc.OuterXml,[Text.UTF8Encoding]::new($false))
+ $info=[Diagnostics.ProcessStartInfo]::new();$info.FileName=Join-Path ([Environment]::SystemDirectory) 'WindowsPowerShell/v1.0/powershell.exe'
+ $info.Arguments='-NoLogo -NoProfile -NonInteractive -File "'+$adapter+'" -ListenerAddress "'+$Selector.Address+'" -PayloadPath "'+$payload+'"'
+ $info.UseShellExecute=$false;$info.CreateNoWindow=$true;$info.RedirectStandardOutput=$true;$info.RedirectStandardError=$true;$info.StandardOutputEncoding=[Text.UTF8Encoding]::new($false);$info.StandardErrorEncoding=[Text.UTF8Encoding]::new($false)
+ Initialize-WelaListenerPipe
+ $process=[Diagnostics.Process]::new();$process.StartInfo=$info;$result=[pscustomobject]@{Started=$false;TerminationConfirmed=$false;Diagnostic=''}
+ try {
+ if(-not $process.Start()){throw 'Native5.1 adapter did not start.'};$result.Started=$true;$childId=$process.Id;$stdout=[Wela.ListenerPipe.Bounded]::Read($process.StandardOutput,65536);$stderr=[Wela.ListenerPipe.Bounded]::Read($process.StandardError,65536)
+ if(-not $process.WaitForExit(20000)){throw 'Native5.1 adapter timed out.'};if(-not [Threading.Tasks.Task]::WaitAll([Threading.Tasks.Task[]]@($stdout,$stderr),5000)){throw 'Fixture adapter output drain timed out.'};$text=$stdout.Result;$errorText=$stderr.Result
+ if($errorText -or $text.Length -gt 65536){throw 'Unexpected native adapter output.'}
+ $receipt=$text|ConvertFrom-Json;Save ('adapter-'+[guid]::NewGuid().ToString('N')+'.json') $receipt
+ $identity=[Security.Principal.WindowsIdentity]::GetCurrent();try{$sid=$identity.User.Value}finally{$identity.Dispose()}
+ Assert ($receipt.EngineMajor -eq 5 -and $receipt.ProcessId -eq $childId -and $receipt.UserSid -ceq $sid) 'Actual native5.1 child identity must match the invoking account and observed PID.'
+ if($process.ExitCode -ne 0 -or $receipt.Status -cne 'Created'){throw $receipt.Diagnostic}
+ [string]$receipt.Xml
+ }finally{Close-WelaListenerAdapterProcess $process $result;if($result.Diagnostic){$script:cleanupErrors+=$result.Diagnostic;Write-Host $result.Diagnostic}}
+}
+
+$services=ReadServices;$firewall=ReadFirewall;$original=$null;$fullOriginal=$null;$removed=@();$created=$false;$failure=$null;$cleanupErrors=@();$count=0
+function Assert($Value,$Message){if(-not $Value){throw $Message};$script:count++}
+try {
+ $os=Get-CimInstance Win32_OperatingSystem;Assert ($os.BuildNumber -in @('20348','26100') -and $os.ProductType -eq 3) 'Standalone Server 2022/2025 fixture required.'
+ $s=@($services|Where-Object Name -eq 'WinRM');Assert ($s.Count -eq 1 -and $s[0].StartMode -in @('Auto','Manual') -and $s[0].State -in @('Running','Stopped')) 'Stable non-disabled WinRM required.'
+ if($s[0].State -ne 'Running'){Start-Service WinRM -ErrorAction Stop}
+ $original=ReadListeners;$fullOriginal=Get-WelaListenerState;Save 'complete-original.json' $fullOriginal;Save 'listeners-original.json' $original;Save 'services-original.json' $services;Save 'firewall-original.json' $firewall
+ # Replacement is a fixture-only, disposable-VM operation. Product must refuse overlaps.
+ foreach($listener in @($original|Where-Object Transport -eq 'HTTP')){
+ Assert ($listener.Address -match '^(\*|IP:[0-9.]+)$' -and $listener.Port -eq '5985' -and $listener.URLPrefix -eq 'wsman' -and $listener.Enabled -in @('true','false') -and -not $listener.CertificateThumbprint -and $listener.RawXml -notmatch 'Source="GPO"') 'Only ordinary local HTTP fixture listeners can be temporarily replaced.'
+ Microsoft.WSMan.Management\Remove-WSManInstance -ResourceURI 'http://schemas.microsoft.com/wbem/wsman/1/config/listener' -SelectorSet @{Address=$listener.Address;Transport='HTTP'} -ErrorAction Stop
+ $removed+=$listener
+ }
+ $ip=@(NetTCPIP\Get-NetIPAddress -AddressFamily IPv4|Where-Object {$_.AddressState -eq 'Preferred' -and $_.IPAddress -notmatch '^(127\.|169\.254\.|0\.)'}|Sort-Object IPAddress|Select-Object -First 1).IPAddress
+ Assert ([bool]$ip) 'An assigned preferred IPv4 address is required.';$selector=@{Address='IP:'+$ip;Transport='HTTP'}
+ $values=@{Port='5985';Hostname='';Enabled='true';URLPrefix='wsman';CertificateThumbprint=''}
+ Save 'selection.json' @{Selector=$selector;Values=$values}
+ $planDir=Join-Path $root 'public-plan';$applyDir=Join-Path $root 'public-apply'
+ Public @('wec-listener','-WecListenerComputerName',[Environment]::MachineName,'-WecListenerLocalAddress',$ip,'-WecListenerOutputPath',$planDir)
+ $plan=Get-Content (Join-Path $planDir 'manifest.json') -Raw|ConvertFrom-Json;Assert ($plan.Status -ceq 'ReviewRequired' -and -not $plan.AdapterStarted) 'Public Plan makes no native create attempt.'
+ $planPath=Join-Path $planDir 'plan.json';Assert ((Get-FileHash $planPath).Hash.ToLowerInvariant() -ceq $plan.PlanHash) 'Public plan hash is exact.'
+ $badDir=Join-Path $root 'bad-hash'
+ Public @('wec-listener','-WecListenerAction','Apply','-WecListenerPlanPath',$planPath,'-WecListenerPlanHash',('f'*64),'-WecListenerOutputPath',$badDir) 1
+ $bad=Get-Content (Join-Path $badDir 'manifest.json') -Raw|ConvertFrom-Json;Assert ($bad.Status -ceq 'Refused' -and -not $bad.AdapterStarted) 'Wrong plan hash refuses before adapter startup.'
+ $created=$true
+ Public @('wec-listener','-WecListenerAction','Apply','-WecListenerPlanPath',$planPath,'-WecListenerPlanHash',$plan.PlanHash,'-WecListenerOutputPath',$applyDir)
+ $applied=Get-Content (Join-Path $applyDir 'manifest.json') -Raw|ConvertFrom-Json
+ Assert ($applied.Status -ceq 'CreatedAndVerified' -and $applied.AdapterStarted -and $applied.NativeCreateAttempted -and $applied.Adapter.TerminationConfirmed -and $applied.Adapter.Receipt.EngineVersion -match '^5\.1\.') 'Public Apply uses the verified native5.1 adapter and confirms native creation.'
+ Assert ($applied.Adapter.Receipt.ProcessId -eq $applied.Adapter.ProcessId -and $applied.Adapter.Receipt.Reader.UserSid -eq $fullOriginal.Local.Reader.UserSid -and $applied.Adapter.Receipt.Reader.AuthenticationId -eq $fullOriginal.Local.Reader.AuthenticationId) 'Actual native worker PID/account/logon is bound.'
+ Assert ($applied.ReadyRuleCredit -eq 0 -and $applied.ServiceChanges -eq 0 -and $applied.AuthenticationChanges -eq 0 -and $applied.FirewallChanges -eq 0) 'No unrelated configuration changes or detection credit.'
+ foreach($artifact in $applied.Artifacts){Assert ((Get-FileHash (Join-Path $applyDir $artifact.Name)).Hash.ToLowerInvariant() -ceq $artifact.Sha256) 'Retained public artifact hash matches.'}
+ $replayDir=Join-Path $root 'replay'
+ Public @('wec-listener','-WecListenerAction','Apply','-WecListenerPlanPath',$planPath,'-WecListenerPlanHash',$plan.PlanHash,'-WecListenerOutputPath',$replayDir) 1
+ $replay=Get-Content (Join-Path $replayDir 'manifest.json') -Raw|ConvertFrom-Json;Assert ($replay.Status -ceq 'Refused' -and -not $replay.AdapterStarted) 'Actual existing listener and changed context refuse replay.'
+ $overlapDir=Join-Path $root 'overlap'
+ Public @('wec-listener','-WecListenerComputerName',[Environment]::MachineName,'-WecListenerLocalAddress',$ip,'-WecListenerOutputPath',$overlapDir) 1
+ $overlap=Get-Content (Join-Path $overlapDir 'manifest.json') -Raw|ConvertFrom-Json;Assert ($overlap.Status -ceq 'Refused' -and -not $overlap.AdapterStarted) 'Public Plan refuses an existing HTTP5985 listener.'
+ $after=ReadListeners;Save 'listeners-created.json' $after;$chosen=@($after|Where-Object {$_.Address -ceq $selector.Address -and $_.Transport -ceq 'HTTP'})
+ Assert ($chosen.Count -eq 1) 'Exactly one assigned-IP listener must exist.'
+ Assert ($chosen[0].Port -ceq '5985' -and $chosen[0].Enabled -ceq 'true' -and $chosen[0].URLPrefix -ceq 'wsman' -and -not $chosen[0].CertificateThumbprint -and -not $chosen[0].Hostname) 'Every fixed listener property must match.'
+ Assert ($chosen[0].ListeningOn.Count -eq 1 -and $chosen[0].ListeningOn[0] -ceq $ip) 'Actual ListeningOn must contain exactly the selected IPv4 address.'
+ $duplicateRejected=$false;$duplicateError=''
+ try {$null=NewCheckpointListener $selector $values}catch{$duplicateRejected=$true;$duplicateError=$_.ToString()}
+ Save 'collision.json' @{Rejected=$duplicateRejected;Diagnostic=$duplicateError};Assert $duplicateRejected 'Windows must reject creating the same listener selector twice.'
+ Assert ((Key (ReadListeners)) -ceq (Key $after)) 'Rejected collision must preserve the listener definition.'
+ $prereq=@(Get-WelaWefCollectorPrerequisites ([pscustomobject]@{CollectorFqdn='fixture.invalid';ListenerAddress=$selector.Address;IngressRuleName='WELA-checkpoint-does-not-exist';IngressLocalAddresses=@($ip);IngressRemoteAddresses=@('192.0.2.0/24')}))
+ Save 'collector-prerequisite.json' $prereq;$field=@($prereq|Where-Object Name -eq 'Existing matching HTTP listener');Assert ($field.Count -eq 1 -and $field[0].Verified) 'Existing collector prerequisite must recognize the actual exact-IP listener.'
+ Write-Host "PASS: $count actual public listener assertions. No WEF delivery proof."
+}catch{$failure=$_.ToString();Write-Host $failure;throw}finally{
+ try {if($created -and @(ReadListeners|Where-Object {$_.Address -ceq $selector.Address -and $_.Transport -ceq 'HTTP'}).Count){Microsoft.WSMan.Management\Remove-WSManInstance -ResourceURI 'http://schemas.microsoft.com/wbem/wsman/1/config/listener' -SelectorSet $selector -ErrorAction Stop}}catch{$cleanupErrors+=$_.ToString()}
+ foreach($listener in $removed){try {$null=NewCheckpointListener @{Address=$listener.Address;Transport=$listener.Transport} @{Port=$listener.Port;Hostname=$listener.Hostname;Enabled=$listener.Enabled;URLPrefix=$listener.URLPrefix;CertificateThumbprint=$listener.CertificateThumbprint}}catch{$cleanupErrors+=$_.ToString()}}
+ $restored=$null;$listenersOk=$false;$firewallOk=$false;$servicesOk=$false;$configurationOk=$false
+ try {$restored=ReadListeners;Save 'listeners-restored.json' $restored;$listenersOk=$null -ne $original -and (Key $original) -ceq (Key $restored)}catch{$cleanupErrors+=$_.ToString()}
+ try {$fullRestored=Get-WelaListenerState;Save 'complete-restored.json' $fullRestored;$configurationOk=(Get-WelaListenerReviewKey $fullOriginal) -ceq (Get-WelaListenerReviewKey $fullRestored)}catch{$cleanupErrors+=$_.ToString()}
+ try {if(@($services|Where-Object Name -eq 'WinRM')[0].State -eq 'Stopped'){Stop-Service WinRM -ErrorAction Stop};$endServices=ReadServices;Save 'services-restored.json' $endServices;$servicesOk=($services|ConvertTo-Json -Compress) -ceq ($endServices|ConvertTo-Json -Compress)}catch{$cleanupErrors+=$_.ToString()}
+ try {$endFirewall=ReadFirewall;Save 'firewall-restored.json' $endFirewall;$firewallOk=($firewall|ConvertTo-Json -Compress) -ceq ($endFirewall|ConvertTo-Json -Compress)}catch{$cleanupErrors+=$_.ToString()}
+ Save 'cleanup.json' @{Failure=$failure;CleanupErrors=$cleanupErrors;FullConfigurationPreserved=$configurationOk;ListenersRestored=$listenersOk;ServicesRestored=$servicesOk;FirewallPreserved=$firewallOk;Complete=($configurationOk -and $listenersOk -and $servicesOk -and $firewallOk -and -not $cleanupErrors.Count);DisposableBoundary='Fixture temporarily replaced ordinary original HTTP listeners and restored their captured configuration; product creation must refuse overlap.'}
+ if(-not $configurationOk -or -not $listenersOk -or -not $servicesOk -or -not $firewallOk -or $cleanupErrors.Count){throw 'Native checkpoint cleanup incomplete; inspect retained artifacts.'}
+}