diff --git a/docs/go-csharp-helper-boundary.md b/docs/go-csharp-helper-boundary.md index 1c3c89e..0c2a1ed 100644 --- a/docs/go-csharp-helper-boundary.md +++ b/docs/go-csharp-helper-boundary.md @@ -158,9 +158,9 @@ WRITE_NOT_READY VERIFY_FAILED ``` -## 6. 第一阶段 op +## 6. 当前只读 helper op -第一阶段只支持四个 op。 +当前 helper 只支持只读探测类 op。`version`、`self_check`、`scan_windows`、`dump_uia` 是第一阶段窗口/UIA 基线;`probe_client_runtime` 是 C30 为 B 路线发送连接器新增的只读运行时探测。 ### 6.1 `version` @@ -182,7 +182,7 @@ VERIFY_FAILED ```json { "helper_name": "ISphereWinHelper", - "helper_version": "0.1.0", + "helper_version": "0.3.0", "protocol": "isphere.helper.v1", "runtime": ".NET Framework" } @@ -274,6 +274,66 @@ runs/real-loggedin-lab/uia-dumps/ C# helper 文件输出通过未来 `out_path` 参数控制。 +### 6.5 `probe_client_runtime` + +用途:为 B 路线发送连接器做只读运行时探测,确认本机是否存在已运行的 `IMPlatformClient.exe` / iSphere 相关进程,以及进程位数、安装路径、关键模块是否加载。 + +这个 op 只做进程和模块枚举: + +- 不发送消息; +- 不上传文件; +- 不点击、不输入; +- 不注入、不 hook; +- 不读取聊天内容。 + +请求示例: + +```json +{ + "protocol": "isphere.helper.v1", + "request_id": "...", + "op": "probe_client_runtime", + "timeout_ms": 5000, + "args": { + "process_names": ["IMPlatformClient", "IMPP.ISphere", "iSphere", "importal"], + "include_modules": true, + "max_modules": 120 + } +} +``` + +响应 data 示例: + +```json +{ + "probe_mode": "read_only_process_module_inventory", + "host": { + "is_64_bit_os": true, + "is_64_bit_process": true + }, + "target_count": 1, + "targets": [ + { + "pid": 1234, + "process_name": "IMPlatformClient", + "main_window_title": "iSphere", + "executable_path": "C:\\...\\IMPlatformClient.exe", + "bitness": "x86", + "assembly_presence": { + "IMPlatformClient.exe": true, + "smack.dll": true, + "IMPP.Interface.dll": true, + "IMPP.ServiceBase.dll": true, + "IMPP.Service.dll": true + }, + "module_probe_ok": true + } + ] +} +``` + +该 op 是 C30 的 B 路线入口检查。它暂不等于发送能力,只用于判断后续是否值得做 sidecar / in-process 调用验证。 + ## 7. 后续 op 扩展顺序 必须等真实登录窗口的 UIA dump 被确认后,再扩展真实动作。 @@ -344,7 +404,7 @@ native/ ## 11. 验收标准 -第一阶段完成后,只验收: +当前只读 helper 基线验收: 1. C# helper 能编译。 2. `version` 返回合法 JSON。 @@ -353,17 +413,20 @@ native/ 5. 打开记事本后,`dump_uia` 能导出记事本控件树。 6. 用户手动登录 iSphere 后,`scan_windows` 能找到候选窗口。 7. 对 iSphere 候选窗口执行 `dump_uia` 能返回控件树。 -8. 第一阶段验收聚焦窗口扫描和 UIA dump 结果。 +8. `probe_client_runtime` 在无目标客户端运行时也能返回合法空结果。 +9. 用户手动登录 iSphere 后,`probe_client_runtime` 能返回目标进程、位数、路径和关键模块探测结果。 +10. 当前验收仍聚焦只读探测;发送、文件上传、点击、输入都不在本节验收范围内。 ## 12. 下一步 -建议下一步只做 C# helper 第一阶段: +当前下一步是 C30 B 路线运行时探测: ```text version self_check scan_windows dump_uia +probe_client_runtime ``` -Go MCP 代码在后续节点实现;C# helper 稳定拿到真实 iSphere 窗口结构后,再实现 Go 的 `helperclient` 和 MCP tools。 +Go MCP 代码已经具备基础 helper 调用能力;`probe_client_runtime` 暂不暴露为 MCP 业务工具。待真实登录客户端上的 B1/B2 探测通过后,再决定是否新增 sidecar 调用层和受控发送工具。 diff --git a/docs/source-discovery/2026-07-10-live-probe-recorder.md b/docs/source-discovery/2026-07-10-live-probe-recorder.md new file mode 100644 index 0000000..7d03a20 --- /dev/null +++ b/docs/source-discovery/2026-07-10-live-probe-recorder.md @@ -0,0 +1,106 @@ +# iSphere Live Probe Recorder + +Date: 2026-07-10 +Purpose: collect read-only runtime evidence from a machine where iSphere / IMPlatformClient can actually log in. + +## Why this recorder exists + +The current development machine cannot log in because the current network has no reachable server. Simulating login would only prove window rendering and would not prove the real logged-in runtime state required by the B-route sidecar connector. + +Therefore the selected path is to carry a dedicated recorder executable to a working environment, run it after manual login, and bring the generated JSON evidence back. + +## Built artifact + +Package directory: + +```text +runs/live-probe-recorder-package/ +``` + +Executable: + +```text +runs/live-probe-recorder-package/ISphereLiveProbeRecorder.exe +``` + +Zip package: + +```text +runs/live-probe-recorder-package.zip +``` + +The package is generated by: + +```powershell +powershell -NoProfile -ExecutionPolicy Bypass -File scripts\package-live-probe-recorder.ps1 +``` + +## Operator steps + +1. Copy `runs/live-probe-recorder-package.zip` to the machine/network where iSphere can connect to its server. +2. Extract the zip. +3. Manually open and log in to iSphere / `IMPlatformClient.exe`. +4. Manually open one direct-contact chat window and one group-chat window if possible. Keep the editor/send/attachment area visible, but do not send anything. +5. Keep the client window open. +6. Double-click `ISphereLiveProbeRecorder.exe`. +7. The program writes a folder under: + +```text +probe-output/isphere-live-probe-/ +``` + +8. Compress that generated folder and return it. + +## What it records + +The recorder writes: + +```text +manifest.json +probe_client_runtime.json +process_details.json +network_inventory.json +filesystem_inventory.json +services_registry_shortcuts.json +scan_windows.json +uia_summary.json +uia/*.json +feasibility_summary.json +README.txt +NEXT_STEP.txt +``` + +The important evidence is: + +- target process PID/name/title/path; +- process bitness; +- loaded module list; +- command-line metadata with basic secret redaction; +- key module presence, including `smack.dll`, `IMPP.Interface.dll`, `IMPP.ServiceBase.dll`, `IMPP.Service.dll`, `TcpFileTransfer.dll`; +- visible window metadata; +- UIA tree for target windows, useful if route A UI/RPA is needed later. +- install/cache/config/log/database candidate metadata; +- version and SHA256 for important binaries; +- redacted text samples for small config/log files; +- uninstall registry entries; +- related services and shortcuts; +- target-process TCP connections; +- related named pipes. + +## What it does not do + +The recorder does not: + +- send messages; +- send or upload files; +- click UI; +- type text; +- inject hooks; +- attach instrumentation; +- modify client files or data. + +## Decision after returned evidence + +If `probe_client_runtime.json` proves the loaded modules and bitness match the static evidence, continue with B-route sidecar design. + +If the running client cannot be inspected, or the expected modules are absent, switch to the A-route constrained UI/RPA fallback. diff --git a/docs/source-discovery/2026-07-10-send-sidecar-b-first-plan.md b/docs/source-discovery/2026-07-10-send-sidecar-b-first-plan.md new file mode 100644 index 0000000..73f4d00 --- /dev/null +++ b/docs/source-discovery/2026-07-10-send-sidecar-b-first-plan.md @@ -0,0 +1,357 @@ +# Send Connector B-First Plan + +Date: 2026-07-10 +Node: C29 / write-side connector route correction +Decision owner: user/business manager + +## Decision + +The selected send route is now: + +1. **Primary route B: in-process / sidecar connector** that reuses the already logged-in `IMPlatformClient.exe` runtime and its existing message/file send classes. +2. **Fallback route A: constrained UI/RPA wrapper** only if B cannot attach, load, or invoke safely. +3. **Network replay/protocol reimplementation remains deferred** because it would require independently reproducing login/session/ack behavior. + +This document supersedes the previous C15/R5 conclusion for future send work. The old conclusion was correct for the evidence available at that time: no write connector had been selected. This node records the newly approved route and the evidence that makes B worth trying first. + +## Business goal + +Expose controlled MCP actions for digital employees: + +- `isphere_send_message` +- `isphere_send_file` + +The implementation must preserve these business controls: + +- target must be resolved from existing contact/group search results; +- text/file content must be previewed before execution; +- execution must use an `idempotency_key` and content hash; +- every outbound attempt must produce an audit record; +- early versions should support sandbox/test-target verification before normal production use. + +## Static evidence summary + +Offline package evidence under: + +```text +runs/offline-evidence-intake/zyl-qqfile-20260709/ +``` + +Relevant extracted binaries: + +```text +zyl/Impp/IMPlatformClient.exe +zyl/Impp/smack.dll +zyl/Impp/IMPP.Interface.dll +zyl/Impp/IMPP.ServiceBase.dll +zyl/Impp/IMPP.Service.dll +zyl/Impp/TcpFileTransfer.dll +zyl/Impp/HYHC.IMPP.DAL.dll +``` + +Relevant IL snapshots: + +```text +runs/offline-evidence-intake/zyl-qqfile-20260709/metadata/c28-il/IMPlatformClient.exe.il +runs/offline-evidence-intake/zyl-qqfile-20260709/metadata/c28-il/smack.dll.il +runs/offline-evidence-intake/zyl-qqfile-20260709/metadata/c28-il/IMPP.Interface.dll.il +runs/offline-evidence-intake/zyl-qqfile-20260709/metadata/c28-il/IMPP.Service.dll.il +runs/offline-evidence-intake/zyl-qqfile-20260709/metadata/c28-il/IMPP.ServiceBase.dll.il +``` + +## Text-send call chain + +### Preferred sidecar candidate + +`IMPlatformClient.exe` contains a high-level helper: + +```text +IMPP.Client.Core.Plugins.Manager.AppContextManager.SendTxtMessageByJid(jid, chatType, content) +``` + +Observed behavior from IL: + +- for direct/P2P chat, it gets a `P2PChatManager` from `IMPPManager.Instance`; +- it resolves a `Chat` by JID; +- it finds or opens a chat form; +- it calls `frmP2PChat.SendTxtMessage(content)`; +- for temporary/reserved group chat, it calls the corresponding room form send method. + +This is the best B-route first target because it reuses the logged-in client's own state and UI/business rules. + +### Central message construction candidate + +`IMPlatformClient.exe` also contains: + +```text +IMPP.Client.Core.MessageEngine.MessageScheduling.SendChatMessage(chat, msgText, toJid, messageType, isSaveMessage, msgGuid, IsReceipt) +``` + +Observed behavior from IL: + +- reads local font/style settings; +- builds message body via `UtilsText.GetMessageStyleBodyText`; +- decides `chat` vs `groupchat`; +- creates `smack.packet.XmppMessage`; +- sets body, subject, offline flag, receipt flag, time, and message type; +- calls `chat.SendMessage(xmppMessage)`; +- optionally persists/parses message through `MessageManager.MessageParsing`. + +This is the cleanest lower-level B candidate if the sidecar can obtain a valid `Chat` object from the running client context. + +### Protocol bottom + +`smack.dll` provides the real wire-send chain: + +```text +com.vision.smack.Chat.SendMessage(XmppMessage) + -> com.vision.smack.XMPPConnection.send(string) + -> Sharp.Xmpp.Core.XmppCore.Send(string) + -> stream.Write(...) +``` + +This proves that the send capability exists in the installed client binaries. It also shows why an external reimplementation is less attractive: it would need the same active connection, stanza format, message id, receipt, and session state. + +## Important trap: plugin interface text send is not enough + +`IMPP.Interface.dll` declares: + +```text +IAppContextManager.SendP2PMessage(string pJid, string pMessage) +IAppContextManager.SendP2PFiles(UserInfo[] pUserInfo, string[] pFiles) +IAppContextManager.SendChatRoomMessage(string pRoomJid, string pMessage, string pluginID) +IAppContextManager.SendPluginMessage(string pluginID, int command, string para) +``` + +But the implementation of: + +```text +IMPP.Client.Core.Plugins.Manager.AppContextManager.SendP2PMessage(...) +``` + +records the plugin call and then throws `NotImplementedException`. + +Therefore the B route must **not** choose `SendP2PMessage` as the text-send entry. The sidecar should prefer `SendTxtMessageByJid` or, if necessary, `MessageScheduling.SendChatMessage` with an existing `Chat` object. + +## File-send call chain + +File sending appears split into upload plus file-message notification. + +### Upload engine + +`IMPP.ServiceBase.dll` defines `IFileTransfer`. + +`IMPP.Service.dll` implements: + +```text +IMPP.Service.BLL.FileTransfer.FileUpload(...) +IMPP.Service.BLL.FileTransfer.AsynFileUpload(...) +IMPP.Service.BLL.FileTransfer.FileDownload(...) +IMPP.Service.BLL.FileTransfer.SyncFileTransfer(...) +``` + +### Offline file send + +`IMPlatformClient.exe` contains: + +```text +IMPP.Client.OffLineFileSend.sendFile() +IMPP.Client.OffLineFileSend.trans_UploadCompleted(fileId) +``` + +Observed behavior: + +- builds a generated file GUID; +- builds remote name as `filename____guid.ext`; +- uses `Config.ServerInfo.HTTPFileServer` and `FileTransferManager.GetDomain`; +- creates `FileUploadPara`; +- calls `IMPPManager.Instance.Service.FileTransfer.FileUpload(uploadPara)`; +- after upload completion, creates an `OFFLINE_FILE_TRANSFER;...` message body; +- calls `Chat.sendFileMessage(...)`; +- sets packet id to `fileId` and records metadata. + +### Native TCP file transfer + +`TcpFileTransfer.dll` is native and exposes strings/exports indicating: + +```text +Send +SendFile +GetTcpFileMsgGuid +_StartWaitForSendFileThread@8 +_StartRecvFileThread@24 +``` + +This is a candidate for online/TCP file transfer, but the first B route should use the managed offline-file path because it is closer to the normal client business flow and already integrates upload metadata with XMPP file messages. + +## Proposed sidecar architecture + +```text +Go MCP Server + -> controlled action contract and audit + -> C# WinHelper / Sidecar command + -> B-route sidecar adapter + -> running IMPlatformClient context + -> existing AppContextManager / MessageScheduling / OffLineFileSend / smack.dll +``` + +The sidecar should be a narrow adapter, not a general-purpose remote code executor. + +### Candidate sidecar operations + +```text +version +self_check +probe_client_runtime +probe_send_entrypoints +resolve_runtime_jid +preview_send_message +send_message_after_approval +preview_send_file +send_file_after_approval +read_send_audit +``` + +The first implementation slice should stop before real production send: + +```text +version +self_check +probe_client_runtime +probe_send_entrypoints +``` + +Then run a live sandbox observation before enabling `send_message_after_approval`. + +## B-route validation gates + +### Gate B1: runtime discovery + +Pass condition: + +- find a running `IMPlatformClient.exe` process; +- record PID, bitness, install path, module list summary; +- confirm the discovered path matches the extracted evidence family; +- confirm whether the sidecar must be x86 or x64. + +Stop condition: + +- no logged-in process; +- process bitness incompatible with available sidecar build; +- module names differ too much from the extracted package. + +### Gate B2: entrypoint availability + +Pass condition: + +- confirm `IMPlatformClient.exe` exposes `AppContextManager` and `MessageScheduling` in the runtime module set; +- confirm `smack.dll` is loaded or loadable from the same install directory; +- confirm `IMPP.Interface.dll` and `IMPP.Service*.dll` are present. + +Stop condition: + +- app is not .NET Framework / cannot inspect CLR runtime safely; +- required assemblies are missing; +- only `SendP2PMessage` is available but `SendTxtMessageByJid`/`MessageScheduling` cannot be reached. + +### Gate B3: non-mutating dynamic observation + +Pass condition: + +- during one operator-approved manual test send, observe the expected chain: + - `SendTxtMessageByJid` or UI send method; + - `MessageScheduling.SendChatMessage`; + - `Chat.SendMessage`; + - `XMPPConnection.send`; +- capture only redacted method names, argument shape, message id/hash, and success/ack indicator. + +Stop condition: + +- method chain differs from static evidence; +- no success/ack indicator can be mapped; +- message id/idempotency cannot be correlated. + +### Gate B4: sidecar dry run + +Pass condition: + +- sidecar can resolve the target JID/chat type; +- sidecar can build a preview object without sending; +- preview contains target, content hash, connector, and proposed entrypoint; +- audit event is appended. + +Stop condition: + +- no deterministic target resolution; +- preview cannot distinguish direct vs group chat; +- no auditable dry-run object. + +### Gate B5: one sandbox send + +Pass condition: + +- one explicit test target only; +- one idempotency key only; +- one message content hash only; +- success/ack or local sent-record evidence captured; +- duplicate retry with same idempotency key is rejected locally. + +Stop condition: + +- duplicate send risk; +- wrong target risk; +- no post-send verification. + +## A-route fallback + +A remains the backup route if B cannot be made reliable. + +A should not be arbitrary desktop control. It should expose only: + +```text +search_contact(query) +open_conversation(target) +write_draft(text) +verify_draft(target, text_hash) +send_after_approval(approval_id) +upload_file(path) +verify_send_result() +``` + +A-route fallback is activated when any of these happen: + +- B cannot attach/probe safely; +- required runtime classes are unavailable in the logged-in process; +- B can observe but cannot invoke without unstable side effects; +- B cannot provide idempotency/audit guarantees; +- sidecar bitness/runtime mismatch is too costly to resolve in the current cycle. + +## MCP contract impact + +The MCP business tools should not expose B/A internals directly. They should keep the stable user-facing shape: + +```text +isphere_send_message(target_type, target_id, content_text, mode, idempotency_key, content_sha256) +isphere_send_file(target_type, target_id, file_path, mode, idempotency_key, file_sha256) +``` + +Connector metadata should show the route: + +```text +connector = "implatform-sidecar" | "uia-rpa" +connector_stage = "preview" | "sandbox_send" | "production" +``` + +## Next implementation loop + +Next node should be **C30 B-route runtime probe**: + +1. Use the C# WinHelper read-only op `probe_client_runtime`. +2. Probe running `IMPlatformClient.exe` process, bitness, path, loaded modules, and installed assembly presence. +3. Do not send, click, type, upload, or attach hooks in this node. +4. Write probe output to `runs/real-loggedin-lab/sidecar-probe/` if a live client is available. +5. Update `capability-source-matrix.md` after B1/B2 pass or fail. + +## Current status + +B is selected as the primary send/file-send route. The first non-mutating runtime probe op has been added to `ISphereWinHelper` as `probe_client_runtime`; local verification passed with no target client running. The next executable work is to run that probe while the real iSphere/`IMPlatformClient.exe` client is logged in, then perform non-mutating entrypoint confirmation. diff --git a/docs/source-discovery/README.md b/docs/source-discovery/README.md index 55e2ac2..4f0eae7 100644 --- a/docs/source-discovery/README.md +++ b/docs/source-discovery/README.md @@ -12,6 +12,8 @@ Build the first business read capability path for `isphere_receive_messages` fro - `docs/source-discovery/2026-07-09-n12-pre-zyl-index.md` - `docs/source-discovery/2026-07-09-n12-pre-zyl-schema-notes.md` - `docs/source-discovery/capability-source-matrix.md` +- `docs/source-discovery/2026-07-10-send-sidecar-b-first-plan.md` +- `docs/source-discovery/2026-07-10-live-probe-recorder.md` - `docs/mcp-core-tools-contract.md` - `docs/mcp-core-business-plan.md` @@ -41,3 +43,7 @@ See `docs/superpowers/plans/2026-07-09-stage-c-log-backed-receive-messages-loop. ## Last completed discovery result Stage B2 showed that selected `MsgLib.db` files are not direct SQLite. Decrypted `PacketReader.ProcessPacket` XML is now the first implementable source for `isphere_receive_messages`. + +C29 corrected the write-side route after static binary evidence review and user decision: `isphere_send_message` should prioritize the running-client sidecar / in-process connector route, with constrained UI/RPA as fallback. The next write-side loop is a non-mutating B-route runtime probe. + +C30 adds a portable `ISphereLiveProbeRecorder.exe` package for environments where iSphere can actually log in. The recorder collects read-only runtime evidence and writes JSON output for B-route sidecar feasibility analysis. diff --git a/docs/source-discovery/capability-source-matrix.md b/docs/source-discovery/capability-source-matrix.md index 9f3adfb..a67dba4 100644 --- a/docs/source-discovery/capability-source-matrix.md +++ b/docs/source-discovery/capability-source-matrix.md @@ -7,11 +7,12 @@ Schema notes: `docs/source-discovery/2026-07-09-n12-pre-zyl-schema-notes.md` ## Source priority -1. Existing bridge / API / local service. -2. Local data, log, cache, or database source. -3. UIA helper source through C# WinHelper. -4. Network/protocol source. -5. Mock source for tests only. +1. Running-client sidecar / in-process connector that reuses the logged-in `IMPlatformClient.exe` runtime. +2. Existing bridge / API / local service. +3. Local data, log, cache, or database source. +4. UIA helper source through C# WinHelper. +5. Network/protocol source. +6. Mock source for tests only. ## Matrix @@ -21,8 +22,8 @@ Schema notes: `docs/source-discovery/2026-07-09-n12-pre-zyl-schema-notes.md` | `isphere_search_contacts` | bare sender/receiver JIDs extracted from normalized log-backed messages loaded from configured PacketReader file or directory source | validated copied `MsgLib.db` tables: `TD_Roster`, `TD_CustomEffigy`, `tblRecent`, `tblChatLevel`, `tblPersonMsg`; decrypted roster/contact stanzas from `Smark.SendReceive`; UIA helper source if display names are still missing | `docs/source-discovery/2026-07-10-msglib-readonly-schema-extraction.md`; `internal/isphere/contacts_test.go`; `internal/tools/isphere_contacts_test.go` | C34 documents optional MsgLib contact display enrichment; R2 adds deterministic exact-match-first ranking, case-insensitive de-duplication across log/MsgLib candidates, and preserves `source`/`raw_ref` | core contact search complete; optional richer fields later | | `isphere_search_groups` | decrypted `PacketReader.ProcessPacket` `type="groupchat"` stanzas and conference/MUC JIDs loaded from configured PacketReader file or directory source | validated copied `MsgLib.db` tables: `tblMsgGroupPersonMsg`, `TD_WorkGroupAuth`, `tblRecent`; UIA helper source if group display names are still missing | `docs/source-discovery/2026-07-10-msglib-readonly-schema-extraction.md`; `internal/isphere/groups_test.go`; `internal/tools/isphere_groups_test.go`; C9 ignored-log scan: PacketReader 86 groupchat/86 conference hits; Smark 24 groupchat/658 conference/631 muc hits | C34 documents optional MsgLib group display enrichment; R2 adds deterministic exact-match-first ranking, case-insensitive de-duplication across log/MsgLib candidates, and preserves `source`/`raw_ref` | core group search complete; optional member/owner hierarchy later | | `isphere_receive_files` | decrypted `PacketReader.ProcessPacket` file-transfer message stanzas via `internal/isphere.ListFilesFromMessages` and `isphere_receive_files` list mode; configured PacketReader file or directory source; `zyl\importal\` cache entries remain unlinked | MsgLib `TD_ReceiveFileRecord` safe metadata through explicit copied-DB path; decrypted `Smark.SendReceive` and `SaveToDB` traces for file-reference reconciliation; future UIA/client connector for download | `docs/source-discovery/2026-07-09-n12-pre-zyl-schema-notes.md#c12-file-transfer-evidence-precheck`; `docs/source-discovery/2026-07-10-file-download-mapping-precheck.md`; `docs/source-discovery/2026-07-10-file-cache-mapping-diagnostic.md` | C22 verifies safe file-list contract args; R3 confirms list metadata is available; R3b diagnostic finds 0 current cache/archive matches, so R4 remains blocked | blocked pending better cache evidence or UI/client download connector | -| `isphere_send_message` | none validated yet | existing bridge/API/local service preferred; UIA write connector only after internal-sandbox action evidence; network/protocol connector only after protocol discovery | `docs/source-discovery/2026-07-10-send-message-source-precheck.md`; `docs/source-discovery/2026-07-10-send-message-connector-selection.md` | C15 and R5 blocked: contract exists, but no committed bridge/API/local service, helper write op, UIA action chain, or protocol connector is validated | blocked pending connector evidence package | -| `isphere_send_file` | none validated yet | depends on send-message connector plus outbound file/upload evidence | `docs/source-discovery/2026-07-10-send-message-source-precheck.md`; `docs/source-discovery/2026-07-10-send-message-connector-selection.md` | blocked behind `isphere_send_message` connector selection and file upload/source policy; R5 found no send connector | blocked pending send connector evidence | +| `isphere_send_message` | B-route selected: running-client sidecar / in-process connector, preferring `AppContextManager.SendTxtMessageByJid(...)` or `MessageScheduling.SendChatMessage(...)` over the unimplemented `SendP2PMessage(...)` interface path | A-route fallback: constrained UI/RPA wrapper only if B cannot pass runtime probe, entrypoint availability, dynamic observation, and idempotent dry-run gates; network/protocol replay remains deferred | `docs/source-discovery/2026-07-10-send-message-source-precheck.md`; `docs/source-discovery/2026-07-10-send-message-connector-selection.md`; `docs/source-discovery/2026-07-10-send-sidecar-b-first-plan.md` | C29 route correction: previous C15/R5 block remains historical evidence; user approved B primary and A backup. Static IL evidence identifies the client-side send chain `IMPlatformClient.exe -> smack.dll`, but no runtime sidecar send is implemented yet | next: C30 B-route runtime probe, then non-mutating manual-send observation before enabling sandbox send | +| `isphere_send_file` | B-route selected after send-message probe: managed offline-file path through `OffLineFileSend.sendFile()`, `IMPP.Service*.dll` `IFileTransfer.FileUpload(...)`, and `Chat.sendFileMessage(...)` | A-route fallback: constrained UI/RPA upload/send-file flow; native `TcpFileTransfer.dll` is a later candidate only after managed path is rejected | `docs/source-discovery/2026-07-10-send-message-source-precheck.md`; `docs/source-discovery/2026-07-10-send-message-connector-selection.md`; `docs/source-discovery/2026-07-10-send-sidecar-b-first-plan.md` | C29 route correction: static IL evidence identifies upload-plus-file-message chain, but runtime probe and one approved sandbox file-send observation are still required | next: wait for C30/C31 message-send probe; then C32 file-send runtime/upload probe | ## Stage C entry rule diff --git a/docs/superpowers/plans/2026-07-10-c30-send-sidecar-runtime-probe.md b/docs/superpowers/plans/2026-07-10-c30-send-sidecar-runtime-probe.md new file mode 100644 index 0000000..71db84a --- /dev/null +++ b/docs/superpowers/plans/2026-07-10-c30-send-sidecar-runtime-probe.md @@ -0,0 +1,146 @@ +# C30 Send Sidecar Runtime Probe Plan + +Date: 2026-07-10 +Route decision: B primary, A fallback +Parent: `docs/source-discovery/2026-07-10-send-sidecar-b-first-plan.md` + +## Objective + +Prove whether the running logged-in iSphere / `IMPlatformClient.exe` client on this machine can support the B-route sidecar connector. + +This node is read-only. It does not send messages, upload files, click UI, type text, inject hooks, or attach runtime instrumentation. + +## Scope + +Use `ISphereWinHelper` op: + +```text +probe_client_runtime +``` + +For a machine where iSphere can actually log in, prefer the portable package: + +```text +runs/live-probe-recorder-package.zip +``` + +It collects the runtime probe plus process details, network metadata, filesystem candidates, registry/service/shortcut inventory, and UIA target-window dumps in one run. + +Collect: + +- target process PID; +- process name; +- main window title/handle; +- executable path; +- process bitness; +- helper host bitness; +- target-process command-line metadata with redaction; +- key module presence: + - `IMPlatformClient.exe` + - `smack.dll` + - `IMPP.Interface.dll` + - `IMPP.ServiceBase.dll` + - `IMPP.Service.dll` + - `TcpFileTransfer.dll` + - `HYHC.IMPP.DAL.dll` +- install/cache/config/log/database candidate metadata; +- key file versions and hashes; +- target-process TCP connections and related named pipes; +- registry uninstall entries, services, shortcuts; +- UIA target-window dumps for A-route fallback. + +## Acceptance gates + +### B1 runtime discovery + +Pass when: + +- at least one target process is returned; +- target score favors `IMPlatformClient` / iSphere; +- executable path is present; +- bitness is known or module probing still succeeds. + +Fail when: + +- no target process is running; +- process cannot be inspected enough to determine path/bitness; +- discovered process does not match iSphere/IMPlatformClient. + +### B2 module availability + +Pass when: + +- `IMPlatformClient.exe` is confirmed; +- `smack.dll` is present in loaded modules or install path evidence; +- `IMPP.Interface.dll`, `IMPP.ServiceBase.dll`, and `IMPP.Service.dll` are present or explainably loadable. + +Fail when: + +- `smack.dll` is absent and no install-path fallback is available; +- service/interface DLLs are absent; +- helper bitness prevents module inspection and no x86/x64 helper variant is available. + +## Commands + +Build and verify helper: + +```powershell +powershell -NoProfile -ExecutionPolicy Bypass -File scripts\verify-win-helper.ps1 +``` + +Run live runtime probe after the user has logged in to iSphere: + +```powershell +$request = @{ + protocol = 'isphere.helper.v1' + request_id = 'c30-live-runtime-probe' + op = 'probe_client_runtime' + timeout_ms = 5000 + args = @{ + process_names = @('IMPlatformClient', 'IMPP.ISphere', 'iSphere', 'importal') + include_modules = $true + max_modules = 160 + } +} | ConvertTo-Json -Depth 8 -Compress + +$request | .\runs\win-helper\ISphereWinHelper.exe | Tee-Object -FilePath .\runs\real-loggedin-lab\sidecar-probe\c30-runtime-probe.json +``` + +If the target is 32-bit and module probing fails from the default helper, rebuild x86 helper: + +```powershell +powershell -NoProfile -ExecutionPolicy Bypass -File scripts\build-win-helper.ps1 -Platform x86 -OutputDir runs\win-helper-x86 +``` + +Then rerun the same request against: + +```powershell +.\runs\win-helper-x86\ISphereWinHelper.exe +``` + +## Output artifacts + +If a logged-in client is available, write: + +```text +runs/real-loggedin-lab/sidecar-probe/c30-runtime-probe.json +``` + +Then update: + +```text +docs/source-discovery/capability-source-matrix.md +``` + +## Next node decision + +If B1/B2 pass: + +- move to C31 non-mutating manual-send observation plan; +- observe whether manual send hits `SendTxtMessageByJid`, `MessageScheduling.SendChatMessage`, `Chat.SendMessage`, and `XMPPConnection.send`. + +If B1/B2 fail: + +- document exact failure; +- decide whether to try x86/x64 helper variant; +- if still blocked, activate A-route constrained UI/RPA fallback. diff --git a/native/ISphereWinHelper/LiveProbeInventory.cs b/native/ISphereWinHelper/LiveProbeInventory.cs new file mode 100644 index 0000000..f3b9e62 --- /dev/null +++ b/native/ISphereWinHelper/LiveProbeInventory.cs @@ -0,0 +1,802 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Globalization; +using System.IO; +using System.Management; +using System.Net; +using System.Runtime.InteropServices; +using System.Security.Cryptography; +using System.Text; +using System.Text.RegularExpressions; +using Microsoft.Win32; + +namespace ISphereWinHelper +{ + internal static class LiveProbeInventory + { + private static readonly string[] InterestingTerms = new[] + { + "isphere", "implatform", "impp", "importal", "smark", "smack", "xmpp", "msg", "loginlog", "packetreader", "sendreceive" + }; + + private static readonly string[] InterestingExtensions = new[] + { + ".exe", ".dll", ".config", ".xml", ".ini", ".json", ".log", ".db", ".sqlite", ".dat", ".idx", ".ldb", ".txt" + }; + + public static Dictionary CollectProcessDetails(Dictionary runtime) + { + var pids = TargetPids(runtime); + var details = new List>(); + foreach (int pid in pids) + { + string query = "SELECT ProcessId,ParentProcessId,Name,ExecutablePath,CommandLine,CreationDate FROM Win32_Process WHERE ProcessId=" + pid.ToString(CultureInfo.InvariantCulture); + foreach (ManagementObject obj in QueryWmi(query)) + { + using (obj) + { + details.Add(new Dictionary + { + { "pid", SafeWmi(obj, "ProcessId") }, + { "parent_pid", SafeWmi(obj, "ParentProcessId") }, + { "name", SafeWmi(obj, "Name") }, + { "executable_path", SafeWmi(obj, "ExecutablePath") }, + { "command_line_redacted", RedactSecrets(SafeWmi(obj, "CommandLine")) }, + { "creation_date", SafeWmi(obj, "CreationDate") } + }); + } + } + } + return new Dictionary + { + { "target_pid_count", pids.Count }, + { "details", details } + }; + } + + public static Dictionary CollectNetwork(Dictionary runtime) + { + var targetPids = TargetPids(runtime); + return new Dictionary + { + { "tcp", CaptureTargetTcpConnections(targetPids) }, + { "named_pipes", CaptureNamedPipes() } + }; + } + + public static Dictionary CollectServicesRegistryShortcuts() + { + return new Dictionary + { + { "services", CaptureServices() }, + { "registry_uninstall", CaptureUninstallEntries() }, + { "shortcuts", CaptureShortcuts() } + }; + } + + public static Dictionary CollectFilesystem(Dictionary runtime) + { + var roots = CandidateRoots(runtime); + var entries = new List>(); + foreach (string root in roots) + { + entries.Add(InventoryRoot(root)); + } + return new Dictionary + { + { "root_count", entries.Count }, + { "roots", entries } + }; + } + + public static Dictionary SummarizeFeasibility(Dictionary runtime, Dictionary filesystem, Dictionary network) + { + var targetPids = TargetPids(runtime); + bool hasTarget = targetPids.Count > 0; + bool hasSmack = AnyAssemblyPresent(runtime, "smack.dll"); + bool hasInterface = AnyAssemblyPresent(runtime, "IMPP.Interface.dll"); + bool hasService = AnyAssemblyPresent(runtime, "IMPP.Service.dll") || AnyAssemblyPresent(runtime, "IMPP.ServiceBase.dll"); + bool hasTcpFileTransfer = AnyAssemblyPresent(runtime, "TcpFileTransfer.dll"); + string route = hasTarget && hasSmack && hasInterface ? "B_ROUTE_PROMISING" : "NEEDS_REVIEW_OR_A_ROUTE"; + + return new Dictionary + { + { "target_process_seen", hasTarget }, + { "target_pid_count", targetPids.Count }, + { "has_smack_loaded", hasSmack }, + { "has_impp_interface_loaded", hasInterface }, + { "has_impp_service_loaded", hasService }, + { "has_tcp_file_transfer_loaded", hasTcpFileTransfer }, + { "initial_route_hint", route }, + { "next_decision", hasTarget ? "review loaded modules, bitness, install files, and UIA target windows" : "run again after manual login in the working environment" } + }; + } + + private static Dictionary InventoryRoot(string root) + { + var files = new List>(); + var directories = new List(); + if (!Directory.Exists(root)) + { + return new Dictionary + { + { "root", root }, + { "exists", false }, + { "directories_sample", directories }, + { "files", files } + }; + } + + foreach (string dir in SafeDirectories(root, 3, 120)) + { + directories.Add(dir); + } + + foreach (string file in SafeFiles(root, 4, 1000)) + { + if (IsInterestingFile(file)) + { + files.Add(DescribeFile(file)); + } + } + + files.Sort(delegate(Dictionary left, Dictionary right) + { + return string.Compare(Convert.ToString(left["path"]), Convert.ToString(right["path"]), StringComparison.OrdinalIgnoreCase); + }); + + return new Dictionary + { + { "root", root }, + { "exists", true }, + { "directories_sample", directories }, + { "interesting_file_count", files.Count }, + { "files", files } + }; + } + + private static Dictionary DescribeFile(string path) + { + var info = new FileInfo(path); + string version = ""; + string productVersion = ""; + try + { + FileVersionInfo vi = FileVersionInfo.GetVersionInfo(path); + version = vi.FileVersion ?? ""; + productVersion = vi.ProductVersion ?? ""; + } + catch + { + } + + bool keyBinary = IsKeyBinary(info.Name); + bool smallText = IsSmallTextFile(info); + return new Dictionary + { + { "path", info.FullName }, + { "name", info.Name }, + { "extension", info.Extension }, + { "length", info.Length }, + { "creation_time_utc", info.CreationTimeUtc.ToString("o") }, + { "last_write_time_utc", info.LastWriteTimeUtc.ToString("o") }, + { "file_version", version }, + { "product_version", productVersion }, + { "sha256", keyBinary || info.Length <= 8 * 1024 * 1024 ? Sha256(info.FullName) : "" }, + { "text_sample_redacted", smallText ? RedactedTextSample(info.FullName, 4096) : "" } + }; + } + + private static SortedSet CandidateRoots(Dictionary runtime) + { + var roots = new SortedSet(StringComparer.OrdinalIgnoreCase); + foreach (Dictionary target in GetDictionaryList(runtime, "targets")) + { + string exe = GetString(target, "executable_path"); + if (!string.IsNullOrWhiteSpace(exe)) + { + AddRoot(roots, Path.GetDirectoryName(exe)); + } + } + + AddRoot(roots, Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), "Temp", "importal")); + AddRoot(roots, Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), "importal")); + AddRoot(roots, Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), "importal")); + AddRoot(roots, Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.CommonApplicationData), "importal")); + AddRoot(roots, Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments), "iSphere")); + AddRoot(roots, Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments), "IMPP")); + return roots; + } + + private static void AddRoot(SortedSet roots, string path) + { + if (string.IsNullOrWhiteSpace(path)) + { + return; + } + try + { + if (Directory.Exists(path)) + { + roots.Add(Path.GetFullPath(path)); + } + } + catch + { + } + } + + private static List> CaptureServices() + { + var services = new List>(); + foreach (ManagementObject obj in QueryWmi("SELECT Name,DisplayName,State,StartMode,PathName,ProcessId FROM Win32_Service")) + { + using (obj) + { + string combined = (SafeWmi(obj, "Name") + " " + SafeWmi(obj, "DisplayName") + " " + SafeWmi(obj, "PathName")).ToLowerInvariant(); + if (!ContainsInterestingTerm(combined)) + { + continue; + } + services.Add(new Dictionary + { + { "name", SafeWmi(obj, "Name") }, + { "display_name", SafeWmi(obj, "DisplayName") }, + { "state", SafeWmi(obj, "State") }, + { "start_mode", SafeWmi(obj, "StartMode") }, + { "path_name_redacted", RedactSecrets(SafeWmi(obj, "PathName")) }, + { "process_id", SafeWmi(obj, "ProcessId") } + }); + } + } + return services; + } + + private static List> CaptureUninstallEntries() + { + var entries = new List>(); + CaptureUninstallHive(entries, RegistryHive.LocalMachine, RegistryView.Registry64); + CaptureUninstallHive(entries, RegistryHive.LocalMachine, RegistryView.Registry32); + CaptureUninstallHive(entries, RegistryHive.CurrentUser, RegistryView.Registry64); + CaptureUninstallHive(entries, RegistryHive.CurrentUser, RegistryView.Registry32); + return entries; + } + + private static void CaptureUninstallHive(List> entries, RegistryHive hive, RegistryView view) + { + try + { + using (RegistryKey baseKey = RegistryKey.OpenBaseKey(hive, view)) + using (RegistryKey uninstall = baseKey.OpenSubKey(@"SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall")) + { + if (uninstall == null) + { + return; + } + foreach (string subName in uninstall.GetSubKeyNames()) + { + using (RegistryKey sub = uninstall.OpenSubKey(subName)) + { + if (sub == null) + { + continue; + } + string displayName = Convert.ToString(sub.GetValue("DisplayName", "")); + string installLocation = Convert.ToString(sub.GetValue("InstallLocation", "")); + string publisher = Convert.ToString(sub.GetValue("Publisher", "")); + string combined = (subName + " " + displayName + " " + installLocation + " " + publisher).ToLowerInvariant(); + if (!ContainsInterestingTerm(combined)) + { + continue; + } + entries.Add(new Dictionary + { + { "hive", hive.ToString() }, + { "view", view.ToString() }, + { "key", subName }, + { "display_name", displayName }, + { "display_version", Convert.ToString(sub.GetValue("DisplayVersion", "")) }, + { "publisher", publisher }, + { "install_location", installLocation }, + { "uninstall_string_redacted", RedactSecrets(Convert.ToString(sub.GetValue("UninstallString", ""))) } + }); + } + } + } + } + catch + { + } + } + + private static List> CaptureShortcuts() + { + var shortcuts = new List>(); + string[] roots = new[] + { + Environment.GetFolderPath(Environment.SpecialFolder.DesktopDirectory), + Environment.GetFolderPath(Environment.SpecialFolder.CommonDesktopDirectory), + Environment.GetFolderPath(Environment.SpecialFolder.StartMenu), + Environment.GetFolderPath(Environment.SpecialFolder.CommonStartMenu) + }; + foreach (string root in roots) + { + if (string.IsNullOrWhiteSpace(root) || !Directory.Exists(root)) + { + continue; + } + foreach (string lnk in SafeFiles(root, 5, 500)) + { + if (!lnk.EndsWith(".lnk", StringComparison.OrdinalIgnoreCase)) + { + continue; + } + string lower = lnk.ToLowerInvariant(); + if (!ContainsInterestingTerm(lower)) + { + continue; + } + FileInfo info = new FileInfo(lnk); + shortcuts.Add(new Dictionary + { + { "path", info.FullName }, + { "length", info.Length }, + { "last_write_time_utc", info.LastWriteTimeUtc.ToString("o") }, + { "sha256", info.Length <= 1024 * 1024 ? Sha256(info.FullName) : "" } + }); + } + } + return shortcuts; + } + + private static List> CaptureNamedPipes() + { + var pipes = new List>(); + try + { + foreach (string pipe in Directory.GetFiles(@"\\.\pipe\")) + { + string lower = pipe.ToLowerInvariant(); + if (!ContainsInterestingTerm(lower)) + { + continue; + } + pipes.Add(new Dictionary { { "path", pipe } }); + } + } + catch (Exception ex) + { + pipes.Add(new Dictionary { { "error", ex.GetType().Name + ": " + ex.Message } }); + } + return pipes; + } + + private const int AF_INET = 2; + private const int TCP_TABLE_OWNER_PID_ALL = 5; + + [DllImport("iphlpapi.dll", SetLastError = true)] + private static extern uint GetExtendedTcpTable(IntPtr pTcpTable, ref int dwOutBufLen, bool sort, int ipVersion, int tblClass, uint reserved); + + [StructLayout(LayoutKind.Sequential)] + private struct MIB_TCPROW_OWNER_PID + { + public uint state; + public uint localAddr; + public uint localPort; + public uint remoteAddr; + public uint remotePort; + public uint owningPid; + } + + private static Dictionary CaptureTargetTcpConnections(HashSet targetPids) + { + var rows = new List>(); + string error = ""; + try + { + int bufferSize = 0; + GetExtendedTcpTable(IntPtr.Zero, ref bufferSize, true, AF_INET, TCP_TABLE_OWNER_PID_ALL, 0); + IntPtr buffer = Marshal.AllocHGlobal(bufferSize); + try + { + uint result = GetExtendedTcpTable(buffer, ref bufferSize, true, AF_INET, TCP_TABLE_OWNER_PID_ALL, 0); + if (result != 0) + { + error = "GetExtendedTcpTable result=" + result.ToString(CultureInfo.InvariantCulture); + } + else + { + int count = Marshal.ReadInt32(buffer); + IntPtr rowPtr = new IntPtr(buffer.ToInt64() + 4); + int rowSize = Marshal.SizeOf(typeof(MIB_TCPROW_OWNER_PID)); + for (int i = 0; i < count; i++) + { + var row = (MIB_TCPROW_OWNER_PID)Marshal.PtrToStructure(rowPtr, typeof(MIB_TCPROW_OWNER_PID)); + int pid = unchecked((int)row.owningPid); + if (targetPids.Contains(pid)) + { + rows.Add(new Dictionary + { + { "pid", pid }, + { "state", TcpStateName(row.state) }, + { "local_address", UIntToIp(row.localAddr) }, + { "local_port", NetworkToHostPort(row.localPort) }, + { "remote_address", UIntToIp(row.remoteAddr) }, + { "remote_port", NetworkToHostPort(row.remotePort) } + }); + } + rowPtr = new IntPtr(rowPtr.ToInt64() + rowSize); + } + } + } + finally + { + Marshal.FreeHGlobal(buffer); + } + } + catch (Exception ex) + { + error = ex.GetType().Name + ": " + ex.Message; + } + + return new Dictionary + { + { "target_pid_count", targetPids.Count }, + { "connection_count", rows.Count }, + { "connections", rows }, + { "error", error } + }; + } + + private static string UIntToIp(uint value) + { + try + { + return new IPAddress(value).ToString(); + } + catch + { + return value.ToString(CultureInfo.InvariantCulture); + } + } + + private static int NetworkToHostPort(uint port) + { + return (int)(((port & 0xFF) << 8) | ((port >> 8) & 0xFF)); + } + + private static string TcpStateName(uint state) + { + switch (state) + { + case 1: return "CLOSED"; + case 2: return "LISTEN"; + case 3: return "SYN_SENT"; + case 4: return "SYN_RCVD"; + case 5: return "ESTABLISHED"; + case 6: return "FIN_WAIT1"; + case 7: return "FIN_WAIT2"; + case 8: return "CLOSE_WAIT"; + case 9: return "CLOSING"; + case 10: return "LAST_ACK"; + case 11: return "TIME_WAIT"; + case 12: return "DELETE_TCB"; + default: return state.ToString(CultureInfo.InvariantCulture); + } + } + + private static IEnumerable QueryWmi(string query) + { + var list = new List(); + try + { + using (var searcher = new ManagementObjectSearcher(query)) + using (ManagementObjectCollection results = searcher.Get()) + { + foreach (ManagementObject obj in results) + { + list.Add(obj); + } + } + } + catch + { + } + return list; + } + + private static string SafeWmi(ManagementObject obj, string property) + { + try + { + object value = obj[property]; + return value == null ? "" : Convert.ToString(value); + } + catch + { + return ""; + } + } + + private static bool AnyAssemblyPresent(Dictionary runtime, string name) + { + foreach (Dictionary target in GetDictionaryList(runtime, "targets")) + { + object rawPresence; + if (!target.TryGetValue("assembly_presence", out rawPresence)) + { + continue; + } + var presence = rawPresence as Dictionary; + if (presence == null) + { + continue; + } + object value; + if (presence.TryGetValue(name, out value) && value is bool && (bool)value) + { + return true; + } + } + return false; + } + + private static HashSet TargetPids(Dictionary runtime) + { + var pids = new HashSet(); + foreach (Dictionary target in GetDictionaryList(runtime, "targets")) + { + int pid = GetInt(target, "pid"); + if (pid > 0) + { + pids.Add(pid); + } + } + return pids; + } + + private static List> GetDictionaryList(Dictionary source, string key) + { + var list = new List>(); + object value; + if (source == null || !source.TryGetValue(key, out value) || value == null) + { + return list; + } + var direct = value as List>; + if (direct != null) + { + return direct; + } + return list; + } + + private static string GetString(Dictionary source, string key) + { + object value; + if (source != null && source.TryGetValue(key, out value) && value != null) + { + return Convert.ToString(value); + } + return ""; + } + + private static int GetInt(Dictionary source, string key) + { + object value; + if (source != null && source.TryGetValue(key, out value) && value != null) + { + try + { + return Convert.ToInt32(value); + } + catch + { + } + } + return 0; + } + + private static List SafeDirectories(string root, int maxDepth, int maxItems) + { + var result = new List(); + SafeDirectories(root, 0, maxDepth, maxItems, result); + return result; + } + + private static void SafeDirectories(string root, int depth, int maxDepth, int maxItems, List result) + { + if (depth > maxDepth || result.Count >= maxItems || !Directory.Exists(root)) + { + return; + } + string[] dirs; + try + { + dirs = Directory.GetDirectories(root); + } + catch + { + return; + } + foreach (string dir in dirs) + { + if (result.Count >= maxItems) + { + return; + } + result.Add(dir); + SafeDirectories(dir, depth + 1, maxDepth, maxItems, result); + } + } + + private static List SafeFiles(string root, int maxDepth, int maxItems) + { + var result = new List(); + SafeFiles(root, 0, maxDepth, maxItems, result); + return result; + } + + private static void SafeFiles(string root, int depth, int maxDepth, int maxItems, List result) + { + if (depth > maxDepth || result.Count >= maxItems || !Directory.Exists(root)) + { + return; + } + string[] files; + try + { + files = Directory.GetFiles(root); + } + catch + { + return; + } + foreach (string file in files) + { + if (result.Count >= maxItems) + { + return; + } + result.Add(file); + } + string[] dirs; + try + { + dirs = Directory.GetDirectories(root); + } + catch + { + return; + } + foreach (string dir in dirs) + { + if (result.Count >= maxItems) + { + return; + } + SafeFiles(dir, depth + 1, maxDepth, maxItems, result); + } + } + + private static bool IsInterestingFile(string path) + { + string name = Path.GetFileName(path).ToLowerInvariant(); + string ext = Path.GetExtension(path).ToLowerInvariant(); + if (ContainsInterestingTerm(name)) + { + return true; + } + foreach (string interestingExt in InterestingExtensions) + { + if (ext == interestingExt) + { + return true; + } + } + return false; + } + + private static bool IsKeyBinary(string name) + { + string lower = name.ToLowerInvariant(); + return lower == "implatformclient.exe" || + lower == "smack.dll" || + lower == "impp.interface.dll" || + lower == "impp.service.dll" || + lower == "impp.servicebase.dll" || + lower == "tcpfiletransfer.dll" || + lower == "hyhc.impp.dal.dll" || + lower == "inetwork.dll" || + lower == "ioclientnetwork.dll"; + } + + private static bool IsSmallTextFile(FileInfo info) + { + if (info.Length > 256 * 1024) + { + return false; + } + string ext = info.Extension.ToLowerInvariant(); + return ext == ".config" || ext == ".xml" || ext == ".ini" || ext == ".json" || ext == ".log" || ext == ".txt"; + } + + private static string RedactedTextSample(string path, int maxChars) + { + try + { + string text = File.ReadAllText(path, Encoding.UTF8); + if (text.Length > maxChars) + { + text = text.Substring(0, maxChars); + } + return RedactSecrets(text); + } + catch + { + try + { + byte[] bytes = File.ReadAllBytes(path); + int len = Math.Min(bytes.Length, maxChars); + string text = Encoding.Default.GetString(bytes, 0, len); + return RedactSecrets(text); + } + catch + { + return ""; + } + } + } + + private static string RedactSecrets(string text) + { + if (string.IsNullOrEmpty(text)) + { + return ""; + } + string redacted = Regex.Replace(text, "(password|passwd|pwd|token|secret|session|cookie|auth|authorization)\\s*[:=]\\s*[^\\s;&<>\\\"]+", "$1=", RegexOptions.IgnoreCase); + redacted = Regex.Replace(redacted, "(UserID|UserName|LoginName)\\s*[:=]\\s*[^\\s;&<>\\\"]+", "$1=", RegexOptions.IgnoreCase); + return redacted; + } + + private static bool ContainsInterestingTerm(string text) + { + if (string.IsNullOrEmpty(text)) + { + return false; + } + string lower = text.ToLowerInvariant(); + foreach (string term in InterestingTerms) + { + if (lower.Contains(term)) + { + return true; + } + } + return false; + } + + private static string Sha256(string path) + { + try + { + using (SHA256 sha = SHA256.Create()) + using (FileStream stream = File.OpenRead(path)) + { + byte[] hash = sha.ComputeHash(stream); + var sb = new StringBuilder(hash.Length * 2); + foreach (byte b in hash) + { + sb.Append(b.ToString("x2", CultureInfo.InvariantCulture)); + } + return sb.ToString(); + } + } + catch + { + return ""; + } + } + } +} diff --git a/native/ISphereWinHelper/LiveProbeRecorder.cs b/native/ISphereWinHelper/LiveProbeRecorder.cs new file mode 100644 index 0000000..d2683d5 --- /dev/null +++ b/native/ISphereWinHelper/LiveProbeRecorder.cs @@ -0,0 +1,259 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Text; + +namespace ISphereWinHelper +{ + internal static class LiveProbeRecorder + { + public static int Run(string[] args) + { + Console.OutputEncoding = Encoding.UTF8; + string outputRoot = ResolveOutputRoot(args); + string runId = DateTime.Now.ToString("yyyyMMdd-HHmmss"); + string outputDir = Path.Combine(outputRoot, "isphere-live-probe-" + runId); + Directory.CreateDirectory(outputDir); + + try + { + WriteText(Path.Combine(outputDir, "README.txt"), Readme()); + WriteJson(Path.Combine(outputDir, "manifest.json"), Manifest(runId, outputDir)); + + var probeArgs = new Dictionary + { + { "process_names", new object[] { "IMPlatformClient", "IMPP.ISphere", "iSphere", "importal" } }, + { "include_modules", true }, + { "max_modules", 180 } + }; + Dictionary runtime = RuntimeProbe.ProbeClientRuntime(probeArgs); + WriteJson(Path.Combine(outputDir, "probe_client_runtime.json"), runtime); + + Dictionary processDetails = LiveProbeInventory.CollectProcessDetails(runtime); + WriteJson(Path.Combine(outputDir, "process_details.json"), processDetails); + + Dictionary windows = WindowScanner.Scan(new Dictionary + { + { "include_all_visible", true } + }); + WriteJson(Path.Combine(outputDir, "scan_windows.json"), windows); + + Dictionary network = LiveProbeInventory.CollectNetwork(runtime); + WriteJson(Path.Combine(outputDir, "network_inventory.json"), network); + + Dictionary servicesRegistryShortcuts = LiveProbeInventory.CollectServicesRegistryShortcuts(); + WriteJson(Path.Combine(outputDir, "services_registry_shortcuts.json"), servicesRegistryShortcuts); + + Dictionary filesystem = LiveProbeInventory.CollectFilesystem(runtime); + WriteJson(Path.Combine(outputDir, "filesystem_inventory.json"), filesystem); + + string uiaDir = Path.Combine(outputDir, "uia"); + Directory.CreateDirectory(uiaDir); + List> uiaSummary = DumpTargetWindowUia(runtime, windows, uiaDir); + WriteJson(Path.Combine(outputDir, "uia_summary.json"), uiaSummary); + + Dictionary feasibility = LiveProbeInventory.SummarizeFeasibility(runtime, filesystem, network); + WriteJson(Path.Combine(outputDir, "feasibility_summary.json"), feasibility); + + WriteText(Path.Combine(outputDir, "NEXT_STEP.txt"), + "把整个目录压缩后发回即可。\r\n" + + "重点文件:probe_client_runtime.json、process_details.json、network_inventory.json、filesystem_inventory.json、services_registry_shortcuts.json、scan_windows.json、uia_summary.json、uia/*.json、feasibility_summary.json\r\n"); + + Console.WriteLine("OK"); + Console.WriteLine("已生成采集目录:"); + Console.WriteLine(outputDir); + Console.WriteLine(); + Console.WriteLine("请把这个目录压缩后发回。"); + Pause(args); + return 0; + } + catch (Exception ex) + { + WriteText(Path.Combine(outputDir, "RECORDER_ERROR.txt"), ex.ToString()); + Console.WriteLine("FAILED"); + Console.WriteLine(ex.ToString()); + Pause(args); + return 1; + } + } + + private static List> DumpTargetWindowUia(Dictionary runtime, Dictionary windows, string uiaDir) + { + var targetPids = TargetPids(runtime); + var summary = new List>(); + object rawWindows; + if (!windows.TryGetValue("windows", out rawWindows)) + { + return summary; + } + + var windowList = rawWindows as List>; + if (windowList == null) + { + return summary; + } + + foreach (Dictionary window in windowList) + { + int pid = GetInt(window, "pid"); + if (!targetPids.Contains(pid)) + { + continue; + } + + string hwnd = GetString(window, "hwnd"); + string title = GetString(window, "title"); + string fileName = "uia-pid" + pid + "-" + hwnd.Replace("0x", "") + ".json"; + string outPath = Path.Combine(uiaDir, fileName); + Dictionary dump = UiaDumper.Dump("live-probe-uia", "dump_uia", new Dictionary + { + { "hwnd", hwnd }, + { "max_depth", 8 }, + { "include_text", true }, + { "max_children", 180 } + }); + WriteJson(outPath, dump); + summary.Add(new Dictionary + { + { "pid", pid }, + { "hwnd", hwnd }, + { "title", title }, + { "path", outPath }, + { "ok", GetBool(dump, "ok") } + }); + } + return summary; + } + + private static HashSet TargetPids(Dictionary runtime) + { + var pids = new HashSet(); + object rawTargets; + if (!runtime.TryGetValue("targets", out rawTargets)) + { + return pids; + } + var targets = rawTargets as List>; + if (targets == null) + { + return pids; + } + foreach (Dictionary target in targets) + { + int pid = GetInt(target, "pid"); + if (pid > 0) + { + pids.Add(pid); + } + } + return pids; + } + + private static Dictionary Manifest(string runId, string outputDir) + { + return new Dictionary + { + { "recorder_name", "ISphereLiveProbeRecorder" }, + { "recorder_version", "0.2.0" }, + { "helper_protocol", HelperProtocol.Protocol }, + { "run_id", runId }, + { "timestamp_local", DateTime.Now.ToString("o") }, + { "timestamp_utc", DateTime.UtcNow.ToString("o") }, + { "output_dir", outputDir }, + { "scope", "read_only_full_inventory_for_B_route_sidecar_and_A_route_rpa_fallback" }, + { "does_not", new object[] { "send_message", "send_file", "upload_file", "click_ui", "type_text", "inject_hook", "modify_client_data" } }, + { "machine_name", Environment.MachineName }, + { "user_domain", Environment.UserDomainName }, + { "user_name", Environment.UserName }, + { "os_version", Environment.OSVersion.ToString() }, + { "is_64_bit_os", Environment.Is64BitOperatingSystem }, + { "is_64_bit_process", Environment.Is64BitProcess }, + { "clr_version", Environment.Version.ToString() } + }; + } + + private static string Readme() + { + return "ISphereLiveProbeRecorder\r\n\r\n" + + "使用方式:\r\n" + + "1. 到可以正常连接服务端的环境。\r\n" + + "2. 手工打开并登录 iSphere / IMPlatformClient。\r\n" + + "3. 尽量手工打开一个联系人聊天窗口、一个群聊窗口,并让输入框/发送按钮/附件按钮所在区域可见;不用发送任何内容。\r\n" + + "4. 保持客户端窗口打开。\r\n" + + "5. 双击 ISphereLiveProbeRecorder.exe。\r\n" + + "6. 程序会在 probe-output 下生成 isphere-live-probe-时间戳 目录。\r\n" + + "7. 把整个目录压缩后发回。\r\n\r\n" + + "采集内容:运行进程、模块、命令行摘要、窗口、目标窗口 UIA 控件树、安装目录关键文件、配置/日志/数据库候选、注册表卸载项、快捷方式、服务、本地 TCP 连接、命名管道。\r\n" + + "不会执行:发送消息、发送文件、上传文件、点击、输入、注入、hook、修改客户端数据。\r\n"; + } + + private static string ResolveOutputRoot(string[] args) + { + for (int i = 0; i < args.Length - 1; i++) + { + if (string.Equals(args[i], "--out", StringComparison.OrdinalIgnoreCase)) + { + return Path.GetFullPath(args[i + 1]); + } + } + return Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "probe-output"); + } + + private static void Pause(string[] args) + { + foreach (string arg in args) + { + if (string.Equals(arg, "--no-pause", StringComparison.OrdinalIgnoreCase)) + { + return; + } + } + if (Environment.UserInteractive) + { + Console.WriteLine(); + Console.WriteLine("按回车退出..."); + try { Console.ReadLine(); } catch { } + } + } + + private static string GetString(Dictionary source, string key) + { + object value; + if (source.TryGetValue(key, out value) && value != null) + { + return Convert.ToString(value); + } + return ""; + } + + private static int GetInt(Dictionary source, string key) + { + object value; + if (source.TryGetValue(key, out value) && value != null) + { + try { return Convert.ToInt32(value); } catch { } + } + return 0; + } + + private static bool GetBool(Dictionary source, string key) + { + object value; + if (source.TryGetValue(key, out value) && value != null) + { + try { return Convert.ToBoolean(value); } catch { } + } + return false; + } + + private static void WriteJson(string path, object value) + { + File.WriteAllText(path, HelperProtocol.ToJson(value), Encoding.UTF8); + } + + private static void WriteText(string path, string value) + { + File.WriteAllText(path, value, Encoding.UTF8); + } + } +} diff --git a/native/ISphereWinHelper/Program.cs b/native/ISphereWinHelper/Program.cs index a95d562..6e02261 100644 --- a/native/ISphereWinHelper/Program.cs +++ b/native/ISphereWinHelper/Program.cs @@ -9,6 +9,11 @@ namespace ISphereWinHelper [STAThread] private static int Main(string[] args) { + if (ShouldRunLiveProbeRecorder(args)) + { + return LiveProbeRecorder.Run(args); + } + string requestText = Console.In.ReadToEnd(); Dictionary request; string requestId = ""; @@ -39,6 +44,9 @@ namespace ISphereWinHelper case "dump_uia": response = UiaDumper.Dump(requestId, op, opArgs); break; + case "probe_client_runtime": + response = HelperProtocol.Success(requestId, op, RuntimeProbe.ProbeClientRuntime(opArgs)); + break; default: response = HelperProtocol.Failure(requestId, op, "UNSUPPORTED_OP", "unsupported op: " + op); break; @@ -55,12 +63,26 @@ namespace ISphereWinHelper } } + private static bool ShouldRunLiveProbeRecorder(string[] args) + { + foreach (string arg in args) + { + if (string.Equals(arg, "--record-live-probe", StringComparison.OrdinalIgnoreCase)) + { + return true; + } + } + + string exeName = Path.GetFileNameWithoutExtension(AppDomain.CurrentDomain.FriendlyName); + return exeName != null && exeName.IndexOf("LiveProbeRecorder", StringComparison.OrdinalIgnoreCase) >= 0; + } + private static Dictionary VersionData() { return new Dictionary { { "helper_name", "ISphereWinHelper" }, - { "helper_version", "0.1.0" }, + { "helper_version", "0.3.0" }, { "protocol", HelperProtocol.Protocol }, { "runtime", ".NET Framework " + Environment.Version } }; diff --git a/native/ISphereWinHelper/RuntimeProbe.cs b/native/ISphereWinHelper/RuntimeProbe.cs new file mode 100644 index 0000000..4124c88 --- /dev/null +++ b/native/ISphereWinHelper/RuntimeProbe.cs @@ -0,0 +1,332 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Runtime.InteropServices; + +namespace ISphereWinHelper +{ + internal static class RuntimeProbe + { + [DllImport("kernel32.dll", SetLastError = true)] + private static extern bool IsWow64Process(IntPtr hProcess, out bool wow64Process); + + public static Dictionary ProbeClientRuntime(Dictionary args) + { + var processNames = GetStringList(args, "process_names", new[] { "IMPlatformClient", "IMPP.ISphere", "iSphere", "importal" }); + var moduleHints = GetStringList(args, "module_hints", new[] + { + "IMPlatformClient.exe", + "smack.dll", + "IMPP.Interface.dll", + "IMPP.ServiceBase.dll", + "IMPP.Service.dll", + "IMPP.Common.dll", + "IMPP.Helper.dll", + "IMPP.Model.dll", + "IMPP.Util.dll", + "IMPP.UI.dll", + "TcpFileTransfer.dll", + "HYHC.IMPP.DAL.dll", + "INetwork.dll", + "IOClientNetwork.dll" + }); + bool includeModules = HelperProtocol.GetBool(args, "include_modules", true); + int maxModules = HelperProtocol.GetInt(args, "max_modules", 120); + if (maxModules < 0) + { + maxModules = 0; + } + + var targets = new List>(); + foreach (Process process in Process.GetProcesses()) + { + using (process) + { + string processName = SafeProcessName(process); + if (!MatchesAny(processName, processNames)) + { + continue; + } + + targets.Add(DescribeProcess(process, moduleHints, includeModules, maxModules)); + } + } + + targets.Sort(delegate(Dictionary left, Dictionary right) + { + int leftScore = Convert.ToInt32(left["score"]); + int rightScore = Convert.ToInt32(right["score"]); + int scoreCompare = rightScore.CompareTo(leftScore); + if (scoreCompare != 0) + { + return scoreCompare; + } + return Convert.ToInt32(left["pid"]).CompareTo(Convert.ToInt32(right["pid"])); + }); + + return new Dictionary + { + { "probe_mode", "read_only_process_module_inventory" }, + { "host", new Dictionary + { + { "is_64_bit_os", Environment.Is64BitOperatingSystem }, + { "is_64_bit_process", Environment.Is64BitProcess }, + { "machine_name", Environment.MachineName } + } + }, + { "process_names", processNames }, + { "module_hints", moduleHints }, + { "target_count", targets.Count }, + { "targets", targets } + }; + } + + private static Dictionary DescribeProcess(Process process, List moduleHints, bool includeModules, int maxModules) + { + string executablePath = ""; + string mainModuleName = ""; + string mainModuleVersion = ""; + string mainModuleError = ""; + + try + { + ProcessModule mainModule = process.MainModule; + if (mainModule != null) + { + executablePath = mainModule.FileName ?? ""; + mainModuleName = mainModule.ModuleName ?? ""; + FileVersionInfo versionInfo = mainModule.FileVersionInfo; + if (versionInfo != null) + { + mainModuleVersion = versionInfo.FileVersion ?? ""; + } + } + } + catch (Exception ex) + { + mainModuleError = ex.GetType().Name + ": " + ex.Message; + } + + bool? isWow64 = GetIsWow64(process); + string bitness = "unknown"; + if (Environment.Is64BitOperatingSystem && isWow64.HasValue) + { + bitness = isWow64.Value ? "x86" : "x64"; + } + else if (!Environment.Is64BitOperatingSystem) + { + bitness = "x86"; + } + + string moduleProbeError = ""; + var modules = new List>(); + var assemblyPresence = NewPresenceMap(moduleHints); + if (includeModules) + { + try + { + int count = 0; + foreach (ProcessModule module in process.Modules) + { + if (count >= maxModules) + { + break; + } + + string moduleName = module.ModuleName ?? ""; + string modulePath = module.FileName ?? ""; + string matchedHint = FindMatchedHint(moduleName, modulePath, moduleHints); + if (!string.IsNullOrEmpty(matchedHint)) + { + assemblyPresence[matchedHint] = true; + } + + modules.Add(new Dictionary + { + { "name", moduleName }, + { "path", modulePath }, + { "matched_hint", matchedHint } + }); + count++; + } + } + catch (Exception ex) + { + moduleProbeError = ex.GetType().Name + ": " + ex.Message; + } + } + + if (!string.IsNullOrEmpty(mainModuleName)) + { + string matchedMain = FindMatchedHint(mainModuleName, executablePath, moduleHints); + if (!string.IsNullOrEmpty(matchedMain)) + { + assemblyPresence[matchedMain] = true; + } + } + + int score = Score(process.ProcessName, executablePath, assemblyPresence); + return new Dictionary + { + { "pid", process.Id }, + { "process_name", process.ProcessName }, + { "main_window_title", SafeMainWindowTitle(process) }, + { "main_window_handle", WindowScanner.FormatHwnd(process.MainWindowHandle) }, + { "has_main_window", process.MainWindowHandle != IntPtr.Zero }, + { "executable_path", executablePath }, + { "main_module_name", mainModuleName }, + { "main_module_version", mainModuleVersion }, + { "main_module_error", mainModuleError }, + { "bitness", bitness }, + { "is_wow64", isWow64.HasValue ? (object)isWow64.Value : null }, + { "module_probe_ok", string.IsNullOrEmpty(moduleProbeError) }, + { "module_probe_error", moduleProbeError }, + { "assembly_presence", assemblyPresence }, + { "modules", modules }, + { "score", score } + }; + } + + private static Dictionary NewPresenceMap(List moduleHints) + { + var presence = new Dictionary(StringComparer.OrdinalIgnoreCase); + foreach (string hint in moduleHints) + { + if (!presence.ContainsKey(hint)) + { + presence.Add(hint, false); + } + } + return presence; + } + + private static int Score(string processName, string executablePath, Dictionary assemblyPresence) + { + int score = 0; + string combined = ((processName ?? "") + " " + (executablePath ?? "")).ToLowerInvariant(); + if (combined.Contains("implatformclient")) score += 80; + if (combined.Contains("isphere")) score += 40; + if (combined.Contains("impp")) score += 20; + if (combined.Contains("importal")) score += 10; + foreach (object value in assemblyPresence.Values) + { + if (value is bool && (bool)value) + { + score += 5; + } + } + return score; + } + + private static bool? GetIsWow64(Process process) + { + try + { + bool isWow64; + if (IsWow64Process(process.Handle, out isWow64)) + { + return isWow64; + } + } + catch + { + } + return null; + } + + private static string SafeProcessName(Process process) + { + try + { + return process.ProcessName ?? ""; + } + catch + { + return ""; + } + } + + private static string SafeMainWindowTitle(Process process) + { + try + { + return process.MainWindowTitle ?? ""; + } + catch + { + return ""; + } + } + + private static bool MatchesAny(string processName, List candidates) + { + foreach (string candidate in candidates) + { + if (string.Equals(processName, candidate, StringComparison.OrdinalIgnoreCase)) + { + return true; + } + } + return false; + } + + private static string FindMatchedHint(string moduleName, string modulePath, List hints) + { + foreach (string hint in hints) + { + if (string.Equals(moduleName, hint, StringComparison.OrdinalIgnoreCase)) + { + return hint; + } + if (!string.IsNullOrEmpty(modulePath) && + modulePath.EndsWith("\\" + hint, StringComparison.OrdinalIgnoreCase)) + { + return hint; + } + } + return ""; + } + + private static List GetStringList(Dictionary source, string key, string[] defaults) + { + var values = new List(); + object raw; + if (source != null && source.TryGetValue(key, out raw) && raw != null) + { + object[] rawArray = raw as object[]; + if (rawArray != null) + { + foreach (object item in rawArray) + { + string value = Convert.ToString(item); + if (!string.IsNullOrWhiteSpace(value)) + { + values.Add(value.Trim()); + } + } + } + else + { + string single = Convert.ToString(raw); + if (!string.IsNullOrWhiteSpace(single)) + { + foreach (string part in single.Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries)) + { + string value = part.Trim(); + if (!string.IsNullOrWhiteSpace(value)) + { + values.Add(value); + } + } + } + } + } + + if (values.Count == 0) + { + values.AddRange(defaults); + } + return values; + } + } +} diff --git a/scripts/build-win-helper.ps1 b/scripts/build-win-helper.ps1 index 4f818f5..d879cef 100644 --- a/scripts/build-win-helper.ps1 +++ b/scripts/build-win-helper.ps1 @@ -61,6 +61,7 @@ $windowsBase = Resolve-Reference "WindowsBase.dll" /out:$outExe ` /reference:System.dll ` /reference:System.Core.dll ` + /reference:System.Management.dll ` /reference:System.Web.Extensions.dll ` /reference:System.Windows.Forms.dll ` /reference:$uiaClient ` diff --git a/scripts/package-live-probe-recorder.ps1 b/scripts/package-live-probe-recorder.ps1 new file mode 100644 index 0000000..03c884d --- /dev/null +++ b/scripts/package-live-probe-recorder.ps1 @@ -0,0 +1,82 @@ +param( + [string]$OutputDir = "runs/live-probe-recorder-package", + [switch]$NoZip +) + +$ErrorActionPreference = "Stop" + +$repo = Resolve-Path -LiteralPath (Join-Path $PSScriptRoot "..") +$packageDir = Join-Path $repo $OutputDir +$buildDir = Join-Path $repo "runs/live-probe-recorder-build" +$helperExe = Join-Path $buildDir "ISphereWinHelper.exe" +$recorderExe = Join-Path $packageDir "ISphereLiveProbeRecorder.exe" + +New-Item -ItemType Directory -Force -Path $packageDir | Out-Null + +& powershell -NoProfile -ExecutionPolicy Bypass -File (Join-Path $repo "scripts\build-win-helper.ps1") -OutputDir "runs/live-probe-recorder-build" +if ($LASTEXITCODE -ne 0) { + throw "build-win-helper.ps1 failed with exit code $LASTEXITCODE" +} + +if (-not (Test-Path -LiteralPath $helperExe)) { + throw "built helper not found: $helperExe" +} + +Copy-Item -LiteralPath $helperExe -Destination $recorderExe -Force + +@" +ISphereLiveProbeRecorder + +使用方式: +1. 把本目录复制到可以正常登录 iSphere/IMPlatformClient 的电脑。 +2. 先手工打开并登录 iSphere/IMPlatformClient。 +3. 尽量手工打开一个联系人聊天窗口、一个群聊窗口,并让输入框/发送按钮/附件按钮所在区域可见;不用发送任何内容。 +4. 保持客户端窗口打开。 +5. 双击 ISphereLiveProbeRecorder.exe。 +6. 程序会在 probe-output 下生成 isphere-live-probe-时间戳 目录。 +7. 把生成的整个目录压缩后发回。 + +它记录: +- 目标进程、位数、路径、加载模块; +- 目标进程命令行摘要; +- 窗口列表; +- 目标窗口 UIA 控件树; +- 安装目录、缓存目录、配置/日志/数据库候选文件元数据; +- 关键 exe/dll 的版本和 SHA256; +- 小型配置/日志文件的脱敏文本片段; +- 注册表卸载项; +- 相关服务; +- 相关快捷方式; +- 目标进程 TCP 连接; +- 相关命名管道。 + +它不会: +- 发送消息; +- 发送/上传文件; +- 点击或输入; +- 注入或 hook; +- 修改客户端文件或数据。 +"@ | Set-Content -LiteralPath (Join-Path $packageDir "README-使用说明.txt") -Encoding UTF8 + +@" +@echo off +setlocal +cd /d "%~dp0" +ISphereLiveProbeRecorder.exe +pause +"@ | Set-Content -LiteralPath (Join-Path $packageDir "双击运行-采集.bat") -Encoding ASCII + +$zipPath = "$packageDir.zip" +if (-not $NoZip) { + if (Test-Path -LiteralPath $zipPath) { + Remove-Item -LiteralPath $zipPath -Force + } + Compress-Archive -Path (Join-Path $packageDir "*") -DestinationPath $zipPath -Force +} + +[ordered]@{ + ok = $true + package_dir = (Resolve-Path -LiteralPath $packageDir).Path + recorder_exe = (Resolve-Path -LiteralPath $recorderExe).Path + zip = if (Test-Path -LiteralPath $zipPath) { (Resolve-Path -LiteralPath $zipPath).Path } else { $null } +} | ConvertTo-Json -Depth 4 -Compress diff --git a/scripts/verify-win-helper.ps1 b/scripts/verify-win-helper.ps1 index 703d9fd..fe6b2cd 100644 --- a/scripts/verify-win-helper.ps1 +++ b/scripts/verify-win-helper.ps1 @@ -65,6 +65,21 @@ if (-not $scan.ok -or $null -eq $scan.data.windows) { throw "scan_windows failed: $($scan | ConvertTo-Json -Depth 8 -Compress)" } +$runtimeProbe = Invoke-HelperJson @{ + protocol = "isphere.helper.v1" + request_id = "verify-runtime-probe" + op = "probe_client_runtime" + timeout_ms = 5000 + args = @{ + process_names = @("IMPlatformClient", "IMPP.ISphere", "iSphere", "importal") + include_modules = $false + max_modules = 0 + } +} +if (-not $runtimeProbe.ok -or $runtimeProbe.data.probe_mode -ne "read_only_process_module_inventory" -or $null -eq $runtimeProbe.data.targets) { + throw "probe_client_runtime failed: $($runtimeProbe | ConvertTo-Json -Depth 8 -Compress)" +} + $dumpMissing = Invoke-HelperJson @{ protocol = "isphere.helper.v1" request_id = "verify-dump-missing" @@ -118,5 +133,6 @@ finally { helper = (Resolve-Path -LiteralPath $helperPath).Path version = $version.data.helper_version scan_window_count = $scan.data.windows.Count + runtime_probe_target_count = $runtimeProbe.data.target_count uia_available = $selfCheck.data.uia_available -} | ConvertTo-Json -Depth 4 -Compress +} | ConvertTo-Json -Depth 4 -Compress