euthyna 0.3.1 → 0.4.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 +10 -4
- package/.agents/skills/euthyna/references/fact-producers.md +23 -17
- package/.agents/skills/euthyna-en/SKILL.md +10 -4
- package/.agents/skills/euthyna-en/references/fact-producers.md +12 -7
- package/README.md +21 -14
- package/package.json +1 -1
- package/plugin.yaml +1 -1
- package/src/cli.js +177 -25
- package/src/facts/deps.js +55 -0
- package/src/facts/history.js +11 -10
|
@@ -76,8 +76,9 @@ license: Apache-2.0
|
|
|
76
76
|
│
|
|
77
77
|
├─ 一次变更(PR / diff / commit)────► 阶段 B:变更面审计
|
|
78
78
|
│ (「这个 PR 安全吗」) 读 references/change-audit.md
|
|
79
|
-
│ ★
|
|
80
|
-
│
|
|
79
|
+
│ ★ 第一条命令:euthyna audit --base <分叉点>
|
|
80
|
+
│ (history + deps 一次跑完,见下节)
|
|
81
|
+
│ ★ 要谈测试覆盖 → 再跑 coverage
|
|
81
82
|
│
|
|
82
83
|
├─ 一个仓库 / 依赖面 ────────────────► 阶段 A:依赖面审计
|
|
83
84
|
│ (「依赖有没有问题」) 读 references/dependency-audit.md
|
|
@@ -100,10 +101,15 @@ license: Apache-2.0
|
|
|
100
101
|
|
|
101
102
|
| 测量 | 回答的问题 | 什么时候**必须**跑 |
|
|
102
103
|
|---|---|---|
|
|
103
|
-
| `
|
|
104
|
+
| `audit` | 上面两个问题**一次跑完**(history 默认追溯最初引入者 + deps 枚举 lockfile 全部依赖),合并一份报告 | **每次审计的第一条命令**(阶段 B 开场) |
|
|
105
|
+
| `history` | 这次变更**删掉**的代码来自哪个提交?那个提交是不是安全修复? | 需要单独重跑 history 时(如换 `--base`) |
|
|
104
106
|
| `coverage` | 某个符号在测试运行中**到底有没有被调用过**? | 报告里要说「测过 / 没测过」时(阶段 B / C) |
|
|
105
107
|
|
|
106
108
|
```powershell
|
|
109
|
+
# 唯一必需的参数是 --base(改前版本 / PR 分叉点):
|
|
110
|
+
node <euthyna 仓库>/bin/euthyna.js audit --base <改前版本> --repo <被审计的仓库>
|
|
111
|
+
|
|
112
|
+
# history / coverage 按需单独跑:
|
|
107
113
|
node <euthyna 仓库>/bin/euthyna.js history --base <改前版本> --repo <被审计的仓库>
|
|
108
114
|
node <euthyna 仓库>/bin/euthyna.js coverage --coverage <覆盖率文件绝对路径> --symbol <符号名>
|
|
109
115
|
```
|
|
@@ -206,7 +212,7 @@ node <euthyna 仓库>/bin/euthyna.js gate <报告文件> --verify --cwd <被审
|
|
|
206
212
|
## 交付前的自检(逐条打勾,缺一条不许说「审完了」)
|
|
207
213
|
|
|
208
214
|
- [ ] 每个疑似发现都走了**完整的**阶段 C,不是「看一眼觉得是假的」
|
|
209
|
-
- [ ]
|
|
215
|
+
- [ ] **跑过 `audit`(或 diff 有删除行时跑过 `history`)**;删自安全修复提交的代码已按最高风险处理
|
|
210
216
|
- [ ] 报告里每句「测过 / 没测过」都有 `coverage` 输出支撑,或已标为「不可评估」
|
|
211
217
|
- [ ] **退出码为 `2` 的判据没有出现在「干净」一栏**
|
|
212
218
|
- [ ] 每条结论都带 `file:line`
|
|
@@ -14,8 +14,8 @@ CLI **不在技能目录里**,它是本项目的独立程序。按可用性从
|
|
|
14
14
|
|
|
15
15
|
| 情况 | 怎么写 | 备注 |
|
|
16
16
|
|---|---|---|
|
|
17
|
-
| 已安装 | `euthyna
|
|
18
|
-
| 有本项目检出 | `node <euthyna 仓库>/bin/euthyna.js
|
|
17
|
+
| 已安装 | `euthyna audit …` | `euthyna` 是包声明的命令名 |
|
|
18
|
+
| 有本项目检出 | `node <euthyna 仓库>/bin/euthyna.js audit …` | 最常用的形式 |
|
|
19
19
|
| 两者都没有 | —— | **把判据标为「不可评估」**,不要手工估算顶上 |
|
|
20
20
|
|
|
21
21
|
### ⚠️ 一个真实的坑:相对路径只在 euthyna 仓库里成立
|
|
@@ -54,17 +54,21 @@ node <euthyna 仓库>/bin/euthyna.js history --base <rev> --repo <被审计的
|
|
|
54
54
|
**只要 diff 里有删除行,就先跑它。** 这不是可选步骤:一个校验被删掉而看着「很干净」的 diff,
|
|
55
55
|
正是安全回归最常见的样子,而模型无法对几百行被删代码逐行做 blame 再回溯提交历史。
|
|
56
56
|
|
|
57
|
+
**通常不必单独跑它** —— `euthyna audit`(见第一节)已经把 history(默认开来源追溯)
|
|
58
|
+
和 deps 一次跑完。单独跑 `history` 的场景:换一个 `--base` 重新对比、只要 history 不要 deps。
|
|
59
|
+
|
|
57
60
|
### 命令
|
|
58
61
|
|
|
59
62
|
```powershell
|
|
60
63
|
# 基础:本次变更删掉的代码来自哪些提交、那些提交像不像安全修复
|
|
64
|
+
# (来源追溯默认开启:删除行归属到「最初引入」该内容的提交)
|
|
61
65
|
node <euthyna 仓库>/bin/euthyna.js history --base <改前版本> --repo <被审计的仓库>
|
|
62
66
|
|
|
63
67
|
# 加测「曾被移除又加回来」的行(有探针上限,比基础版慢)
|
|
64
68
|
node <euthyna 仓库>/bin/euthyna.js history --base <改前版本> --pickaxe --repo <被审计的仓库>
|
|
65
69
|
|
|
66
|
-
#
|
|
67
|
-
node <euthyna 仓库>/bin/euthyna.js history --base <改前版本> --origins --repo <被审计的仓库>
|
|
70
|
+
# 关闭来源追溯,退回 blame 的「最后修改」语义(更快;输出会写明这一点)
|
|
71
|
+
node <euthyna 仓库>/bin/euthyna.js history --base <改前版本> --no-origins --repo <被审计的仓库>
|
|
68
72
|
```
|
|
69
73
|
|
|
70
74
|
`--base` 是**改前版本**(PR 的分叉点),`--head` 默认 `HEAD`。
|
|
@@ -74,13 +78,14 @@ node <euthyna 仓库>/bin/euthyna.js history --base <改前版本> --origins --r
|
|
|
74
78
|
|
|
75
79
|
`history` 的归属和分类各有一个**写死的近似**,读输出前先知道它们:
|
|
76
80
|
|
|
77
|
-
1.
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
最初引入),并说明「最初引入」。
|
|
82
|
-
|
|
83
|
-
|
|
81
|
+
1. **归属:默认用 `git log -S` 追「最初引入」;blame 的「最后修改」只是回退。** 一条安全
|
|
82
|
+
修复行如果之后被格式化/重构碰过(移动、复制),blame 会把来源归到最后碰它的那个提交
|
|
83
|
+
——追引入者正是为了不中这个招。来源追溯用 `git log -S <行内容>` 定位**最初引入**该内容
|
|
84
|
+
的提交;与 blame 不一致时,事实会同时写出两个提交(`detail.blameCommit` = 最后修改、
|
|
85
|
+
`evidence.commit` = 最初引入),并说明「最初引入」。
|
|
86
|
+
追溯只对长度 ≥ 12 字符的行做(太通用的行,-S 会指向整个历史的第一个提交,那不是来源);
|
|
87
|
+
被追溯的行有探针上限,触顶时剩余行保留 blame 归属并在「未评估的判据」里写明;
|
|
88
|
+
`--no-origins` 显式关闭时同样在「未评估的判据」里写明用的是 blame 语义。
|
|
84
89
|
2. **分类:先看提交信息,再看提交 diff,再看被删行本身。** 消息含糊
|
|
85
90
|
("update utils")但 diff 动了危险 API、或被删行本身就是安全代码(如
|
|
86
91
|
`if (!authorized) return`)时,都会标 `security`——依据写在
|
|
@@ -226,12 +231,13 @@ euthyna euthyna-coverage — 符号调用计数(只能证伪)
|
|
|
226
231
|
|
|
227
232
|
```
|
|
228
233
|
阶段 B(变更面审计)
|
|
229
|
-
├─
|
|
230
|
-
|
|
231
|
-
|
|
234
|
+
├─ 第一条命令 ────► audit → history + deps 一次跑完
|
|
235
|
+
│ → 有删除行:结果决定风险是否拉到最高
|
|
236
|
+
├─ 涉及回归判断 ──► history --pickaxe → 结果决定是否按「回归」处理
|
|
237
|
+
└─ 要谈测试覆盖 ──► coverage → 只证伪;计数非零不构成证据
|
|
232
238
|
|
|
233
239
|
阶段 C(结论面验证)
|
|
234
|
-
└─ 结论依赖「测过没有」──► coverage
|
|
240
|
+
└─ 结论依赖「测过没有」──► coverage → 门禁 4(演示)的真假由它约束
|
|
235
241
|
```
|
|
236
242
|
|
|
237
243
|
**产出器给的是事实,不是裁定。** 它说「这行来自一个安全修复提交」,
|
|
@@ -265,8 +271,8 @@ node <euthyna 仓库>/bin/euthyna.js gate <报告文件> --verify --cwd <被审
|
|
|
265
271
|
|
|
266
272
|
- `history` 的分类器**偏向宽**:漏掉安全提交比多报更危险,所以它宁可把 `fix` 也报出来。
|
|
267
273
|
分类结果是**线索**,提交信息要自己读。
|
|
268
|
-
- `history`
|
|
269
|
-
|
|
274
|
+
- `history` 的归属**默认追「最初引入」**(`git log -S`,有探针上限,只对 ≥ 12 字符的行做追溯);
|
|
275
|
+
要退回 blame 的「最后修改」语义用 `--no-origins`(更快,输出会写明)。两个近似都写在本文件「归属语义」一节。
|
|
270
276
|
- `history` 只覆盖**被删除的行**。新增的代码它不管(`--pickaxe` 除外)。
|
|
271
277
|
- `coverage` **只能证伪**。见上文。
|
|
272
278
|
- `coverage` 依赖调用计数,**不处理动态派发、反射、字符串调用**。
|
|
@@ -85,8 +85,9 @@ What did the user give?
|
|
|
85
85
|
│
|
|
86
86
|
├─ A change (PR / diff / commit) ───────────────► Stage B: change audit
|
|
87
87
|
│ ("is this PR safe") read references/change-audit.md
|
|
88
|
-
│ ★
|
|
89
|
-
│
|
|
88
|
+
│ ★ first command: euthyna audit --base <fork point>
|
|
89
|
+
│ (history + deps in one run, see below)
|
|
90
|
+
│ ★ if test coverage matters → then run coverage
|
|
90
91
|
│
|
|
91
92
|
├─ A repository / dependency surface ───────────► Stage A: dependency audit
|
|
92
93
|
│ ("do the dependencies have problems") read references/dependency-audit.md
|
|
@@ -111,10 +112,15 @@ reliably — are shipped by this skill as a zero-dependency CLI:
|
|
|
111
112
|
|
|
112
113
|
| Measurement | Question it answers | When it is **mandatory** |
|
|
113
114
|
|---|---|---|
|
|
114
|
-
| `
|
|
115
|
+
| `audit` | **Both questions below in one run** (history traces original introducers by default + deps enumerates every lockfile dependency), one merged report | **the first command of every audit** (opening Stage B) |
|
|
116
|
+
| `history` | Which commit does the code this change **deleted** come from? Was that commit a security fix? | when re-running history alone (e.g. a different `--base`) |
|
|
115
117
|
| `coverage` | Was this symbol **ever actually invoked** by a test run? | when the report claims "tested / not tested" (Stage B / C) |
|
|
116
118
|
|
|
117
119
|
```powershell
|
|
120
|
+
# the only required argument is --base (pre-change revision / PR fork point):
|
|
121
|
+
node <euthyna repo>/bin/euthyna.js audit --base <pre-change revision> --repo <audited repo>
|
|
122
|
+
|
|
123
|
+
# history / coverage run alone when needed:
|
|
118
124
|
node <euthyna repo>/bin/euthyna.js history --base <pre-change revision> --repo <audited repo>
|
|
119
125
|
node <euthyna repo>/bin/euthyna.js coverage --coverage <absolute path to coverage file> --symbol <symbol name>
|
|
120
126
|
```
|
|
@@ -229,7 +235,7 @@ adjudication, it only guarantees that a conclusion without evidence cannot leave
|
|
|
229
235
|
## Pre-delivery self-check (tick every item; without all of them you may not say "audit done")
|
|
230
236
|
|
|
231
237
|
- [ ] Every suspected finding went through **complete** Stage C, not "looked at it and it seems false"
|
|
232
|
-
- [ ] **Ran `history` when the diff has deleted lines**; code deleted from a security-fix commit handled at highest risk
|
|
238
|
+
- [ ] **Ran `audit` (or `history` when the diff has deleted lines)**; code deleted from a security-fix commit handled at highest risk
|
|
233
239
|
- [ ] Every "tested / not tested" sentence in the report is backed by `coverage` output, or marked "not evaluable"
|
|
234
240
|
- [ ] **No criterion with exit code `2` appears in a "clean" column**
|
|
235
241
|
- [ ] Every conclusion carries a `file:line`
|
|
@@ -14,8 +14,8 @@ The CLI is **not in the skill directory** — it is a separate program of this p
|
|
|
14
14
|
|
|
15
15
|
| Situation | How to write it | Notes |
|
|
16
16
|
|---|---|---|
|
|
17
|
-
| Installed | `euthyna
|
|
18
|
-
| Project checkout available | `node <euthyna repo>/bin/euthyna.js
|
|
17
|
+
| Installed | `euthyna audit …` | `euthyna` is the command name declared by the package |
|
|
18
|
+
| Project checkout available | `node <euthyna repo>/bin/euthyna.js audit …` | the most common form |
|
|
19
19
|
| Neither | — | **mark the criterion as "not evaluable"**; do not substitute a manual estimate |
|
|
20
20
|
|
|
21
21
|
### ⚠️ A real pitfall: relative paths only work inside the euthyna repository
|
|
@@ -54,17 +54,21 @@ Both commands were verified: run from `D:\` with `--repo` pointing at another re
|
|
|
54
54
|
**If the diff contains deleted lines, run it first.** This is not optional: a diff that looks "very clean" because a check was deleted
|
|
55
55
|
is exactly what a security regression usually looks like, and a model cannot blame hundreds of deleted lines one by one and then trace back through commit history.
|
|
56
56
|
|
|
57
|
+
**Usually you do not need to run it alone** — `euthyna audit` (section 1) already runs history (with origin tracing on by default)
|
|
58
|
+
and deps in one pass. Run `history` alone to compare against a different `--base`, or when you want history without deps.
|
|
59
|
+
|
|
57
60
|
### Commands
|
|
58
61
|
|
|
59
62
|
```powershell
|
|
60
63
|
# basic: which commits do this change's deleted lines come from, and do those commits look like security fixes
|
|
64
|
+
# (origin tracing is on by default: deleted lines are attributed to the commit that "originally introduced" the content)
|
|
61
65
|
node <euthyna repo>/bin/euthyna.js history --base <pre-change revision> --repo <audited repo>
|
|
62
66
|
|
|
63
67
|
# additionally check for "removed and added back" lines (has a probe cap; slower than the basic version)
|
|
64
68
|
node <euthyna repo>/bin/euthyna.js history --base <pre-change revision> --pickaxe --repo <audited repo>
|
|
65
69
|
|
|
66
|
-
#
|
|
67
|
-
node <euthyna repo>/bin/euthyna.js history --base <pre-change revision> --origins --repo <audited repo>
|
|
70
|
+
# disable origin tracing and fall back to blame's "last modified" semantics (faster; the output states this)
|
|
71
|
+
node <euthyna repo>/bin/euthyna.js history --base <pre-change revision> --no-origins --repo <audited repo>
|
|
68
72
|
```
|
|
69
73
|
|
|
70
74
|
`--base` is the **pre-change revision** (the PR's fork point); `--head` defaults to `HEAD`.
|
|
@@ -74,7 +78,7 @@ When the fork point is uncertain, use the result of `git merge-base <target bran
|
|
|
74
78
|
|
|
75
79
|
`history`'s attribution and classification each carry a **hard-coded approximation** — know them before reading the output:
|
|
76
80
|
|
|
77
|
-
1. **Attribution:
|
|
81
|
+
1. **Attribution: `git log -S` traces the "original introducer" by default; blame's "last modified" is the fallback.** If a security-fix line was later touched by formatting/refactoring (moved, copied), blame attributes the origin to the last commit that touched it — tracing the introducer exists precisely to avoid that trap. Origin tracing uses `git log -S <line content>` to locate the commit that **originally introduced** the content; when it differs from blame, the fact writes out both commits (`detail.blameCommit` = last modified, `evidence.commit` = originally introduced) and states so. Tracing applies only to lines ≥ 12 characters long (for overly generic lines, `-S` would point at the first commit of the entire history, which is not the origin); traced lines are probe-capped, and when the cap is hit the remaining lines keep their blame attribution, stated under "criteria not evaluated"; disabling it explicitly with `--no-origins` is likewise stated there.
|
|
78
82
|
2. **Classification: look at the commit message first, then the commit diff, then the deleted line itself.** When the message is vague ("update utils") but the diff touches a dangerous API, or the deleted line is itself security code (e.g. `if (!authorized) return`), it is marked `security` — the basis is recorded in `detail.classificationBasis` (`message` / `diff` / `deleted-line`). The deliberately wide bias is intentional: missing a security commit is more dangerous than over-reporting.
|
|
79
83
|
|
|
80
84
|
### What the output looks like (real capture)
|
|
@@ -213,7 +217,8 @@ The difference between `2` and `0` is the essence of why this contract exists: *
|
|
|
213
217
|
|
|
214
218
|
```
|
|
215
219
|
Stage B (change-surface audit)
|
|
216
|
-
├─
|
|
220
|
+
├─ first command ───────────► audit → history + deps in one pass
|
|
221
|
+
│ → deleted lines present: the result decides whether risk is raised to maximum
|
|
217
222
|
├─ regression judgment ──────► history --pickaxe → the result decides whether to treat it as a "regression"
|
|
218
223
|
└─ test coverage to discuss ─► coverage → can only falsify; a non-zero count is not evidence
|
|
219
224
|
|
|
@@ -251,7 +256,7 @@ that a conclusion without evidence is not handed over wearing the face of "estab
|
|
|
251
256
|
## 7. Known limits (do not use beyond them)
|
|
252
257
|
|
|
253
258
|
- `history`'s classifier is **biased wide**: missing a security commit is more dangerous than over-reporting, so it would rather report `fix` too. The classification is a **lead** — read the commit message yourself.
|
|
254
|
-
- `history`'s attribution
|
|
259
|
+
- `history`'s attribution **traces the "original introducer" by default** (`git log -S`, probe-capped, only lines ≥ 12 characters); to fall back to blame's "last modified" semantics use `--no-origins` (faster; the output states this). Both approximations are documented in this file's "Attribution semantics" section.
|
|
255
260
|
- `history` only covers **deleted lines**. It does not handle newly added code (except with `--pickaxe`).
|
|
256
261
|
- `coverage` **can only falsify**. See above.
|
|
257
262
|
- `coverage` relies on invocation counts and **does not handle dynamic dispatch, reflection, or string-based calls**. Conclusions of count 0 must be discounted when such mechanisms exist.
|
package/README.md
CHANGED
|
@@ -10,6 +10,10 @@
|
|
|
10
10
|
|
|
11
11
|
<p align="center">
|
|
12
12
|
<a href="https://github.com/slow-stack/euthyna/actions/workflows/ci.yml"><img src="https://github.com/slow-stack/euthyna/actions/workflows/ci.yml/badge.svg" alt="CI"></a>
|
|
13
|
+
<a href="https://www.npmjs.com/package/euthyna"><img src="https://img.shields.io/npm/v/euthyna?style=flat&color=007EC6" alt="npm version"></a>
|
|
14
|
+
<a href="https://www.npmjs.com/package/euthyna"><img src="https://img.shields.io/npm/dm/euthyna?style=flat&color=007EC6" alt="npm downloads"></a>
|
|
15
|
+
<a href="https://www.npmjs.com/package/euthyna"><img src="https://img.shields.io/npm/l/euthyna?style=flat&color=007EC6" alt="license: Apache-2.0"></a>
|
|
16
|
+
<a href="https://search.sigstore.dev/?logIndex=2915999059"><img src="https://img.shields.io/badge/npm%20provenance-verified-007EC6?style=flat" alt="npm provenance verified"></a>
|
|
13
17
|
<a href="https://www.skills.sh/slow-stack/euthyna"><img src="https://skills.sh/b/slow-stack/euthyna.svg" alt="Agent skills installs"></a>
|
|
14
18
|
<a href="https://awesome-dsh-plugin.com/p/slow-stack/euthyna/"><img src="https://img.shields.io/badge/DSH%20plugin-listed-blue" alt="Listed in the awesome-dsh-plugin catalog"></a>
|
|
15
19
|
</p>
|
|
@@ -53,10 +57,11 @@ Three fact producers — a zero-dependency Node CLI:
|
|
|
53
57
|
- **`history`** — for every line a change deletes, it attributes the line to a commit
|
|
54
58
|
and classifies that commit from its message, its own diff, and the deleted line's
|
|
55
59
|
content (a deleted `if (!authorized)` is flagged even under a "tweaks" subject). By
|
|
56
|
-
default the attribution
|
|
57
|
-
|
|
58
|
-
a security
|
|
59
|
-
|
|
60
|
+
default the attribution traces the commit that *first introduced* the content with
|
|
61
|
+
`git log -S` (`--no-origins` falls back to blame's "last touched", which is faster
|
|
62
|
+
but mis-attributes a security line later moved by a formatting commit). If the
|
|
63
|
+
deleted code came from a security fix, that is flagged. This is git archaeology no
|
|
64
|
+
model can do from reading a diff.
|
|
60
65
|
- **`coverage`** — was this symbol *ever actually invoked* by a test? It has exactly two
|
|
61
66
|
answers: never invoked (established), or entered but that proves nothing about any
|
|
62
67
|
specific call site (unknown). **It never reports "executed"** — V8 coverage marks
|
|
@@ -163,20 +168,20 @@ replying in chat.
|
|
|
163
168
|
|
|
164
169
|
```sh
|
|
165
170
|
npm install -g euthyna
|
|
166
|
-
euthyna
|
|
171
|
+
euthyna audit --repo <path> --base main --head HEAD # history + deps in one run
|
|
167
172
|
euthyna coverage --coverage coverage/coverage-final.json --symbol <name>
|
|
168
|
-
euthyna deps --repo <path> --dep <name>
|
|
173
|
+
euthyna deps --repo <path> --all # or --dep <name> for a single dep
|
|
169
174
|
euthyna gate <adjudication-report.md> --verify --cwd <repo> # mechanically check the six gates
|
|
170
175
|
```
|
|
171
176
|
|
|
172
|
-
Or without a global install: `npx euthyna
|
|
177
|
+
Or without a global install: `npx euthyna audit --repo <path> --base main`.
|
|
173
178
|
|
|
174
179
|
From a checkout instead (development):
|
|
175
180
|
|
|
176
181
|
```sh
|
|
177
182
|
git clone https://github.com/slow-stack/euthyna
|
|
178
|
-
cd euthyna && npm test #
|
|
179
|
-
node bin/euthyna.js
|
|
183
|
+
cd euthyna && npm test # 207 tests; no install step exists
|
|
184
|
+
node bin/euthyna.js audit --repo <path> --base main --head HEAD
|
|
180
185
|
```
|
|
181
186
|
|
|
182
187
|
What `history` reports looks like this:
|
|
@@ -191,9 +196,10 @@ What `history` reports looks like this:
|
|
|
191
196
|
|
|
192
197
|
Every deleted line is blamed back to a commit, and the `复现`
|
|
193
198
|
(Reproduce) command lets you re-derive the claim yourself without trusting the report.
|
|
194
|
-
Add `--json` for the structured fact report
|
|
195
|
-
removed and are now being added back
|
|
196
|
-
commit that first introduced their content
|
|
199
|
+
Add `--json` for the structured fact report and `--pickaxe` to detect lines that were
|
|
200
|
+
removed and are now being added back. Origin tracing is on by default (deleted lines
|
|
201
|
+
are attributed to the commit that first introduced their content); `--no-origins`
|
|
202
|
+
falls back to blame's last-modifier attribution, which is faster.
|
|
197
203
|
|
|
198
204
|
### Exit codes are part of the contract
|
|
199
205
|
|
|
@@ -319,8 +325,9 @@ euthyna/
|
|
|
319
325
|
|
|
320
326
|
## 🧭 Status
|
|
321
327
|
|
|
322
|
-
Early, and honest about it. Working: the
|
|
323
|
-
|
|
328
|
+
Early, and honest about it. Working: the fact producers (`euthyna audit` runs history
|
|
329
|
+
and the full dependency surface in one command — the skill documents it as the first
|
|
330
|
+
command of every audit), the skill, the benchmark harness. Not yet built: the
|
|
324
331
|
git-history / coverage work needed to close the remaining recall gap.
|
|
325
332
|
|
|
326
333
|
The design notes are available in English and Chinese; the `-zh` files are the originals.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "euthyna",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
4
4
|
"description": "Deterministic fact producers for security auditing: which deleted code came from a security fix, and which changed symbols no test ever executed.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./plugin/index.js",
|
package/plugin.yaml
CHANGED
package/src/cli.js
CHANGED
|
@@ -18,7 +18,7 @@ import path from 'node:path';
|
|
|
18
18
|
import { readFile } from 'node:fs/promises';
|
|
19
19
|
import { collectHistoryFacts } from './facts/history.js';
|
|
20
20
|
import { collectCoverageFacts } from './facts/coverage.js';
|
|
21
|
-
import { collectDependencyFacts } from './facts/deps.js';
|
|
21
|
+
import { collectDependencyFacts, listDependencies, detectManifest, manifestTypeForFile } from './facts/deps.js';
|
|
22
22
|
import { parseGateReport, validateFindings, verifyFinding, renderGateReport } from './gate.js';
|
|
23
23
|
import { makeReport, renderReport, safeTextLines, KIND } from './contract.js';
|
|
24
24
|
import { repoToplevel, revParse } from './git.js';
|
|
@@ -49,6 +49,12 @@ const DEPS_PRODUCER = {
|
|
|
49
49
|
purpose: '依赖锁定版本(读取 lockfile,不含漏洞判定)',
|
|
50
50
|
purposeEn: 'dependency pinned versions (reads lockfiles, no vulnerability verdict)'
|
|
51
51
|
};
|
|
52
|
+
const AUDIT_PRODUCER = {
|
|
53
|
+
name: 'euthyna-audit',
|
|
54
|
+
version: '0.1.0',
|
|
55
|
+
purpose: '变更面审计:删除代码来源 + 依赖锁定版本,一次测量',
|
|
56
|
+
purposeEn: 'change-surface audit: deleted-code provenance + dependency pins, one run'
|
|
57
|
+
};
|
|
52
58
|
|
|
53
59
|
/** The producer object that goes into a report, with its purpose in the active language. */
|
|
54
60
|
function producerFor(producer, lang) {
|
|
@@ -65,20 +71,23 @@ euthyna —— 给 AI 编码 agent 用的确定性事实产出器
|
|
|
65
71
|
它不是扫描器。它只回答两个模型算不准的问题,并把答案写成带证据的事实。
|
|
66
72
|
|
|
67
73
|
用法:
|
|
68
|
-
euthyna
|
|
74
|
+
euthyna audit --base <rev> [--head <rev>] [--repo <dir>] [--pickaxe] [--no-origins] [--json]
|
|
75
|
+
euthyna history --base <rev> [--head <rev>] [--repo <dir>] [--pickaxe] [--no-origins] [--json]
|
|
69
76
|
euthyna coverage --coverage <file> --symbol <name> [--file <path>] [--json]
|
|
70
|
-
euthyna deps [--repo <dir>] [--lockfile <file>] --dep <name>
|
|
77
|
+
euthyna deps [--repo <dir>] [--lockfile <file>] (--dep <name> | --all) [--json]
|
|
71
78
|
euthyna gate <报告文件> [--verify] [--cwd <dir>] [--json]
|
|
72
79
|
|
|
73
80
|
语言: 默认中文。加 --lang en 或设环境变量 EUTHYNA_LANG=en 切换英文输出。
|
|
74
81
|
|
|
75
82
|
命令:
|
|
83
|
+
audit 一次跑完变更面审计:history + deps(枚举 lockfile 全部依赖),合并报告
|
|
84
|
+
这是技能文档默认推荐入口;coverage 因需要覆盖率文件和符号名不在其中
|
|
76
85
|
history 本次变更删掉了哪些代码、它们分别由哪个提交引入、该提交是不是安全修复
|
|
77
|
-
--base
|
|
78
|
-
--head
|
|
79
|
-
--pickaxe
|
|
80
|
-
--origins
|
|
81
|
-
|
|
86
|
+
--base 必填,比较的基线版本(如 main、HEAD~5、某个 commit)
|
|
87
|
+
--head 可选,默认 HEAD
|
|
88
|
+
--pickaxe 额外检查「曾被移除又加回来」的新增行(有探针上限)
|
|
89
|
+
--no-origins 关闭来源追溯;默认用 git log -S 把删除行归属到「最初引入」
|
|
90
|
+
该内容的提交(有探针上限,比纯 blame 慢)
|
|
82
91
|
coverage 某个符号在测试运行中到底有没有被调用过
|
|
83
92
|
--coverage 覆盖率数据文件,c8 的 coverage-final.json
|
|
84
93
|
--symbol 要查询的符号名,可重复
|
|
@@ -86,7 +95,7 @@ euthyna —— 给 AI 编码 agent 用的确定性事实产出器
|
|
|
86
95
|
deps 某个依赖在 lockfile 里被锁定/声明成什么版本(供应链声明的裁决依据)
|
|
87
96
|
--repo 可选,依赖清单所在目录(默认当前目录,自动检测)
|
|
88
97
|
--lockfile 可选,显式指定清单文件(支持 package-lock.json / Cargo.lock / go.mod)
|
|
89
|
-
--dep
|
|
98
|
+
--dep 要查询的依赖名,可重复;--all 查询清单里全部依赖
|
|
90
99
|
注:只报版本事实,不判「是否含漏洞」——版本到 CVE 的映射归判定层
|
|
91
100
|
gate 检查一份审计报告是否符合 6 门禁契约(不测量,只核对报告的自我声明)
|
|
92
101
|
<报告文件> 报告的 markdown 文件,裁定格式见技能 SKILL.md
|
|
@@ -112,31 +121,36 @@ It is not a scanner. It answers only the two questions a model cannot compute
|
|
|
112
121
|
reliably, and writes the answers as facts with evidence.
|
|
113
122
|
|
|
114
123
|
Usage:
|
|
115
|
-
euthyna
|
|
124
|
+
euthyna audit --base <rev> [--head <rev>] [--repo <dir>] [--pickaxe] [--no-origins] [--json]
|
|
125
|
+
euthyna history --base <rev> [--head <rev>] [--repo <dir>] [--pickaxe] [--no-origins] [--json]
|
|
116
126
|
euthyna coverage --coverage <file> --symbol <name> [--file <path>] [--json]
|
|
117
|
-
euthyna deps [--repo <dir>] [--lockfile <file>] --dep <name>
|
|
127
|
+
euthyna deps [--repo <dir>] [--lockfile <file>] (--dep <name> | --all) [--json]
|
|
118
128
|
euthyna gate <report file> [--verify] [--cwd <dir>] [--json]
|
|
119
129
|
|
|
120
130
|
Language: Chinese by default. Pass --lang en or set the environment variable
|
|
121
131
|
EUTHYNA_LANG=en for English output.
|
|
122
132
|
|
|
123
133
|
Commands:
|
|
124
|
-
|
|
134
|
+
audit the change-surface audit in one run: history + deps (every lockfile
|
|
135
|
+
dependency enumerated), one merged report. The default entry point
|
|
136
|
+
the skill documents; coverage stays separate (needs a coverage file
|
|
137
|
+
and symbol names only the adjudicator knows)
|
|
138
|
+
history which lines did this change delete, which commit introduced them,
|
|
125
139
|
and was that commit a security fix?
|
|
126
|
-
--base
|
|
127
|
-
--head
|
|
128
|
-
--pickaxe
|
|
129
|
-
--origins
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
coverage
|
|
140
|
+
--base required, the baseline revision to compare against
|
|
141
|
+
--head optional, defaults to HEAD
|
|
142
|
+
--pickaxe also check added lines that were once removed and now return (probe-capped)
|
|
143
|
+
--no-origins disable origin tracing; by default git log -S attributes deleted
|
|
144
|
+
lines to the commit that FIRST introduced their content
|
|
145
|
+
(probe-capped, slower than plain blame)
|
|
146
|
+
coverage was this symbol ever actually invoked by a test run?
|
|
133
147
|
--coverage the coverage data file, c8's coverage-final.json
|
|
134
148
|
--symbol symbol name(s) to query, repeatable
|
|
135
149
|
--file optional, restrict to one file
|
|
136
|
-
deps
|
|
150
|
+
deps what version does the lockfile pin or declare for a dependency? (the basis for supply-chain claims)
|
|
137
151
|
--repo optional, directory containing the manifest (default cwd, auto-detected)
|
|
138
152
|
--lockfile optional, explicit manifest path (package-lock.json / Cargo.lock / go.mod)
|
|
139
|
-
--dep dependency name(s) to query, repeatable
|
|
153
|
+
--dep dependency name(s) to query, repeatable; --all queries every dependency in the manifest
|
|
140
154
|
Note: reports version facts only, never "is vulnerable" — version-to-CVE mapping
|
|
141
155
|
belongs to the adjudication layer
|
|
142
156
|
gate Check an audit report against the six-gate contract (no measurement; only the report's self-declared claims)
|
|
@@ -252,12 +266,15 @@ async function runHistory(flags, lang) {
|
|
|
252
266
|
};
|
|
253
267
|
}
|
|
254
268
|
|
|
269
|
+
// Origin tracing is the default (see runAudit); --no-origins opts out.
|
|
270
|
+
const origins = flags['no-origins'] !== true;
|
|
271
|
+
|
|
255
272
|
const { facts, evaluated, notEvaluated, measured } = await collectHistoryFacts({
|
|
256
273
|
cwd: toplevel,
|
|
257
274
|
base,
|
|
258
275
|
head,
|
|
259
276
|
pickaxe: flags.pickaxe === true,
|
|
260
|
-
origins
|
|
277
|
+
origins,
|
|
261
278
|
lang
|
|
262
279
|
});
|
|
263
280
|
|
|
@@ -318,12 +335,42 @@ async function runCoverage(flags, lang) {
|
|
|
318
335
|
async function runDeps(flags, lang) {
|
|
319
336
|
const t = T(lang);
|
|
320
337
|
const cwd = path.resolve(flags.repo ? String(flags.repo) : process.cwd());
|
|
321
|
-
|
|
322
|
-
|
|
338
|
+
let lockfile = flags.lockfile ? String(flags.lockfile) : undefined;
|
|
339
|
+
let deps = asArray(flags.dep).map(String);
|
|
340
|
+
|
|
341
|
+
// --all: query every dependency the manifest mentions. Used by `euthyna
|
|
342
|
+
// audit`; also available directly. The manifest is located first so the
|
|
343
|
+
// names come from the same file the per-dep resolver will read.
|
|
344
|
+
if (flags.all === true) {
|
|
345
|
+
const target = lockfile
|
|
346
|
+
? { path: path.resolve(lockfile), type: manifestTypeForFile(path.basename(path.resolve(lockfile))) }
|
|
347
|
+
: await detectManifest(cwd);
|
|
348
|
+
if (!target?.type) {
|
|
349
|
+
return {
|
|
350
|
+
exit: EXIT.UNMEASURED,
|
|
351
|
+
error: t(
|
|
352
|
+
'--all 找不到受支持的依赖清单(package-lock.json / Cargo.lock / go.mod)',
|
|
353
|
+
'--all found no supported dependency manifest (package-lock.json / Cargo.lock / go.mod)'
|
|
354
|
+
)
|
|
355
|
+
};
|
|
356
|
+
}
|
|
357
|
+
const text = await readFile(target.path, 'utf8');
|
|
358
|
+
deps = listDependencies(target.type, text);
|
|
359
|
+
lockfile = target.path;
|
|
360
|
+
if (deps.length === 0) {
|
|
361
|
+
return {
|
|
362
|
+
exit: EXIT.UNMEASURED,
|
|
363
|
+
error: t(
|
|
364
|
+
`${target.path} 中没有枚举出任何依赖`,
|
|
365
|
+
`${target.path} enumerates no dependencies`
|
|
366
|
+
)
|
|
367
|
+
};
|
|
368
|
+
}
|
|
369
|
+
}
|
|
323
370
|
|
|
324
371
|
// Asking for nothing is a caller mistake, not a measurement failure.
|
|
325
372
|
if (deps.length === 0) {
|
|
326
|
-
return { exit: EXIT.USAGE, error: t('deps 需要至少一个 --dep <name
|
|
373
|
+
return { exit: EXIT.USAGE, error: t('deps 需要至少一个 --dep <name>(或 --all)', 'deps requires at least one --dep <name> (or --all)') };
|
|
327
374
|
}
|
|
328
375
|
|
|
329
376
|
const { facts, evaluated, notEvaluated, measured } = await collectDependencyFacts({
|
|
@@ -343,6 +390,109 @@ async function runDeps(flags, lang) {
|
|
|
343
390
|
|
|
344
391
|
return { exit: measured ? EXIT.CLEAN : EXIT.UNMEASURED, report };
|
|
345
392
|
}
|
|
393
|
+
/**
|
|
394
|
+
* `euthyna audit` — the change-surface measurement in one command.
|
|
395
|
+
*
|
|
396
|
+
* history (deleted-code provenance) + deps (every lockfile dependency), one
|
|
397
|
+
* combined report, exit code = the worse of the two. This is the wiring the
|
|
398
|
+
* README called "not yet built": an agent loads the skill and runs ONE command
|
|
399
|
+
* instead of being told which producers to invoke in which order.
|
|
400
|
+
*
|
|
401
|
+
* coverage is deliberately not part of audit: it needs a coverage data file and
|
|
402
|
+
* symbol names only the adjudicator knows, so there is nothing honest to
|
|
403
|
+
* auto-measure there.
|
|
404
|
+
*/
|
|
405
|
+
async function runAudit(flags, lang) {
|
|
406
|
+
const t = T(lang);
|
|
407
|
+
const cwd = path.resolve(flags.repo ? String(flags.repo) : process.cwd());
|
|
408
|
+
|
|
409
|
+
if (!flags.base) {
|
|
410
|
+
return { exit: EXIT.USAGE, error: t('audit 需要 --base <rev>', 'audit requires --base <rev>') };
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
// --- history half (mirrors runHistory's repo/base/head resolution) ---
|
|
414
|
+
const toplevel = await repoToplevel(cwd);
|
|
415
|
+
if (!toplevel) {
|
|
416
|
+
return {
|
|
417
|
+
exit: EXIT.UNMEASURED,
|
|
418
|
+
report: makeReport({
|
|
419
|
+
producer: producerFor(AUDIT_PRODUCER, lang),
|
|
420
|
+
subject: { repo: cwd },
|
|
421
|
+
facts: [],
|
|
422
|
+
notEvaluated: [
|
|
423
|
+
{
|
|
424
|
+
kind: KIND.HISTORY,
|
|
425
|
+
reason: t(
|
|
426
|
+
`${cwd} 不在任何 git 仓库内,无法测量历史`,
|
|
427
|
+
`${cwd} is not inside any git repository; history cannot be measured`
|
|
428
|
+
)
|
|
429
|
+
}
|
|
430
|
+
]
|
|
431
|
+
})
|
|
432
|
+
};
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
const base = await revParse(toplevel, String(flags.base));
|
|
436
|
+
if (!base) {
|
|
437
|
+
return {
|
|
438
|
+
exit: EXIT.USAGE,
|
|
439
|
+
error: t(`无法解析 --base "${flags.base}" 为一个提交`, `cannot resolve --base "${flags.base}" to a commit`)
|
|
440
|
+
};
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
const head = await revParse(toplevel, String(flags.head ?? 'HEAD'));
|
|
444
|
+
if (!head) {
|
|
445
|
+
return {
|
|
446
|
+
exit: EXIT.USAGE,
|
|
447
|
+
error: t(`无法解析 --head "${flags.head ?? 'HEAD'}" 为一个提交`, `cannot resolve --head "${flags.head ?? 'HEAD'}" to a commit`)
|
|
448
|
+
};
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
const origins = flags['no-origins'] !== true;
|
|
452
|
+
const history = await collectHistoryFacts({
|
|
453
|
+
cwd: toplevel,
|
|
454
|
+
base,
|
|
455
|
+
head,
|
|
456
|
+
pickaxe: flags.pickaxe === true,
|
|
457
|
+
origins,
|
|
458
|
+
lang
|
|
459
|
+
});
|
|
460
|
+
|
|
461
|
+
// --- deps half: measure EVERY dependency the manifest pins, not a hand-picked list ---
|
|
462
|
+
// runDeps reports "no manifest / manifest unreadable" through {error}; that is
|
|
463
|
+
// a not-evaluated criterion here, not something to swallow silently.
|
|
464
|
+
const depResult = await runDeps({ repo: toplevel, all: true }, lang);
|
|
465
|
+
const depFacts = depResult.report?.facts ?? [];
|
|
466
|
+
const depEvaluated = depResult.report?.coverage?.evaluated ?? [];
|
|
467
|
+
const depNotEvaluated = depResult.report?.coverage?.notEvaluated ?? [];
|
|
468
|
+
if (depResult.error) {
|
|
469
|
+
depNotEvaluated.push({ kind: KIND.DEPENDENCY, reason: depResult.error });
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
const facts = [...history.facts, ...depFacts];
|
|
473
|
+
const evaluated = [...history.evaluated, ...depEvaluated];
|
|
474
|
+
const notEvaluated = [...history.notEvaluated, ...depNotEvaluated];
|
|
475
|
+
|
|
476
|
+
const report = makeReport({
|
|
477
|
+
producer: producerFor(AUDIT_PRODUCER, lang),
|
|
478
|
+
subject: { repo: toplevel, base, head },
|
|
479
|
+
facts,
|
|
480
|
+
evaluated,
|
|
481
|
+
notEvaluated
|
|
482
|
+
});
|
|
483
|
+
|
|
484
|
+
// Exit = the worse of the two halves. A missing lockfile keeps its
|
|
485
|
+
// notEvaluated reason (missing data is not clean) but cannot fail a run the
|
|
486
|
+
// history half answered.
|
|
487
|
+
const flagged = facts.some(
|
|
488
|
+
f => (f.kind === KIND.HISTORY || f.kind === KIND.REINTRODUCTION) &&
|
|
489
|
+
f.detail && f.detail.classification === 'security'
|
|
490
|
+
);
|
|
491
|
+
const exit = !history.measured ? EXIT.UNMEASURED : flagged ? EXIT.FLAGGED : EXIT.CLEAN;
|
|
492
|
+
|
|
493
|
+
return { exit, report };
|
|
494
|
+
}
|
|
495
|
+
|
|
346
496
|
async function runGate(flags, positional, lang) {
|
|
347
497
|
const t = T(lang);
|
|
348
498
|
const file = positional[1];
|
|
@@ -501,6 +651,8 @@ export async function main(argv = process.argv.slice(2)) {
|
|
|
501
651
|
let result;
|
|
502
652
|
if (command === 'history') {
|
|
503
653
|
result = await runHistory(flags, lang);
|
|
654
|
+
} else if (command === 'audit') {
|
|
655
|
+
result = await runAudit(flags, lang);
|
|
504
656
|
} else if (command === 'coverage') {
|
|
505
657
|
result = await runCoverage(flags, lang);
|
|
506
658
|
} else if (command === 'deps') {
|
package/src/facts/deps.js
CHANGED
|
@@ -200,6 +200,61 @@ function typeForFile(name) {
|
|
|
200
200
|
return SUPPORTED.find((s) => s.file === name)?.type ?? null;
|
|
201
201
|
}
|
|
202
202
|
|
|
203
|
+
/**
|
|
204
|
+
* Every dependency name a manifest mentions, deduplicated and sorted.
|
|
205
|
+
*
|
|
206
|
+
* Powers `euthyna audit`, which measures the whole dependency surface without
|
|
207
|
+
* anyone typing 200 `--dep` flags. Names only — version facts still come from
|
|
208
|
+
* the same resolver path a per-dep query uses, so audit adds no new parsing.
|
|
209
|
+
*/
|
|
210
|
+
export function listDependencies(type, text) {
|
|
211
|
+
const names = new Set();
|
|
212
|
+
if (type === 'npm') {
|
|
213
|
+
const root = JSON.parse(text);
|
|
214
|
+
for (const key of Object.keys(root.packages ?? {})) {
|
|
215
|
+
if (!key.includes('node_modules/')) continue;
|
|
216
|
+
const name = key.split('node_modules/').pop();
|
|
217
|
+
if (name) names.add(name);
|
|
218
|
+
}
|
|
219
|
+
const walk = (tree) => {
|
|
220
|
+
for (const [name, meta] of Object.entries(tree ?? {})) {
|
|
221
|
+
if (!meta || typeof meta !== 'object') continue;
|
|
222
|
+
if (meta.version) names.add(name);
|
|
223
|
+
if (meta.dependencies && typeof meta.dependencies === 'object') walk(meta.dependencies);
|
|
224
|
+
}
|
|
225
|
+
};
|
|
226
|
+
walk(root.dependencies);
|
|
227
|
+
} else if (type === 'cargo') {
|
|
228
|
+
for (const block of text.split('[[package]]')) {
|
|
229
|
+
const name = /^\s*name\s*=\s*"([^"]+)"/m.exec(block);
|
|
230
|
+
if (name) names.add(name[1]);
|
|
231
|
+
}
|
|
232
|
+
} else if (type === 'go') {
|
|
233
|
+
let inBlock = false;
|
|
234
|
+
for (const raw of text.split('\n')) {
|
|
235
|
+
const line = raw.trim();
|
|
236
|
+
if (line.startsWith('require (')) { inBlock = true; continue; }
|
|
237
|
+
if (line === ')') { inBlock = false; continue; }
|
|
238
|
+
let match = /^require\s+(\S+)\s+v[\w.+-]+/.exec(line);
|
|
239
|
+
if (!match && inBlock) match = /^(\S+)\s+v[\w.+-]+/.exec(line);
|
|
240
|
+
if (match) names.add(match[1]);
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
return [...names].sort();
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
export { SUPPORTED as SUPPORTED_MANIFESTS, UNSUPPORTED as UNSUPPORTED_MANIFESTS };
|
|
247
|
+
|
|
248
|
+
/** First supported manifest found in cwd, or null: {path, type}. */
|
|
249
|
+
export async function detectManifest(cwd) {
|
|
250
|
+
return (await detectIn(cwd))[0] ?? null;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
/** Manifest type for a filename ('npm' | 'cargo' | 'go'), or null when unsupported. */
|
|
254
|
+
export function manifestTypeForFile(name) {
|
|
255
|
+
return typeForFile(name);
|
|
256
|
+
}
|
|
257
|
+
|
|
203
258
|
async function detectIn(cwd) {
|
|
204
259
|
const found = [];
|
|
205
260
|
for (const spec of SUPPORTED) {
|
package/src/facts/history.js
CHANGED
|
@@ -187,11 +187,12 @@ export function classifySubject(subject, { securityPattern = SECURITY_PATTERN, f
|
|
|
187
187
|
* Attribution semantics, stated explicitly because they are the point of this
|
|
188
188
|
* function and easy to over-claim:
|
|
189
189
|
*
|
|
190
|
-
* -
|
|
191
|
-
*
|
|
192
|
-
*
|
|
193
|
-
*
|
|
194
|
-
*
|
|
190
|
+
* - origins (the default) answers "who FIRST introduced the line's content"
|
|
191
|
+
* by probing `git log -S` and re-attributes the line when the introducing
|
|
192
|
+
* commit differs and classifies security or fix. Pass origins=false for
|
|
193
|
+
* plain blame semantics ("who last touched the line") — a security-fix line
|
|
194
|
+
* that a later formatting commit moved is then attributed to the formatter,
|
|
195
|
+
* not the fix, which is exactly the mis-attribution origins exists to catch.
|
|
195
196
|
* - classification looks at the commit message first, then at whether the
|
|
196
197
|
* deleted line content itself is security vocabulary, then at whether the
|
|
197
198
|
* commit's own diff touched security-vocabulary lines. All three err broad
|
|
@@ -204,8 +205,8 @@ export function classifySubject(subject, { securityPattern = SECURITY_PATTERN, f
|
|
|
204
205
|
* @param {string} options.head
|
|
205
206
|
* @param {boolean} [options.pickaxe] also look for reintroduced lines
|
|
206
207
|
* @param {number} [options.maxPickaxe] cap on pickaxe probes (reported when hit)
|
|
207
|
-
* @param {boolean} [options.origins]
|
|
208
|
-
*
|
|
208
|
+
* @param {boolean} [options.origins=true] re-attribute deleted lines to the commit
|
|
209
|
+
* that first introduced their content
|
|
209
210
|
* @param {number} [options.maxOrigins] cap on origin probes (reported when hit)
|
|
210
211
|
*/
|
|
211
212
|
export async function collectHistoryFacts({
|
|
@@ -214,7 +215,7 @@ export async function collectHistoryFacts({
|
|
|
214
215
|
head = 'HEAD',
|
|
215
216
|
pickaxe = false,
|
|
216
217
|
maxPickaxe = 40,
|
|
217
|
-
origins =
|
|
218
|
+
origins = true,
|
|
218
219
|
maxOrigins = 40,
|
|
219
220
|
securityPattern = SECURITY_PATTERN,
|
|
220
221
|
fixPattern = FIX_PATTERN,
|
|
@@ -407,9 +408,9 @@ export async function collectHistoryFacts({
|
|
|
407
408
|
notEvaluatedEntry(
|
|
408
409
|
'history-origins',
|
|
409
410
|
t(
|
|
410
|
-
'
|
|
411
|
+
'--no-origins 已关闭来源追溯,删除行的归属基于 git blame 的「最后修改」语义;' +
|
|
411
412
|
'被删行本身或提交 diff 含安全关键词时仍会标为 security',
|
|
412
|
-
'--origins
|
|
413
|
+
'--no-origins disabled origin tracing; deleted lines are attributed by git blame\'s "last modified" semantics; ' +
|
|
413
414
|
'lines whose content or commit diff contains security vocabulary are still classified security'
|
|
414
415
|
)
|
|
415
416
|
)
|