feat: add iSphere live probe recorder

This commit is contained in:
zhaoyilun
2026-07-10 10:59:48 +08:00
parent 349fbe4a44
commit f7914a63dc
13 changed files with 2209 additions and 16 deletions

View File

@@ -158,9 +158,9 @@ WRITE_NOT_READY
VERIFY_FAILED 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` ### 6.1 `version`
@@ -182,7 +182,7 @@ VERIFY_FAILED
```json ```json
{ {
"helper_name": "ISphereWinHelper", "helper_name": "ISphereWinHelper",
"helper_version": "0.1.0", "helper_version": "0.3.0",
"protocol": "isphere.helper.v1", "protocol": "isphere.helper.v1",
"runtime": ".NET Framework" "runtime": ".NET Framework"
} }
@@ -274,6 +274,66 @@ runs/real-loggedin-lab/uia-dumps/
C# helper 文件输出通过未来 `out_path` 参数控制。 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 扩展顺序 ## 7. 后续 op 扩展顺序
必须等真实登录窗口的 UIA dump 被确认后,再扩展真实动作。 必须等真实登录窗口的 UIA dump 被确认后,再扩展真实动作。
@@ -344,7 +404,7 @@ native/
## 11. 验收标准 ## 11. 验收标准
第一阶段完成后,只验收: 当前只读 helper 基线验收:
1. C# helper 能编译。 1. C# helper 能编译。
2. `version` 返回合法 JSON。 2. `version` 返回合法 JSON。
@@ -353,17 +413,20 @@ native/
5. 打开记事本后,`dump_uia` 能导出记事本控件树。 5. 打开记事本后,`dump_uia` 能导出记事本控件树。
6. 用户手动登录 iSphere 后,`scan_windows` 能找到候选窗口。 6. 用户手动登录 iSphere 后,`scan_windows` 能找到候选窗口。
7. 对 iSphere 候选窗口执行 `dump_uia` 能返回控件树。 7. 对 iSphere 候选窗口执行 `dump_uia` 能返回控件树。
8. 第一阶段验收聚焦窗口扫描和 UIA dump 结果。 8. `probe_client_runtime` 在无目标客户端运行时也能返回合法空结果。
9. 用户手动登录 iSphere 后,`probe_client_runtime` 能返回目标进程、位数、路径和关键模块探测结果。
10. 当前验收仍聚焦只读探测;发送、文件上传、点击、输入都不在本节验收范围内。
## 12. 下一步 ## 12. 下一步
建议下一步只做 C# helper 第一阶段 当前下一步是 C30 B 路线运行时探测
```text ```text
version version
self_check self_check
scan_windows scan_windows
dump_uia dump_uia
probe_client_runtime
``` ```
Go MCP 代码在后续节点实现C# helper 稳定拿到真实 iSphere 窗口结构后,再实现 Go 的 `helperclient` 和 MCP tools Go MCP 代码已经具备基础 helper 调用能力;`probe_client_runtime` 暂不暴露为 MCP 业务工具。待真实登录客户端上的 B1/B2 探测通过后,再决定是否新增 sidecar 调用层和受控发送工具

View File

@@ -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-<timestamp>/
```
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.

View File

@@ -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.

View File

@@ -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-index.md`
- `docs/source-discovery/2026-07-09-n12-pre-zyl-schema-notes.md` - `docs/source-discovery/2026-07-09-n12-pre-zyl-schema-notes.md`
- `docs/source-discovery/capability-source-matrix.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-tools-contract.md`
- `docs/mcp-core-business-plan.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 ## 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`. 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.

View File

@@ -7,11 +7,12 @@ Schema notes: `docs/source-discovery/2026-07-09-n12-pre-zyl-schema-notes.md`
## Source priority ## Source priority
1. Existing bridge / API / local service. 1. Running-client sidecar / in-process connector that reuses the logged-in `IMPlatformClient.exe` runtime.
2. Local data, log, cache, or database source. 2. Existing bridge / API / local service.
3. UIA helper source through C# WinHelper. 3. Local data, log, cache, or database source.
4. Network/protocol source. 4. UIA helper source through C# WinHelper.
5. Mock source for tests only. 5. Network/protocol source.
6. Mock source for tests only.
## Matrix ## 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_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_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\<hash>` 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_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\<hash>` 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_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` | 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_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 ## Stage C entry rule

View File

@@ -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.

View File

