Skip to content

fix(verify): secret 门禁加上精确 (path, literal) 假凭据夹具登记簿——放行面从「零」变成 13 条逐字登记、逐条带 owner/review/reason,让脱敏测试重新可搬家,并把 #2296 刻意留下的 13 个 TestSanitizeSubAgentResult_* 搬出巨石文件(ADR-028,#2295) - #2307

Merged
DeliciousBuding merged 1 commit into
masterfrom
fix/secret-fixture-allowlist
Sep 3, 2026

Conversation

@DeliciousBuding

Copy link
Copy Markdown
Collaborator

缺陷本体(实测复现,不是推断)

check-secrets.sh 的六条字面量规则对 diff 新增行无条件生效,而脱敏测试的被测对象
恰恰是「真形状的假凭据」⇒ 这类测试文件结构上不可重构。把 #2296 刻意留下的 13 个
TestSanitizeSubAgentResult_* 逐字搬到 subagent_result_sanitize_test.go、登记簿缺席时实测:

secret guard: …/subagent_result_sanitize_test.go:55: possible API key detected
…(55/56/57/58/62/63/179/204/252 共 **9 行**判红)

安全测试因为安全门禁本身而不可重构。#2296 因此把 13 个 Test 留在 546 行的巨石文件里;
本 PR 用登记簿把它们搬出来(546 → 188 行 + 新文件 364 行)。

放行面怎么收窄的(ADR-028)

scripts/verify/secret-fixture-allowlist.json:条目 = 精确 (path, literal) +
强制非空 owner / review / reason

  • 禁止目录级 / glob / 包级 / regex 豁免——加载器对 *?[]\、绝对路径、..
    尾斜杠一律 fail-closed;
  • literal 必须逐字等于某条字面量规则的贪婪匹配结果,且确实出现在登记路径里
    (同 literal 换路径 = 红;死条目 = 红);
  • maxEntries 是计数棘轮:13 条,增长必须在同一 PR 里显式抬高它,让评审看见放行面变宽;
  • 只有这六条字面量规则可登记:私钥块(含空白,而登记器禁止空白 literal)、
    secret-like 赋值规则、敏感路径规则都不可登记 ⇒ 放行面不因本机制变宽;
  • 登记簿自己没有路径豁免:它也走同一套字面量规则,里面出现的凭据形状字面量必须是
    已登记 literal 之一 —— 往 reason 里塞一个未登记的真凭据同样判红。

顺带修一处真实漏检:旧实现每规则每行只看第一处正则匹配,所以「已登记夹具 +
未登记真凭据」同一行时后者被前者掩盖。改成对一行里的每一处匹配都判一次。

暗卷(11 发变异,逐发证明断言真的在管)

变异 翻红的自测
M1 完全不查登记簿 registered (path, literal) fixture passes
M2 去掉「登记簿自身按已登记 literal 放行」 同上(证明它靠规则、不靠路径豁免)
M3 每行只看第一处匹配 unregistered literal after a registered one on the same line fails
M4 加载器不校验 literal 是否真在登记路径 registered literal absent from its path fails
M5 加载器不校验 maxEntries entry count over maxEntries fails
M6c 加载器对树不做任何校验 non-existent path / literal absent / glob path 三条一起翻
M7 加载器不拒绝死条目 dead entry (literal is not a credential shape) fails
M8 加载器不拒绝空字段 entry with an empty owner fails
M9 只按 literal 匹配、忽略 path registered literal moved to another path fails
M10 只按 path 匹配、忽略 literal 3 条一起翻
M11 只去掉 isfile 检查 不翻(如实登记:open() 的 OSError die 是第二道 fail-closed,与它冗余)

自测 23 → 36 全绿。其中 7 条完整性用例只 stage 登记簿自己、夹具文件只留在磁盘上,
这样退出码非 0 只可能来自加载器校验——第一版把它们和夹具一起 stage,实测会因为
「夹具字面量未登记」这条无关路径而假绿(M4~M8 五发变异一发都不翻)。

