538 lines
27 KiB
PowerShell
538 lines
27 KiB
PowerShell
param(
|
|
[string]$IlDir = "runs/offline-evidence-intake/zyl-qqfile-20260709/metadata/c28-il",
|
|
[string]$BinaryDir = "runs/offline-real-client-window/full/zyl/Impp",
|
|
[string]$OutputDir = "runs/offline-broute-reverse",
|
|
[string]$ReportPath = "docs/source-discovery/2026-07-12-broute-bridge-static-map.md",
|
|
[int]$WindowRadius = 4,
|
|
[int]$MaxEvidencePerCandidate = 18
|
|
)
|
|
|
|
$ErrorActionPreference = "Stop"
|
|
|
|
$repo = (Resolve-Path -LiteralPath (Join-Path $PSScriptRoot "..")).Path
|
|
$ilDirFull = Join-Path $repo $IlDir
|
|
$binaryDirFull = Join-Path $repo $BinaryDir
|
|
$outDir = Join-Path $repo $OutputDir
|
|
$reportFullPath = Join-Path $repo $ReportPath
|
|
New-Item -ItemType Directory -Force -Path $outDir | Out-Null
|
|
New-Item -ItemType Directory -Force -Path (Split-Path -Parent $reportFullPath) | Out-Null
|
|
|
|
$textSources = @(
|
|
@{ path = "IMPlatformClient.exe.il"; role = "main client IL; login, plugin manager, local HTTP starter, chat/runtime" },
|
|
@{ path = "IMPP.Common.dll.il"; role = "common helpers; WinCommand, ProcessComm, DTOs" },
|
|
@{ path = "IMPP.ISphere.exe.il"; role = "ISphere companion app IL; CEF and command helpers" },
|
|
@{ path = "IMPP.UI.dll.il"; role = "UI/browser support IL" },
|
|
@{ path = "IMPP.Service.dll.il"; role = "file service implementation IL" },
|
|
@{ path = "smack.dll.il"; role = "XMPP/socks5/network IL" }
|
|
)
|
|
|
|
$binarySources = @(
|
|
@{ path = "HttpServerLib.dll"; role = "local HTTP server framework binary" },
|
|
@{ path = "IMPlatformClient.Web.exe"; role = "CEF/web helper process binary" },
|
|
@{ path = "INetwork.dll"; role = "native network abstraction" },
|
|
@{ path = "IOClientNetwork.dll"; role = "native network implementation" },
|
|
@{ path = "IMPlatformClient.exe"; role = "main client binary" }
|
|
)
|
|
|
|
$liveProbeSources = @(
|
|
"runs/returned-live-probe/isphere-live-probe-20260710-121958/isphere-live-probe-20260710-121958/services_registry_shortcuts.json",
|
|
"runs/returned-live-probe/isphere-live-probe-20260710-121958/isphere-live-probe-20260710-121958/network_inventory.json"
|
|
)
|
|
|
|
$candidates = @(
|
|
@{
|
|
key = "local_http_win_server"
|
|
display = "frmLogin.StartWinServer / HttpServerLib.HttpService"
|
|
category = "localhost_http"
|
|
classification = "requires_logged_in_process"
|
|
usable_for_broute_send = $false
|
|
recommendation = "do_not_pursue_before_in_process_adapter"
|
|
reason = "Static IL builds a local HTTP service from logged-in user/auth state, but no send route or active live local listener was confirmed."
|
|
patterns = @("StartWinServer", "HttpServerLib", "HttpService::\.ctor", "HttpServer::Start", "GetFreePort", "0\.0\.0\.0", "messageAuth", "RepairDatabase")
|
|
},
|
|
@{
|
|
key = "http_route_framework"
|
|
display = "HttpServerLib route/module framework"
|
|
category = "localhost_http"
|
|
classification = "static_only_unknown"
|
|
usable_for_broute_send = $false
|
|
recommendation = "no_existing_ipc_first"
|
|
reason = "HttpServerLib has ServiceModule/RouteAttribute primitives, but client IL does not show RegisterModule or send-message routes."
|
|
patterns = @("RouteAttribute", "RouteMethod", "ServiceModule", "ServiceRoute", "RegisterModule", "OnGet\s*\(", "OnPost\s*\(", "ExecuteRoute", "SearchRoute")
|
|
},
|
|
@{
|
|
key = "plugin_named_pipe_stub"
|
|
display = "PluginLightAppMulProcessCommunication NamedPipeServerStream"
|
|
category = "named_pipe"
|
|
classification = "not_a_bridge"
|
|
usable_for_broute_send = $false
|
|
recommendation = "do_not_use"
|
|
reason = "A NamedPipeServerStream field exists, but StartLightAppMultProssCommunication has a one-instruction ret body."
|
|
patterns = @("NamedPipeServerStream", "PluginLightAppMulProcessCommunication", "StartLightAppMultProssCommunication", "MulProcess", "LightApp")
|
|
},
|
|
@{
|
|
key = "process_comm_wm_copydata"
|
|
display = "IMPP.Common.Helper.ProcessComm WM_COPYDATA helper"
|
|
category = "window_message"
|
|
classification = "static_only_unknown"
|
|
usable_for_broute_send = $false
|
|
recommendation = "not_a_send_bridge"
|
|
reason = "ProcessComm can send WM_COPYDATA to a foreground or titled window, but no receive handler or send-command contract is confirmed."
|
|
patterns = @("IMPP\.Common\.Helper\.ProcessComm", "\bProcessComm\b", "COPYDATASTRUCT", "WM_COPYDATA", "SendMessageToTargetWindow", "FindWindow\(string lpClassName", "GetForegroundWindow\(\)")
|
|
},
|
|
@{
|
|
key = "plugin_manager_command_surface"
|
|
display = "PluginManager / IMPlugin command surface"
|
|
category = "plugin"
|
|
classification = "requires_logged_in_process"
|
|
usable_for_broute_send = $false
|
|
recommendation = "plugin_extension_not_first"
|
|
reason = "Plugin evidence is UI/app-store/context-menu oriented and depends on logged-in PluginManager state; no Assembly.Load/LoadFrom or send API bridge was confirmed."
|
|
patterns = @("PluginManager", "IMPlugin::get_Command", "LoadPluginContextMenu", "FrmAppStore", "FrmCefHomePage", "ShowPlugins", "Assembly\.Load", "LoadFrom", "AddPluginInfo")
|
|
},
|
|
@{
|
|
key = "cef_web_helper"
|
|
display = "IMPlatformClient.Web / CEF helper"
|
|
category = "child_process"
|
|
classification = "requires_logged_in_process"
|
|
usable_for_broute_send = $false
|
|
recommendation = "preview_only_not_send"
|
|
reason = "The web helper/CEF surface exists, but static evidence points to UI/plugin browser support, not an external send/file API."
|
|
patterns = @("IMPlatformClient\.Web", "CefProcessName", "CefRuntime", "WinCefBrowser", "NavigateTo", "SendProcessMessage", "V8Context")
|
|
},
|
|
@{
|
|
key = "native_network_stack"
|
|
display = "INetwork / IOClientNetwork native stack"
|
|
category = "native_network"
|
|
classification = "not_a_bridge"
|
|
usable_for_broute_send = $false
|
|
recommendation = "do_not_pursue_as_ipc"
|
|
reason = "Native NETIO send/recv symbols are low-level transport, not a local command bridge into logged-in chat objects."
|
|
patterns = @("INetwork", "IOClientNetwork", "NETIO_", "NETIO_C_", "StreamClient", "RegistRecvCallback", "SendPacket", "BasePipe")
|
|
},
|
|
@{
|
|
key = "live_probe_named_pipe"
|
|
display = "returned live-probe named pipe inventory"
|
|
category = "named_pipe"
|
|
classification = "static_only_unknown"
|
|
usable_for_broute_send = $false
|
|
recommendation = "do_not_assume_isphere_api"
|
|
reason = "Live probe saw \\.\\pipe\\zfpinject-msg-server, but no matching iSphere static string or send contract was found in the configured samples."
|
|
patterns = @("zfpinject-msg-server", "named_pipes", "connections", "remote_port", "10088")
|
|
},
|
|
@{
|
|
key = "local_tcp_listener_candidates"
|
|
display = "local TcpListener candidates"
|
|
category = "tcp_listener"
|
|
classification = "not_a_bridge"
|
|
usable_for_broute_send = $false
|
|
recommendation = "not_existing_ipc"
|
|
reason = "TcpListener evidence is tied to TCP file manage or smack socks5, not a command API for text/file send. Returned live probe had only one remote XMPP connection."
|
|
patterns = @("TcpListener", "TCPFileManager::_listener", "TCPSocks5Server", "GetActiveTcpListeners", "BeginAcceptSocket", "remote_port")
|
|
}
|
|
)
|
|
|
|
function Get-RelativePath([string]$Path) {
|
|
if ($Path.StartsWith($repo, [System.StringComparison]::OrdinalIgnoreCase)) {
|
|
return $Path.Substring($repo.Length).TrimStart('\', '/')
|
|
}
|
|
return $Path
|
|
}
|
|
|
|
function Get-TextEvidenceKind([string]$Line) {
|
|
if ($Line -match "^\s*\.class\b") { return "type_definition" }
|
|
if ($Line -match "^\s*\.method\b") { return "method_header" }
|
|
if ($Line -match "\bend of method\b") { return "method_end" }
|
|
if ($Line -match "\bcall(?:virt)?\b") { return "callsite" }
|
|
if ($Line -match "\bnewobj\b") { return "constructor_call" }
|
|
if ($Line -match "\bldstr\b") { return "string_literal" }
|
|
if ($Line -match "^\s*(instance|static|StartWinServer|GetFreePort|SendMessageToTargetWindow)\b.*\(") { return "signature_continuation" }
|
|
return "reference"
|
|
}
|
|
|
|
function Get-MethodContext($Lines, [int]$Index) {
|
|
$start = [Math]::Max(0, $Index - 120)
|
|
$methodHeader = $null
|
|
$classHeader = $null
|
|
for ($i = $Index; $i -ge $start; $i--) {
|
|
$line = $Lines[$i]
|
|
if ($null -eq $methodHeader -and $line -match "^\s*\.method\b") {
|
|
$methodHeader = [pscustomobject]@{ line_number = $i + 1; text = $line.Trim() }
|
|
}
|
|
if ($null -eq $classHeader -and $line -match "^\s*\.class\b") {
|
|
$classHeader = [pscustomobject]@{ line_number = $i + 1; text = $line.Trim() }
|
|
}
|
|
if ($methodHeader -and $classHeader) { break }
|
|
}
|
|
[pscustomobject]@{ method_header = $methodHeader; class_header = $classHeader }
|
|
}
|
|
|
|
function New-TextEvidence($Lines, [int]$Index, [string]$SourceRel) {
|
|
$from = [Math]::Max(0, $Index - $WindowRadius)
|
|
$to = [Math]::Min($Lines.Count - 1, $Index + $WindowRadius)
|
|
$window = New-Object System.Collections.Generic.List[object]
|
|
for ($i = $from; $i -le $to; $i++) {
|
|
$window.Add([pscustomobject]@{ line_number = $i + 1; text = $Lines[$i].TrimEnd() })
|
|
}
|
|
$context = Get-MethodContext -Lines $Lines -Index $Index
|
|
[pscustomobject]@{
|
|
source = $SourceRel
|
|
source_type = "text_il"
|
|
line_number = $Index + 1
|
|
string_index = $null
|
|
kind = Get-TextEvidenceKind -Line $Lines[$Index]
|
|
text = $Lines[$Index].Trim()
|
|
method_header = $context.method_header
|
|
class_header = $context.class_header
|
|
window = $window
|
|
}
|
|
}
|
|
|
|
function Get-PrintableStrings([string]$Path, [int]$MinLength = 4) {
|
|
$bytes = [System.IO.File]::ReadAllBytes($Path)
|
|
$strings = New-Object System.Collections.Generic.List[string]
|
|
|
|
$sb = New-Object System.Text.StringBuilder
|
|
foreach ($b in $bytes) {
|
|
if ($b -ge 32 -and $b -le 126) {
|
|
[void]$sb.Append([char]$b)
|
|
} else {
|
|
if ($sb.Length -ge $MinLength) { $strings.Add($sb.ToString()) }
|
|
$sb.Clear() | Out-Null
|
|
}
|
|
}
|
|
if ($sb.Length -ge $MinLength) { $strings.Add($sb.ToString()) }
|
|
|
|
$sb = New-Object System.Text.StringBuilder
|
|
for ($i = 0; $i -lt ($bytes.Length - 1); $i += 2) {
|
|
$b = $bytes[$i]
|
|
$z = $bytes[$i + 1]
|
|
if ($z -eq 0 -and $b -ge 32 -and $b -le 126) {
|
|
[void]$sb.Append([char]$b)
|
|
} else {
|
|
if ($sb.Length -ge $MinLength) { $strings.Add($sb.ToString()) }
|
|
$sb.Clear() | Out-Null
|
|
}
|
|
}
|
|
if ($sb.Length -ge $MinLength) { $strings.Add($sb.ToString()) }
|
|
return $strings.ToArray()
|
|
}
|
|
|
|
function Get-HttpServerReflection([string]$Path) {
|
|
$result = [ordered]@{ path = Get-RelativePath -Path $Path; ok = $false; error = ""; types = @() }
|
|
try {
|
|
$asm = [System.Reflection.Assembly]::LoadFile($Path)
|
|
$types = New-Object System.Collections.Generic.List[object]
|
|
foreach ($type in $asm.GetTypes()) {
|
|
if ($type.FullName -notmatch "HTTPServerLib\.(HttpServer|HttpService|ServiceModule|ServiceRoute|RouteAttribute|IServer|ActionResult)") { continue }
|
|
$ctors = @($type.GetConstructors([System.Reflection.BindingFlags]'Public,NonPublic,Instance') | ForEach-Object {
|
|
$params = @($_.GetParameters() | ForEach-Object { $_.ParameterType.Name + " " + $_.Name })
|
|
[pscustomobject]@{ is_public = $_.IsPublic; signature = "$($type.Name)($([string]::Join(', ', $params)))" }
|
|
})
|
|
$methods = @($type.GetMethods([System.Reflection.BindingFlags]'Public,NonPublic,Instance,Static,DeclaredOnly') | ForEach-Object {
|
|
$params = @($_.GetParameters() | ForEach-Object { $_.ParameterType.Name + " " + $_.Name })
|
|
[pscustomobject]@{ name = $_.Name; signature = "$($_.ReturnType.Name) $($_.Name)($([string]::Join(', ', $params)))" }
|
|
})
|
|
$types.Add([pscustomobject]@{ full_name = $type.FullName; constructors = $ctors; methods = $methods })
|
|
}
|
|
$result.ok = $true
|
|
$result.types = $types.ToArray()
|
|
} catch {
|
|
$result.error = $_.Exception.Message
|
|
}
|
|
return [pscustomobject]$result
|
|
}
|
|
|
|
$textSourceResults = New-Object System.Collections.Generic.List[object]
|
|
$binarySourceResults = New-Object System.Collections.Generic.List[object]
|
|
$candidateBuckets = @{}
|
|
foreach ($candidate in $candidates) {
|
|
$candidateBuckets[[string]$candidate['key']] = New-Object System.Collections.Generic.List[object]
|
|
}
|
|
|
|
foreach ($source in $textSources) {
|
|
$sourceFullPath = Join-Path $ilDirFull ([string]$source['path'])
|
|
$sourceRel = Get-RelativePath -Path $sourceFullPath
|
|
if (-not (Test-Path -LiteralPath $sourceFullPath -PathType Leaf)) {
|
|
$textSourceResults.Add([pscustomobject]@{ path = $sourceRel; role = $source['role']; exists = $false; line_count = 0; size_bytes = $null })
|
|
continue
|
|
}
|
|
$item = Get-Item -LiteralPath $sourceFullPath
|
|
$lines = [System.IO.File]::ReadAllLines($sourceFullPath)
|
|
$textSourceResults.Add([pscustomobject]@{ path = $sourceRel; role = $source['role']; exists = $true; line_count = $lines.Count; size_bytes = $item.Length })
|
|
foreach ($candidate in $candidates) {
|
|
$key = [string]$candidate['key']
|
|
$compiled = @($candidate['patterns'] | ForEach-Object { [regex]::new($_, [System.Text.RegularExpressions.RegexOptions]::IgnoreCase) })
|
|
for ($i = 0; $i -lt $lines.Count; $i++) {
|
|
$line = $lines[$i]
|
|
foreach ($rx in $compiled) {
|
|
if ($rx.IsMatch($line)) {
|
|
$candidateBuckets[$key].Add((New-TextEvidence -Lines $lines -Index $i -SourceRel $sourceRel))
|
|
break
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
foreach ($source in $binarySources) {
|
|
$sourceFullPath = Join-Path $binaryDirFull ([string]$source['path'])
|
|
$sourceRel = Get-RelativePath -Path $sourceFullPath
|
|
if (-not (Test-Path -LiteralPath $sourceFullPath -PathType Leaf)) {
|
|
$binarySourceResults.Add([pscustomobject]@{ path = $sourceRel; role = $source['role']; exists = $false; string_count = 0; size_bytes = $null })
|
|
continue
|
|
}
|
|
$item = Get-Item -LiteralPath $sourceFullPath
|
|
$strings = Get-PrintableStrings -Path $sourceFullPath -MinLength 4
|
|
$binarySourceResults.Add([pscustomobject]@{ path = $sourceRel; role = $source['role']; exists = $true; string_count = $strings.Count; size_bytes = $item.Length })
|
|
foreach ($candidate in $candidates) {
|
|
$key = [string]$candidate['key']
|
|
$compiled = @($candidate['patterns'] | ForEach-Object { [regex]::new($_, [System.Text.RegularExpressions.RegexOptions]::IgnoreCase) })
|
|
for ($i = 0; $i -lt $strings.Count; $i++) {
|
|
$s = $strings[$i]
|
|
foreach ($rx in $compiled) {
|
|
if ($rx.IsMatch($s)) {
|
|
$candidateBuckets[$key].Add([pscustomobject]@{
|
|
source = $sourceRel
|
|
source_type = "binary_string"
|
|
line_number = $null
|
|
string_index = $i
|
|
kind = "binary_string"
|
|
text = $s
|
|
method_header = $null
|
|
class_header = $null
|
|
window = @()
|
|
})
|
|
break
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
$liveProbe = New-Object System.Collections.Generic.List[object]
|
|
foreach ($relPath in $liveProbeSources) {
|
|
$fullPath = Join-Path $repo $relPath
|
|
if (Test-Path -LiteralPath $fullPath -PathType Leaf) {
|
|
$raw = Get-Content -LiteralPath $fullPath -Raw
|
|
$liveProbe.Add([pscustomobject]@{ path = $relPath; exists = $true; length = $raw.Length; data = ($raw | ConvertFrom-Json) })
|
|
foreach ($candidate in $candidates) {
|
|
$key = [string]$candidate['key']
|
|
$compiled = @($candidate['patterns'] | ForEach-Object { [regex]::new($_, [System.Text.RegularExpressions.RegexOptions]::IgnoreCase) })
|
|
foreach ($rx in $compiled) {
|
|
if ($rx.IsMatch($raw)) {
|
|
$candidateBuckets[$key].Add([pscustomobject]@{
|
|
source = $relPath
|
|
source_type = "live_probe_json"
|
|
line_number = $null
|
|
string_index = $null
|
|
kind = "live_probe_json"
|
|
text = ($raw -replace "\s+", " ").Trim()
|
|
method_header = $null
|
|
class_header = $null
|
|
window = @()
|
|
})
|
|
break
|
|
}
|
|
}
|
|
}
|
|
} else {
|
|
$liveProbe.Add([pscustomobject]@{ path = $relPath; exists = $false; length = 0; data = $null })
|
|
}
|
|
}
|
|
|
|
$httpServerPath = Join-Path $binaryDirFull "HttpServerLib.dll"
|
|
$httpReflection = if (Test-Path -LiteralPath $httpServerPath -PathType Leaf) { Get-HttpServerReflection -Path $httpServerPath } else { [pscustomobject]@{ path = Get-RelativePath -Path $httpServerPath; ok = $false; error = "missing"; types = @() } }
|
|
$httpReflectionPath = Join-Path $outDir "httpserverlib-reflection.json"
|
|
$httpReflection | ConvertTo-Json -Depth 10 | Set-Content -LiteralPath $httpReflectionPath -Encoding UTF8
|
|
|
|
if ($httpReflection.ok) {
|
|
$reflectionText = ($httpReflection.types | ConvertTo-Json -Depth 8 -Compress)
|
|
foreach ($candidate in $candidates) {
|
|
$key = [string]$candidate['key']
|
|
$compiled = @($candidate['patterns'] | ForEach-Object { [regex]::new($_, [System.Text.RegularExpressions.RegexOptions]::IgnoreCase) })
|
|
foreach ($rx in $compiled) {
|
|
if ($rx.IsMatch($reflectionText)) {
|
|
$candidateBuckets[$key].Add([pscustomobject]@{
|
|
source = Get-RelativePath -Path $httpReflectionPath
|
|
source_type = "reflection"
|
|
line_number = $null
|
|
string_index = $null
|
|
kind = "reflection_metadata"
|
|
text = "HttpServerLib reflection exposes matching types/methods; see httpserverlib-reflection.json"
|
|
method_header = $null
|
|
class_header = $null
|
|
window = @()
|
|
})
|
|
break
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
$candidateResults = New-Object System.Collections.Generic.List[object]
|
|
foreach ($candidate in $candidates) {
|
|
$key = [string]$candidate['key']
|
|
$allEvidence = $candidateBuckets[$key].ToArray()
|
|
$evidenceForReport = @(
|
|
$allEvidence |
|
|
Sort-Object @{ Expression = { if ($_.source_type -eq "text_il") { 0 } elseif ($_.source_type -eq "reflection") { 1 } elseif ($_.source_type -eq "live_probe_json") { 2 } else { 3 } } }, source, line_number, string_index |
|
|
Select-Object -First $MaxEvidencePerCandidate
|
|
)
|
|
$classification = if ($allEvidence.Count -eq 0) { "not_found" } else { [string]$candidate['classification'] }
|
|
$candidateResults.Add([pscustomobject]@{
|
|
key = $key
|
|
display = $candidate['display']
|
|
category = $candidate['category']
|
|
classification = $classification
|
|
usable_for_broute_send = [bool]$candidate['usable_for_broute_send']
|
|
recommendation = $candidate['recommendation']
|
|
reason = $candidate['reason']
|
|
match_count = $allEvidence.Count
|
|
evidence = $evidenceForReport
|
|
})
|
|
}
|
|
|
|
$recommendation = [pscustomobject]@{
|
|
pursue_existing_ipc_or_plugin_first = $false
|
|
decision = "in_process_adapter_research"
|
|
fallback_boundary = "external_sidecar_preview_only_until_bridge_exists"
|
|
rationale = "Offline static evidence found no existing local IPC/plugin route that exposes send-message or send-file. HttpServerLib and plugin surfaces either require logged-in runtime state or are UI/framework-only; the named-pipe stub is empty."
|
|
}
|
|
|
|
$summary = [pscustomobject]@{
|
|
generated_at = (Get-Date).ToUniversalTime().ToString("o")
|
|
repository = $repo
|
|
il_dir = $IlDir
|
|
binary_dir = $BinaryDir
|
|
text_sources = $textSourceResults
|
|
binary_sources = $binarySourceResults
|
|
live_probe = $liveProbe
|
|
httpserverlib_reflection = Get-RelativePath -Path $httpReflectionPath
|
|
candidates = $candidateResults
|
|
recommendation = $recommendation
|
|
originals_copied = $false
|
|
originals_modified = $false
|
|
}
|
|
|
|
$jsonPath = Join-Path $outDir "bridge-map.json"
|
|
$summary | ConvertTo-Json -Depth 14 | Set-Content -LiteralPath $jsonPath -Encoding UTF8
|
|
|
|
$linesOut = New-Object System.Collections.Generic.List[string]
|
|
$linesOut.Add("# B-Route Bridge/IPC/Plugin Static Map")
|
|
$linesOut.Add("")
|
|
$linesOut.Add("Date: 2026-07-12")
|
|
$linesOut.Add("")
|
|
$linesOut.Add("## Scope")
|
|
$linesOut.Add("")
|
|
$linesOut.Add("This report uses offline IL, binary string metadata, reflection metadata for ``HttpServerLib.dll``, and returned live-probe JSON. It does not log in, open sockets, attach hooks, inject into a process, replay traffic, or mutate client binaries.")
|
|
$linesOut.Add("")
|
|
$linesOut.Add("Machine-readable evidence: ``$OutputDir/bridge-map.json``")
|
|
$linesOut.Add("Reflection metadata: ``$OutputDir/httpserverlib-reflection.json``")
|
|
$linesOut.Add("")
|
|
$linesOut.Add("## Recommendation")
|
|
$linesOut.Add("")
|
|
$linesOut.Add("| Question | Answer |")
|
|
$linesOut.Add("| --- | --- |")
|
|
$linesOut.Add("| Should B-route pursue existing IPC/plugin before an in-process adapter? | **No** |")
|
|
$linesOut.Add("| Offline decision | ``$($recommendation.decision)`` |")
|
|
$linesOut.Add("| External sidecar boundary | ``$($recommendation.fallback_boundary)`` |")
|
|
$linesOut.Add("| Reason | $($recommendation.rationale) |")
|
|
$linesOut.Add("")
|
|
$linesOut.Add("## Sources")
|
|
$linesOut.Add("")
|
|
$linesOut.Add("### Text/IL sources")
|
|
$linesOut.Add("")
|
|
$linesOut.Add("| Source | Role | Exists | Lines | Size bytes |")
|
|
$linesOut.Add("| --- | --- | --- | ---: | ---: |")
|
|
foreach ($sourceResult in $textSourceResults) {
|
|
$exists = if ($sourceResult.exists) { "yes" } else { "no" }
|
|
$sizeText = if ($null -ne $sourceResult.size_bytes) { [string]$sourceResult.size_bytes } else { "" }
|
|
$linesOut.Add("| ``$($sourceResult.path)`` | $($sourceResult.role) | $exists | $($sourceResult.line_count) | $sizeText |")
|
|
}
|
|
$linesOut.Add("")
|
|
$linesOut.Add("### Binary/string sources")
|
|
$linesOut.Add("")
|
|
$linesOut.Add("| Source | Role | Exists | Strings | Size bytes |")
|
|
$linesOut.Add("| --- | --- | --- | ---: | ---: |")
|
|
foreach ($sourceResult in $binarySourceResults) {
|
|
$exists = if ($sourceResult.exists) { "yes" } else { "no" }
|
|
$sizeText = if ($null -ne $sourceResult.size_bytes) { [string]$sourceResult.size_bytes } else { "" }
|
|
$linesOut.Add("| ``$($sourceResult.path)`` | $($sourceResult.role) | $exists | $($sourceResult.string_count) | $sizeText |")
|
|
}
|
|
$linesOut.Add("")
|
|
$linesOut.Add("## Candidate classification")
|
|
$linesOut.Add("")
|
|
$linesOut.Add("| Candidate | Category | Classification | Usable for B-route send | Recommendation | Matches |")
|
|
$linesOut.Add("| --- | --- | --- | --- | --- | ---: |")
|
|
foreach ($candidateResult in $candidateResults) {
|
|
$usable = if ($candidateResult.usable_for_broute_send) { "yes" } else { "no" }
|
|
$linesOut.Add("| ``$($candidateResult.display)`` | ``$($candidateResult.category)`` | ``$($candidateResult.classification)`` | $usable | ``$($candidateResult.recommendation)`` | $($candidateResult.match_count) |")
|
|
}
|
|
$linesOut.Add("")
|
|
$linesOut.Add("## Verified facts")
|
|
$linesOut.Add("")
|
|
$linesOut.Add("- ``frmLogin.StartWinServer`` constructs ``HTTPServerLib.HttpService`` on ``0.0.0.0`` and starts ``HttpServer.Start`` in a thread, but it depends on ``IMPPManager.UserInfo`` and ``ServiceManager.UnifiedAuthentication`` state.")
|
|
$linesOut.Add("- ``HttpServerLib.dll`` exposes route/module primitives such as ``HttpService.RegisterModule``, ``ServiceModule``, ``ServiceRoute``, ``RouteAttribute``, ``OnGet``, and ``OnPost``.")
|
|
$linesOut.Add("- Main client IL does not show a concrete ``RegisterModule`` call or send-message/send-file HTTP route registration.")
|
|
$linesOut.Add("- ``PluginLightAppMulProcessCommunication`` declares a ``NamedPipeServerStream`` field, but ``StartLightAppMultProssCommunication`` returns immediately.")
|
|
$linesOut.Add("- Returned live probe showed one remote TCP connection to port ``10088`` and a named pipe ``\\.\\pipe\\zfpinject-msg-server``; neither proves an iSphere send API.")
|
|
$linesOut.Add("- ``PluginManager``/``IMPlugin`` evidence is UI/app-store/context-menu oriented; no local assembly-load plugin bridge or send route was confirmed.")
|
|
$linesOut.Add("")
|
|
$linesOut.Add("## Candidate evidence")
|
|
foreach ($candidateResult in $candidateResults) {
|
|
$linesOut.Add("")
|
|
$linesOut.Add("### $($candidateResult.display)")
|
|
$linesOut.Add("")
|
|
$linesOut.Add("Classification: ``$($candidateResult.classification)``; usable for B-route send: ``$($candidateResult.usable_for_broute_send)``; matches: $($candidateResult.match_count).")
|
|
$linesOut.Add("")
|
|
$linesOut.Add("Reason: $($candidateResult.reason)")
|
|
if (@($candidateResult.evidence).Count -eq 0) {
|
|
$linesOut.Add("")
|
|
$linesOut.Add("No evidence found in configured offline sources.")
|
|
continue
|
|
}
|
|
$linesOut.Add("")
|
|
$linesOut.Add("| Source | Kind | Snippet | Context |")
|
|
$linesOut.Add("| --- | --- | --- | --- |")
|
|
foreach ($evidence in $candidateResult.evidence) {
|
|
$sourceLoc = if ($evidence.line_number) { "$($evidence.source):$($evidence.line_number)" } elseif ($null -ne $evidence.string_index) { "$($evidence.source)#str$($evidence.string_index)" } else { $evidence.source }
|
|
$method = if ($evidence.method_header) { "L$($evidence.method_header.line_number): $($evidence.method_header.text)" } else { "" }
|
|
$snippet = (($evidence.text -replace "\|", "\\|") -replace "`r?`n", " ")
|
|
if ($snippet.Length -gt 220) { $snippet = $snippet.Substring(0, 220) + "..." }
|
|
$method = ($method -replace "\|", "\\|")
|
|
$linesOut.Add("| ``$sourceLoc`` | ``$($evidence.kind)`` | ``$snippet`` | ``$method`` |")
|
|
}
|
|
}
|
|
$linesOut.Add("")
|
|
$linesOut.Add("## Decision")
|
|
$linesOut.Add("")
|
|
$linesOut.Add("No existing IPC/plugin/local HTTP bridge is confirmed usable for B-route text send or file send. The next B-route branch should be ``in_process_adapter_research``. Until that exists, an external sidecar can only do preview/probe work, while A-route/RPA remains backup-only.")
|
|
$linesOut.Add("")
|
|
$linesOut.Add("## Next work")
|
|
$linesOut.Add("")
|
|
$linesOut.Add("1. Update the B-route next plan and capability source matrix with ``in_process_adapter_research`` as the offline decision.")
|
|
$linesOut.Add("2. Create a separate plan before implementing any in-process adapter work.")
|
|
$linesOut.Add("3. Do not promote RPA beyond backup in this branch.")
|
|
|
|
($linesOut -join "`n") + "`n" | Set-Content -LiteralPath $reportFullPath -Encoding UTF8
|
|
|
|
$requiredNames = @("HttpServerLib", "NamedPipe", "Plugin", "IMPlatformClient.Web", "ProcessComm", "INetwork", "IOClientNetwork", "in_process_adapter_research")
|
|
$reportText = Get-Content -LiteralPath $reportFullPath -Raw
|
|
$missing = @($requiredNames | Where-Object { $reportText -notlike "*$_*" })
|
|
if ($missing.Count -gt 0) {
|
|
throw "Report missing required names: $($missing -join ', ')"
|
|
}
|
|
|
|
[pscustomobject]@{
|
|
ok = $true
|
|
json_path = $jsonPath
|
|
report_path = $reportFullPath
|
|
reflection_path = $httpReflectionPath
|
|
candidates = $candidateResults.Count
|
|
decision = $recommendation.decision
|
|
pursue_existing_ipc_or_plugin_first = $false
|
|
originals_copied = $false
|
|
originals_modified = $false
|
|
} | ConvertTo-Json -Depth 6
|