@@ -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<string, object> CollectProcessDetails(Dictionary<string, object> runtime)
{
var pids = TargetPids(runtime);
var details = new List<Dictionary<string, object>>();
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<string, object>
{
{ "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<string, object>
{
{ "target_pid_count", pids.Count },
{ "details", details }
};
}
public static Dictionary<string, object> CollectNetwork(Dictionary<string, object> runtime)
{
var targetPids = TargetPids(runtime);
return new Dictionary<string, object>
{
{ "tcp", CaptureTargetTcpConnections(targetPids) },
{ "named_pipes", CaptureNamedPipes() }
};
}
public static Dictionary<string, object> CollectServicesRegistryShortcuts()
{
return new Dictionary<string, object>
{
{ "services", CaptureServices() },
{ "registry_uninstall", CaptureUninstallEntries() },
{ "shortcuts", CaptureShortcuts() }
};
}
public static Dictionary<string, object> CollectFilesystem(Dictionary<string, object> runtime)
{
var roots = CandidateRoots(runtime);
var entries = new List<Dictionary<string, object>>();
foreach (string root in roots)
{
entries.Add(InventoryRoot(root));
}
return new Dictionary<string, object>
{
{ "root_count", entries.Count },
{ "roots", entries }
};
}
public static Dictionary<string, object> SummarizeFeasibility(Dictionary<string, object> runtime, Dictionary<string, object> filesystem, Dictionary<string, object> 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<string, object>
{
{ "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<string, object> InventoryRoot(string root)
{
var files = new List<Dictionary<string, object>>();
var directories = new List<string>();
if (!Directory.Exists(root))
{
return new Dictionary<string, object>
{
{ "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<string, object> left, Dictionary<string, object> right)
{
return string.Compare(Convert.ToString(left["path"]), Convert.ToString(right["path"]), StringComparison.OrdinalIgnoreCase);
});
return new Dictionary<string, object>
{
{ "root", root },
{ "exists", true },
{ "directories_sample", directories },
{ "interesting_file_count", files.Count },
{ "files", files }
};
}
private static Dictionary<string, object> 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<string, object>
{
{ "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<string> CandidateRoots(Dictionary<string, object> runtime)
{
var roots = new SortedSet<string>(StringComparer.OrdinalIgnoreCase);
foreach (Dictionary<string, object> 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<string> roots, string path)
{
if (string.IsNullOrWhiteSpace(path))
{
return;
}
try
{
if (Directory.Exists(path))
{
roots.Add(Path.GetFullPath(path));
}
}
catch
{
}
}
private static List<Dictionary<string, object>> CaptureServices()
{
var services = new List<Dictionary<string, object>>();
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<string, object>
{
{ "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<Dictionary<string, object>> CaptureUninstallEntries()
{
var entries = new List<Dictionary<string, object>>();
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<Dictionary<string, object>> 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<string, object>
{
{ "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<Dictionary<string, object>> CaptureShortcuts()
{
var shortcuts = new List<Dictionary<string, object>>();
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<string, object>
{
{ "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<Dictionary<string, object>> CaptureNamedPipes()
{
var pipes = new List<Dictionary<string, object>>();
try
{
foreach (string pipe in Directory.GetFiles(@"\\.\pipe\"))
{
string lower = pipe.ToLowerInvariant();
if (!ContainsInterestingTerm(lower))
{
continue;
}
pipes.Add(new Dictionary<string, object> { { "path", pipe } });
}
}
catch (Exception ex)
{
pipes.Add(new Dictionary<string, object> { { "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<string, object> CaptureTargetTcpConnections(HashSet<int> targetPids)
{
var rows = new List<Dictionary<string, object>>();
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<string, object>
{
{ "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<string, object>
{
{ "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<ManagementObject> QueryWmi(string query)
{
var list = new List<ManagementObject>();
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<string, object> runtime, string name)
{
foreach (Dictionary<string, object> target in GetDictionaryList(runtime, "targets"))
{
object rawPresence;
if (!target.TryGetValue("assembly_presence", out rawPresence))
{
continue;
}
var presence = rawPresence as Dictionary<string, object>;
if (presence == null)
{
continue;
}
object value;
if (presence.TryGetValue(name, out value) && value is bool && (bool)value)
{
return true;
}
}
return false;
}
private static HashSet<int> TargetPids(Dictionary<string, object> runtime)
{
var pids = new HashSet<int>();
foreach (Dictionary<string, object> target in GetDictionaryList(runtime, "targets"))
{
int pid = GetInt(target, "pid");
if (pid > 0)
{
pids.Add(pid);
}
}
return pids;
}
private static List<Dictionary<string, object>> GetDictionaryList(Dictionary<string, object> source, string key)
{
var list = new List<Dictionary<string, object>>();
object value;
if (source == null || !source.TryGetValue(key, out value) || value == null)
{
return list;
}
var direct = value as List<Dictionary<string, object>>;
if (direct != null)
{
return direct;
}
return list;
}
private static string GetString(Dictionary<string, object> 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<string, object> 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<string> SafeDirectories(string root, int maxDepth, int maxItems)
{
var result = new List<string>();
SafeDirectories(root, 0, maxDepth, maxItems, result);
return result;
}
private static void SafeDirectories(string root, int depth, int maxDepth, int maxItems, List<string> 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<string> SafeFiles(string root, int maxDepth, int maxItems)
{
var result = new List<string>();
SafeFiles(root, 0, maxDepth, maxItems, result);
return result;
}
private static void SafeFiles(string root, int depth, int maxDepth, int maxItems, List<string> 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=<redacted>", RegexOptions.IgnoreCase);
redacted = Regex.Replace(redacted, "(UserID|UserName|LoginName)\\s*[:=]\\s*[^\\s;&<>\\\"]+", "$1=<redacted>", 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 "";
}
}
}
}

View File

@@ -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<string, object>
{
{ "process_names", new object[] { "IMPlatformClient", "IMPP.ISphere", "iSphere", "importal" } },
{ "include_modules", true },
{ "max_modules", 180 }
};
Dictionary<string, object> runtime = RuntimeProbe.ProbeClientRuntime(probeArgs);
WriteJson(Path.Combine(outputDir, "probe_client_runtime.json"), runtime);
Dictionary<string, object> processDetails = LiveProbeInventory.CollectProcessDetails(runtime);
WriteJson(Path.Combine(outputDir, "process_details.json"), processDetails);
Dictionary<string, object> windows = WindowScanner.Scan(new Dictionary<string, object>
{
{ "include_all_visible", true }
});
WriteJson(Path.Combine(outputDir, "scan_windows.json"), windows);
Dictionary<string, object> network = LiveProbeInventory.CollectNetwork(runtime);
WriteJson(Path.Combine(outputDir, "network_inventory.json"), network);
Dictionary<string, object> servicesRegistryShortcuts = LiveProbeInventory.CollectServicesRegistryShortcuts();
WriteJson(Path.Combine(outputDir, "services_registry_shortcuts.json"), servicesRegistryShortcuts);
Dictionary<string, object> filesystem = LiveProbeInventory.CollectFilesystem(runtime);
WriteJson(Path.Combine(outputDir, "filesystem_inventory.json"), filesystem);
string uiaDir = Path.Combine(outputDir, "uia");
Directory.CreateDirectory(uiaDir);
List<Dictionary<string, object>> uiaSummary = DumpTargetWindowUia(runtime, windows, uiaDir);
WriteJson(Path.Combine(outputDir, "uia_summary.json"), uiaSummary);
Dictionary<string, object> 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<Dictionary<string, object>> DumpTargetWindowUia(Dictionary<string, object> runtime, Dictionary<string, object> windows, string uiaDir)
{
var targetPids = TargetPids(runtime);
var summary = new List<Dictionary<string, object>>();
object rawWindows;
if (!windows.TryGetValue("windows", out rawWindows))
{
return summary;
}
var windowList = rawWindows as List<Dictionary<string, object>>;
if (windowList == null)
{
return summary;
}
foreach (Dictionary<string, object> 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<string, object> dump = UiaDumper.Dump("live-probe-uia", "dump_uia", new Dictionary<string, object>
{
{ "hwnd", hwnd },
{ "max_depth", 8 },
{ "include_text", true },
{ "max_children", 180 }
});
WriteJson(outPath, dump);
summary.Add(new Dictionary<string, object>
{
{ "pid", pid },
{ "hwnd", hwnd },
{ "title", title },
{ "path", outPath },
{ "ok", GetBool(dump, "ok") }
});
}
return summary;
}
private static HashSet<int> TargetPids(Dictionary<string, object> runtime)
{
var pids = new HashSet<int>();
object rawTargets;
if (!runtime.TryGetValue("targets", out rawTargets))
{
return pids;
}
var targets = rawTargets as List<Dictionary<string, object>>;
if (targets == null)
{
return pids;
}
foreach (Dictionary<string, object> target in targets)
{
int pid = GetInt(target, "pid");
if (pid > 0)
{
pids.Add(pid);
}
}
return pids;
}
private static Dictionary<string, object> Manifest(string runId, string outputDir)
{
return new Dictionary<string, object>
{
{ "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<string, object> source, string key)
{
object value;
if (source.TryGetValue(key, out value) && value != null)
{
return Convert.ToString(value);
}
return "";
}
private static int GetInt(Dictionary<string, object> 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<string, object> 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);
}
}
}

View File

@@ -9,6 +9,11 @@ namespace ISphereWinHelper
[STAThread] [STAThread]
private static int Main(string[] args) private static int Main(string[] args)
{ {
if (ShouldRunLiveProbeRecorder(args))
{
return LiveProbeRecorder.Run(args);
}
string requestText = Console.In.ReadToEnd(); string requestText = Console.In.ReadToEnd();
Dictionary<string, object> request; Dictionary<string, object> request;
string requestId = ""; string requestId = "";
@@ -39,6 +44,9 @@ namespace ISphereWinHelper
case "dump_uia": case "dump_uia":
response = UiaDumper.Dump(requestId, op, opArgs); response = UiaDumper.Dump(requestId, op, opArgs);
break; break;
case "probe_client_runtime":
response = HelperProtocol.Success(requestId, op, RuntimeProbe.ProbeClientRuntime(opArgs));
break;
default: default:
response = HelperProtocol.Failure(requestId, op, "UNSUPPORTED_OP", "unsupported op: " + op); response = HelperProtocol.Failure(requestId, op, "UNSUPPORTED_OP", "unsupported op: " + op);
break; 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<string, object> VersionData() private static Dictionary<string, object> VersionData()
{ {
return new Dictionary<string, object> return new Dictionary<string, object>
{ {
{ "helper_name", "ISphereWinHelper" }, { "helper_name", "ISphereWinHelper" },
{ "helper_version", "0.1.0" }, { "helper_version", "0.3.0" },
{ "protocol", HelperProtocol.Protocol }, { "protocol", HelperProtocol.Protocol },
{ "runtime", ".NET Framework " + Environment.Version } { "runtime", ".NET Framework " + Environment.Version }
}; };

View File

@@ -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<string, object> ProbeClientRuntime(Dictionary<string, object> 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<Dictionary<string, object>>();
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<string, object> left, Dictionary<string, object> 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<string, object>
{
{ "probe_mode", "read_only_process_module_inventory" },
{ "host", new Dictionary<string, object>
{
{ "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<string, object> DescribeProcess(Process process, List<string> 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<Dictionary<string, object>>();
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<string, object>
{
{ "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<string, object>
{
{ "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<string, object> NewPresenceMap(List<string> moduleHints)
{
var presence = new Dictionary<string, object>(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<string, object> 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<string> 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<string> 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<string> GetStringList(Dictionary<string, object> source, string key, string[] defaults)
{
var values = new List<string>();
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;
}
}
}

View File

@@ -61,6 +61,7 @@ $windowsBase = Resolve-Reference "WindowsBase.dll"
/out:$outExe ` /out:$outExe `
/reference:System.dll ` /reference:System.dll `
/reference:System.Core.dll ` /reference:System.Core.dll `
/reference:System.Management.dll `
/reference:System.Web.Extensions.dll ` /reference:System.Web.Extensions.dll `
/reference:System.Windows.Forms.dll ` /reference:System.Windows.Forms.dll `
/reference:$uiaClient ` /reference:$uiaClient `

View File

@@ -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

View File

@@ -65,6 +65,21 @@ if (-not $scan.ok -or $null -eq $scan.data.windows) {
throw "scan_windows failed: $($scan | ConvertTo-Json -Depth 8 -Compress)" 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 @{ $dumpMissing = Invoke-HelperJson @{
protocol = "isphere.helper.v1" protocol = "isphere.helper.v1"
request_id = "verify-dump-missing" request_id = "verify-dump-missing"
@@ -118,5 +133,6 @@ finally {
helper = (Resolve-Path -LiteralPath $helperPath).Path helper = (Resolve-Path -LiteralPath $helperPath).Path
version = $version.data.helper_version version = $version.data.helper_version
scan_window_count = $scan.data.windows.Count scan_window_count = $scan.data.windows.Count
runtime_probe_target_count = $runtimeProbe.data.target_count
uia_available = $selfCheck.data.uia_available uia_available = $selfCheck.data.uia_available
} | ConvertTo-Json -Depth 4 -Compress } | ConvertTo-Json -Depth 4 -Compress