"""安全探针统一的「判据 + 退出码」小工具(供 test/security-hardening/*.py 使用)。 为什么需要它 ------------ 这些探针早期只 `print` 观察结果、结尾写死 `return 0` ⇒ **无论安全控制是否生效都 exit 0**: 人眼看到的是 ❌/异常现象,自动化(CI、部署前置检查、agent 验收)看到的却是绿灯。 这与 `tsc --noEmit` 空跑、`tcp_e2e_smoke.py` 永远 exit 0 属**同一类假绿**。 统一口径(与 AGENTS.md「验收流程」一致) -------------------------------------- 0 = 全部判据通过 1 = 有判据失败(失败项额外打到 stderr,便于只看 stderr 定位) 2 = 环境/连接问题(调用方自行 `raise SystemExit(2)`,例如读不到 .env、连不上 Redis/后端) 用法 ---- import sys from verdict import Verdict v = Verdict("probe_upload(测试环境 uploads 安全)") v.check("A1 头像上传 svg 应被拒", st != 200, f"status={st}") ... sys.exit(v.exit_code()) 约定 ---- - 判据只写**明确、可复现**的期望("应被拒" = status != 200;"应通过" = status == 200); 拿不准阈值/口径的现象(如限流具体第几次触发)**不要**写成判据,保持为观察输出, 否则会制造误报 —— 误报与假绿同样有害。 - 判据数可能为 0(纯取证脚本):此时 exit_code() 返回 0,但会打印"未设置判据"提醒。 """ import sys class Verdict: def __init__(self, title): self.title = title self.checks = [] # [(label, passed, detail)] def check(self, label, passed, detail=""): """记录一条判据;返回 bool 便于就地分支""" passed = bool(passed) self.checks.append((label, passed, detail)) return passed @property def failed(self): return [(l, d) for l, p, d in self.checks if not p] def exit_code(self): """打印结论并返回退出码(0=全通过;1=有失败)""" n_pass = len(self.checks) - len(self.failed) print(f"\n===== 判据结论:{self.title} =====") for label, passed, detail in self.checks: suffix = f" ({detail})" if detail else "" print(f" {'✅' if passed else '❌'} {label}{suffix}") print(f" 合计 {n_pass}/{len(self.checks)} 判据通过") if not self.checks: print(" ⚠️ 本脚本未设置任何判据(纯取证脚本)—— 不要拿它的退出码当验收判据") return 0 if self.failed: print("\n未通过判据(stderr):", file=sys.stderr) for label, detail in self.failed: suffix = f" ({detail})" if detail else "" print(f" ❌ {label}{suffix}", file=sys.stderr) print(f"合计 {n_pass}/{len(self.checks)} 判据通过,{len(self.failed)} 项失败", file=sys.stderr) return 1 return 0