搬家守恒(13 个 Test,0 行 src 改动)

  • Test 名集合:21 = 8 + 13,sorted(old) == sorted(new1+new2),missing/extra 均为空;
  • 非空行多重集合:A-only = 0 行,B-only = 5 行 = 新文件 header 6 行减去从旧文件
    移走的 "unicode/utf8"(两文件共有,抵消);
  • 546 → 188 + 364 = 552 行,+6 = header 8 − import 1 − gofmt 收掉的连续空行 1;
  • 整包 go test -v=== RUN 379 = 379、--- PASS 378 = 378、--- SKIP 1 = 1、
    FAIL 0 = 0,且两次 === RUN 名称集合的 diff 为空;
  • 这 13 个 Test 里没有 time.Sleep ⇒ test-sleep 双基线无需改动。

leak_guard 取证(本地 hook 用 LEAK_GUARD_SKIP=1 提交,没有改夹具内容绕过)

登记簿必须逐字写明被放行的 literal,本地 hook 会拦其中 5 条(ghp_ ×2、AKIA ×2、JWT ×1)。
取证:13 条 literal 全部已在 origin/master 的跟踪文件里逐字存在git grep -F 逐条
实测),本 PR 没有给仓库新增任何一个凭据形状字符串,只是把其中 8 条从
process_executor_test.go 搬到同包的 subagent_result_sanitize_test.go;CI 门禁对同一份
staged 内容 check-secrets.sh --staged rc=0;且这 13 条无一为真凭据
(AKIAIOSFODNN7EXAMPLE 是 AWS 官方文档示例 key,那条 JWT 是 jwt.io 公开调试令牌
sub=1234567890,其余是 abc123def456 / 1234567890abcdefghijklmnopqrstuvwxyz 合成阶梯)。

Co-authored-by: Cursor cursor@vectorcontrol.tech

…登记、逐条带 owner/review/reason,让脱敏测试重新可搬家,并把 #2296 刻意留下的 13 个 TestSanitizeSubAgentResult_* 搬出巨石文件(ADR-028,#2295)

## 缺陷本体(实测复现,不是推断)

check-secrets.sh 的六条字面量规则对 diff **新增行**无条件生效,而脱敏测试的被测对象
恰恰是「真形状的假凭据」⇒ 这类测试文件结构上不可重构。把 #2296 刻意留下的 13 个
TestSanitizeSubAgentResult_* 逐字搬到 subagent_result_sanitize_test.go、登记簿缺席时实测:

    secret guard: …/subagent_result_sanitize_test.go:55: possible API key detected
    …(55/56/57/58/62/63/179/204/252 共 **9 行**判红)

安全测试因为安全门禁本身而不可重构。#2296 因此把 13 个 Test 留在 546 行的巨石文件里;
本 PR 用登记簿把它们搬出来(546 → 188 行 + 新文件 364 行)。

## 放行面怎么收窄的(ADR-028)

`scripts/verify/secret-fixture-allowlist.json`:条目 = 精确 `(path, literal)` +
强制非空 `owner` / `review` / `reason`。

