Files
workphone-sdk/sdk/scripts/wechat_interface_audit.py

182 lines
6.7 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/usr/bin/env python3
"""
微信 Hook 接口完整性审计 — 禁止空接口/占位冒充
检查项:
1. WECHAT_ACTIONS ↔ ACTION_TO_RPC ↔ wechat_hook_v2.js rpc.exports 三方对齐
2. ACTION_ALIASES173 catalog→ canonical 均在 WECHAT_ACTIONS
3. (可选)真机 hook/execute 探针 / 全量 128 探针
用法:
python3 sdk/scripts/wechat_interface_audit.py
python3 sdk/scripts/wechat_interface_audit.py --probe -d xgfe65eimrrofyws
python3 sdk/scripts/wechat_interface_audit.py --probe-full -d xgfe65eimrrofyws
"""
from __future__ import annotations
import argparse
import json
import re
import sys
from datetime import datetime
from pathlib import Path
ROOT = Path(__file__).resolve().parents[2]
JS = ROOT / "sdk/agent/hook/wechat_hook_v2.js"
HE = ROOT / "sdk/agent/hook/hook_executor.py"
SKILL = ROOT / "sdk/app/skills/wechat/skill_v2.py"
OUT = ROOT / "sdk/tmp"
def _load_rpc_keys() -> set[str]:
text = JS.read_text(encoding="utf-8", errors="ignore")
return set(re.findall(r"^\s+(\w+)\s*:\s*function", text, re.M))
def _load_action_rpc() -> dict[str, str]:
text = HE.read_text(encoding="utf-8", errors="ignore")
block = text.split("ACTION_TO_RPC:", 1)[1].split("ACTION_ALIASES:", 1)[0]
return dict(re.findall(r'"([a-z_]+)"\s*:\s*"(\w+)"', block))
def _load_action_aliases() -> dict[str, str]:
text = HE.read_text(encoding="utf-8", errors="ignore")
block = text.split("ACTION_ALIASES:", 1)[1].split("\n\n", 1)[0]
return dict(re.findall(r'"([a-z_]+)"\s*:\s*"([a-z_]+)"', block))
def _load_wechat_actions() -> dict[str, str]:
text = SKILL.read_text(encoding="utf-8", errors="ignore")
block = text.split("WECHAT_ACTIONS = {", 1)[1].split("\n}\n", 1)[0]
return dict(re.findall(r'"([a-z_]+)"\s*:\s*"(\w+)"', block))
def audit() -> dict:
rpc = _load_rpc_keys()
action_rpc = _load_action_rpc()
aliases = _load_action_aliases()
skill_actions = _load_wechat_actions()
catalog = sorted(set(action_rpc) | set(aliases))
missing_rpc = sorted(set(action_rpc.values()) - rpc)
skill_not_in_executor = sorted(set(skill_actions) - set(action_rpc))
# diag_* 为 Hook 内部联调诊断项JS 有真实现),不进对外 130 action 清单,豁免对齐
executor_not_in_skill = sorted(
a for a in set(action_rpc) - set(skill_actions)
if not a.startswith("diag_")
)
skill_rpc_mismatch = [
a for a, r in skill_actions.items()
if a in action_rpc and action_rpc[a] != r
]
alias_unresolved = sorted(
a for a, target in aliases.items()
if target not in skill_actions
)
ok = not (
missing_rpc
or skill_not_in_executor
or skill_rpc_mismatch
or executor_not_in_skill
or alias_unresolved
)
return {
"timestamp": datetime.now().isoformat(),
"rpc_exports": len(rpc),
"action_to_rpc": len(action_rpc),
"wechat_actions": len(skill_actions),
"action_aliases": len(aliases),
"catalog_actions": len(catalog),
"missing_rpc_in_js": missing_rpc,
"skill_not_in_executor": skill_not_in_executor,
"executor_not_in_skill": executor_not_in_skill,
"skill_rpc_mismatch": skill_rpc_mismatch,
"alias_unresolved": alias_unresolved,
"complete": ok,
}
def _probe_actions(device_id: str, base: str, actions: list[str], params_map: dict) -> list[dict]:
import httpx
rows = []
with httpx.Client(timeout=60) as c:
for action in actions:
params = dict(params_map.get(action, {}))
r = c.post(
f"{base}/api/v3/hook/execute",
json={
"device_id": device_id,
"platform": "wechat",
"action": action,
"params": params,
"hook_only": True,
},
)
data = r.json()
ch = data.get("channel_used") or data.get("_channel_used") or data.get("channel") or ""
inner = data.get("data") if isinstance(data.get("data"), dict) else {}
ok = bool(inner.get("success") or data.get("success")) and "offline" not in str(ch).lower()
if inner.get("success") is False:
ok = False
rows.append({"action": action, "ok": ok, "channel": ch, "error": inner.get("error") or inner.get("message")})
return rows
def probe(device_id: str, base: str, limit: int = 20) -> list[dict]:
action_rpc = _load_action_rpc()
safe = ["ping", "get_wechat_version", "get_hook_status", "get_contacts", "get_profile", "get_messages"]
actions = [a for a in safe if a in action_rpc][:limit]
params = {
"get_messages": {"conversation_id": "filehelper", "limit": 3},
"get_contacts": {"limit": 5},
}
return _probe_actions(device_id, base, actions, params)
def probe_full(device_id: str, base: str) -> list[dict]:
sys.path.insert(0, str(ROOT / "sdk" / "scripts"))
from matrix_hook_catalog_verify import SAFE_PARAMS # type: ignore
actions = sorted(_load_wechat_actions().keys())
return _probe_actions(device_id, base, actions, SAFE_PARAMS)
def main() -> int:
parser = argparse.ArgumentParser()
parser.add_argument("--probe", action="store_true", help="6 项安全探针")
parser.add_argument("--probe-full", action="store_true", help="128 canonical 全量探针")
parser.add_argument("-d", "--device-id", default="xgfe65eimrrofyws")
parser.add_argument("--base", default="http://127.0.0.1:8899")
args = parser.parse_args()
report = audit()
OUT.mkdir(parents=True, exist_ok=True)
out = OUT / f"wechat_interface_audit_{int(datetime.now().timestamp())}.json"
if args.probe or args.probe_full:
rows = probe_full(args.device_id, args.base) if args.probe_full else probe(args.device_id, args.base)
report["probe"] = rows
report["probe_pass"] = sum(1 for x in rows if x["ok"])
report["probe_total"] = len(rows)
out.write_text(json.dumps(report, ensure_ascii=False, indent=2), encoding="utf-8")
print(f"rpc.exports: {report['rpc_exports']}")
print(f"ACTION_TO_RPC: {report['action_to_rpc']}")
print(f"WECHAT_ACTIONS: {report['wechat_actions']}")
print(f"Catalog(含alias): {report['catalog_actions']}")
print(f"缺失 RPC: {len(report['missing_rpc_in_js'])}")
print(f"Skill 未映射 executor: {len(report['skill_not_in_executor'])}")
print(f"Alias 未解析: {len(report.get('alias_unresolved', []))}")
print(f"完整对齐: {'' if report['complete'] else ''}")
print(f"报告: {out}")
if args.probe or args.probe_full:
print(f"真机探针: {report.get('probe_pass', 0)}/{report.get('probe_total', 0)}")
return 0 if report["complete"] else 1
if __name__ == "__main__":
sys.exit(main())