feat: add iSphere live probe recorder
This commit is contained in:
@@ -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 调用层和受控发送工具。
|
||||
|
||||
106
docs/source-discovery/2026-07-10-live-probe-recorder.md
Normal file
106
docs/source-discovery/2026-07-10-live-probe-recorder.md
Normal 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.
|
||||
357
docs/source-discovery/2026-07-10-send-sidecar-b-first-plan.md
Normal file
357
docs/source-discovery/2026-07-10-send-sidecar-b-first-plan.md
Normal 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.
|
||||
@@ -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.
|
||||
|
||||
@@ -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\<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_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
|
||||
|
||||
|
||||
@@ -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.
|
||||
802
native/ISphereWinHelper/LiveProbeInventory.cs
Normal file
802
native/ISphereWinHelper/LiveProbeInventory.cs
Normal 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 "";
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
259
native/ISphereWinHelper/LiveProbeRecorder.cs
Normal file
259
native/ISphereWinHelper/LiveProbeRecorder.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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<string, object> 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<string, object> VersionData()
|
||||
{
|
||||
return new Dictionary<string, object>
|
||||
{
|
||||
{ "helper_name", "ISphereWinHelper" },
|
||||
{ "helper_version", "0.1.0" },
|
||||
{ "helper_version", "0.3.0" },
|
||||
{ "protocol", HelperProtocol.Protocol },
|
||||
{ "runtime", ".NET Framework " + Environment.Version }
|
||||
};
|
||||
|
||||
332
native/ISphereWinHelper/RuntimeProbe.cs
Normal file
332
native/ISphereWinHelper/RuntimeProbe.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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 `
|
||||
|
||||
82
scripts/package-live-probe-recorder.ps1
Normal file
82
scripts/package-live-probe-recorder.ps1
Normal 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
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user