euthyna 0.1.1 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,256 @@
1
+ ---
2
+ name: euthyna
3
+ description: 代码安全审计的判定纪律与交付门禁。当用户要求对一段代码、一个 PR、一次变更或一条已有的漏洞结论做安全审查与验证时使用。它做三件事:补上模型算不准的确定性事实(调用方、测试覆盖、git 历史来源),用 6 道门禁判定每条结论真假,拿不出证据的一律降级为「观察」而不是「发现」。不适用于:仅询问某个 CVE 的详情、仅要求跑一次现成扫描器、纯文档或格式化改动、以及用户只要一句快速摘要且明确接受风险时。
4
+ whenToUse: 用户要求审计代码或仓库的安全面、验证一条疑似漏洞结论真假、判定某个发现是否可利用、审查一次变更是否引入安全回归,或要求「审完再交付」时使用。只查密钥、只查依赖版本这类单一主题,直接用对应专项技能,不要触发本技能。
5
+ disable-model-invocation: true
6
+ user-invocable: true
7
+ ---
8
+
9
+ # euthyna — 代码安全审计的判定纪律
10
+
11
+ > εὔθυνα:雅典官员离任时必须交出账目接受审查,通不过就无法体面离任。
12
+ > 本技能的核心机制即此:**说"审完了"不算数,证据交齐、门禁通过,才算交付。**
13
+
14
+ **本技能不找 bug,它管住结论。** 找 bug 由模型与现成工具做;本技能负责:
15
+ 每条结论必须过门禁,过不了就只能降级为「观察」。
16
+
17
+ ---
18
+
19
+ ## 何时用 / 何时不用
20
+
21
+ **用**:
22
+
23
+ - 用户要求审计代码、仓库、一次变更(PR / commit / diff)的安全面
24
+ - 用户拿来一条疑似漏洞,问「这是真的吗」「是否可利用」
25
+ - 用户要求审查某次变更是否把已有的安全防护改弱了
26
+ - 用户要求「验证完再交付」
27
+
28
+ **不用**(直接拒绝或转其他技能):
29
+
30
+ - 只问某个 CVE / 某个依赖的详情 → 用依赖审计类工具
31
+ - 只要求跑一次现成扫描器并贴出结果 → 那是工具调用,不是审计
32
+ - 纯文档、格式化、lint 类改动 → 无安全面
33
+ - 用户明确只要快速摘要并接受风险 → 说明这是「非审计模式」,不套用本技能的裁定格式
34
+
35
+ ---
36
+
37
+ ## 三条不可跳过的规则
38
+
39
+ 这三条是本技能与「让模型仔细点」的全部区别。任何阶段、任何路径都不许跳过。
40
+
41
+ ### 规则 1:可测量的量一律由命令产出,模型只做判读
42
+
43
+ 调用方数量、测试覆盖率、依赖版本、行号、提交哈希——**这些一律不许靠印象回答**。
44
+ 模型可以在命令不可用时说「不可测量」,但不许估算后当成事实写进结论。
45
+
46
+ > 这条来自一次实测翻车:某包在 GitHub 上显示 5+ 人维护,而 npm 的实际权限列表只有 1 人;
47
+ > 另一个每周下载 1.6 亿次的包在某个数据源里显示零下载。**手感估数字必错。**
48
+
49
+ ### 规则 2:证据强制到 `file:line`,禁止占位符
50
+
51
+ - 每条主张引用具体位置,格式 `path:L123`,有版本时写 `path:L123 (commit abc1234)`
52
+ - 禁用 `probably` / `likely` / `大概是` —— 去追真实代码,追不了就明说追不了
53
+ - 演示代码(PoC)里禁止 `TODO`、`...`、`// 攻击者在这里做某事`
54
+ - **一旦发现 PoC 用了人为绕过**(mock、stub、禁用检查),该 PoC 判定**无效**
55
+
56
+ ### 规则 3:缺数据 ≠ 干净
57
+
58
+ 每个判据只能落到三态:**已评估-干净 / 已评估-标记 / 因某原因不可评估**。
59
+
60
+ - 一次什么都没测到的运行,产出的是「不可评估」,**不是**「没发现问题」
61
+ - 引用任何「干净」结论之前,先看覆盖表
62
+ - **数字必须逐字引用**,不得重新推导、四舍五入或修饰
63
+ - 没发现不等于背书
64
+
65
+ ---
66
+
67
+ ## 入口决策树
68
+
69
+ ```
70
+ 用户给了什么?
71
+
72
+ ├─ 一条已有的疑似漏洞断言 ──────────► 阶段 C:结论面验证
73
+ │ (「这是真的吗」「可利用吗」) 读 references/verification-gates.md
74
+ │ ★ 结论依赖「测过没有」→ 跑 coverage
75
+
76
+ ├─ 一次变更(PR / diff / commit)────► 阶段 B:变更面审计
77
+ │ (「这个 PR 安全吗」) 读 references/change-audit.md
78
+ │ ★ diff 里有删除行 → 先跑 history
79
+ │ ★ 要谈测试覆盖 → 先跑 coverage
80
+
81
+ ├─ 一个仓库 / 依赖面 ────────────────► 阶段 A:依赖面审计
82
+ │ (「依赖有没有问题」) 读 references/dependency-audit.md
83
+
84
+ └─ 一个仓库的整体安全审计 ───────────► 阶段 B 为主,A 与 C 作为子步骤
85
+ ```
86
+
87
+ ★ = **在动手人工读代码之前**先跑,不是读完之后的补充说明。
88
+ 产出器给的确定性事实,决定了人工阅读该往哪里使劲。
89
+
90
+ **可以做多个阶段,但阶段之间不许互相替代。**
91
+ 特别是:**阶段 B 报出的疑似发现,必须逐个走阶段 C 验证**,不许直接写进报告当结论。
92
+
93
+ ---
94
+
95
+ ## 本技能自带的两个确定性测量
96
+
97
+ 调用方、覆盖率、依赖漏洞、密钥这些**不重造**(见文末分工表)。但有两件事没人做、
98
+ 且模型自己算不准,所以本技能自带一个零依赖 CLI:
99
+
100
+ | 测量 | 回答的问题 | 什么时候**必须**跑 |
101
+ |---|---|---|
102
+ | `history` | 这次变更**删掉**的代码来自哪个提交?那个提交是不是安全修复? | diff 里有删除行时(阶段 B) |
103
+ | `coverage` | 某个符号在测试运行中**到底有没有被调用过**? | 报告里要说「测过 / 没测过」时(阶段 B / C) |
104
+
105
+ ```powershell
106
+ node <euthyna 仓库>/bin/euthyna.js history --base <改前版本> --repo <被审计的仓库>
107
+ node <euthyna 仓库>/bin/euthyna.js coverage --coverage <覆盖率文件绝对路径> --symbol <符号名>
108
+ ```
109
+
110
+ ⚠️ **`node bin/euthyna.js` 这种相对路径一定失败。** 审计时当前目录是**被审计的项目**,
111
+ 不是 euthyna 仓库,会报 `Cannot find module`。要用 euthyna 仓库的路径,
112
+ 并把被审计仓库用 `--repo` 显式传进去(`--base` / `--head` 相对它解析)。
113
+
114
+ **退出码**——`2` 与 `0` 的区别就是这套东西存在的意义:
115
+
116
+ | 码 | 含义 | 怎么办 |
117
+ |---|---|---|
118
+ | `0` | 已测量,无安全相关发现 | 继续,但**要读「未评估的判据」一节** |
119
+ | `10` | 已测量,存在 `security` 分类的事实 | **提高优先级**,逐条走阶段 C,不要直接当结论 |
120
+ | `1` | 用法错误 | 修命令。这不是测量结果 |
121
+ | **`2`** | **完全无法测量** | **不得当作干净**。换测量方式,或把判据标为「不可评估」 |
122
+
123
+ 命令不可用时(没装、没有检出):**把判据标为「不可评估」,不要手工估算顶上。**
124
+
125
+ 操作手册(命令、真实输出样例、失效情形):`references/fact-producers.md`。
126
+
127
+ ---
128
+
129
+ ## 事实契约(测量与判定之间的接口)
130
+
131
+ 本技能定义的是**测量层与判定层之间的接口**:`docs/fact-contract-zh.md`。
132
+
133
+ 要点:
134
+
135
+ - 契约只装**事实**,不装分数、不装严重性等级、不装修复建议
136
+ - 每条事实必须带**复现命令**或**产出工具+版本**
137
+ - **分数不得用作门禁**(这是从 `dsh-trust-check` 学到的:它的契约明写「不要拿 `score` 或 `band` 做门禁」)
138
+ - `approximate` 的事实**不得参与门禁**
139
+ - **`unknown` 不是「干净」**——三态里最常被误用的就是它
140
+
141
+ 事实的读法(能推出什么、**不能**推出什么):`references/fact-contract.md`。
142
+
143
+ ---
144
+
145
+ ## 裁定格式
146
+
147
+ 无论走哪条路径,产出必须落到这个形状:
148
+
149
+ ```
150
+ BUG #N TRUE POSITIVE — [一句话描述]
151
+ 门禁全部通过。证据:path:L123 (commit abc1234)
152
+ 复现:<可重跑的命令或 PoC 位置>
153
+ 可利用性:EASY / MEDIUM / HARD
154
+ 影响:具体到资金量 / 被提升的权限 / 被暴露的数据
155
+
156
+ BUG #N FALSE POSITIVE — [拒绝理由]
157
+ 门禁 N(门禁名)FAIL:<具体证据>
158
+ 例:第 98 行校验保证 packet_size >= 16,故 (packet_size - header_size) >= 8。
159
+ 下溢在数学上不可能。
160
+
161
+ BUG #N INCONCLUSIVE — [无法判定的原因]
162
+ 门禁 N(门禁名)未评估:<为什么>
163
+ ```
164
+
165
+ **第三态 `inconclusive` 必须存在。**
166
+ Trail of Bits 的原始 fp-check 只有两态,因为它靠 Stop hook 无限强制继续直到补完;
167
+ **DSH 上做不到**(Stop hook 必须自带限流,见 `docs/dsh-stop-gate-zh.md`)。
168
+ 所以必须有这个诚实出口——不许把「没查完」写成 `FALSE POSITIVE`。
169
+
170
+ ---
171
+
172
+ ## 门禁的程序化校验(euthyna gate)
173
+
174
+ 上面的格式不是摆设:`euthyna gate <报告文件>` 会机械地逐条核对——
175
+
176
+ - **TRUE POSITIVE**:证据必须到 `path:L123`、必须有复现命令、必须说明影响、六门禁必须全过;
177
+ - **FALSE POSITIVE**:至少一条门禁 FAIL 且带具体证据;
178
+ - **INCONCLUSIVE**:至少一条门禁未评估,且没有任何 FAIL。
179
+
180
+ 缺证据、缺复现、或门禁与裁定互相矛盾(如 TRUE POSITIVE 却带 FAIL 门禁)的 finding,
181
+ 会被**降级为「观察」**并以退出码 `10` 报出——不需要裁定者自觉,校验器替你卡住。
182
+
183
+ ```powershell
184
+ node <euthyna 仓库>/bin/euthyna.js gate <报告文件>
185
+ # 重跑每条复现命令核验(白名单工具、按 argv 执行不走 shell):
186
+ node <euthyna 仓库>/bin/euthyna.js gate <报告文件> --verify --cwd <被审计仓库>
187
+ ```
188
+
189
+ - 退出码 `0` = 全部通过;`10` = 有 finding 被降级;`2` = 报告无法读取 / 没有可校验的 finding。
190
+ - `--verify` 下复现命令跑不通同样构成降级——「说能复现」不算数,跑通才算。
191
+
192
+ **报告交出去之前,先过一遍 `euthyna gate`。** 它校验的是报告自己的自我声明,
193
+ 不测量目标仓库——因此它不能代替裁定,只能保证「拿不出证据的结论不会以已确证的面貌出去」。
194
+
195
+ ---
196
+
197
+ ## 报告必须落盘
198
+
199
+ - 只输出到聊天算**失败**。必须写报告文件
200
+ - 文件名:`<PROJECT>_EUTHYNA_AUDIT_<YYYY-MM-DD>.md`
201
+ - 写文件失败的降级顺序:当前工作目录 → 用户桌面 → 临时目录 → **最后手段**才输出到聊天并提示用户手动保存
202
+
203
+ ---
204
+
205
+ ## 交付前的自检(逐条打勾,缺一条不许说「审完了」)
206
+
207
+ - [ ] 每个疑似发现都走了**完整的**阶段 C,不是「看一眼觉得是假的」
208
+ - [ ] **diff 有删除行时跑过 `history`**;删自安全修复提交的代码已按最高风险处理
209
+ - [ ] 报告里每句「测过 / 没测过」都有 `coverage` 输出支撑,或已标为「不可评估」
210
+ - [ ] **退出码为 `2` 的判据没有出现在「干净」一栏**
211
+ - [ ] 每条结论都带 `file:line`
212
+ - [ ] 每条结论都标了三态之一(干净 / 标记 / 不可评估)
213
+ - [ ] 覆盖表写清了**哪些判据没评估、为什么**
214
+ - [ ] 恶魔代言人 13 问逐条答过(含防假阴性的第 12、13 问)
215
+ - [ ] 报告文件已落盘
216
+ - [ ] 报告已通过 `euthyna gate` 校验(退出码 `0`;有降级要读降级原因)
217
+ - [ ] 没有把「缺数据」写成「干净」
218
+
219
+ ---
220
+
221
+ ## 参考文件
222
+
223
+ 按需加载。**不要一次全读**——渐进披露是这套东西能在长会话里保持有效的原因。
224
+
225
+ | 文件 | 内容 | 什么时候读 |
226
+ |---|---|---|
227
+ | `references/verification-gates.md` | **阶段 C**:路由、6 门禁、误报清单、恶魔代言人、PoC 规则 | 判定一条已有断言时(**最常用**) |
228
+ | `references/change-audit.md` | **阶段 B**:基线、深度 vs 风险、来源归属、爆炸半径、对抗建模 | 审一次变更时 |
229
+ | `references/dependency-audit.md` | **阶段 A**:清单/锁文件、三态、文体规范、禁止事项 | 审依赖面时 |
230
+ | `references/bug-classes.md` | 9 类缺陷的专项检查与**默认假设方向** | 定了缺陷类别之后 |
231
+ | `references/meta-mechanisms.md` | 六个元机制:让流程不退化成走过场 | **审计开始时读一次** |
232
+ | `references/fact-producers.md` | **操作手册**:两个测量何时跑、命令怎么写、退出码怎么处理、真实输出样例、失效情形 | **跑产出器之前**(阶段 B / C) |
233
+ | `references/fact-contract.md` | 拿到事实之后能推出什么、**以及它不能推出什么** | 读完产出器输出之后 |
234
+
235
+ > **技能文本自包含**:以上文件都在 `references/` 内,不依赖技能目录之外的路径,
236
+ > 可以整目录复制到任何技能根使用。
237
+ >
238
+ > ⚠️ 但**产出器本身是独立程序,不在技能目录里**。复制技能不会把 CLI 一起带走。
239
+ > 新环境里要单独安装、或提供本项目的一份检出,否则相关判据只能标为「不可评估」——
240
+ > 那也是一个诚实的结果,比手工估算强。见 `references/fact-producers.md` 第一节。
241
+
242
+ ---
243
+
244
+ ## 与既有生态的分工(避免重复劳动)
245
+
246
+ | 已经有人做的事 | euthyna 怎么做 |
247
+ |---|---|
248
+ | 调用图 / 爆炸半径(`dsh-tool-lens`、`dsh-blast-radius`) | **不重造**,消费其输出 |
249
+ | 覆盖率(`dsh-code-coverage`) | 不重造,消费其输出 |
250
+ | 依赖漏洞(`dsh-dep-vuln-scan` 等) | 不重造,消费其输出 |
251
+ | 密钥扫描(`dsh-code-security`) | 不重造,消费其输出 |
252
+ | 交付门禁机制(`dsh-doublecheck`、`formalswarm`) | 机制可借鉴;euthyna 的门禁内容是**安全专属**的 6 门禁 |
253
+ | 证据生命周期与 PoC 实验室(`dsh-omv`) | 可并存:`dsh-omv` 可作为事实契约的**消费方** |
254
+
255
+ **euthyna 只补两件没人做的确定性测量**(见 `docs/positioning-zh.md` §7.2):
256
+ git 安全回归的机械判定,以及「符号 × 真实执行覆盖」的 join。
@@ -0,0 +1,130 @@
1
+ # 缺陷类别专项要求
2
+
3
+ > 判定一条结论时,**先定类别,再套对应的默认假设**。
4
+ >
5
+ > 因为不同类别下,「什么算可疑」和「什么算误报」是**相反的**。用同一套直觉判所有类别,
6
+ > 必然在某些类别上系统性出错。
7
+
8
+ ---
9
+
10
+ ## 0. 默认假设按类别翻转
11
+
12
+ 这是本文件最重要的一条,也是最反直觉的一条:
13
+
14
+ | 类别 | 默认假设的方向 |
15
+ |---|---|
16
+ | **内存破坏** | 默认**是误报**。在内存安全语言里几乎总是误报,除非有编译器缺陷或类型系统漏洞 |
17
+ | **逻辑缺陷** | 默认**不是误报**。「逻辑缺陷会通过每一个边界检查」——**不要让干净的静态分析说服你它是误报** |
18
+ | 其余类别 | 逐类见下 |
19
+
20
+ 把这两行搞反,就会在内存破坏上刷出一堆误报,同时在逻辑缺陷上漏掉真漏洞。
21
+
22
+ ---
23
+
24
+ ## 1. 内存破坏
25
+
26
+ **先做语言核查**:safe Rust、未使用 `unsafe.Pointer`/cgo 的 Go、以及所有托管语言
27
+ (Java / C# / Python / JS)里的内存破坏**几乎总是误报**。先过这一关,能省掉绝大部分工作。
28
+
29
+ 过了语言核查再查:
30
+
31
+ - 具体破坏了什么(栈 / 堆 / 全局 / 对象)
32
+ - 破坏的大小与偏移**是否攻击者可控**
33
+ - 是有用的原语(任意读、任意写、虚表覆写)还是仅仅崩溃
34
+ - 分配器加固是否在位
35
+ - UAF 的对象生命周期:释放后是否真能再被引用
36
+ - 类型混淆:要给出**类型不匹配的证明**,不是「看起来像」
37
+
38
+ ---
39
+
40
+ ## 2. 逻辑缺陷
41
+
42
+ **默认假设反过来:不要让「静态分析很干净」说服你这是误报。**
43
+
44
+ - 对照 **spec / RFC / 设计文档**判,不是只读代码
45
+ - 绘制**全部状态转换**——能否到达开发者未预料的状态?
46
+ - 找出**从未被强制的隐式假设**
47
+ - 认证类缺陷要验证**全部**认证与授权路径,不是主路径
48
+
49
+ ---
50
+
51
+ ## 3. 竞争条件
52
+
53
+ - 实际竞争窗口是**纳秒还是秒**?两者是完全不同的问题
54
+ - 攻击者能否**扩大窗口**(慢速文件系统、大分配、CPU 争用)
55
+ - 核验线程模型:单线程 / 事件循环 / 多线程,结论完全不同
56
+ - 检查全部同步原语,而不是只看有没有锁
57
+ - 文件系统类要看**符号链接竞争**(TOCTOU 的经典形态)
58
+
59
+ ---
60
+
61
+ ## 4. 整数问题
62
+
63
+ - 每一点的**精确类型与取值范围**,不是「大概是个整数」
64
+ - 有符号溢出(C/C++ 里是未定义行为)vs 无符号回绕(**有定义**)——不能混为一谈
65
+ - 追踪所有 cast / conversion / promotion
66
+ - **结果是否真的用于危险用途**:分配大小、数组下标、循环边界。用于日志打印的溢出不是漏洞
67
+ - 开启 `-Wconversion`、`-Wsign-compare` 这类检查,让编译器替你找
68
+
69
+ ---
70
+
71
+ ## 5. 加密弱点
72
+
73
+ - 对照 NIST / IETF 标准与已知攻击检查参数
74
+ - 核验随机源:是不是密码学安全的
75
+ - **nonce 重用要证明实践中确实能发生两次**,不是「理论上可能」
76
+ - 时序侧信道要看两点:攻击者能否触达,以及网络抖动是否让远程攻击不切实际
77
+ - 与参考实现的测试向量对比
78
+
79
+ ---
80
+
81
+ ## 6. 注入
82
+
83
+ - 追攻击者输入从入口到 sink 的**完整路径**,沿途是否净化、转义
84
+ - 框架的自动转义是否启用,**且未被绕过**
85
+ - XSS 要看上下文:HTML 正文 / 属性 / JS / URL,**各需不同的转义**,一种转义不能覆盖全部
86
+ - 路径穿越要看:访问检查**之前**是否已经规范化
87
+ - 用**真实 payload** 走完所有中间处理,不要在手写的示例里验证
88
+
89
+ ---
90
+
91
+ ## 7. 信息泄露
92
+
93
+ - **具体泄露什么**:栈泄露(能拿 ASLR 基址或 canary)是严重的;静态字符串毫无价值
94
+ - 泄露的数据对进一步利用**是否真的有用**
95
+ - 未初始化内存要**证明读点确实未初始化**
96
+ - 时序侧信道看测量精度与噪声水平
97
+ - 错误信息**是否真能到达攻击者**——写进日志不算
98
+
99
+ ---
100
+
101
+ ## 8. 拒绝服务
102
+
103
+ - 资源消耗比与放大倍数**是否有意义**
104
+ - 资源可回收还是永久耗尽——两者严重性差别很大
105
+ - **复杂度主张要证明实际最坏输入能触发**,不要只声称 O(n²)。
106
+ 要指出具体输入、给出实测耗时
107
+ - 崩溃是否**可靠**触发
108
+ - 服务是否自动重启:100 毫秒重启和需要人工介入是两个完全不同的问题
109
+
110
+ ---
111
+
112
+ ## 9. 反序列化
113
+
114
+ - 攻击者**是否确实控制**到达反序列化点的数据
115
+ - classpath / import 图里**有没有可用的 gadget 链**——
116
+ **没有 gadget 链,不安全反序列化只是设计异味,不是可利用缺陷**
117
+ - 库与版本,以及该版本已知的链
118
+ - 类型限制 / allowlist 过滤器是否在位
119
+ - 语言特定形态:Java `ObjectInputStream`、Python `pickle`、PHP `unserialize`、.NET `BinaryFormatter`
120
+
121
+ ---
122
+
123
+ ## 用法
124
+
125
+ 1. 在阶段 C 的**第零步**就给断言定类别
126
+ 2. 类别决定默认假设的方向(见 §0)
127
+ 3. 走数据流与可利用性分析时,**叠加**该类别的专项检查(上表)
128
+ 4. 门禁评估时,该类别特有的「什么算阻止」也按上面的定义判
129
+
130
+ > 类别定错,后面全错。**不确定时明说不确定,不要挑一个像的凑。**
@@ -0,0 +1,219 @@
1
+ # 阶段 B:变更面审计
2
+
3
+ > 审一次变更(PR / commit / diff),回答一个问题:**这次改动有没有让系统变弱?**
4
+ >
5
+ > 前置:`../SKILL.md` 的三条不可跳过规则。
6
+
7
+ ---
8
+
9
+ ## B.0 先建立基线
10
+
11
+ **这是第一动作,不是准备工作。** 没有基线,后面所有判断都只能靠感觉。
12
+
13
+ 在**变更前**的版本上,把它们记下来:
14
+
15
+ | 要记的 | 为什么 |
16
+ |---|---|
17
+ | 系统级不变量 | 「什么必须永远为真」——不知道就没法判断这次有没有破坏它 |
18
+ | 信任边界与权限级 | 哪些数据可信、哪些不可信;代码以什么身份运行 |
19
+ | 既有的校验模式 | 这个代码库惯用的校验长什么样,在哪里做 |
20
+ | 关键函数的调用关系 | 改动的影响半径靠它判断 |
21
+ | 状态流 | 数据从哪来、经过什么、到哪去 |
22
+ | **代码本应做什么** | 不是它现在做什么——是它的设计意图 |
23
+
24
+ 漏掉最后一项,就会把「实现与设计不符」误判成「实现有 bug」,或者反过来。
25
+
26
+ ---
27
+
28
+ ## B.1 分清两件事:分析深度 vs 风险等级
29
+
30
+ 这两件事经常被混为一谈,混了就必然出错。
31
+
32
+ **分析深度**由**改动规模**决定——它只决定你花多少力气:
33
+
34
+ | 改动规模 | 策略 | 做法 |
35
+ |---|---|---|
36
+ | < 20 个文件 | 深读 | 读完全部相关代码,全量 blame |
37
+ | 20–200 | 聚焦 | 只追一跳依赖,优先高风险文件 |
38
+ | 200+ | 外科 | 只做关键路径 |
39
+
40
+ **风险等级**由**改了什么**决定——它跟规模**无关**:
41
+
42
+ | 级别 | 触发条件 |
43
+ |---|---|
44
+ | 高 | 认证、加密、外部调用、资金流转、**任何校验被删除** |
45
+ | 中 | 业务逻辑、状态变更、新增公开接口 |
46
+ | 低 | 注释、测试、界面、日志 |
47
+
48
+ > **一行改动可以是最高风险,两千行可以是零风险。** 按规模判断风险是最常见的偷懒。
49
+
50
+ ---
51
+
52
+ ## B.2 逐文件读两个版本
53
+
54
+ 对每个变更文件,不要只看 diff。**把改动前后的版本并排读**,然后逐段回答四个问题:
55
+
56
+ | 问题 | 要说什么 |
57
+ |---|---|
58
+ | 改之前是什么 | 原逻辑 |
59
+ | 改之后是什么 | 新逻辑 |
60
+ | **改动做了什么** | 一句话概括意图 |
61
+ | **安全上意味着什么** | 攻击面变大还是变小?校验变强还是变弱? |
62
+
63
+ diff 只显示「哪些行变了」,不显示「行为怎么变了」。删掉一行可能什么都没删(比如那行本来就是死代码),
64
+ 加一行可能把整个执行顺序改了。
65
+
66
+ ### 被删掉的代码要额外查来源
67
+
68
+ **这是本阶段最容易被跳过、也最有价值的一步。**
69
+
70
+ 对每一段被删的代码,用本项目的产出器查它的来源:
71
+
72
+ ```powershell
73
+ node <euthyna 仓库>/bin/euthyna.js history --base <改前版本> --repo <被审计的仓库>
74
+ ```
75
+
76
+ ⚠️ **不能写成 `node bin/euthyna.js`**——审计时当前目录是**被审计的项目**,不是 euthyna 仓库,
77
+ 相对路径会报 `Cannot find module`。`--repo` 指的是**被审计的仓库**,`--base` 相对它解析。
78
+
79
+ 它会告诉你:这段被删的代码是哪个提交引入的、那个提交的信息里有没有安全关键词。
80
+ **从 `fix` / `CVE` / `security` 提交里删掉的代码,风险默认拉到最高**,除非能证明它是多余的。
81
+
82
+ 不要自己跑 `git blame` 再肉眼读提交信息——那正是本产出器存在的原因,
83
+ 机器判定同一个 diff 跑两次结果一致,人读两次可能不一致。
84
+
85
+ 读输出时注意「⚠ 未评估的判据」一节:它非空时,这次运行**不能**读成「查过了没问题」。
86
+ 退出码 `2` 表示完全无法测量,**不得当作干净**。命令与退出码详见 `fact-producers.md`。
87
+
88
+ ### 三条红线
89
+
90
+ | 现象 | 默认处理 |
91
+ |---|---|
92
+ | 被删的代码来自安全修复提交 | 按**最高**风险处理,除非证明它已无必要 |
93
+ | 校验被删除且没有等价替代 | 按**最高**风险处理 |
94
+ | 代码曾经被加进来 → 因安全原因移除 → **现在又被加回来** | 按**回归**处理 |
95
+
96
+ 第三条用产出器可以机械检出:
97
+
98
+ ```powershell
99
+ node <euthyna 仓库>/bin/euthyna.js history --base <改前版本> --pickaxe --repo <被审计的仓库>
100
+ ```
101
+
102
+ ---
103
+
104
+ ## B.3 测试覆盖:缺口要升级风险,不是记录一下
105
+
106
+ **没有测试不是「顺便提一句」的事,它是风险升级的理由。**
107
+
108
+ | 情况 | 处理 |
109
+ |---|---|
110
+ | 新增函数且无测试 | 中风险升**高** |
111
+ | 修改了校验且测试没跟着变 | **高风险** |
112
+ | 复杂逻辑(> 20 行)且无测试 | **高风险** |
113
+
114
+ 想说出「已覆盖」,必须拿真实执行证据,不能靠「这个文件有测试文件」推断:
115
+
116
+ ```powershell
117
+ node <euthyna 仓库>/bin/euthyna.js coverage --coverage <覆盖率文件绝对路径> --symbol <被改的函数>
118
+ ```
119
+
120
+ 没有覆盖率文件时:**要么用项目自己的方式跑一次测试产出它,要么把该判据标为「不可评估」**——
121
+ `coverage` 会以退出码 `2` 明确告诉你「没能测量」,那不是「没被覆盖」。
122
+
123
+ > ⚠️ 这个产出器**只能证伪,不能证实**。它说「没被调用过」是可信的;
124
+ > 它说「被调用了 N 次」**不能**推出「相关调用点被执行过」。
125
+ > 详见 `fact-contract.md`。
126
+
127
+ ---
128
+
129
+ ## B.4 爆炸半径
130
+
131
+ 统计每个被修改的函数有多少调用方:
132
+
133
+ | 调用方数量 | 等级 |
134
+ |---|---|
135
+ | 1–5 | 低 |
136
+ | 6–20 | 中 |
137
+ | 21–50 | 高 |
138
+ | 50+ | 极高 |
139
+
140
+ **优先级由「风险 × 半径」共同决定**:
141
+
142
+ | | 半径极高 | 半径高/中 | 半径低 |
143
+ |---|---|---|---|
144
+ | **风险高** | P0——深度分析 + 全部依赖 | P1 | P2 |
145
+ | 风险中/低 | P1 | P2 | P3 |
146
+
147
+ 数字要由工具产出,不要目测。可选的上游产出器见 `../SKILL.md` 的生态分工一节。
148
+
149
+ ---
150
+
151
+ ## B.5 高风险变更要做对抗建模
152
+
153
+ 对每个高风险改动,回答:
154
+
155
+ **1. 攻击者是谁**
156
+
157
+ | 维度 | 取值 |
158
+ |---|---|
159
+ | WHO | 未认证外部用户 / 已认证普通用户 / 恶意管理员 / 被攻陷的依赖 / 抢跑者 |
160
+ | WHAT | 只能用公开接口 / 需要已认证角色 / 需要特定权限 |
161
+ | WHERE | 具体端点或函数 |
162
+
163
+ **2. 攻击路径**:入口点 → 具体调用与参数 → 如何到达脆弱代码 → 代码里发生了什么 → 达成什么。
164
+ **必须给出可达性证明**:那个函数确实是公开可达的、攻击者确实有这个权限、路径确实经过真实接口。
165
+
166
+ **3. 可利用性三档**
167
+
168
+ | 档 | 条件 |
169
+ |---|---|
170
+ | 易 | 公开接口、无需特殊权限、单次调用 |
171
+ | 中 | 需要特定条件或提升的权限、多步骤 |
172
+ | 难 | 需要特权或罕见条件、需要大量资源 |
173
+
174
+ **4. 完整利用场景**:起始位置 → 逐步(命令与参数 / 为什么有效 / 系统状态如何变化)→ **具体可测的影响**。
175
+
176
+ > 影响**禁止**写「可能导致问题」。必须落到确切的数据、具体的权限、确切的量。
177
+ > 写不出来,通常说明你还没追到影响。
178
+
179
+ **5. 与基线交叉比对**
180
+
181
+ - 是否违反了 B.0 记下的系统级不变量?
182
+ - 是否跨越或破坏了信任边界?
183
+ - 是否绕过了这个代码库惯用的校验模式?
184
+ - 是否是此前已修复问题的回归?
185
+
186
+ ---
187
+
188
+ ## B.6 报告
189
+
190
+ **只输出到聊天算失败。** 必须落盘,否则发现会丢失。
191
+
192
+ 报告至少覆盖:
193
+
194
+ | 章节 | 内容 |
195
+ |---|---|
196
+ | 结论摘要 | 严重性分布 + 总体判断 + 关键指标 |
197
+ | 变更概览 | 改了什么、多大、涉及哪些面 |
198
+ | 发现 | 每条含位置、来源提交、爆炸半径、测试覆盖、历史上下文、攻击场景 |
199
+ | 测试覆盖分析 | 缺口在哪、为什么算缺口 |
200
+ | 爆炸半径分析 | 数字与分级 |
201
+ | 历史上下文 | 删掉的代码来自哪里、是不是安全修复、有没有回归 |
202
+ | 建议 | 分三级:立即阻断 / 上生产前 / 技术债 |
203
+ | 分析方法 | 用了什么策略、**局限是什么**、置信度 |
204
+ | 附录 | 证据 |
205
+
206
+ > **「局限」一节不是走过场。** 没测什么、为什么没测,必须写清楚——
207
+ > 读者据此判断这份报告的结论能覆盖到哪。
208
+
209
+ **不适用的场景**(明说,不要硬套):全新代码(无基线可比)、纯文档改动、格式化/lint、
210
+ 提问者明确只要快速摘要并接受风险。
211
+
212
+ ---
213
+
214
+ ## B.7 与阶段 C 的衔接
215
+
216
+ **本阶段报出的每一条疑似发现,都必须逐个走阶段 C 验证,不许直接当结论写进报告。**
217
+
218
+ 阶段 B 产出的是「疑点」,阶段 C 才产出「裁定」。
219
+ 把疑点当结论,就是本项目要解决的那个问题本身。