PRA-only-demo/scripts/audit-catalog-params.py

128 lines
4.6 KiB
Python
Raw Normal View History

#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""审计能力参数 v2:catalog 字段 vs 实现读取。
策略:
- 直接读取:handler 函数体内 text(args,["k"]) / args.k / pick(args,[...]) 等模式
- 间接读取:body 中把 args 整体传给函数(xxx(args)),或必填 key 出现在同文件其他位置(兜底)
- A 类(假必填)只在「直接+间接+全文兜底都读不到」时报告,保证高置信
- B 类(隐藏参数)只报直接读取的 key(保守)
"""
import json
import re
import os
ROOT = r"D:\code\PRA-only-demo"
CAP_DIRS = [
os.path.join(ROOT, "packages", "rpa-core", "src", "capabilities"),
os.path.join(ROOT, "src", "main", "capabilities"),
]
with open(os.path.join(ROOT, "resources", "catalog.json"), encoding="utf-8") as f:
catalog = {c["id"]: c for c in json.load(f)["components"]}
src_blobs = []
for d in CAP_DIRS:
for fn in os.listdir(d):
if fn.endswith(".ts") and not fn.endswith(".test.ts"):
with open(os.path.join(d, fn), encoding="utf-8") as f:
src_blobs.append((fn, f.read()))
def find_register_bodies(src):
out = []
for m in re.finditer(r'register\(\s*(\[[^\]]*\]|"[A-Za-z_.0-9]+")\s*,\s*(async)?\s*\(', src):
ids = re.findall(r'"([A-Za-z_.0-9]+)"', m.group(1))
i = m.end() - 1
depth = 0
start_body = None
j = i
while j < len(src):
ch = src[j]
if ch == "(":
depth += 1
elif ch == ")":
depth -= 1
if depth == 0:
break
elif ch == "{" and start_body is None and depth >= 2:
start_body = j
j += 1
if start_body is None:
body = src[i + 1:j]
else:
d2 = 0
k = start_body
while k < len(src):
if src[k] == "{":
d2 += 1
elif src[k] == "}":
d2 -= 1
if d2 == 0:
break
k += 1
body = src[start_body:k]
out.append((ids, body))
return out
PAT_DIRECT = [
re.compile(r'\w+\(\s*args\s*,\s*\[([^\]]*)\)'),
re.compile(r'pick\(\s*args\s*,\s*\[([^\]]*)\)'),
re.compile(r'\bargs\.([a-zA-Z_][a-zA-Z0-9_]*)'),
re.compile(r"args\[['\"]([^'\"]+)['\"]\]"),
]
INTERNAL = {"nodeId", "componentId", "timeoutMs", "time_out", "timeout", "evidenceDir"}
info = {} # cid -> dict(direct=set, indirect=bool)
for fn, src in src_blobs:
for ids, body in find_register_bodies(src):
direct = set()
for pat in PAT_DIRECT:
for mm in pat.finditer(body):
g = mm.group(1)
if g is None:
continue
if "," in g or '"' in g or "'" in g:
direct.update(re.findall(r"['\"]([^'\"]+)['\"]", g))
else:
direct.add(g)
# args 整体传走 → 间接(keys 未知)
indirect = bool(re.search(r'\(\s*args\s*[,)]|\bargs\s*\)', body)) and not direct
# selectorOf(args) 这种:args 作为参数出现在调用里
if re.search(r'\w+\(\s*args\s*\)|\w+\(\s*ctx\s*,\s*node\s*,\s*args', body):
indirect = True
for cid in ids:
rec = info.setdefault(cid, {"direct": set(), "indirect": False, "file": fn})
rec["direct"] |= direct
rec["indirect"] = rec["indirect"] or indirect
A, B = [], []
for cid, comp in sorted(catalog.items()):
rec = info.get(cid)
if not rec:
continue
fields = comp.get("fields") or []
cat_keys = {f["key"] for f in fields}
required = {f["key"] for f in fields if f.get("required")}
direct = rec["direct"]
if rec["indirect"]:
# keys 未知:不判 A(无法确定),不判 B
continue
a = {k for k in required if k not in direct}
# 兜底:全目录源码找 key(带引号形式,或 .key 词边界的直接访问)
if a:
all_src = "\n".join(s for _, s in src_blobs)
a = {k for k in a if not (re.search(r'["\']' + re.escape(k) + r'["\']', all_src) or re.search(r'\.' + re.escape(k) + r'\b', all_src))}
if a:
A.append((cid, sorted(a)))
b = {k for k in direct if k not in cat_keys and k not in INTERNAL and len(k) > 2}
if b:
B.append((cid, sorted(b)))
print(f"=== A 类:必填但实现全链路都不读(高置信假必填) {len(A)} 个 ===")
for cid, keys in A:
print(f" {cid}: {keys}")
print()
print(f"=== B 类:实现直接读取但目录未定义(隐藏参数,保守列表) {len(B)} 个 ===")
for cid, keys in B:
print(f" {cid}: {keys}")