euthyna — 代码安全审计的判定纪律
εὔθυνα:雅典官员离任时必须交出账目接受审查,通不过就无法体面离任。 本技能的核心机制即此:说"审完了"不算数,证据交齐、门禁通过,才算交付。
本技能不找 bug,它管住结论。 找 bug 由模型与现成工具做;本技能负责: 每条结论必须过门禁,过不了就只能降级为「观察」。
何时用 / 何时不用
用:
- 用户要求审计代码、仓库、一次变更(PR / commit / diff)的安全面
- 用户拿来一条疑似漏洞,问「这是真的吗」「是否可利用」
- 用户要求审查某次变更是否把已有的安全防护改弱了
- 用户要求「验证完再交付」
不用(直接拒绝或转其他技能):
- 只问某个 CVE / 某个依赖的详情 → 用依赖审计类工具
- 只要求跑一次现成扫描器并贴出结果 → 那是工具调用,不是审计
- 纯文档、格式化、lint 类改动 → 无安全面
- 用户明确只要快速摘要并接受风险 → 说明这是「非审计模式」,不套用本技能的裁定格式
三条不可跳过的规则
这三条是本技能与「让模型仔细点」的全部区别。任何阶段、任何路径都不许跳过。
规则 1:可测量的量一律由命令产出,模型只做判读
调用方数量、测试覆盖率、依赖版本、行号、提交哈希——这些一律不许靠印象回答。 模型可以在命令不可用时说「不可测量」,但不许估算后当成事实写进结论。
这条来自一次实测翻车:某包在 GitHub 上显示 5+ 人维护,而 npm 的实际权限列表只有 1 人; 另一个每周下载 1.6 亿次的包在某个数据源里显示零下载。手感估数字必错。
规则 2:证据强制到 file:line,禁止占位符
- 每条主张引用具体位置,格式
path:L123,有版本时写path:L123 (commit abc1234) - 禁用
probably/likely/大概是—— 去追真实代码,追不了就明说追不了 - 演示代码(PoC)里禁止
TODO、...、// 攻击者在这里做某事 - 一旦发现 PoC 用了人为绕过(mock、stub、禁用检查),该 PoC 判定无效
规则 3:缺数据 ≠ 干净
每个判据只能落到三态:已评估-干净 / 已评估-标记 / 因某原因不可评估。
- 一次什么都没测到的运行,产出的是「不可评估」,不是「没发现问题」
- 引用任何「干净」结论之前,先看覆盖表
- 数字必须逐字引用,不得重新推导、四舍五入或修饰
- 没发现不等于背书
入口决策树
用户给了什么?
│
├─ 一条已有的疑似漏洞断言 ──────────► 阶段 C:结论面验证
│ (「这是真的吗」「可利用吗」) 读 references/verification-gates.md
│ ★ 结论依赖「测过没有」→ 跑 coverage
│
├─ 一次变更(PR / diff / commit)────► 阶段 B:变更面审计
│ (「这个 PR 安全吗」) 读 references/change-audit.md
│ ★ 第一条命令:euthyna audit --base <分叉点>
│ (history + deps 一次跑完,见下节)
│ ★ 要谈测试覆盖 → 再跑 coverage
│
├─ 一个仓库 / 依赖面 ────────────────► 阶段 A:依赖面审计
│ (「依赖有没有问题」) 读 references/dependency-audit.md
│
└─ 一个仓库的整体安全审计 ───────────► 阶段 B 为主,A 与 C 作为子步骤
★ = 在动手人工读代码之前先跑,不是读完之后的补充说明。 产出器给的确定性事实,决定了人工阅读该往哪里使劲。
可以做多个阶段,但阶段之间不许互相替代。 特别是:阶段 B 报出的疑似发现,必须逐个走阶段 C 验证,不许直接写进报告当结论。
本技能自带的两个确定性测量
调用方、覆盖率、依赖漏洞、密钥这些不重造(见文末分工表)。但有两件事没人做、 且模型自己算不准,所以本技能自带一个零依赖 CLI:
| 测量 | 回答的问题 | 什么时候必须跑 |
|---|---|---|
audit |
上面两个问题一次跑完(history 默认追溯最初引入者 + deps 枚举 lockfile 全部依赖),合并一份报告 | 每次审计的第一条命令(阶段 B 开场) |
history |
这次变更删掉的代码来自哪个提交?那个提交是不是安全修复? | 需要单独重跑 history 时(如换 --base) |
coverage |
某个符号在测试运行中到底有没有被调用过? | 报告里要说「测过 / 没测过」时(阶段 B / C) |
# 唯一必需的参数是 --base(改前版本 / PR 分叉点):
node <euthyna 仓库>/bin/euthyna.js audit --base <改前版本> --repo <被审计的仓库>
# history / coverage 按需单独跑:
node <euthyna 仓库>/bin/euthyna.js history --base <改前版本> --repo <被审计的仓库>
node <euthyna 仓库>/bin/euthyna.js coverage --coverage <覆盖率文件绝对路径> --symbol <符号名>
⚠️ node bin/euthyna.js 这种相对路径一定失败。 审计时当前目录是被审计的项目,
不是 euthyna 仓库,会报 Cannot find module。要用 euthyna 仓库的路径,
并把被审计仓库用 --repo 显式传进去(--base / --head 相对它解析)。
退出码——2 与 0 的区别就是这套东西存在的意义:
| 码 | 含义 | 怎么办 |
|---|---|---|
0 |
已测量,无安全相关发现 | 继续,但要读「未评估的判据」一节 |
10 |
已测量,存在 security 分类的事实 |
提高优先级,逐条走阶段 C,不要直接当结论 |
1 |
用法错误 | 修命令。这不是测量结果 |
2 |
完全无法测量 | 不得当作干净。换测量方式,或把判据标为「不可评估」 |
命令不可用时(没装、没有检出):把判据标为「不可评估」,不要手工估算顶上。
操作手册(命令、真实输出样例、失效情形):references/fact-producers.md。
执行纪律(复现命令与 PoC)
--verify 与 PoC 会在用户的机器上执行命令。跑任何复现/验证命令前:
- 先取得用户明确批准,把要跑的命令原文列出来——不跑没报备的命令
- 能在沙箱/临时容器里跑的(如 Docker),优先沙箱;无法沙箱时说明风险再跑
- 报告里的 PoC 是给「拿到报告的人」复现用的;裁定过程中 agent 自己执行的每条命令同样适用本节
事实契约(测量与判定之间的接口)
本技能定义的是测量层与判定层之间的接口:docs/fact-contract-zh.md。
要点:
- 契约只装事实,不装分数、不装严重性等级、不装修复建议
- 每条事实必须带复现命令或产出工具+版本
- 分数不得用作门禁(这是从
dsh-trust-check学到的:它的契约明写「不要拿score或band做门禁」) approximate的事实不得参与门禁unknown不是「干净」——三态里最常被误用的就是它
事实的读法(能推出什么、不能推出什么):references/fact-contract.md。
裁定格式
无论走哪条路径,产出必须落到这个形状:
BUG #N TRUE POSITIVE — [一句话描述]
门禁全部通过。证据:path:L123 (commit abc1234)
复现:<可重跑的命令或 PoC 位置>
可利用性:EASY / MEDIUM / HARD
影响:具体到资金量 / 被提升的权限 / 被暴露的数据
BUG #N FALSE POSITIVE — [拒绝理由]
门禁 N(门禁名)FAIL:<具体证据>
例:第 98 行校验保证 packet_size >= 16,故 (packet_size - header_size) >= 8。
下溢在数学上不可能。
BUG #N INCONCLUSIVE — [无法判定的原因]
门禁 N(门禁名)未评估:<为什么>
第三态 inconclusive 必须存在。
Trail of Bits 的原始 fp-check 只有两态,因为它靠 Stop hook 无限强制继续直到补完;
DSH 上做不到(Stop hook 必须自带限流,见 docs/dsh-stop-gate-zh.md)。
所以必须有这个诚实出口——不许把「没查完」写成 FALSE POSITIVE。
门禁的程序化校验(euthyna gate)
上面的格式不是摆设:euthyna gate <报告文件> 会机械地逐条核对——
- TRUE POSITIVE:证据必须到
path:L123、必须有复现命令、必须说明影响、六门禁必须全过; - FALSE POSITIVE:至少一条门禁 FAIL 且带具体证据;
- INCONCLUSIVE:至少一条门禁未评估,且没有任何 FAIL。
缺证据、缺复现、或门禁与裁定互相矛盾(如 TRUE POSITIVE 却带 FAIL 门禁)的 finding,
会被**降级为「观察」**并以退出码 10 报出——不需要裁定者自觉,校验器替你卡住。
node <euthyna 仓库>/bin/euthyna.js gate <报告文件>
# 重跑每条复现命令核验(白名单工具、按 argv 执行不走 shell):
node <euthyna 仓库>/bin/euthyna.js gate <报告文件> --verify --cwd <被审计仓库>
- 退出码
0= 全部通过;10= 有 finding 被降级;2= 报告无法读取 / 没有可校验的 finding。 --verify下复现命令跑不通同样构成降级——「说能复现」不算数,跑通才算。
报告交出去之前,先过一遍 euthyna gate。 它校验的是报告自己的自我声明,
不测量目标仓库——因此它不能代替裁定,只能保证「拿不出证据的结论不会以已确证的面貌出去」。
报告必须落盘
- 只输出到聊天算失败。必须写报告文件
- 文件名:
<PROJECT>_EUTHYNA_AUDIT_<YYYY-MM-DD>.md - 写文件失败的降级顺序:当前工作目录 → 用户桌面 → 临时目录 → 最后手段才输出到聊天并提示用户手动保存
交付前的自检(逐条打勾,缺一条不许说「审完了」)
- 每个疑似发现都走了完整的阶段 C,不是「看一眼觉得是假的」
- 跑过
audit(或 diff 有删除行时跑过history);删自安全修复提交的代码已按最高风险处理 - 报告里每句「测过 / 没测过」都有
coverage输出支撑,或已标为「不可评估」 - 退出码为
2的判据没有出现在「干净」一栏 - 每条结论都带
file:line - 每条结论都标了三态之一(干净 / 标记 / 不可评估)
- 覆盖表写清了哪些判据没评估、为什么
- 恶魔代言人 13 问逐条答过(含防假阴性的第 12、13 问)
- 报告文件已落盘
- 报告已通过
euthyna gate校验(退出码0;有降级要读降级原因) - 没有把「缺数据」写成「干净」
参考文件
按需加载。不要一次全读——渐进披露是这套东西能在长会话里保持有效的原因。
| 文件 | 内容 | 什么时候读 |
|---|---|---|
references/verification-gates.md |
阶段 C:路由、6 门禁、误报清单、恶魔代言人、PoC 规则 | 判定一条已有断言时(最常用) |
references/change-audit.md |
阶段 B:基线、深度 vs 风险、来源归属、爆炸半径、对抗建模 | 审一次变更时 |
references/dependency-audit.md |
阶段 A:清单/锁文件、三态、文体规范、禁止事项 | 审依赖面时 |
references/bug-classes.md |
9 类缺陷的专项检查与默认假设方向 | 定了缺陷类别之后 |
references/meta-mechanisms.md |
六个元机制:让流程不退化成走过场 | 审计开始时读一次 |
references/fact-producers.md |
操作手册:两个测量何时跑、命令怎么写、退出码怎么处理、真实输出样例、失效情形 | 跑产出器之前(阶段 B / C) |
references/fact-contract.md |
拿到事实之后能推出什么、以及它不能推出什么 | 读完产出器输出之后 |
技能文本自包含:以上文件都在
references/内,不依赖技能目录之外的路径, 可以整目录复制到任何技能根使用。⚠️ 但产出器本身是独立程序,不在技能目录里。复制技能不会把 CLI 一起带走。 新环境里要单独安装、或提供本项目的一份检出,否则相关判据只能标为「不可评估」—— 那也是一个诚实的结果,比手工估算强。见
references/fact-producers.md第一节。
与既有生态的分工(避免重复劳动)
| 已经有人做的事 | euthyna 怎么做 |
|---|---|
调用图 / 爆炸半径(dsh-tool-lens、dsh-blast-radius) |
不重造,消费其输出 |
覆盖率(dsh-code-coverage) |
不重造,消费其输出 |
依赖漏洞(dsh-dep-vuln-scan 等) |
不重造,消费其输出 |
密钥扫描(dsh-code-security) |
不重造,消费其输出 |
交付门禁机制(dsh-doublecheck、formalswarm) |
机制可借鉴;euthyna 的门禁内容是安全专属的 6 门禁 |
证据生命周期与 PoC 实验室(dsh-omv) |
可并存:dsh-omv 可作为事实契约的消费方 |
euthyna 只补两件没人做的确定性测量(见 docs/positioning-zh.md §7.2):
git 安全回归的机械判定,以及「符号 × 真实执行覆盖」的 join。