* **禁止**目录级 / glob / 包级 / regex 豁免——加载器对 `*?[]\`、绝对路径、`..`、
  尾斜杠一律 fail-closed;
* `literal` 必须**逐字**等于某条字面量规则的贪婪匹配结果,且**确实出现在**登记路径里
  (同 literal 换路径 = 红;死条目 = 红);
* `maxEntries` 是计数棘轮:13 条,增长必须在同一 PR 里显式抬高它,让评审看见放行面变宽;
* **只有这六条字面量规则可登记**:私钥块(含空白,而登记器禁止空白 literal)、
  secret-like 赋值规则、敏感路径规则都不可登记 ⇒ 放行面不因本机制变宽;
* **登记簿自己没有路径豁免**:它也走同一套字面量规则,里面出现的凭据形状字面量必须是
  已登记 literal 之一 —— 往 `reason` 里塞一个未登记的真凭据同样判红。

顺带修一处真实漏检:旧实现每规则每行只看**第一处**正则匹配,所以「已登记夹具 +
未登记真凭据」同一行时后者被前者掩盖。改成对一行里的每一处匹配都判一次。

## 暗卷(11 发变异,逐发证明断言真的在管)

| 变异 | 翻红的自测 |
|---|---|
| M1 完全不查登记簿 | registered (path, literal) fixture passes |
| M2 去掉「登记簿自身按已登记 literal 放行」 | 同上(证明它靠规则、不靠路径豁免) |
| M3 每行只看第一处匹配 | unregistered literal after a registered one on the same line fails |
| M4 加载器不校验 literal 是否真在登记路径 | registered literal absent from its path fails |
| M5 加载器不校验 maxEntries | entry count over maxEntries fails |
| M6c 加载器对树不做任何校验 | non-existent path / literal absent / glob path 三条一起翻 |
| M7 加载器不拒绝死条目 | dead entry (literal is not a credential shape) fails |
| M8 加载器不拒绝空字段 | entry with an empty owner fails |
| M9 只按 literal 匹配、忽略 path | registered literal moved to another path fails |
| M10 只按 path 匹配、忽略 literal | 3 条一起翻 |
| M11 只去掉 isfile 检查 | **不翻**(如实登记:`open()` 的 OSError die 是第二道 fail-closed,与它冗余) |

自测 23 → **36 全绿**。其中 7 条完整性用例**只 stage 登记簿自己**、夹具文件只留在磁盘上,
这样退出码非 0 只可能来自加载器校验——第一版把它们和夹具一起 stage,实测会因为
「夹具字面量未登记」这条无关路径而假绿(M4~M8 五发变异一发都不翻)。

## 搬家守恒(13 个 Test,0 行 src 改动)

* Test 名集合:21 = 8 + 13,`sorted(old) == sorted(new1+new2)`,missing/extra 均为空;
* 非空行多重集合:A-only = **0 行**,B-only = 5 行 = 新文件 header 6 行减去从旧文件
  移走的 `"unicode/utf8"`(两文件共有,抵消);
* 546 → 188 + 364 = 552 行,+6 = header 8 − import 1 − gofmt 收掉的连续空行 1;
* 整包 `go test -v`:`=== RUN` 379 = 379、`--- PASS` 378 = 378、`--- SKIP` 1 = 1、
  FAIL 0 = 0,且两次 `=== RUN` 名称集合的 `diff` 为空;
* 这 13 个 Test 里没有 `time.Sleep` ⇒ test-sleep 双基线无需改动。

## leak_guard 取证(本地 hook 用 LEAK_GUARD_SKIP=1 提交,没有改夹具内容绕过)

登记簿必须逐字写明被放行的 literal,本地 hook 会拦其中 5 条(ghp_ ×2、AKIA ×2、JWT ×1)。
取证:13 条 literal **全部已在 origin/master 的跟踪文件里逐字存在**(`git grep -F` 逐条
实测),本 PR 没有给仓库新增任何一个凭据形状字符串,只是把其中 8 条从
process_executor_test.go 搬到同包的 subagent_result_sanitize_test.go;CI 门禁对同一份
staged 内容 `check-secrets.sh --staged` rc=0;且这 13 条无一为真凭据
(AKIAIOSFODNN7EXAMPLE 是 AWS 官方文档示例 key,那条 JWT 是 jwt.io 公开调试令牌
sub=1234567890,其余是 abc123def456 / 1234567890abcdefghijklmnopqrstuvwxyz 合成阶梯)。

Co-authored-by: Cursor <cursor@vectorcontrol.tech>
@coderabbitai

coderabbitai Bot commented Sep 3, 2026

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Team

Run ID: 83327eef-23c6-47d3-b642-17a942f29525

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant