Bind native listener adapter modules and normalize cross-engine context JSON

This commit is contained in:
Shirofune-Security committed 2026-09-21 23:03:17 +09:00
1 parent 3e9ba8c403
commit 2301bf1e85
3 files changed
+18 -5

No files matched your search

+2 -2
View File
@@ -18,14 +18,14 @@ Plan writes review artifacts, including `plan.json` and `manifest.json` with `Pl
The fixed desired listener is `Address=IP:<selected IPv4>`, transport `HTTP`, port `5985`, URL prefix `wsman`, enabled, with blank hostname and certificate thumbprint. Wildcard listeners, any existing HTTP5985 listener and an existing selected Address/Transport pair prevent creation. WELA leaves those listeners in place for manual review. It does not narrow, replace, disable or remove an existing endpoint.
The plan binds the actual machine, operator/logon context, assigned address, implementation and original WinRM configuration/policy/listeners and firewall observations. Apply checks the reviewed hash and fresh context, writes pending evidence before its single creation attempt, then checks actual native configuration and `ListeningOn`. The fixed local creation worker uses the trusted native Windows PowerShell 5.1 engine under both Windows PowerShell 5.1 and PowerShell 7 hosts, with no execution-policy override. Its actual process, token and engine are retained as evidence. A host that cannot run this fixed adapter must resolve that prerequisite before applying.
The plan binds the actual machine, operator/logon context, assigned address, implementation and original WinRM configuration/policy/listeners and firewall observations. Apply checks the reviewed hash and fresh context, writes pending evidence before its single creation attempt, then checks actual native configuration and `ListeningOn`. The fixed local creation worker uses the trusted native Windows PowerShell 5.1 engine under both Windows PowerShell 5.1 and PowerShell 7 hosts, with the fixed native 5.1 module directory and no execution-policy override. Its actual process, token and engine are retained as evidence. A host that cannot run this fixed adapter must resolve that prerequisite before applying.
| Result | Meaning |
| --- | --- |
| `ReviewRequired` | Plan artifacts are ready for review; no listener was created. |
| `CreatedAndVerified` | The new listener and expected native readback were observed, with the required preservation checks. |
| `Refused` | Preconditions, evidence or context failed before a creation attempt. |
| `CreateAttemptedUnverified` | Creation was attempted but the final state could not be completely verified. Review the pending/native evidence and current listeners before taking further action. |
| `CreateAttemptedUnverified` | The adapter started and creation was attempted or cannot be ruled out; the final state could not be completely verified. Review the pending/native evidence and current listeners before taking further action. |
No atomic Windows compare-and-set is available; another administrator or policy process can race observation and creation. There is no automatic rollback. An interrupted process can leave pending evidence and a created listener without a completed report. Use the retained original and current snapshots to identify what changed; this command never deletes a listener as a recovery shortcut.
+9 -2
View File
@@ -1,5 +1,11 @@
# 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-WelaListenerKey {
param($Value)
# Windows PowerShell 5.1 escapes these HTML characters even with default JSON settings.
# Normalize the same spelling in both engines before binding nested context strings.
$json=ConvertTo-Json -InputObject $Value -Depth 24 -Compress
$json.Replace('<','\u003c').Replace('>','\u003e').Replace('&','\u0026').Replace("'",'\u0027')
}
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.'}
@@ -30,7 +36,7 @@ function ConvertFrom-WelaListenerXml {
$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
$result.ListeningOn=$listening;$result.PolicyOwned=$policy;$result.RawXml=$Xml
[pscustomobject]$result
}
function Read-WelaListenerInventory {
@@ -206,6 +212,7 @@ function Start-WelaListenerAdapter {
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.EnvironmentVariables['PSModulePath']=Join-Path ([Environment]::SystemDirectory) 'WindowsPowerShell/v1.0/Modules'
$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
+7 -1
View File
@@ -8,6 +8,12 @@ $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}
$special=[pscustomobject]@{Xml='<a x="v">&</a>';Name="O'Neil"}
$specialKey=Get-WelaListenerKey $special
Assert ($specialKey -notmatch "[<>&']" -and $specialKey.Contains('\u003c') -and $specialKey.Contains('\u0027')) 'Context JSON spelling is consistent across native5.1 and host7.'
Assert (($specialKey|ConvertFrom-Json).Xml -ceq $special.Xml -and ($specialKey|ConvertFrom-Json).Name -ceq $special.Name) 'Canonical JSON escaping preserves exact values.'
Assert ((Get-WelaListenerKey (Copy-TestListener ([pscustomobject]@{Nested=$specialKey}))) -ceq (Get-WelaListenerKey ([pscustomobject]@{Nested=$specialKey}))) 'Nested context JSON keeps its reviewed value.'
$selection=Get-WelaListenerSelection 'test-host' '192.0.2.10'
$xml='<cfg:Listener xmlns:cfg="http://schemas.microsoft.com/wbem/wsman/1/config/listener" xml:lang="en-US"><cfg:Address>IP:192.0.2.10</cfg:Address><cfg:Transport>HTTP</cfg:Transport><cfg:Port>5985</cfg:Port><cfg:Hostname/><cfg:Enabled>true</cfg:Enabled><cfg:URLPrefix>wsman</cfg:URLPrefix><cfg:CertificateThumbprint/><cfg:ListeningOn>192.0.2.10</cfg:ListeningOn></cfg:Listener>'
Assert ($selection.ComputerName -ceq 'TEST-HOST') 'Actual computer selection is canonical.'
@@ -87,7 +93,7 @@ try {
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 'schema'){$text=$text -replace '"SchemaVersion"\s*:\s*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()