Skip to content

ci(deps): OSV 门禁的逃生口立三条约定,并把前两条做成机械门禁 (#4965) - #5036

Merged
xuyushun441-sys merged 1 commit into
mainfrom
claude/issue-4965-osv-exemption-conventions
Aug 4, 2026
Merged

ci(deps): OSV 门禁的逃生口立三条约定,并把前两条做成机械门禁 (#4965)#5036
xuyushun441-sys merged 1 commit into
mainfrom
claude/issue-4965-osv-exemption-conventions

Conversation

@xuyushun441-sys

Copy link
Copy Markdown
Contributor

Fixes #4965

OSV 门禁对无修复版本的公告,唯一出路是 osv-scanner.toml[[IgnoredVulns]]。但仓库里此前没有这个文件 —— 逃生口一次也没走过,也就没有任何约定:谁能加、写什么理由、多久复核。没有约定的逃生口会在第一次紧急情况下被随手用坏,然后永久留在那里。

本 PR 按维护者裁决落三条约定,前两条做成机械门禁,第三条明确标记为纪律

落点

文件 作用
osv-scanner.toml(新) 零豁免台账。三条约定写成文件头注释 + 一个注释掉的模板条目(id / ignoreUntil / reason 格式)
scripts/check-osv-exemptions.mjs(新) 约定 1、2 的强制门禁,带 --self-test 双向自证
.github/workflows/validate-deps.yml 新增 check step(排在 scanner 之前);注释从"指向一个不存在的文件"改为指向新台账与三条约定;paths 触发器补上这两个新文件
.changeset/osv-exemption-conventions.md 空 frontmatter,仅 CI,不发布任何包

package.json 未改动,没有新增依赖 —— 脚本零依赖,TOML 子集手写解析(与 check-override-consistency.mjs 手写 pnpm-workspace.yaml 解析同一套做法)。

为什么必须有脚本:对 osv-scanner v2.3.8 的实测

用真实二进制 + 离线 OSV 库,在一个故意命中 5 条公告的 lockfile 上逐项验证:

台账写法 scanner v2.3.8 的行为
ignoreUntil 公告被过滤、退出 0、无任何告警,永久有效
ignoreUntil 已过期 公告重新报出、退出 1
reason = "probe" 照单全收 —— scanner 从不读 reason
ignoreUntil 加引号 整个配置文件被丢弃("Ignored invalid config file ... toml: ..."),扫描照跑

源码侧对应 internal/config/config.goshouldIgnoreTimestamp:IgnoreUntil 为零值时直接返回"应当忽略"。

结论(回答 issue 的三个问题):到期转红是 scanner 原生行为,机制本身不缺 —— 缺的是"必须填"。所以脚本不重复实现过期判定的语义,只保证每条豁免都带着一个合法、未过期、且不荒唐的日期存在,让原生机制真的有东西可到期。

约定 1 的机械形状

  • ignoreUntil 必填,必须是裸 TOML 日期(加引号会静默废掉整份文件里其它所有豁免);
  • 必须未过期(等于今天即判红 —— scanner 那时已经停止忽略了);
  • 默认 30 天,硬上限 90 天。上限是维护者三条裁决之外我补的一个推论,请一并裁:没有上限的话,ignoreUntil = 2099-01-01 同样满足"必须带过期时间",只是给永久豁免戴了顶帽子。上限只会随时间推移越来越容易满足,不会把当初写下时合规的条目判红。数值在脚本顶部 MAX_WINDOW_DAYS 一行可调。

约定 2 的机械形状

reason 必填,且必须含至少一个 https:// advisory 链接 + 链接之外至少 40 字符的真正理由。判据是"复核的人能不能据此判断前提是否还成立":Not exploitable 六个字给不了任何可核对的东西。

另外拒绝 [[IgnoredVulns]] 以外的表:[[PackageOverrides]](ignore = true)同样能压掉发现,而三条约定完全没有覆盖它 —— 留着就是绕过约定的现成后门。真要用,先定它的约定,再教这个脚本认它。

约定 3:纪律,不是门禁

"豁免必须单独成 PR、打 osv-exemption 标签"写进了台账文件头(加豁免的人必然读到)与 workflow 注释,并在有豁免存在时由脚本每次打印提醒 —— 但没有做成门禁。原因是便宜的机械形状会误伤好情形:"osv-scanner.toml 必须单独改动"会把"终于修好依赖、顺手删掉豁免"的 PR 也判红,而那正是我们最想鼓励的 PR。因此规则表述为:删除豁免可以夹带在修复 PR 里,新增或续期不行。

osv-exemption 标签当前不存在(已确认 404),需要维护者建一个 —— 或者告诉我改用现成标签。

self-test(双向)

$ node scripts/check-osv-exemptions.mjs --self-test
check-osv-exemptions self-test (both directions):
  ✓ missing/empty ledger → green
  ✓ comments-only ledger (zero exemptions) → green
  ✓ well-formed exemption inside the window → green
  ✓ multi-line reason → green
  ✓ expired ignoreUntil → red
  ✓ ignoreUntil == today → red (the scanner already stopped ignoring it)
  ✓ missing ignoreUntil → red
  ✓ quoted ignoreUntil → red
  ✓ ignoreUntil beyond the ceiling → red
  ✓ reason without an advisory link → red
  ✓ reason that is only a link → red
  ✓ untouched template placeholders → red
  ✓ missing reason → red
  ✓ unknown key → red
  ✓ duplicate id → red
  ✓ [[PackageOverrides]] escape hatch → red
  ✓ top-level key outside a table → red

✓ self-test passed: valid ledgers accepted, every convention breach rejected.

零豁免状态下门禁行为不变(实测)

同一份 pnpm-lock.yaml,带 / 不带本文件,scanner 结果逐条一致:

=== WITH ledger ===
Loaded filter from: /home/user/objectstack-4965/osv-scanner.toml
Total 4 packages affected by 8 known vulnerabilities (0 Critical, 2 High, 6 Medium, 0 Low, 0 Unknown) from 1 ecosystem.
EXIT_WITH=1

=== WITHOUT ledger ===
Total 4 packages affected by 8 known vulnerabilities (0 Critical, 2 High, 6 Medium, 0 Low, 0 Unknown) from 1 ecosystem.
EXIT_WITHOUT=1

Loaded filter from: 一行同时证明了根目录的 osv-scanner.toml 确实被 --lockfile=pnpm-lock.yaml 这条扫描路径发现(scanner 只读被扫文件所在目录的配置,而 lockfile 就在根目录)。

端到端演示:脚本抓住了 scanner 放行的东西

在真实 lockfile 上临时插入一条缺 ignoreUntil 的豁免:

--- scanner ---
GHSA-7p8r-x3mc-p8w7 and 1 alias have been filtered out because: ...
Filtered 1 vulnerability from output          ← scanner 接受,永久生效

--- checker ---
  - exemption #1 (line 87): no `ignoreUntil`. OSV-Scanner reads a missing expiry
    as "ignore forever" and says nothing — that is precisely the silent permanent
    exemption #4965 forbids. Set one (default 30 days: `ignoreUntil = 2026-09-03`).
CHECKER_EXIT=1

改为已过期的 ignoreUntil = 2026-07-01:scanner 重新报出该公告(原生到期),checker 同时以 EXPIRED 34 day(s) ago 判红,并要求重新裁决而不是顺手延期。

顺带说明(不在本 PR 范围)

本地扫描显示 main 当前有 8 条都有修复版本的公告(undici / hono / fast-uri),即 OSV 门禁此刻是红的;已由 #5032 记录,本 PR 不碰 —— 那些是"有修复就吃修复"的情形,恰恰不该走豁免。

验证命令

node scripts/check-osv-exemptions.mjs --self-test   # 17/17 通过
node scripts/check-osv-exemptions.mjs               # ✓ zero exemptions
npx eslint scripts/check-osv-exemptions.mjs         # 干净

Generated by Claude Code

OSV 门禁对**无修复版本**的公告,唯一出路是 `osv-scanner.toml` 的
`[[IgnoredVulns]]`。但仓库里此前没有这个文件,逃生口一次也没走过,也就
没有任何约定:谁能加、写什么理由、多久复核。没有约定的逃生口会在第一次
紧急情况下被随手用坏,然后永久留在那里。

新增零豁免的 `osv-scanner.toml`(三条约定写在文件头 + 一个注释掉的模板
条目)与 `scripts/check-osv-exemptions.mjs`。约定 1、2 机械强制,约定 3
是纪律。

对 osv-scanner v2.3.8 实测(真实二进制 + 离线 OSV 库,故意让 5 条公告命中):

  - 缺 `ignoreUntil` → 公告被过滤、退出 0、无任何告警,永久有效。
    静默的永久豁免与永远红的 job 是同一个失败模式,只是更安静。
  - `ignoreUntil` 已过期 → 公告重新报出、退出 1。**到期转红是 scanner 原生
    行为**,所以缺的从来不是过期机制,而是"必须填"这件事。
  - `reason = "probe"` → 照单全收,scanner 从不读它。
  - `ignoreUntil` 加了引号 → **整个配置文件被丢弃**("Ignored invalid config
    file"),扫描照跑;一个引号能让文件里其它所有豁免一起静默失效。

因此 check 脚本要求:`ignoreUntil` 必填、必须是裸 TOML 日期、未过期、且不
超过 90 天上限(默认 30 天 —— 没有上限的话 `2099-01-01` 同样满足"必须带
过期时间",只是给永久豁免戴了顶帽子);`reason` 必填,须含 advisory 链接
与链接之外一句真正的理由;并拒绝 `[[IgnoredVulns]]` 以外的表(
`[[PackageOverrides]]` 也能压掉发现,而三条约定没有覆盖它)。

`--self-test` 双向自证:17 个用例,合法台账判绿、每一条约定违反判红。

零豁免状态下门禁行为与改动前逐条一致:同一 lockfile,带/不带本文件都是
"4 packages / 8 vulnerabilities / exit 1",且日志出现
"Loaded filter from: <repo>/osv-scanner.toml",证明根目录配置确实被发现。

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018iARDqtrhQgz6fVHDeDkbQ
@vercel

vercel Bot commented Aug 4, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

1 Skipped Deployment
Project Deployment Actions Updated (UTC)
objectstack Ignored Ignored Aug 4, 2026 12:34am

Request Review

@github-actions github-actions Bot added documentation Improvements or additions to documentation ci/cd tooling size/l labels Aug 4, 2026

Copy link
Copy Markdown
Contributor Author

CI 结果说明(本 PR 的 Validate Package Dependencies 是红的,但不是本 PR 造成的):

Verify OSV exemptions carry an expiry and a reason ........ success   ← 本 PR 新增的 step
Audit dependencies for known vulnerabilities (OSV-Scanner) . failure  ← 既有红,见 #5032

新增 step 在真实 CI 里的输出:

  ✓ [[PackageOverrides]] escape hatch → red
  ✓ top-level key outside a table → red

✓ self-test passed: valid ledgers accepted, every convention breach rejected.
✓ osv-scanner.toml holds zero OSV exemptions (the intended steady state).

scanner step 的失败与本分支无关:同一个 step 在 2026-08-03 19:55 的其它分支上就已经红了(run 30847850410),原因是 main 上 undici / hono / fast-uri 的 8 条公告 —— 全部有修复版本,由 #5032 跟进,正确解法是升级依赖,恰恰不该走本 PR 引入的豁免路径。

顺带,CI 的实时 OSV API 结果与我本地用离线库跑出的 8 条逐条一致,且日志里出现

Loaded filter from: /github/workspace/osv-scanner.toml

—— 在真实 runner 上再次确认了根目录台账确实被 --lockfile=pnpm-lock.yaml 这条扫描路径加载,而零豁免状态下扫描结果与加载前完全相同。


Generated by Claude Code

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

Labels

ci/cd documentation Improvements or additions to documentation size/l tooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

validate-deps: decide how the OSV gate should express an advisory with no fix available

2 participants