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.
- package/.agents/skills/euthyna/SKILL.md +256 -0
- package/.agents/skills/euthyna/references/bug-classes.md +130 -0
- package/.agents/skills/euthyna/references/change-audit.md +219 -0
- package/.agents/skills/euthyna/references/dependency-audit.md +116 -0
- package/.agents/skills/euthyna/references/fact-contract.md +129 -0
- package/.agents/skills/euthyna/references/fact-producers.md +274 -0
- package/.agents/skills/euthyna/references/meta-mechanisms.md +173 -0
- package/.agents/skills/euthyna/references/verification-gates.md +293 -0
- package/LICENSE +202 -202
- package/README.md +1 -1
- package/cordis.patch.yml +4 -0
- package/package.json +23 -3
- package/plugin/index.js +60 -0
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
# 阶段 A:依赖面审计
|
|
2
|
+
|
|
3
|
+
> 审的是「你依赖的东西」,不是「你自己的代码」。回答:**这次引入/升级的依赖,带来了什么风险?**
|
|
4
|
+
>
|
|
5
|
+
> 前置:`../SKILL.md` 的三条不可跳过规则。**本阶段从不读依赖的源码。**
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## A.0 这个阶段做什么、不做什么
|
|
10
|
+
|
|
11
|
+
| 做 | 不做 |
|
|
12
|
+
|---|---|
|
|
13
|
+
| 确认清单文件与锁文件、解析精确版本 | 判断项目能不能安装、能不能构建(**从不安装、从不构建、从不执行**) |
|
|
14
|
+
| 用确定性工具取漏洞、陈旧度、维护状态 | 许可合规审计 |
|
|
15
|
+
| 把工具结果翻译成判断 | 扫目标自身源码的漏洞或密钥(那是阶段 B / 阶段 C) |
|
|
16
|
+
| 说清哪些判据**不可评估** | 对采集器不支持的生态即兴发挥(说「不支持」,不要硬凑) |
|
|
17
|
+
|
|
18
|
+
**本阶段只读 registry、公告与仓库元数据。**
|
|
19
|
+
|
|
20
|
+
---
|
|
21
|
+
|
|
22
|
+
## A.1 为什么不自己造测量工具
|
|
23
|
+
|
|
24
|
+
本框架**刻意不重写**依赖扫描。原因不是懒:
|
|
25
|
+
|
|
26
|
+
1. 已有成熟工具覆盖了这件事,重写只会引入新风险,而测量逻辑本身必须是确定性的
|
|
27
|
+
2. 漏洞数据源(OSV 等)是持续更新的,自建副本必然过期
|
|
28
|
+
3. **这个阶段的价值在判断,不在测量**
|
|
29
|
+
|
|
30
|
+
所以本阶段的产出器是**编排层**:调用现成工具、把它们的输出归一成事实、标注哪些没测。
|
|
31
|
+
|
|
32
|
+
可用的发现源(装哪个用哪个,**装前先审计源码**):
|
|
33
|
+
|
|
34
|
+
| 用途 | 候选 |
|
|
35
|
+
|---|---|
|
|
36
|
+
| 依赖漏洞(含修复版本) | 任何基于 OSV 的扫描器 |
|
|
37
|
+
| 锁文件与清单解析 | 工具自带,或 `npm ls` / `pnpm why` 这类包管理器命令 |
|
|
38
|
+
| 引入时间(哪个提交加进来的) | `git log --oneline --follow -- <锁文件>` |
|
|
39
|
+
|
|
40
|
+
> ⚠️ **装载检测与降级**:这些工具**一个都没装**也要能跑完流程。
|
|
41
|
+
> 降级时如实标注「本判据未评估」,**不要**用模型记忆里的 CVE 数据顶上——
|
|
42
|
+
> 那正是本框架禁止的事。
|
|
43
|
+
|
|
44
|
+
---
|
|
45
|
+
|
|
46
|
+
## A.2 第一步:确认输入
|
|
47
|
+
|
|
48
|
+
**目标目录必须有** `package.json` / `pyproject.toml` / `requirements*.txt` / `go.mod` 之一。
|
|
49
|
+
没有就**直接说没有并停止**,不要为一个采集器不解析的生态即兴审计。
|
|
50
|
+
|
|
51
|
+
**能精确解析版本的**:`package-lock.json`、`npm-shrinkwrap.json`、`uv.lock`、Go 1.17+ 的 `go.mod`。
|
|
52
|
+
|
|
53
|
+
**不能的**:`yarn.lock`、`pnpm-lock.yaml`、`poetry.lock`。
|
|
54
|
+
存在这些时要**明说**,版本回退到 pin 或最新发布版,并标注这是近似。
|
|
55
|
+
|
|
56
|
+
**检查网络身份**:未认证的 GitHub API 只有 60 次/小时。采集器每个依赖要发若干请求,
|
|
57
|
+
未认证时仓库类判据会大面积不可评估——**说出来,而不是悄悄降级或重试到死**。
|
|
58
|
+
|
|
59
|
+
---
|
|
60
|
+
|
|
61
|
+
## A.3 三态,不是两态
|
|
62
|
+
|
|
63
|
+
每个依赖、每条判据,只能落到三种状态之一:
|
|
64
|
+
|
|
65
|
+
| 状态 | 含义 |
|
|
66
|
+
|---|---|
|
|
67
|
+
| 已评估 · 干净 | 确实查过,且在这个判据下没问题 |
|
|
68
|
+
| 已评估 · 标记 | 确实查过,有问题 |
|
|
69
|
+
| **因某原因不可评估** | 没查成——**这一项必须写明原因** |
|
|
70
|
+
|
|
71
|
+
**缺失的测量绝不是干净的结论。**
|
|
72
|
+
|
|
73
|
+
采集器非零退出时,它是在**拒绝报告**——**逐字转述它的消息,不要重试、不要绕过**。
|
|
74
|
+
一个什么都没测到的运行,产出的是「不可评估」,**不是**「没发现问题」。
|
|
75
|
+
|
|
76
|
+
### 引用纪律
|
|
77
|
+
|
|
78
|
+
- 「无公告」只意味着「在**被评估的范围内**没有」——引用干净结论之前先看覆盖表
|
|
79
|
+
- **数字必须逐字引用**,不得重新推导、四舍五入或修饰
|
|
80
|
+
- **没发现不等于背书**
|
|
81
|
+
|
|
82
|
+
---
|
|
83
|
+
|
|
84
|
+
## A.4 模型能加、采集器加不了的部分
|
|
85
|
+
|
|
86
|
+
测量归工具,判断归模型。以下是模型该加的价值:
|
|
87
|
+
|
|
88
|
+
| 加什么 | 注意 |
|
|
89
|
+
|---|---|
|
|
90
|
+
| 这个读者**该先动哪个** | 按风险排序,不要按字母序 |
|
|
91
|
+
| 升级路径 | 补丁版还是大版本?大版本意味着破坏性变更 |
|
|
92
|
+
| 废弃依赖的替代候选 | **命名前先验证候选在 registry 里确实存在**,并标注这是判断而非测量 |
|
|
93
|
+
| 安装脚本风险 | 被标记的安装脚本能否用 `--ignore-scripts` 规避 |
|
|
94
|
+
|
|
95
|
+
---
|
|
96
|
+
|
|
97
|
+
## A.5 报告文体
|
|
98
|
+
|
|
99
|
+
本阶段的报告常被**逐字粘进工单**,所以:
|
|
100
|
+
|
|
101
|
+
- 无人称、无缩略、无感叹号
|
|
102
|
+
- 主动语态且**以事为主体**:写「升级到 1.19.0 清掉全部 25 条公告」,不写「建议升级 axios」
|
|
103
|
+
- 不用强化词(very / significant / fortunately)
|
|
104
|
+
- **不做动机猜测**
|
|
105
|
+
- 时态:审计动作用过去时,依赖状态用现在时,后果用将来时
|
|
106
|
+
- 建议只说**动作与代价**,**不指认责任方**
|
|
107
|
+
|
|
108
|
+
---
|
|
109
|
+
|
|
110
|
+
## A.6 与阶段 C 的衔接
|
|
111
|
+
|
|
112
|
+
依赖审计报出的问题通常**不需要**走阶段 C——它们的证据来自漏洞数据库,本身就是确定的。
|
|
113
|
+
|
|
114
|
+
但有一类例外:**「这个漏洞在本项目里是否真的可达」**。
|
|
115
|
+
如果某个依赖漏洞的受影响函数在你这里根本没有调用路径,那它是一个**需要判定的断言**,
|
|
116
|
+
不是既成事实。遇到这种,走阶段 C。
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
# 事实契约(判定层消费指南)
|
|
2
|
+
|
|
3
|
+
> 这是一份**消费指南**,不是设计文档。它回答:拿到一条事实之后,判定层能从中推出什么、不能推出什么。
|
|
4
|
+
>
|
|
5
|
+
> 完整契约见仓库的 `docs/fact-contract-zh.md`。
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## 一条事实长什么样
|
|
10
|
+
|
|
11
|
+
```jsonc
|
|
12
|
+
{
|
|
13
|
+
"id": "fact-007",
|
|
14
|
+
"kind": "history", // 事实类别
|
|
15
|
+
"statement": "本次变更删除了 4 行来自提交 ab877f9d70 的代码……",
|
|
16
|
+
"status": "established", // established | refuted | unknown
|
|
17
|
+
"evidence": { "file": "src/a.js", "line": 104, "commit": "ab877f9d70" },
|
|
18
|
+
"method": "command",
|
|
19
|
+
"command": "git blame --porcelain -L 104,104 <rev> -- src/a.js",
|
|
20
|
+
"confidence": "exact", // exact | approximate
|
|
21
|
+
"detail": { } // 类别特有的结构化载荷
|
|
22
|
+
}
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
---
|
|
26
|
+
|
|
27
|
+
## 三条消费规则
|
|
28
|
+
|
|
29
|
+
### 规则 1:`unknown` 不是「干净」
|
|
30
|
+
|
|
31
|
+
三态是硬约定:
|
|
32
|
+
|
|
33
|
+
| status | 判定层能做什么 |
|
|
34
|
+
|---|---|
|
|
35
|
+
| `established` | 可以据此下结论 |
|
|
36
|
+
| `refuted` | 可以据此下结论(反向) |
|
|
37
|
+
| **`unknown`** | **什么都不能推**。它既不是支持也不是反对,是「不知道」 |
|
|
38
|
+
|
|
39
|
+
把 `unknown` 当成「没问题」,是这套契约唯一会致命的使用错误。
|
|
40
|
+
|
|
41
|
+
### 规则 2:`approximate` 不得参与门禁
|
|
42
|
+
|
|
43
|
+
`confidence: approximate` 的事实**明确被排除在门禁判定之外**。
|
|
44
|
+
|
|
45
|
+
这条不是保守,是来自实践:有工具在「无法确定作用域」时会合成一个近似结果,
|
|
46
|
+
并明确标注它**即使在强制模式下也不参与阻断**。本框架沿用同一约定。
|
|
47
|
+
|
|
48
|
+
门禁只吃 `exact`。
|
|
49
|
+
|
|
50
|
+
### 规则 3:缺数据先看覆盖表
|
|
51
|
+
|
|
52
|
+
报告里的 `coverage.notEvaluated` 列了**没测什么、为什么**。
|
|
53
|
+
|
|
54
|
+
**引用任何「干净」的结论之前,先读这一节。**
|
|
55
|
+
`notEvaluated` 非空时,不要把它读成「测过了没问题」。
|
|
56
|
+
|
|
57
|
+
---
|
|
58
|
+
|
|
59
|
+
## 事实里**没有**什么(这是设计,不是遗漏)
|
|
60
|
+
|
|
61
|
+
| 不存在 | 为什么 |
|
|
62
|
+
|---|---|
|
|
63
|
+
| 分数(score / 0–100) | 分数不可复算,会变成「拿分数当门禁」 |
|
|
64
|
+
| 严重性等级 | 严重性是**判断**,不是事实 |
|
|
65
|
+
| 修复建议 | 同上 |
|
|
66
|
+
| 「可能」「大概率」 | 模糊词会掩盖缺失 |
|
|
67
|
+
|
|
68
|
+
**测量的归测量,判断的归判断。** 一条事实如果带着 `severity: high`,
|
|
69
|
+
说明产出它的那一层已经替判定层做了决定,而那个决定是不可复算的。
|
|
70
|
+
|
|
71
|
+
---
|
|
72
|
+
|
|
73
|
+
## 各类事实的读法
|
|
74
|
+
|
|
75
|
+
### `history` —— 被删代码的来源
|
|
76
|
+
|
|
77
|
+
| 字段 | 读法 |
|
|
78
|
+
|---|---|
|
|
79
|
+
| `detail.classification` | `security` / `fix` / `none`。**分类器的两种错误代价不对称**:漏掉安全提交比多报更危险,所以它偏向宽 |
|
|
80
|
+
| `detail.byFile` | **每个文件各有多少行**。`blamedLines` 是总数,可能跨多个文件——不要以为证据里那个文件包含全部 |
|
|
81
|
+
| `detail.commitSubject` | 提交信息原文。**不要只看分类,读一眼信息本身** |
|
|
82
|
+
| `command` | 可直接执行的复现命令,含真实行区间 |
|
|
83
|
+
|
|
84
|
+
**注意**:`history` 只说「这行来自哪个提交、那个提交像不像安全修复」,
|
|
85
|
+
**不说**「删掉它就是个漏洞」。后者是判定层的事。
|
|
86
|
+
|
|
87
|
+
### `reintroduction` —— 被移除又加回
|
|
88
|
+
|
|
89
|
+
`detail.securityOrigin` 非 `null` 表示:该行的出现次数曾被一个信息里含安全关键词的提交改变过。
|
|
90
|
+
配合「该行在 base 版本中不存在」,可推出**它曾被移除、现在被加回**。
|
|
91
|
+
|
|
92
|
+
**这不等于「这是个回归」**——它等于「这里需要人看一眼」。判定层据此决定要不要拉高优先级。
|
|
93
|
+
|
|
94
|
+
### `test_coverage` —— 调用计数
|
|
95
|
+
|
|
96
|
+
⚠️ **这个类别只能证伪,不能证实。**
|
|
97
|
+
|
|
98
|
+
| 事实 | 能推出 |
|
|
99
|
+
|---|---|
|
|
100
|
+
| 调用计数 = 0(`status: established`) | ✅ **确实没被调用过**。所有依赖它的行为都没被执行验证 |
|
|
101
|
+
| 调用计数 > 0(`status: unknown`) | ❌ **不能**推出「执行过」。进入函数不等于任何特定调用点被执行 |
|
|
102
|
+
|
|
103
|
+
原因:底层覆盖率数据会把**从未执行的调用点报成已覆盖**(抛出异常后的直线代码就是典型),
|
|
104
|
+
所以「已覆盖」这个方向本身不可靠。**产出器里根本没有「已执行」这个输出**,这是刻意的。
|
|
105
|
+
|
|
106
|
+
推论:**不要**用「计数非零」去关闭一个「这里没测试」的担忧。要证明确实执行过,
|
|
107
|
+
只能靠能真正触达该调用点的演示。
|
|
108
|
+
|
|
109
|
+
### `artifact` —— 产物证据
|
|
110
|
+
|
|
111
|
+
报告文件、演示运行的退出码与用例数。**门禁 1 与门禁 4 靠它。**
|
|
112
|
+
|
|
113
|
+
读法:退出码与用例数必须是**真实捕获的输出**,不是预期值。
|
|
114
|
+
用例数为 0 的「通过」不是通过。
|
|
115
|
+
|
|
116
|
+
---
|
|
117
|
+
|
|
118
|
+
## 产出器的退出码
|
|
119
|
+
|
|
120
|
+
配套 CLI 的退出码是对外契约,可直接用于 CI:
|
|
121
|
+
|
|
122
|
+
| 码 | 含义 | 判定层该怎么反应 |
|
|
123
|
+
|---|---|---|
|
|
124
|
+
| `0` | 已测量,无安全相关发现 | 正常继续 |
|
|
125
|
+
| `10` | 已测量,且存在 `security` 分类的事实 | **提高优先级**,不要直接当结论 |
|
|
126
|
+
| `1` | 用法错误 | 修命令 |
|
|
127
|
+
| **`2`** | **完全无法测量** | **不得当作干净**。要么换测量方式,要么把相关判据标为不可评估 |
|
|
128
|
+
|
|
129
|
+
`2` 与 `0` 的区别是这份契约存在意义的缩影:**没能测量,和测了没问题,是两回事。**
|
|
@@ -0,0 +1,274 @@
|
|
|
1
|
+
# 事实产出器:什么时候跑、怎么跑、输出怎么读
|
|
2
|
+
|
|
3
|
+
> 本技能自带两个**确定性测量**,由一个零依赖 Node CLI 提供。
|
|
4
|
+
> 它们回答的是模型算不准的问题,**不是**「有哪些漏洞」。
|
|
5
|
+
>
|
|
6
|
+
> 这一页是操作手册:什么情况下必须跑、命令怎么写、输出里哪一节决定你能下什么结论。
|
|
7
|
+
> 拿到事实之后「能推出什么、不能推出什么」,看 `fact-contract.md`。
|
|
8
|
+
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
## 一、先解决「命令在哪」
|
|
12
|
+
|
|
13
|
+
CLI **不在技能目录里**,它是本项目的独立程序。按可用性从高到低:
|
|
14
|
+
|
|
15
|
+
| 情况 | 怎么写 | 备注 |
|
|
16
|
+
|---|---|---|
|
|
17
|
+
| 已安装 | `euthyna history …` | `euthyna` 是包声明的命令名 |
|
|
18
|
+
| 有本项目检出 | `node <euthyna 仓库>/bin/euthyna.js history …` | 最常用的形式 |
|
|
19
|
+
| 两者都没有 | —— | **把判据标为「不可评估」**,不要手工估算顶上 |
|
|
20
|
+
|
|
21
|
+
### ⚠️ 一个真实的坑:相对路径只在 euthyna 仓库里成立
|
|
22
|
+
|
|
23
|
+
`node bin/euthyna.js …` 这种写法**只在当前目录恰好是 euthyna 仓库时**能跑。
|
|
24
|
+
审计时你的当前目录是**被审计的项目**,于是:
|
|
25
|
+
|
|
26
|
+
```
|
|
27
|
+
Error: Cannot find module 'D:\axe-core\axe-core\bin\euthyna.js'
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
`history` 有 `--repo`,把仓库路径显式传进去,从任何目录都能跑:
|
|
31
|
+
|
|
32
|
+
```powershell
|
|
33
|
+
node <euthyna 仓库>/bin/euthyna.js history --base <rev> --repo <被审计的仓库>
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
- `--repo` 指的是**被审计的仓库**,不是 euthyna 的路径
|
|
37
|
+
- `--base` / `--head` 都相对**被审计的仓库**解析,与当前目录无关
|
|
38
|
+
- `--coverage` 没有 `--repo`,把覆盖率文件写成**绝对路径**即可
|
|
39
|
+
|
|
40
|
+
两条命令都已验证:从 `D:\` 运行、`--repo` 指向另一个仓库,退出码与预期一致。
|
|
41
|
+
|
|
42
|
+
---
|
|
43
|
+
|
|
44
|
+
## 二、`history` —— 被删代码的来源
|
|
45
|
+
|
|
46
|
+
### 什么时候必须跑
|
|
47
|
+
|
|
48
|
+
| 触发场景 | 对应阶段 |
|
|
49
|
+
|---|---|
|
|
50
|
+
| 审查一次变更(PR / diff / commit),且**有代码被删除** | 阶段 B |
|
|
51
|
+
| 用户问「这次改动有没有把某个防护改弱」 | 阶段 B |
|
|
52
|
+
| 需要判断某段代码是不是「曾被移除又加回来」 | 阶段 B |
|
|
53
|
+
|
|
54
|
+
**只要 diff 里有删除行,就先跑它。** 这不是可选步骤:一个校验被删掉而看着「很干净」的 diff,
|
|
55
|
+
正是安全回归最常见的样子,而模型无法对几百行被删代码逐行做 blame 再回溯提交历史。
|
|
56
|
+
|
|
57
|
+
### 命令
|
|
58
|
+
|
|
59
|
+
```powershell
|
|
60
|
+
# 基础:本次变更删掉的代码来自哪些提交、那些提交像不像安全修复
|
|
61
|
+
node <euthyna 仓库>/bin/euthyna.js history --base <改前版本> --repo <被审计的仓库>
|
|
62
|
+
|
|
63
|
+
# 加测「曾被移除又加回来」的行(有探针上限,比基础版慢)
|
|
64
|
+
node <euthyna 仓库>/bin/euthyna.js history --base <改前版本> --pickaxe --repo <被审计的仓库>
|
|
65
|
+
|
|
66
|
+
# 把删除行归属到「最初引入」该内容的提交(而非 blame 的「最后修改者」,有探针上限)
|
|
67
|
+
node <euthyna 仓库>/bin/euthyna.js history --base <改前版本> --origins --repo <被审计的仓库>
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
`--base` 是**改前版本**(PR 的分叉点),`--head` 默认 `HEAD`。
|
|
71
|
+
分叉点不确定时用 `git merge-base <目标分支> HEAD` 的结果。
|
|
72
|
+
|
|
73
|
+
### 归属语义(两个固有近似,已确证≠精确)
|
|
74
|
+
|
|
75
|
+
`history` 的归属和分类各有一个**写死的近似**,读输出前先知道它们:
|
|
76
|
+
|
|
77
|
+
1. **归属:blame 是「最后修改」,不是「最初引入」。** 一条安全修复行如果之后被
|
|
78
|
+
格式化/重构碰过(移动、复制),blame 会把来源归到最后碰它的那个提交。
|
|
79
|
+
`--origins` 用 `git log -S <行内容>` 定位**最初引入**该内容的提交;两者不一致时
|
|
80
|
+
事实会同时写出两个提交(`detail.blameCommit` = 最后修改、`evidence.commit` =
|
|
81
|
+
最初引入),并说明「最初引入」。
|
|
82
|
+
`--origins` 只对长度 ≥ 12 字符的行做追溯(太通用的行,-S 会指向整个历史的
|
|
83
|
+
第一个提交,那不是来源);未启用时输出会在「未评估的判据」里写明这一点。
|
|
84
|
+
2. **分类:先看提交信息,再看提交 diff,再看被删行本身。** 消息含糊
|
|
85
|
+
("update utils")但 diff 动了危险 API、或被删行本身就是安全代码(如
|
|
86
|
+
`if (!authorized) return`)时,都会标 `security`——依据写在
|
|
87
|
+
`detail.classificationBasis`(`message` / `diff` / `deleted-line`)。
|
|
88
|
+
分类偏向宽是有意的:漏掉安全提交比多报更危险。
|
|
89
|
+
|
|
90
|
+
### 输出长什么样(真实捕获)
|
|
91
|
+
|
|
92
|
+
```
|
|
93
|
+
euthyna euthyna-history — 被删除代码的来源归属与安全分类
|
|
94
|
+
目标: D:/axe-core/axe-core
|
|
95
|
+
范围: 8390a5a9…..397f5fce…
|
|
96
|
+
|
|
97
|
+
已确证 (8)
|
|
98
|
+
• 本次变更删除了 4 行来自提交 ab877f9d70 的代码,分布在 2 个文件。提交信息:
|
|
99
|
+
"fix(link-in-text-block): don't match style or script text (#3775)",分类:fix
|
|
100
|
+
证据: lib/checks/color/link-in-text-block-evaluate.js (ab877f9d70)
|
|
101
|
+
复现: git blame --porcelain -L 104,104 -L 114,114 8390a5a9… -- lib/checks/…
|
|
102
|
+
|
|
103
|
+
⚠ 未评估的判据(缺数据不等于干净):
|
|
104
|
+
• reintroduction: 未启用 --pickaxe,未检查「被移除又加回」的代码行
|
|
105
|
+
|
|
106
|
+
已评估的判据:
|
|
107
|
+
• history: 105 条 (euthyna-history)
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
### 怎么读
|
|
111
|
+
|
|
112
|
+
| 看到什么 | 意味着什么 |
|
|
113
|
+
|---|---|
|
|
114
|
+
| 「已确证」一节的 `分类:security` | **风险拉到最高**,除非能证明那段代码已无必要 |
|
|
115
|
+
| `分类:fix` | 不是安全修复,但**读一眼提交信息本身**——分类器偏向宽,不能只看标签 |
|
|
116
|
+
| `分布在 N 个文件` | 证据里那个文件**不包含全部**被删行。不要以为只有这一个文件受影响 |
|
|
117
|
+
| 底部的「⚠ 未评估的判据」 | 这一节非空时,**不许**把这次运行读成「查过了没问题」 |
|
|
118
|
+
| `已确证 (0)` 且未评估一节为空 | 确实没有删除行。这是一次**完成的测量**,不是失败 |
|
|
119
|
+
|
|
120
|
+
**不要自己跑 `git blame` 肉眼读提交信息。** 机器判定同一个 diff 跑两次结果一致,人读两次可能不一致。
|
|
121
|
+
|
|
122
|
+
---
|
|
123
|
+
|
|
124
|
+
## 三、`coverage` —— 符号调用计数
|
|
125
|
+
|
|
126
|
+
### 什么时候必须跑
|
|
127
|
+
|
|
128
|
+
| 触发场景 | 对应阶段 |
|
|
129
|
+
|---|---|
|
|
130
|
+
| 报告里要写「这个函数**有测试覆盖**」 | 阶段 B / C |
|
|
131
|
+
| 报告里要写「这个函数**没有测试覆盖**」 | 阶段 B / C |
|
|
132
|
+
| 一条结论的成立依赖「那条路径被测试跑过」 | 阶段 C |
|
|
133
|
+
|
|
134
|
+
换句话说:**任何关于「测没测过」的话,写之前先跑它。**
|
|
135
|
+
|
|
136
|
+
### 命令
|
|
137
|
+
|
|
138
|
+
```powershell
|
|
139
|
+
node <euthyna 仓库>/bin/euthyna.js coverage `
|
|
140
|
+
--coverage <覆盖率文件绝对路径> `
|
|
141
|
+
--symbol <符号名> --symbol <符号名>
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
`--symbol` 可重复,一次查多个。加 `--file` 可限定到某个文件。
|
|
145
|
+
|
|
146
|
+
**两种覆盖率格式都读**(自动识别,无需参数):
|
|
147
|
+
|
|
148
|
+
| 格式 | 来源 | 说明 |
|
|
149
|
+
|---|---|---|
|
|
150
|
+
| c8 / v8-to-istanbul `coverage-final.json` | JS 项目(c8 默认落在 `coverage/coverage-final.json`) | 每文件有 `fnMap` + `f`(调用计数数组) |
|
|
151
|
+
| coverage.py JSON(format 3) | Python 项目(`coverage json` 产出) | 顶层 `{meta, files}`;`functions` 里「函数名 → 已执行行列表」,**无调用计数**。从未调用的函数仍在列表中且 `executed_lines` 为空 |
|
|
152
|
+
|
|
153
|
+
coverage.py 的产出命令(Python 目标):
|
|
154
|
+
```powershell
|
|
155
|
+
uv run --with coverage python -m coverage run `
|
|
156
|
+
--include="*/src/<package>/<file>.py" `
|
|
157
|
+
-m pytest <test-file> -o addopts="" -p no:randomly
|
|
158
|
+
uv run --with coverage python -m coverage json -o <输出路径>.json
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
> 两个 Python 专属注意事项(在 crewAI 案例研究中实测):
|
|
162
|
+
> 1. **禁用 xdist**(`-o addopts=""`)——coverage 默认收集不到子进程的 trace,`-n auto`
|
|
163
|
+
> 时报告是空的。仓库自带 xdist 默认值时用 `-o addopts=""` 覆盖。
|
|
164
|
+
> 2. `--include` 按文件路径限定比 `--source` 对 src 布局更可靠(`--source=包名` 可能报
|
|
165
|
+
> `module-not-imported`)。
|
|
166
|
+
|
|
167
|
+
覆盖率文件从哪来:项目自己产出就用它。项目不产出覆盖率时,**要么用项目自己的方式跑一次
|
|
168
|
+
测试产出它,要么把该判据标为「不可评估」**。
|
|
169
|
+
|
|
170
|
+
### 输出长什么样(真实捕获,一次查询命中两种结果)
|
|
171
|
+
|
|
172
|
+
```
|
|
173
|
+
euthyna euthyna-coverage — 符号调用计数(只能证伪)
|
|
174
|
+
|
|
175
|
+
已确证 (1)
|
|
176
|
+
• 符号 cleanup 在本次测试运行中一次都没有被调用(调用计数为 0)
|
|
177
|
+
—— 任何依赖它的行为都没有被执行验证
|
|
178
|
+
证据: …\src\handlers.js:29
|
|
179
|
+
|
|
180
|
+
无法判定 (1)
|
|
181
|
+
• 符号 checkPermission 被调用了 2 次,但调用计数非零**不能**证明任何特定调用点执行过
|
|
182
|
+
—— 本事实只能证伪,不能证实
|
|
183
|
+
证据: …\src\authz.js:4
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
### 怎么读(本节是整个工具最容易用错的地方)
|
|
187
|
+
|
|
188
|
+
| 事实 | 能推出 |
|
|
189
|
+
|---|---|
|
|
190
|
+
| 计数为 0,`status: established` | ✅ **确实没被调用过**。可以据此说「这条路径没有被执行验证」 |
|
|
191
|
+
| 计数 > 0,`status: unknown` | ❌ **什么都不能推**。进过这个函数 ≠ 你要问的那个调用点执行过 |
|
|
192
|
+
|
|
193
|
+
原因在 `fact-contract.md` 里有完整说明:底层覆盖率数据会把**从未执行的调用点报成已覆盖**
|
|
194
|
+
(抛出异常之后的直线代码就是典型),所以「已覆盖」这个方向本身不可靠。
|
|
195
|
+
**产出器里根本没有「已执行」这个输出,这是刻意的。**
|
|
196
|
+
|
|
197
|
+
推论:**不要**用「计数非零」去关掉一个「这里没测试」的担忧。要证明确实执行过,
|
|
198
|
+
只能靠能真正触达该调用点的演示(阶段 C 的门禁 4)。
|
|
199
|
+
|
|
200
|
+
---
|
|
201
|
+
|
|
202
|
+
## 四、退出码:对每个码该做什么
|
|
203
|
+
|
|
204
|
+
| 码 | 含义 | 你该做什么 |
|
|
205
|
+
|---|---|---|
|
|
206
|
+
| `0` | 已测量,无安全相关发现 | 继续。**但记得读「未评估的判据」一节** |
|
|
207
|
+
| `10` | 已测量,存在 `security` 分类的事实 | **提高优先级**,然后逐条走阶段 C。不要直接当结论 |
|
|
208
|
+
| `1` | 用法错误 | 修命令(`--base` 写错、符号名写成空等)。这不是测量结果 |
|
|
209
|
+
| **`2`** | **完全无法测量** | **不得当作干净**。要么换测量方式,要么把相关判据标为「不可评估」 |
|
|
210
|
+
|
|
211
|
+
`2` 与 `0` 的区别是这份契约存在意义的缩影:**没能测量,和测了没问题,是两回事。**
|
|
212
|
+
|
|
213
|
+
### 会走到 `2` 的常见情形
|
|
214
|
+
|
|
215
|
+
| 情形 | 输出里的说明 | 处理 |
|
|
216
|
+
|---|---|---|
|
|
217
|
+
| 目录不在 git 仓库内 | 未评估:不在任何 git 仓库内 | 换到真实仓库,或标不可评估 |
|
|
218
|
+
| 覆盖率文件不存在 | 未评估:`ENOENT`,未运行测试 | 产出覆盖率,或标不可评估 |
|
|
219
|
+
| 符号名拼错 / 不存在 | 查不到该符号 | 核对符号名;确实不存在就说明它不在测量范围内 |
|
|
220
|
+
|
|
221
|
+
**这几种都不算「干净」。** 报告里必须把它们写进「未评估的判据」,并说明原因。
|
|
222
|
+
|
|
223
|
+
---
|
|
224
|
+
|
|
225
|
+
## 五、把它们接进流程的位置
|
|
226
|
+
|
|
227
|
+
```
|
|
228
|
+
阶段 B(变更面审计)
|
|
229
|
+
├─ 有删除行 ──────► history → 结果决定风险是否拉到最高
|
|
230
|
+
├─ 涉及回归判断 ──► history --pickaxe → 结果决定是否按「回归」处理
|
|
231
|
+
└─ 要谈测试覆盖 ──► coverage → 只证伪;计数非零不构成证据
|
|
232
|
+
|
|
233
|
+
阶段 C(结论面验证)
|
|
234
|
+
└─ 结论依赖「测过没有」──► coverage → 门禁 4(演示)的真假由它约束
|
|
235
|
+
```
|
|
236
|
+
|
|
237
|
+
**产出器给的是事实,不是裁定。** 它说「这行来自一个安全修复提交」,
|
|
238
|
+
**不说**「删掉它就是个漏洞」——后者是阶段 C 的事。
|
|
239
|
+
|
|
240
|
+
---
|
|
241
|
+
|
|
242
|
+
## 六、`gate` —— 门禁的程序化校验(不是测量)
|
|
243
|
+
|
|
244
|
+
`history` / `coverage` 产出事实;`gate` **不测量**,它核对一份审计报告的自我声明
|
|
245
|
+
是否符合 6 门禁契约。报告格式见 `SKILL.md` 的「裁定格式」。
|
|
246
|
+
|
|
247
|
+
```powershell
|
|
248
|
+
node <euthyna 仓库>/bin/euthyna.js gate <报告文件>
|
|
249
|
+
node <euthyna 仓库>/bin/euthyna.js gate <报告文件> --verify --cwd <被审计仓库>
|
|
250
|
+
```
|
|
251
|
+
|
|
252
|
+
| 看到什么 | 意味着什么 |
|
|
253
|
+
|---|---|
|
|
254
|
+
| 退出码 `0` | 每条 finding 都带了它该带的证据:TP 有 `path:L123` 证据 + 复现命令 + 影响 + 六门禁全过;FP 有 FAIL 门禁;INCONCLUSIVE 有未评估门禁 |
|
|
255
|
+
| 退出码 `10` | 有 finding 被**降级为「观察」**——缺证据 / 缺复现 / 门禁与裁定矛盾。读降级原因,补齐或把结论改成观察 |
|
|
256
|
+
| 退出码 `2` | 报告读不了,或里面没有可校验的 finding。**不是干净** |
|
|
257
|
+
| `--verify` 的 `复现核验 ✗` | 复现命令真的跑了但没跑通——「说能复现」不算数 |
|
|
258
|
+
|
|
259
|
+
`gate` 只校验报告自己的话,不去审计目标仓库;所以它不能代替裁定,只保证
|
|
260
|
+
拿不出证据的结论不会以已确证的面貌交出去。
|
|
261
|
+
|
|
262
|
+
---
|
|
263
|
+
|
|
264
|
+
## 七、已知边界(不要越界使用)
|
|
265
|
+
|
|
266
|
+
- `history` 的分类器**偏向宽**:漏掉安全提交比多报更危险,所以它宁可把 `fix` 也报出来。
|
|
267
|
+
分类结果是**线索**,提交信息要自己读。
|
|
268
|
+
- `history` 的归属默认是 blame 的「最后修改」语义;要「最初引入」用 `--origins`
|
|
269
|
+
(有探针上限,且只对 ≥ 12 字符的行做追溯)。两个近似都写在本文件「归属语义」一节。
|
|
270
|
+
- `history` 只覆盖**被删除的行**。新增的代码它不管(`--pickaxe` 除外)。
|
|
271
|
+
- `coverage` **只能证伪**。见上文。
|
|
272
|
+
- `coverage` 依赖调用计数,**不处理动态派发、反射、字符串调用**。
|
|
273
|
+
计数为 0 的结论在存在这些机制时要打折扣。
|
|
274
|
+
- 两者都**不产出分数、严重性、修复建议**。这是契约,不是遗漏。
|