159 lines
7.8 KiB
Python
159 lines
7.8 KiB
Python
#!/usr/bin/env python3
|
||
# -*- coding: utf-8 -*-
|
||
"""生成真机110功能验收矩阵。
|
||
|
||
该脚本只分析本地项目代码,不执行任何微信外发动作。输出用于指导后续无线 Frida 真机验证:
|
||
1. 每项功能是否需要真机画面证据。
|
||
2. 每项功能是否需要后端日志证据。
|
||
3. 每项功能是否属于高风险,只能在测试对象或空数据下验证。
|
||
"""
|
||
from __future__ import annotations
|
||
|
||
import json
|
||
import re
|
||
from pathlib import Path
|
||
from datetime import datetime
|
||
|
||
ROOT = Path(__file__).resolve().parents[1]
|
||
OUT = ROOT / "开发文档" / "6、测试" / "wireless_frida_real_device_acceptance_20260518"
|
||
OUT.mkdir(parents=True, exist_ok=True)
|
||
|
||
CANDIDATE_FILES = [
|
||
ROOT / "sdk/app/routers/wechat_full.py",
|
||
ROOT / "sdk/app/routers/frida_wireless.py",
|
||
ROOT / "sdk/app/routers/adb.py",
|
||
ROOT / "sdk/app/routers/devices.py",
|
||
ROOT / "sdk/app/services/wechat_adb_engine.py",
|
||
ROOT / "sdk/app/agent/hook/wechat_hook_v2.js",
|
||
ROOT / "tools/wireless_frida_wechat_batch_verify.py",
|
||
]
|
||
|
||
HIGH_RISK_RE = re.compile(r"add|accept|delete|remove|post|send|forward|revoke|register|login|logout|switch|invite|quit|set|clear|follow|unfollow|like|comment|share|batchExecute|getSimPhone|moments|friend|group|official|phone|sim|feedback|report|unblock|手机号|添加好友|朋友圈|群发|解封|支付|转账|红包|举报|反馈|外发|删除|清空|资料改写", re.I)
|
||
READ_ONLY_RE = re.compile(r"^(get|list|status|info|ping|check|probe|current|health|ready|devices|projects|logs|screenshot|dump|globalSearch|find)", re.I)
|
||
UI_SAFE_RE = re.compile(r"tap|swipe|key|input|launch|open|back|scroll|click|navigate|tab", re.I)
|
||
|
||
CN_MAP = {
|
||
"ping": "心跳检测",
|
||
"processinfo": "进程信息读取",
|
||
"javainfo": "Java上下文读取",
|
||
"currentactivity": "当前Activity读取",
|
||
"classprobe": "微信关键类探测",
|
||
"getContacts": "联系人列表读取",
|
||
"getMessages": "消息记录读取",
|
||
"sendMessage": "发送消息",
|
||
"addFriend": "添加好友",
|
||
"postMoments": "发朋友圈",
|
||
"getSimPhone": "获取手机号",
|
||
"checkLoginState": "登录状态检查",
|
||
"getDeviceInfo": "设备信息读取",
|
||
"getNetworkInfo": "网络信息读取",
|
||
"getStorageInfo": "存储信息读取",
|
||
}
|
||
|
||
|
||
def classify(name: str) -> tuple[str, str, str]:
|
||
if HIGH_RISK_RE.search(name):
|
||
return "高风险受控", "仅允许测试对象/空数据/安全闸验证,不触发真实外发或真实改写", "接口存在、参数校验、后端拦截日志、安全闸截图"
|
||
if READ_ONLY_RE.search(name):
|
||
return "只读可真机执行", "允许在真机直接执行读取或状态检查", "后端返回数据、手机当前画面、Frida/ADB日志"
|
||
if UI_SAFE_RE.search(name):
|
||
return "UI安全操作", "允许在真机执行导航、点击、滑动、输入但不提交", "前后截图、当前Activity、命令日志"
|
||
return "待人工确认", "默认只做空数据/只读验证,避免误触发外部动作", "接口契约、后端日志、失败原因"
|
||
|
||
|
||
def _line_in_rpc_exports(text: str, pos: int) -> bool:
|
||
start = text.rfind("rpc.exports", 0, pos)
|
||
if start < 0:
|
||
return False
|
||
end = text.find("function _", start)
|
||
if end < 0:
|
||
end = len(text)
|
||
return start <= pos < end
|
||
|
||
|
||
def extract() -> list[dict]:
|
||
rows: list[dict] = []
|
||
seen = set()
|
||
for p in CANDIDATE_FILES:
|
||
if not p.exists():
|
||
continue
|
||
text = p.read_text(encoding="utf-8", errors="ignore")
|
||
patterns = [
|
||
("fastapi_route", r"@router\.(get|post|put|delete|patch)\(['\"]([^'\"]+)['\"]"),
|
||
("python_function", r"^(?:async\s+def|def)\s+([a-zA-Z_][\w]*)\s*\(", re.M),
|
||
("frida_rpc", r"^\s*([a-zA-Z_$][\w$]*)\s*:\s*function\s*\(", re.M),
|
||
]
|
||
for pat in patterns:
|
||
kind = pat[0]
|
||
flags = pat[2] if len(pat) > 2 else 0
|
||
for m in re.finditer(pat[1], text, flags):
|
||
if kind == "fastapi_route":
|
||
method, route = m.group(1).upper(), m.group(2)
|
||
name = f"{method} {route}"
|
||
else:
|
||
name = m.group(1)
|
||
# 只保留 rpc.exports 中暴露给 Python/后端调用的函数,避免把内部 helper 当作110接口。
|
||
if kind == "frida_rpc" and not _line_in_rpc_exports(text, m.start()):
|
||
continue
|
||
if name.startswith("_"):
|
||
continue
|
||
key = (kind, name, str(p.relative_to(ROOT)))
|
||
if key in seen:
|
||
continue
|
||
seen.add(key)
|
||
risk, execute_rule, evidence = classify(name)
|
||
rows.append({
|
||
"id": len(rows) + 1,
|
||
"name": name,
|
||
"display_name": CN_MAP.get(name, name),
|
||
"source_type": kind,
|
||
"source_file": str(p.relative_to(ROOT)),
|
||
"risk_level": risk,
|
||
"real_device_execution": execute_rule,
|
||
"required_evidence": evidence,
|
||
"backend_effect": "必须记录调用时间、通道、入参摘要、返回摘要、错误栈与耗时",
|
||
"phone_final_effect": "必须采集执行前后手机画面或当前Activity;只读项可采集当前画面与返回值",
|
||
"acceptance_assertion": "通过=真机无线在线+命令/RPC成功+证据齐全+未触发禁止动作",
|
||
})
|
||
# 优先保留能覆盖110项的功能,按风险与来源排序。
|
||
rows.sort(key=lambda r: (0 if r["risk_level"] != "高风险受控" else 1, r["source_file"], r["name"]))
|
||
for i, r in enumerate(rows[:160], 1):
|
||
r["id"] = i
|
||
return rows[:160]
|
||
|
||
|
||
def write(rows: list[dict]) -> None:
|
||
(OUT / "真机110功能验收矩阵.json").write_text(json.dumps(rows, ensure_ascii=False, indent=2), encoding="utf-8")
|
||
selected = rows[:110]
|
||
md = [
|
||
"# 真机 110 功能验收矩阵",
|
||
"",
|
||
f"生成时间:{datetime.now().strftime('%Y-%m-%d %H:%M:%S')}",
|
||
"",
|
||
"本矩阵把验证口径从“接口跑通”升级为“**真机执行、后端记录、最终画面、结果断言**”。其中,高风险受控项不在真实对象上外发或改写,只允许通过测试对象、空数据或安全总闸验证。",
|
||
"",
|
||
"| 序号 | 功能 | 来源 | 风险层级 | 真机执行口径 | 必要证据 | 验收断言 |",
|
||
"|---:|---|---|---|---|---|---|",
|
||
]
|
||
for r in selected:
|
||
md.append(
|
||
f"| {r['id']} | `{r['display_name']}` | `{r['source_file']}` | {r['risk_level']} | {r['real_device_execution']} | {r['required_evidence']} | {r['acceptance_assertion']} |"
|
||
)
|
||
md += [
|
||
"",
|
||
"## 执行原则",
|
||
"",
|
||
"真机验证必须在已拔掉 Type-C 的无线链路上进行,手机端结果以当前页面、前后截图、Activity 状态、返回值和后端日志共同判定。对于添加好友、获取手机号、群发、朋友圈、反馈/举报、解封等功能,若没有明确测试账号、测试手机号、测试群或用户二次确认,不进入真实提交环节。",
|
||
"",
|
||
"## 后续脚本改造要求",
|
||
"",
|
||
"脚本需要为每个功能写入 `before_screenshot`、`backend_log`、`after_screenshot`、`assertion`、`risk_gate` 五类字段。失败项不能只标记失败,还要写明是哪一层失败:无线连接、Frida RPC、后端调用、手机页面、最终结果或安全闸。",
|
||
]
|
||
(OUT / "真机110功能验收矩阵.md").write_text("\n".join(md) + "\n", encoding="utf-8")
|
||
print(json.dumps({"total_extracted": len(rows), "selected_for_110": len(selected), "out": str(OUT)}, ensure_ascii=False, indent=2))
|
||
|
||
|
||
if __name__ == "__main__":
|
||
rows = extract()
|
||
write(rows)
|