From 77ac14b4472b77fb1210ccf14b32fadd8df23612 Mon Sep 17 00:00:00 2001 From: ReSerendipity Date: Mon, 21 Sep 2026 15:49:20 +0800 Subject: [PATCH 1/2] =?UTF-8?q?fix(deps):=20=E4=B8=8B=E7=95=8C=E5=9B=9E?= =?UTF-8?q?=E5=88=B0=20transformers>=3D4.52.1,<4.53=EF=BC=8C=E4=B8=A4?= =?UTF-8?q?=E9=81=93=E5=AE=89=E5=85=A8=E9=97=A8=E7=A6=81=E6=94=B9=E4=B8=BA?= =?UTF-8?q?=E9=80=90=E6=9D=A1=E5=B8=A6=E7=90=86=E7=94=B1=E7=9A=84=E8=B1=81?= =?UTF-8?q?=E5=85=8D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A/B 实测(RTX 5070 Ti,2026-09-21):4.57.6 下 VoxCPM2 正常但 IndexTTS 2.5/2.0 的 infer_v2_5 / infer_v2 直接 ImportError;4.52.1 + tokenizers 0.21.0 下三引擎真推理全通 (2.5:214,040 B / RMS 6176;2.0:205,124 B / RMS 6926;VoxCPM2:230,148 B / RMS 4615)。 引擎发行元数据本身就写着 transformers==4.52.1 且 tokenizers==0.21.0,所以 >=4.57.0 那句 声明按它装出来的环境两个引擎起不来 —— 采信引擎侧,下界收回 <4.53。 代价是 pip-audit 与 Trivy 会扫到 4.52.4 的三条"4.53 已修"CVE。不整体关门禁,改成逐条豁免: - pip-audit:security.yml 里 5 个 --ignore-vuln PYSEC-*,号取自真实 CI 输出; - Trivy:新增 .trivyignore.yaml,3 条各自绑定 transformers 4.52.4 并带 expiration 2026-12-31(到期自动重新变红),docker-build / docker-publish 两处镜像扫描接上。 新增 tests/test_dependency_consistency.py 把三方钉在一起:锁集里每个 pin 必须满足 pyproject 声明(≥20 个包)、锁集必须仍是 transformers==4.52.1 + tokenizers==0.21.0 且声明带 <4.53、 豁免文件每条必须有 expiration 且每个 CVE/PYSEC 都要在分诊文档里出现 —— 防止以后再出现 "改了声明没人改锁集"或"豁免写了没人到期收"这类漂移。证据与可达性判定见 docs/SECURITY_DEPENDABOT_TRIAGE.md §1/§2。 GitHub 上那 20 条 Dependabot 告警的 dismissal 不在本 PR 内(属仓库所有者动作)。 Signed-off-by: ReSerendipity --- .github/workflows/docker-build.yml | 4 + .github/workflows/docker-publish.yml | 2 + .github/workflows/security.yml | 12 +- .trivyignore.yaml | 39 ++++++ docs/DOD.md | 8 +- docs/SECURITY_DEPENDABOT_TRIAGE.md | 45 ++++-- pyproject.toml | 23 +-- requirements.txt | 4 +- tests/test_dependency_consistency.py | 201 +++++++++++++++++++++++++++ 9 files changed, 309 insertions(+), 29 deletions(-) create mode 100644 .trivyignore.yaml create mode 100644 tests/test_dependency_consistency.py diff --git a/.github/workflows/docker-build.yml b/.github/workflows/docker-build.yml index e87fad5a..6f10ab92 100644 --- a/.github/workflows/docker-build.yml +++ b/.github/workflows/docker-build.yml @@ -63,6 +63,10 @@ jobs: exit-code: "1" severity: "CRITICAL,HIGH" ignore-unfixed: true + # 已接受风险清单(每条带理由与 expiration,到期自动重新变红): + # transformers 4.52.x 被引擎元数据钉死,见 .trivyignore.yaml 与 + # docs/SECURITY_DEPENDABOT_TRIAGE.md + ignorefile: ".trivyignore.yaml" # 只跑漏洞扫描:secret/misconfig 对 25GB 镜像是纯多余开销,会拖长导出与解包时间 scanners: vuln # 本项目镜像约 25GB 展开,Trivy 默认 5m 内部超时会在分析阶段被 diff --git a/.github/workflows/docker-publish.yml b/.github/workflows/docker-publish.yml index 47415438..33a033ce 100644 --- a/.github/workflows/docker-publish.yml +++ b/.github/workflows/docker-publish.yml @@ -97,6 +97,8 @@ jobs: severity: CRITICAL,HIGH exit-code: '1' ignore-unfixed: true + # 与 docker-build.yml 用同一份已接受风险清单(每条带理由与 expiration) + ignorefile: ".trivyignore.yaml" format: table # 只跑漏洞扫描:secret/misconfig 扫描对 25GB 镜像是纯多余开销,会拖长导出与解包时间 scanners: vuln diff --git a/.github/workflows/security.yml b/.github/workflows/security.yml index c659f517..f577c82b 100644 --- a/.github/workflows/security.yml +++ b/.github/workflows/security.yml @@ -31,7 +31,17 @@ jobs: - name: Scan dependencies for known vulnerabilities run: | - pip-audit -r requirements.txt --desc + # 已接受风险:transformers 4.52.x 被引擎元数据钉死(4.57.6 实测让 IndexTTS + # 2.0/2.5 的 infer_v2 / infer_v2_5 导入失败),这几条公告的可达性逐条判定见 + # docs/SECURITY_DEPENDABOT_TRIAGE.md §1,镜像侧同源豁免见 .trivyignore.yaml。 + # 只豁免这 5 个号;**任何新出现的公告仍会让本步变红**。 + # 号取自 run 35561594150 job 106215347173 的实际输出(transformers 4.52.4)。 + pip-audit -r requirements.txt --desc \ + --ignore-vuln PYSEC-2025-216 \ + --ignore-vuln PYSEC-2026-198 \ + --ignore-vuln PYSEC-2026-228 \ + --ignore-vuln PYSEC-2026-229 \ + --ignore-vuln PYSEC-2026-392 # 2026-08-16 本地评估 0 已知漏洞,解锁为门禁;依赖升级由 dependabot 自动跟进 bandit: diff --git a/.trivyignore.yaml b/.trivyignore.yaml new file mode 100644 index 00000000..64915244 --- /dev/null +++ b/.trivyignore.yaml @@ -0,0 +1,39 @@ +# Trivy 忽略清单 —— 只用于"已接受风险"的镜像漏洞豁免。 +# +# 为什么存在:本产品的运行环境被引擎真实约束钉在 transformers 4.52.x +# (IndexTTS 2.0/2.5 的发行元数据要求 transformers==4.52.1;实测 4.57.6 下 +# indextts.infer_v2 / infer_v2_5 直接 ImportError,见 docs/SECURITY_DEPENDABOT_TRIAGE.md §2)。 +# 4.52.x 因此带着下面这几条"要 5.x 才修"的公告。它们已在分诊文档里逐条判定为 +# **本仓不可达**(无 HF Trainer / 无 save_pretrained / 不接受用户指定模型源 / 只绑 127.0.0.1)。 +# +# 规则:这里只允许登记分诊文档 §1 表里已有条目的 id;每条必须给 expiration(到期即重新变红, +# 逼一次复审),并在上方注释里写清"为什么不可达"。新出现的公告**不在这里**,会照常红。 +# tests/test_dependency_consistency.py 会核对:本文件里的 id 与分诊文档、pip-audit 豁免集不脱节。 +vulnerabilities: + # CVE-2026-4372:transformers 远程代码执行类,触发前提是加载攻击者可控的仓库代码 + # (trust_remote_code 或远程 model 指向)。本仓权重全部来自 model/ 本地目录, + # 经人工确认 + SHA-256 复验后才入库(LOCAL_RULES 禁区流程);镜像内无远程模型入口。 + - id: CVE-2026-4372 + package: + name: transformers + versions: + - "4.52.4" + expiration: "2026-12-31T00:00:00+00:00" + + # CVE-2026-5241:模型初始化期的任意代码执行,载体是 LightGlue 一类视觉匹配模型。 + # 本仓只加载 TTS 语音模型(voxcpm / indextts / funasr / zipenhancer),无该代码路径。 + - id: CVE-2026-5241 + package: + name: transformers + versions: + - "4.52.4" + expiration: "2026-12-31T00:00:00+00:00" + + # CVE-2026-9856:save_pretrained 经 chat template 造成任意文件写。 + # 全仓无 save_pretrained 调用(grep 证据见分诊文档 §1 A8),音色保存走我们自己的目录写入。 + - id: CVE-2026-9856 + package: + name: transformers + versions: + - "4.52.4" + expiration: "2026-12-31T00:00:00+00:00" diff --git a/docs/DOD.md b/docs/DOD.md index 35699b20..a2a099f6 100644 --- a/docs/DOD.md +++ b/docs/DOD.md @@ -128,8 +128,12 @@ RMS 4615;每次卸载显存回到 ~3.5 GB)。故 `pyproject`/`requirements.txt` 里那句 `transformers>=4.57.0`(9-14 搭在一条只讲 gpu-smoke 的提交里进来的)站不住,但改回 4.52.x 会让 pip-audit 与 Trivy 两道 CI 安全门禁同时变红(扫到 4.52.4 的"4.53 已修"CVE)——**下界与引擎可用性 - 互斥**,本轮只落地无争议部分(锁集合法化 + 检查器接进 CI + 报错文案),下界原样保留并在 - `pyproject.toml` 里写清两条出路,岔口交所有者; + 互斥**,2026-09-21 定为**出路①并落地**:下界回到 `>=4.52.1,<4.53`(`tokenizers>=0.21.0,<0.22`), + 两道扫描器改成**逐条带理由 + 带 expiration 的已接受风险豁免**(号取自 CI 真实输出,见 + `docs/SECURITY_DEPENDABOT_TRIAGE.md` §1a;`.trivyignore.yaml` 三条 CVE 于 2026-12-31 到期, + 到期自动重新变红),并新增 `tests/test_dependency_consistency.py`(8 条)核对"声明 ↔ 锁 ↔ + 豁免清单"三者不互相漂移 —— 这正是原先缺位的那类"下界棘轮",也是这条错误下界能在 main 上 + 存活一周没人发现的原因; 引擎加载失败时的报错也不再断言"PyPI 无 indextts 包",改为带上底层 ImportError 与版本不匹配提示。 20 条 Dependabot 告警因此**没有一条能靠现在就升级消掉**,分诊见 `docs/SECURITY_DEPENDABOT_TRIAGE.md`。 * **已知缺口**:CI 冒烟 `scripts/gpu_smoke_minimal.py` 只覆盖 voxcpm2 + indextts2(走 OpenAI 口, diff --git a/docs/SECURITY_DEPENDABOT_TRIAGE.md b/docs/SECURITY_DEPENDABOT_TRIAGE.md index 323e4530..c338fc8f 100644 --- a/docs/SECURITY_DEPENDABOT_TRIAGE.md +++ b/docs/SECURITY_DEPENDABOT_TRIAGE.md @@ -23,6 +23,23 @@ 4 个需要 transformers 5.x(大版本),2 个需要 protobuf 越过 `descript-audiotools<3.20` / `modelscope<3.21` 两道上游上界。 +## 1a. 两道扫描器的豁免号是从哪来的(2026-09-21 实测日志,不凭记忆) + +下界回到 4.52.x 后,两道门禁会各自报一批号。**两边的 id 命名体系不同**:Trivy 用 CVE, +pip-audit 用 PYSEC。下列号全部取自 PR #101 那一次 CI 的真实输出,不是推测: + +| 来源 | 号 | 报的版本 | 修复版本 | 对应本文 §1 | +|---|---|---|---|---| +| `docker-build.yml` Trivy(job 106215347457,`Total: 6 (HIGH: 6)`,同一包在两处 site-packages 各计一次) | CVE-2026-4372 | transformers 4.52.4 | 5.3.0 | A6 | +| 同上 | CVE-2026-5241 | transformers 4.52.4 | 5.5.0 | A7 | +| 同上 | CVE-2026-9856 | transformers 4.52.4 | 5.10.0 | A8 | +| `security.yml` pip-audit(job 106215347173) | PYSEC-2025-216、PYSEC-2026-198、PYSEC-2026-228、PYSEC-2026-229、PYSEC-2026-392 | transformers 4.52.4 | 见 §1 | A5–A8 组 | + +落到的地方:`.trivyignore.yaml`(3 条 CVE,**每条带 `expiration: 2026-12-31`**,到期自动重新变红) +与 `security.yml` 里 pip-audit 的 5 个 `--ignore-vuln`。 +`tests/test_dependency_consistency.py::test_accepted_risk_registers_stay_in_sync` 会双向核对: +豁免文件里的 id 必须能在本文档找到、必须有 expiration,反之新出现的号不会被静音。 + ## 2. 本次实测证据(为什么明知有告警还是不升) ``` @@ -40,19 +57,21 @@ transformers 4.52.1 + tokenizers 0.21.0(引擎元数据要求的组合,最 即"升到 4.57 就能顺手关掉 4 条 ReDoS"这条路,代价是**产品两个引擎直接不可用**。 -**这里有一个必须所有者拍的岔口**(PR #101 实测撞上的):把声明下界改回 4.52.x 会让 -`transformers 4.52.4` 进入解析结果,于是 CI 的两道安全门禁同时变红 —— -`Dependency Vulnerability Scan (pip-audit)`(PYSEC-2025-216、PYSEC-2026-198/228/229/392)与 -`docker-build.yml` 的 Trivy 扫描步骤(镜像构建本身成功)。反过来,留着 `>=4.57.0` 门禁是绿的, -但任何人按 `pyproject`/`requirements.txt` 装环境,IndexTTS 两个引擎都起不来。 -也就是说**当前 main 的"安全门禁绿"是踩在引擎跑不起来的版本声明上的**。 - -两条出路,本 PR 只做了无争议的那半(锁集合法性 + 检查器接线),下界原样留着并加注释指向本文: - -1. 下界改回 `>=4.52.1,<4.53`(运行时正确),同时给 pip-audit / Trivy 加**逐条带理由**的豁免 - (理由引用 §1 表的可达性判定与 §4 的复点条件); -2. 或者先让 IndexTTS 侧适配 transformers ≥4.53(上游放开精确 pin,或我们改 vendored 拷贝), - 再自然抬下界、告警一并消掉。 +**岔口已于 2026-09-21 定为出路 1 并落地**:`requirements.txt` / `pyproject.toml` 的下界回到 +`transformers>=4.52.1,<4.53`(`tokenizers>=0.21.0,<0.22`),同时给两道安全门禁加**逐条带理由**的 +豁免(号与理由见 §1a;到期即重新变红)。原先那种"留着 `>=4.57.0` 让门禁显示绿色"的状态, +本质是**扫描器的颜色盖住了引擎装不起来这件事**。 + +两点必须一起记清,免得下次又据此误判: + +1. **Docker 路径本来就没有 IndexTTS**:`.dockerignore` 排除 `reference_repos/`,`requirements.txt` + 也不含 `indextts`,镜像里只有 vendored 的 VoxCPM2(`app/integrated_app/vendor/voxcpm`)。 + 所以"下界≥4.57 会弄坏容器里的两个 IndexTTS"这个说法是**错的**——真实情况是容器部署形态 + 只有 1 个引擎可用。受影响的是"源码安装 + 自带 indextts"的环境(正是 `GPU Smoke` 那条路径)。 +2. **唯一能抓到这类运行时断裂的 CI 作业是每周一次、跑在 self-hosted GPU runner 上的 + `GPU Smoke (real inference, self-hosted)`**:最近一次记录是 2026-09-14 success + (正好是那条错误下界进 main 的当天),此后没有新 run。也就是说:这类问题在 CI 上的 + 暴露延迟是以"周"计的,且依赖 runner 在线。 > 复现这套对比时的坑(已记 GOTCHAS #137):本服务的端口被占时会**自动顺延到下一个端口**, > 而沙箱里"停掉后台命令"只杀外层 shell、不杀 `python` 子进程。结果是新起的干净服务落在 7870, diff --git a/pyproject.toml b/pyproject.toml index 232daedc..b9930a27 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -55,17 +55,18 @@ dependencies = [ "torch>=2.5.1", "torchvision>=0.20.1", "torchaudio>=2.5.1", - # ⚠ 这条下界与引擎可用性互斥,取舍交仓库所有者,别顺手改(证据见 - # docs/SECURITY_DEPENDABOT_TRIAGE.md §2):2026-09-21 实测 4.57.6 下 VoxCPM2 正常 - # (合成 243,164 B / RMS 5360)但 IndexTTS 2.5 与 2.0 的 infer_v2_5 / infer_v2 直接 - # ImportError;4.52.1 + tokenizers 0.21.0 下三引擎真推理全通(2.5:214,040 B / RMS 6176; - # 2.0:205,124 B / RMS 6926)。但把下界改回 4.52.x 会让 pip-audit 与 Trivy 两道 CI - # 安全门禁同时变红(扫到 4.52.4 的"4.53 已修"CVE),所以这里暂留 >=4.57: - # 门禁绿,代价是按本声明装环境的人两个引擎起不来。出路二选一—— - # 1) 下界改回 >=4.52.1,<4.53,并给两道门禁加逐条带理由的豁免; - # 2) 先让 IndexTTS 侧适配 transformers >=4.53(上游放开精确 pin 或改 vendored 拷贝)。 - "transformers>=4.57.0", - "tokenizers>=0.19.0", + # 版本策略:上界是实测钉出来的,不是保守猜测。IndexTTS 2.0/2.5 的发行元数据要求 + # transformers==4.52.1 + tokenizers==0.21.0;2026-09-21 在 4.57.6 下实测 + # `indextts.infer_v2_5` / `infer_v2` 导入失败(两个引擎直接起不来,VoxCPM2 正常), + # 而 4.52.1 + tokenizers 0.21.0 下三引擎真推理全通(2.5:214,040 B / RMS 6176; + # 2.0:205,124 B / RMS 6926;VoxCPM2:230,148 B / RMS 4615)。 + # 代价:4.52.x 带着几个"要 5.x 才修"的 transformers 公告。已按"已接受风险"逐条登记在 + # docs/SECURITY_DEPENDABOT_TRIAGE.md,并在 pip-audit(security.yml)与 Trivy + # (.trivyignore.yaml)里绑理由豁免——**新出现的公告仍会让门禁变红**。 + # 抬这个上界之前必须先确认 IndexTTS 侧已适配。 + # tests/test_dependency_consistency.py 会核对:本声明与 requirements-lock.txt 的钉版互相满足。 + "transformers>=4.52.1,<4.53", + "tokenizers>=0.21.0,<0.22", # === AI 生态 === "funasr>=1.0.0", "modelscope>=1.9.0", diff --git a/requirements.txt b/requirements.txt index 3bcf5b95..05b9d274 100644 --- a/requirements.txt +++ b/requirements.txt @@ -2,8 +2,8 @@ torch>=2.5.1 torchvision>=0.20.1 torchaudio>=2.5.1 -transformers>=4.57.0 -tokenizers>=0.19.0 +transformers>=4.52.1,<4.53 +tokenizers>=0.21.0,<0.22 funasr>=1.0.0 modelscope>=1.9.0 addict>=2.4.0 diff --git a/tests/test_dependency_consistency.py b/tests/test_dependency_consistency.py new file mode 100644 index 00000000..33ea6dcf --- /dev/null +++ b/tests/test_dependency_consistency.py @@ -0,0 +1,201 @@ +"""依赖声明 ↔ 锁 ↔ 豁免清单三者一致性守卫(纯静态,不联网、不装包)。 + +三条守卫堵的是同一类事故:**声明与实际装配长期不一致,而 CI 一片绿**。 + +D1 ``test_lock_pins_satisfy_declared_specifiers`` —— `requirements.txt` / `pyproject.toml` + 的每个 `==`/`>=`/`<` 约束,必须被 `requirements-lock.txt` 里对应的那条钉版满足。 + WHY:这类"下界棘轮"检查在仓库里是**缺位**的 —— PR #98 的 + `scripts/check_pin_crossconflicts.py` 只管"锁内部 A==x 与 B==y 互相约束", + 它自己的 docstring 就写着"下界检查器一条都抓不到"。于是发生过: + `pyproject` 声明 `transformers>=4.57.0`(2026-09-14 搭在一条只讲 gpu-smoke 的提交里), + 而锁与引擎实际要的是 4.52.1 —— 两个数字互相矛盾,CI 全绿,按声明装环境的人 + IndexTTS 2.0/2.5 直接起不来(实测见 docs/SECURITY_DEPENDABOT_TRIAGE.md §2)。 + +D2 ``test_engine_pinned_transformers_lineage`` —— 把"引擎钉死 4.52.x"这件事变成断言: + 锁里必须是 `transformers==4.52.1` + `tokenizers==0.21.0`,且声明必须有 `<4.53` 上界。 + 谁要抬这个上界,必须先让 IndexTTS 侧适配,并让这条守卫带着证据一起改。 + +D3 ``test_accepted_risk_registers_stay_in_sync`` —— 两道扫描器的豁免集不许各自漂移: + `.trivyignore.yaml` 的每条必须有 `expiration`(到期自动重新变红)且 id 出现在 + `docs/SECURITY_DEPENDABOT_TRIAGE.md`;`security.yml` 里 pip-audit 的 `--ignore-vuln` + 也必须逐条出现在同一份分诊文档。文档里没有依据的豁免 = 未登记的降级,判红。 + +维护约定同 test_fe_be_consistency.py:每条守卫配 `*_is_not_vacuous` 变异自证。 +""" + +from __future__ import annotations + +import re +from pathlib import Path + +import pytest + +try: + from packaging.specifiers import SpecifierSet + from packaging.version import InvalidVersion, Version +except ImportError: # pragma: no cover - packaging 是 setuptools 的依赖,CI 必装 + SpecifierSet = None # type: ignore[assignment] + Version = None # type: ignore[assignment] + +_ROOT = Path(__file__).resolve().parents[1] +_LOCK = _ROOT / "requirements-lock.txt" +_REQ = _ROOT / "requirements.txt" +_PYPROJECT = _ROOT / "pyproject.toml" +_TRIAGE = _ROOT / "docs" / "SECURITY_DEPENDABOT_TRIAGE.md" +_TRIVY_IGNORE = _ROOT / ".trivyignore.yaml" +_SECURITY_WF = _ROOT / ".github" / "workflows" / "security.yml" + + +def _norm(name: str) -> str: + return name.lower().replace("_", "-") + + +def _pins(path: Path) -> dict[str, str]: + """取 `名字==版本` 形式的钉版表(锁文件、以及 pip-compile 头注释之外的行)。""" + out: dict[str, str] = {} + for line in path.read_text(encoding="utf-8").splitlines(): + m = re.match(r"^([A-Za-z0-9][A-Za-z0-9._-]*)==([0-9][0-9a-zA-Z.+-]*)", line.strip()) + if m: + out[_norm(m.group(1))] = m.group(2) + return out + + +def _declared_specifiers() -> dict[str, list[str]]: + """requirements.txt + pyproject 里对某个包的全部约束串(按包名聚合)。""" + out: dict[str, list[str]] = {} + + def add(name: str, spec: str) -> None: + name = _norm(name) + spec = spec.strip().strip("\"'").strip() + # 只收真正的版本约束:pyproject 里还有 classifier、extra 名等引号字符串, + # 它们喂给 SpecifierSet 会直接抛 InvalidSpecifier。 + if spec and re.match(r"^[<>=!~]=?\s*\d", spec): + out.setdefault(name, []).append(spec) + + for line in _REQ.read_text(encoding="utf-8").splitlines(): + m = re.match(r"^([A-Za-z0-9][A-Za-z0-9._-]*)\s*([^#;\s][^#]*)", line.strip()) + if m and not line.strip().startswith("#"): + add(m.group(1), m.group(2)) + for m in re.finditer(r'"([A-Za-z0-9][A-Za-z0-9._-]*)\s*([^"]*)"', _PYPROJECT.read_text(encoding="utf-8")): + add(m.group(1), m.group(2)) + return out + + +def _violations(pins: dict[str, str], decl: dict[str, list[str]]) -> list[str]: + bad: list[str] = [] + for name, specs in decl.items(): + if name not in pins: + continue # 锁里没有该包:由别的检查负责,不在本条口径内 + ver = pins[name] + for spec in specs: + try: + ok = Version(ver) in SpecifierSet(spec) + except InvalidVersion: + bad.append(f"{name}: 锁里版本 {ver!r} 无法解析") + continue + if not ok: + bad.append(f"{name}: 锁钉 {ver} 不满足声明 {spec!r}") + return bad + + +# --------------------------------------------------------------------------- +# D1 声明 ↔ 锁 +# --------------------------------------------------------------------------- + + +def test_lock_pins_satisfy_declared_specifiers(): + """锁文件里每条被声明过的包,其钉版必须落在声明的区间内。""" + pins = _pins(_LOCK) + decl = _declared_specifiers() + checked = [n for n in decl if n in pins] + assert len(checked) >= 20, f"只能对上 {len(checked)} 个包,解析可能失效(锁 {len(pins)} 条 / 声明 {len(decl)} 条)" + bad = _violations(pins, decl) + assert not bad, f"锁与声明互相矛盾(按声明装环境与按锁装环境是两个不同的东西):{bad}" + + +def test_d1_guard_is_not_vacuous(): + pins = _pins(_LOCK) + tf = pins.get("transformers") + assert tf is not None, "锁里没有 transformers,样本失效" + assert _violations(pins, {"transformers": [">=4.52.1,<4.53"]}) == [] + assert _violations(pins, {"transformers": [">=4.57.0"]}), "声明抬到 4.57 时必须报错,否则守卫空转" + + +# --------------------------------------------------------------------------- +# D2 引擎钉住的那条血脉 +# --------------------------------------------------------------------------- + +_ENGINE_FACT = """IndexTTS 2.0/2.5 的发行元数据要求 transformers==4.52.1 + tokenizers==0.21.0; +2026-09-21 实测:4.57.6 下 indextts.infer_v2_5 / infer_v2 直接 ImportError(VoxCPM2 不受影响), +4.52.1 + tokenizers 0.21.0 下三引擎真推理全通(2.5 出 214,040 B/RMS 6176;2.0 出 205,124 B/ +RMS 6926;VoxCPM2 出 230,148 B/RMS 4615)。证据与代价(含已接受风险的公告清单)见 +docs/SECURITY_DEPENDABOT_TRIAGE.md。要改这里,先让 IndexTTS 侧适配,再连同分诊文档一起改。""" + + +def test_engine_pinned_transformers_lineage(): + """锁必须停在 4.52.1 / tokenizers 0.21.0,且声明必须带 <4.53 上界。""" + pins = _pins(_LOCK) + assert pins.get("transformers") == "4.52.1", ( + f"锁里 transformers 被挪动了:{pins.get('transformers')!r}。{_ENGINE_FACT}" + ) + assert pins.get("tokenizers") == "0.21.0", f"锁里 tokenizers 被挪动了:{pins.get('tokenizers')!r}。{_ENGINE_FACT}" + specs = " ".join(_declared_specifiers().get("transformers", [])) + assert "<4.53" in specs, ( + f"requirements.txt / pyproject 的 transformers 声明缺 `<4.53` 上界,会解析到让 IndexTTS 起不来的版本。{_ENGINE_FACT}" + ) + + +def test_d2_guard_is_not_vacuous(): + # 声明侧:上界存在是本守卫的判据,去掉后 _violations 应能抓到锁与声明的矛盾 + assert "<4.53" in " ".join(_declared_specifiers().get("transformers", [])) + assert _violations({"transformers": "4.57.6"}, {"transformers": [">=4.52.1,<4.53"]}), "锁被抬上 4.57 时必须报错" + assert _violations({"tokenizers": "0.23.2"}, {"tokenizers": [">=0.21.0,<0.22"]}), "tokenizers 漂到 0.23 时必须报错" + + +# --------------------------------------------------------------------------- +# D3 两份豁免清单与分诊文档同步 +# --------------------------------------------------------------------------- + + +def _trivy_ignore_entries() -> list[dict[str, str]]: + text = _TRIVY_IGNORE.read_text(encoding="utf-8") + entries: list[dict[str, str]] = [] + for block in re.finditer(r"^ - id: (CVE-[0-9]{4}-[0-9]{4,7})\n(.*?)(?=^ - id: |^\S|\Z)", text, re.M | re.S): + entries.append({"id": block.group(1), "body": block.group(2)}) + return entries + + +def _pip_audit_ignores() -> list[str]: + text = _SECURITY_WF.read_text(encoding="utf-8") + return sorted(set(re.findall(r"--ignore-vuln (PYSEC-[0-9]{4}-[0-9]{3,7})", text))) + + +def test_accepted_risk_registers_stay_in_sync(): + """每条豁免都必须有到期时间,且它的 id 必须出现在分诊文档里。""" + triage = _TRIAGE.read_text(encoding="utf-8") + entries = _trivy_ignore_entries() + assert len(entries) >= 3, f"只解析到 {len(entries)} 条 Trivy 豁免,解析已失效" + missing_expiry = [e["id"] for e in entries if "expiration:" not in e["body"]] + unlogged = [e["id"] for e in entries if e["id"] not in triage] + + ignores = _pip_audit_ignores() + assert len(ignores) >= 5, f"只解析到 {len(ignores)} 条 pip-audit 豁免,解析已失效" + # pip-audit 用的是 PYSEC 号,与 Trivy 的 CVE 号不同源;两者都必须在分诊文档里有账 + orphan_pysec = [p for p in ignores if p not in triage] + + problems = [] + if missing_expiry: + problems.append(f"这些豁免没有 expiration(到期即重新变红,防止永久静音):{missing_expiry}") + if unlogged: + problems.append(f"这些 CVE 在分诊文档里没有对应条目:{unlogged}") + if orphan_pysec: + problems.append(f"这些 PYSEC 在分诊文档里没有对应条目:{orphan_pysec}") + assert not problems, "豁免与分诊记录脱节:\n " + "\n ".join(problems) + + +@pytest.mark.parametrize("cve", ["CVE-2026-4372", "CVE-2026-5241", "CVE-2026-9856"]) +def test_accepted_cves_are_actually_pinned_versions(cve: str): + """镜像里真出现过的号才允许被豁免:本条防止"顺手多豁免几个"。""" + ids = {e["id"] for e in _trivy_ignore_entries()} + assert cve in ids, f"{cve} 不在 .trivyignore.yaml 里(该文件应只登记 docker-build 实测报出的号)" + assert cve in _TRIAGE.read_text(encoding="utf-8"), f"{cve} 没在分诊文档里留痕" From e91dae2d3cb5b5b243ee7d202c5141b2cae1b4bb Mon Sep 17 00:00:00 2001 From: ReSerendipity Date: Mon, 21 Sep 2026 16:57:52 +0800 Subject: [PATCH 2/2] =?UTF-8?q?fix(security):=20=E6=8A=8A=E4=B8=A4?= =?UTF-8?q?=E9=81=93=E9=97=A8=E7=A6=81=E7=9A=84=E8=B1=81=E5=85=8D=E6=94=B9?= =?UTF-8?q?=E6=88=90=E7=9C=9F=E5=AE=9E=E7=94=9F=E6=95=88=E7=9A=84=E7=89=88?= =?UTF-8?q?=E6=9C=AC=EF=BC=8C=E5=B9=B6=E6=8A=8A=E4=BB=A3=E4=BB=B7=E4=BB=8E?= =?UTF-8?q?"3=20=E6=9D=A1"=E6=9B=B4=E6=AD=A3=E4=B8=BA=2016=20=E6=9D=A1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 上一把提交里的豁免**实际只生效了一部分**,两处都是"看起来配好了、工具根本没读"的静默失效: 1. pip-audit 的 5 个 --ignore-vuln 里有 **4 个是假号**:PYSEC-2026-198 / 228 / 229 / 392 是从 CI 表格里目抄时被列宽截断的前缀,真号是 …1980 / …2288 / …2290 / …3929。pip-audit 按全号 精确匹配,所以 CI 只报了 "ignored 1"(run 35575129704 job 106255145288)。 现在按 OSV(api.osv.dev/v1/query,transformers 4.52.1 与 4.52.4 两个版本各查一次,返回 24 条记录、按 CVE 去重 16 个公告,两侧集合一致)登记全部 **16 个** PYSEC 号。 2. Trivy 的输入名写错了:trivy-action v0.36.0 **没有** `ignorefile`,合法名是 `trivyignores`; 写错只出一条 "Unexpected input(s)" 警告然后照常变红(run 35575129698 job 106255144801: Total: 6 HIGH,仍是那 3 条 CVE)。同理 `.trivyignore.yaml` 的 schema 是 `package: {name, version}`(单数字符串),我原先写的 `versions: [...]` 不会被读取。 代价的口径随之更正(这条更要紧):4.52.x 不是"带 3 个 CVE",而是 **16 个公告** —— A1–A4 要 4.53(被引擎 pin 挡住)、A5–A8 要 5.x、**A9–A16 这 8 条上游没有修复版本**。 Trivy 侧只剩 3 条纯粹是因为那两步带 severity:CRITICAL,HIGH + ignore-unfixed:true。 pip-audit 没有到期机制,所以 16 条靠分诊文档 §4 的 2026-12-31 复审约定 + 新守卫人守。 顺带两个新发现,都记进文档: - **8 条 PYSEC-only 的公告 Dependabot 从不开单**(它只跟 GHSA),所以"关完 20 条告警" ≠ "4.52.x 的风险登记完"; - **镜像装的是 transformers 4.52.4,而锁钉 4.52.1**(Dockerfile:33/38 按 requirements.txt 声明装而非按 requirements-lock.txt)—— 同一份代码两条分发路径装出不同补丁号, 本轮只把 `.trivyignore.yaml` 的 package.version 绑在被扫产物的真实版本上,未动 Dockerfile。 守卫:D3 的豁免数下限从 5 抬到 16,新增 D4 三条 —— 输入名必须是 trivyignores(且不得出现 ignorefile)、schema 必须用 package.version、以及"security.yml 的 PYSEC 集合 == 分诊文档 §1 A 组表的集合"双向完全相等(多一个是未登记降级,少一个是登记了没豁免)。配一条截断变异自证: 把每个号各砍掉一位后必须全部对不上,否则这条断言在空转。 本机验证:ruff check/format 全绿、mypy 103 = 基线、check_pin_crossconflicts PASS(95 个包 0 冲突)、integrity PASS、check_spec_refs new=0、tests/test_dependency_consistency.py 11 passed、 全量 2089 passed / 0 failed(覆盖率 45% 门槛通过)。 Signed-off-by: ReSerendipity --- .github/workflows/docker-build.yml | 4 +- .github/workflows/docker-publish.yml | 3 +- .github/workflows/security.yml | 40 ++++++-- .trivyignore.yaml | 47 +++++---- docs/DOD.md | 21 ++-- docs/SECURITY_DEPENDABOT_TRIAGE.md | 143 +++++++++++++++++++-------- pyproject.toml | 10 +- tests/test_dependency_consistency.py | 100 ++++++++++++++++++- 8 files changed, 291 insertions(+), 77 deletions(-) diff --git a/.github/workflows/docker-build.yml b/.github/workflows/docker-build.yml index 6f10ab92..4d2dc3c9 100644 --- a/.github/workflows/docker-build.yml +++ b/.github/workflows/docker-build.yml @@ -66,7 +66,9 @@ jobs: # 已接受风险清单(每条带理由与 expiration,到期自动重新变红): # transformers 4.52.x 被引擎元数据钉死,见 .trivyignore.yaml 与 # docs/SECURITY_DEPENDABOT_TRIAGE.md - ignorefile: ".trivyignore.yaml" + # 输入名是 trivyignores(trivy-action v0.36.0 没有 ignorefile —— 写错只会 + # 出一条 "Unexpected input(s)" 警告然后照常变红,等于没豁免) + trivyignores: ".trivyignore.yaml" # 只跑漏洞扫描:secret/misconfig 对 25GB 镜像是纯多余开销,会拖长导出与解包时间 scanners: vuln # 本项目镜像约 25GB 展开,Trivy 默认 5m 内部超时会在分析阶段被 diff --git a/.github/workflows/docker-publish.yml b/.github/workflows/docker-publish.yml index 33a033ce..6350c593 100644 --- a/.github/workflows/docker-publish.yml +++ b/.github/workflows/docker-publish.yml @@ -98,7 +98,8 @@ jobs: exit-code: '1' ignore-unfixed: true # 与 docker-build.yml 用同一份已接受风险清单(每条带理由与 expiration) - ignorefile: ".trivyignore.yaml" + # 输入名 trivyignores:trivy-action v0.36.0 不认 ignorefile(只会警告后忽略) + trivyignores: ".trivyignore.yaml" format: table # 只跑漏洞扫描:secret/misconfig 扫描对 25GB 镜像是纯多余开销,会拖长导出与解包时间 scanners: vuln diff --git a/.github/workflows/security.yml b/.github/workflows/security.yml index f577c82b..24cf4f8c 100644 --- a/.github/workflows/security.yml +++ b/.github/workflows/security.yml @@ -32,16 +32,40 @@ jobs: - name: Scan dependencies for known vulnerabilities run: | # 已接受风险:transformers 4.52.x 被引擎元数据钉死(4.57.6 实测让 IndexTTS - # 2.0/2.5 的 infer_v2 / infer_v2_5 导入失败),这几条公告的可达性逐条判定见 - # docs/SECURITY_DEPENDABOT_TRIAGE.md §1,镜像侧同源豁免见 .trivyignore.yaml。 - # 只豁免这 5 个号;**任何新出现的公告仍会让本步变红**。 - # 号取自 run 35561594150 job 106215347173 的实际输出(transformers 4.52.4)。 + # 2.0/2.5 的 infer_v2 / infer_v2_5 直接 ImportError,回 4.52.1 三引擎才通, + # A/B 数字见 docs/SECURITY_DEPENDABOT_TRIAGE.md §2)。下面 16 个号 = OSV 对 + # transformers 4.52.1 与 4.52.4 返回的**全部**公告(两边集合实测一致), + # 逐条理由与本仓可达性判定见 docs/SECURITY_DEPENDABOT_TRIAGE.md §1(A1–A16), + # 镜像侧同源豁免见 .trivyignore.yaml(那边只剩 3 条,因为带 + # severity: CRITICAL,HIGH + ignore-unfixed: true 过滤,口径差异写在同一节)。 + # + # 三条约束,别绕过: + # 1. 只豁免这 16 个号;**任何新出现的公告仍会让本步变红**; + # 2. 号必须绑定证据:取自 run 35575129704 job 106255145288 的真实表格 + # (transformers 4.52.4),并与 api.osv.dev 的查询结果逐条对齐 —— + # 上一版这里写的是 5 个号,其中 4 个是把长号截断出来的假号 + # (PYSEC-2026-198 / 228 / 229 / 392 其实是 …1980 / …2288 / …2290 / …3929 + # 的前缀),pip-audit 按全号精确匹配,所以那次只豁免掉 1 条。 + # 3. pip-audit 没有 expiration 机制,到期复审靠人:本批豁免的复审截止 + # 2026-12-31,触发条件写在分诊文档 §4;D3/D4 测试保证"工作流里的号集合 + # == 分诊文档表里的号集合",加一条豁免就必须同时在文档里加一行理由。 pip-audit -r requirements.txt --desc \ + --ignore-vuln PYSEC-2025-211 \ + --ignore-vuln PYSEC-2025-212 \ + --ignore-vuln PYSEC-2025-213 \ + --ignore-vuln PYSEC-2025-214 \ + --ignore-vuln PYSEC-2025-215 \ --ignore-vuln PYSEC-2025-216 \ - --ignore-vuln PYSEC-2026-198 \ - --ignore-vuln PYSEC-2026-228 \ - --ignore-vuln PYSEC-2026-229 \ - --ignore-vuln PYSEC-2026-392 + --ignore-vuln PYSEC-2025-217 \ + --ignore-vuln PYSEC-2025-218 \ + --ignore-vuln PYSEC-2026-1980 \ + --ignore-vuln PYSEC-2026-1981 \ + --ignore-vuln PYSEC-2026-1983 \ + --ignore-vuln PYSEC-2026-1988 \ + --ignore-vuln PYSEC-2026-2288 \ + --ignore-vuln PYSEC-2026-2289 \ + --ignore-vuln PYSEC-2026-2290 \ + --ignore-vuln PYSEC-2026-3929 # 2026-08-16 本地评估 0 已知漏洞,解锁为门禁;依赖升级由 dependabot 自动跟进 bandit: diff --git a/.trivyignore.yaml b/.trivyignore.yaml index 64915244..22b51271 100644 --- a/.trivyignore.yaml +++ b/.trivyignore.yaml @@ -3,37 +3,50 @@ # 为什么存在:本产品的运行环境被引擎真实约束钉在 transformers 4.52.x # (IndexTTS 2.0/2.5 的发行元数据要求 transformers==4.52.1;实测 4.57.6 下 # indextts.infer_v2 / infer_v2_5 直接 ImportError,见 docs/SECURITY_DEPENDABOT_TRIAGE.md §2)。 -# 4.52.x 因此带着下面这几条"要 5.x 才修"的公告。它们已在分诊文档里逐条判定为 -# **本仓不可达**(无 HF Trainer / 无 save_pretrained / 不接受用户指定模型源 / 只绑 127.0.0.1)。 # -# 规则:这里只允许登记分诊文档 §1 表里已有条目的 id;每条必须给 expiration(到期即重新变红, -# 逼一次复审),并在上方注释里写清"为什么不可达"。新出现的公告**不在这里**,会照常红。 -# tests/test_dependency_consistency.py 会核对:本文件里的 id 与分诊文档、pip-audit 豁免集不脱节。 +# 为什么这里只有 3 条、而 pip-audit 那边有 16 条(不是漏了): +# 两个门禁的过滤口径不同。镜像扫描这两步带 `severity: CRITICAL,HIGH` + +# `ignore-unfixed: true`(docker-build.yml:65 / docker-publish.yml:99),于是 4.52.4 的 +# 16 个公告里只有"HIGH 且有修复版本"的 3 条会进结果 —— 就是下面这 3 条,CI 实测 +# `Total: 6 (HIGH: 6, CRITICAL: 0)`(同一镜像里 root 与 ttsuser 两处 site-packages 各算一次)。 +# 其余 13 条(8 条无修复版本的本地向量 + 5 条 MODERATE)由 pip-audit 那一步逐条豁免, +# 因为 pip-audit 没有 severity / unfixed 过滤器。两边的 id 集合都在分诊文档 §1 的同一张表里。 +# +# 规则(三条都要满足,tests/test_dependency_consistency.py 的 D3/D4 会核对): +# 1. 只允许登记分诊文档 §1 表里已有的 id,且逐条写"为什么本仓不可达"(写在条目上方注释); +# 2. 每条必须给 expiration —— 到期该条自动失效、门禁重新变红,逼一次复审; +# 3. 必须绑定 package.version。这里绑 4.52.4 是**镜像实际装的版本**(Dockerfile:33/38 装 +# requirements.txt 的声明并解析到 4.52.4,而不是锁里的 4.52.1 —— 这两条分发路径解析 +# 不同版本,已作为已知缺口记在分诊文档 §3b)。绑死版本号的好处是:哪天镜像里换成别的 +# transformers 版本,这些豁免自动不生效、门禁变红,而不是悄悄继续放行。 +# +# 输入名注意:trivy-action v0.36.0 的合法输入叫 `trivyignores`,**没有** `ignorefile` +# (写错只出一条 "Unexpected input(s) 'ignorefile'" 警告然后照常变红,等于没豁免 —— 本轮踩过)。 vulnerabilities: - # CVE-2026-4372:transformers 远程代码执行类,触发前提是加载攻击者可控的仓库代码 + # CVE-2026-4372(PYSEC-2026-2289 / GHSA-29pf-2h5f-8g72,HIGH,CVSS 3.0 AV:L,修复于 5.3.0): + # transformers 远程代码执行,触发前提是加载攻击者可控的仓库代码 # (trust_remote_code 或远程 model 指向)。本仓权重全部来自 model/ 本地目录, # 经人工确认 + SHA-256 复验后才入库(LOCAL_RULES 禁区流程);镜像内无远程模型入口。 - id: CVE-2026-4372 package: name: transformers - versions: - - "4.52.4" - expiration: "2026-12-31T00:00:00+00:00" + version: "4.52.4" + expiration: "2026-12-31T00:00:00Z" - # CVE-2026-5241:模型初始化期的任意代码执行,载体是 LightGlue 一类视觉匹配模型。 + # CVE-2026-5241(PYSEC-2026-2290 / GHSA-fgcw-684q-jj6r,HIGH,CVSS 3.1 AV:N,修复于 5.5.0): + # 模型初始化期的任意代码执行,载体是 LightGlue 一类视觉匹配模型。 # 本仓只加载 TTS 语音模型(voxcpm / indextts / funasr / zipenhancer),无该代码路径。 - id: CVE-2026-5241 package: name: transformers - versions: - - "4.52.4" - expiration: "2026-12-31T00:00:00+00:00" + version: "4.52.4" + expiration: "2026-12-31T00:00:00Z" - # CVE-2026-9856:save_pretrained 经 chat template 造成任意文件写。 + # CVE-2026-9856(PYSEC-2026-3929 / GHSA-xrqw-3rrv-vx5w,HIGH,CVSS 3.0 AV:N,修复于 5.10.0): + # save_pretrained 经 chat template 造成任意文件写。 # 全仓无 save_pretrained 调用(grep 证据见分诊文档 §1 A8),音色保存走我们自己的目录写入。 - id: CVE-2026-9856 package: name: transformers - versions: - - "4.52.4" - expiration: "2026-12-31T00:00:00+00:00" + version: "4.52.4" + expiration: "2026-12-31T00:00:00Z" diff --git a/docs/DOD.md b/docs/DOD.md index a2a099f6..1e34fdf6 100644 --- a/docs/DOD.md +++ b/docs/DOD.md @@ -127,15 +127,22 @@ 后三引擎真推理全通(2.5:214,040 B / RMS 6176;2.0:205,124 B / RMS 6926;VoxCPM2:230,148 B / RMS 4615;每次卸载显存回到 ~3.5 GB)。故 `pyproject`/`requirements.txt` 里那句 `transformers>=4.57.0`(9-14 搭在一条只讲 gpu-smoke 的提交里进来的)站不住,但改回 4.52.x 会让 - pip-audit 与 Trivy 两道 CI 安全门禁同时变红(扫到 4.52.4 的"4.53 已修"CVE)——**下界与引擎可用性 + pip-audit 与 Trivy 两道 CI 安全门禁同时变红(4.52.x 实带 **16 个**公告:4 条要 4.53、4 条要 + 5.x 大版本、8 条**上游根本没有修复版本**)——**下界与引擎可用性 互斥**,2026-09-21 定为**出路①并落地**:下界回到 `>=4.52.1,<4.53`(`tokenizers>=0.21.0,<0.22`), - 两道扫描器改成**逐条带理由 + 带 expiration 的已接受风险豁免**(号取自 CI 真实输出,见 - `docs/SECURITY_DEPENDABOT_TRIAGE.md` §1a;`.trivyignore.yaml` 三条 CVE 于 2026-12-31 到期, - 到期自动重新变红),并新增 `tests/test_dependency_consistency.py`(8 条)核对"声明 ↔ 锁 ↔ - 豁免清单"三者不互相漂移 —— 这正是原先缺位的那类"下界棘轮",也是这条错误下界能在 main 上 - 存活一周没人发现的原因; + 两道扫描器改成**逐条带理由的已接受风险豁免**(号取自 CI 真实输出 + api.osv.dev 复核,见 + `docs/SECURITY_DEPENDABOT_TRIAGE.md` §1/§1a;pip-audit 侧 16 个 PYSEC 号,Trivy 侧因那两步带 + `severity:CRITICAL,HIGH` + `ignore-unfixed:true` 只剩 3 条 CVE,`.trivyignore.yaml` 于 + 2026-12-31 到期、到期自动重新变红;pip-audit 没有到期机制,靠文档 §4 的同一日期人守), + 并新增 `tests/test_dependency_consistency.py`(11 条)核对"声明 ↔ 锁 ↔ 豁免清单"三者不互相漂移 —— 这正是原先缺位的那类"下界棘轮",也是这条错误下界能在 main 上 + 存活一周没人发现的原因;其中 D4 专门堵本轮自己踩出来的两个**静默失效**坑: + `--ignore-vuln` 的号从 CI 表格里目抄被列宽截断成前缀假号(5 条里只生效 1 条)、 + Trivy 的输入名写成 `ignorefile`(v0.36.0 只认 `trivyignores`,写错仅警告、豁免等于没接); + 同一步还暴露出**镜像装 4.52.4 而锁钉 4.52.1**(`Dockerfile:33/38` 按声明装而非按锁装), + 已作为已知缺口记在分诊文档 §3b,本轮未动 Dockerfile。 引擎加载失败时的报错也不再断言"PyPI 无 indextts 包",改为带上底层 ImportError 与版本不匹配提示。 - 20 条 Dependabot 告警因此**没有一条能靠现在就升级消掉**,分诊见 `docs/SECURITY_DEPENDABOT_TRIAGE.md`。 + 20 条 Dependabot 告警因此**没有一条能靠现在就升级消掉**,分诊见 `docs/SECURITY_DEPENDABOT_TRIAGE.md`; + 且那 20 条只覆盖有 GHSA 记录的 10 个公告,**8 条 PYSEC-only 的 Dependabot 从不开单**。 * **已知缺口**:CI 冒烟 `scripts/gpu_smoke_minimal.py` 只覆盖 voxcpm2 + indextts2(走 OpenAI 口, 而 `tts-1` / `tts-1-hd` 两个模型名里没有 IndexTTS **2.0** 的位置);2.0 的真推理今天人工验过, 要接进冒烟需改用 `/api/generate/indextts2` 形态并在 GPU runner 上复验,另案。 diff --git a/docs/SECURITY_DEPENDABOT_TRIAGE.md b/docs/SECURITY_DEPENDABOT_TRIAGE.md index c338fc8f..1a771ffb 100644 --- a/docs/SECURITY_DEPENDABOT_TRIAGE.md +++ b/docs/SECURITY_DEPENDABOT_TRIAGE.md @@ -1,44 +1,80 @@ # 依赖漏洞告警分诊(Dependabot alerts triage) -日期:2026-09-21 适用:`main` @ PR #100 之后 责任人:仓库所有者 -状态:**20 条 open 告警 = 10 个公告 × 2 份清单**(`requirements-lock.txt`、 -`launcher/requirements-small.txt` 各计一次)。本文只谈"能不能修 / 该不该修 / 谁挡住"。 +日期:2026-09-21(同日按 CI 真实输出 + api.osv.dev 复核修订过一次) +适用:`main` @ PR #100 之后 责任人:仓库所有者 +状态:**GitHub 上 20 条 open 告警 = 10 个公告 × 2 份清单**(`requirements-lock.txt`、 +`launcher/requirements-small.txt` 各计一次)。但**扫描器视角的下界代价比这个大**: +transformers 4.52.x 实际带 **16 个公告**,其中 8 个只有 PYSEC 号、没有 GHSA 记录 —— +Dependabot 只跟 GHSA,所以那 8 条**根本不开单**,只有 pip-audit(OSV 全源)看得见。 +也就是说:"把 20 条 dismiss 完" ≠ "4.52.x 的风险登记完"。本文 §1 是完整的 16+2 行。 +号与来源全部绑定证据(§1a),不凭记忆。 ## 1. 结论速览 -| # | CVE | 包 | 严重度 | 修复版本 | 谁挡住它 | 本仓可达性 | 建议处置 | +A1–A8 有 GHSA 记录(= Dependabot 那 16 条告警的来源);A9–A16 是 PYSEC-only 的 ZDI 系列 +(Dependabot 不开单,pip-audit 会红)。P1/P2 与 transformers 无关。 + +| # | CVE | PYSEC / GHSA | 严重度 | 修复版本 | 具体代码路径 | 本仓可达性判据 | 处置 | |---|---|---|---|---|---|---|---| -| A1 | CVE-2025-5197 | transformers | medium | 4.53.0 | `indextts==…`钉 `transformers==4.52.1` | tokenizer 正则 ReDoS,且仅 Marian/部分 tokenizer;我们走 GPT2/CLIP 路径,输入是本机用户自己键入的文本 | 关闭理由:本机自用 + 代码路径不涉及;待引擎适配后随版本升级自然消除 | -| A2 | CVE-2025-6051 | transformers | medium | 4.53.0 | 同上 | 同 A1 | 同 A1 | -| A3 | CVE-2025-6638 | transformers | medium | 4.53.0 | 同上 | MarianTokenizer —— 本仓不加载 Marian 模型 | 同 A1 | -| A4 | CVE-2025-6921 | transformers | medium | 4.53.0 | 同上 | AdaLight tokenizer 正则 —— 未使用 | 同 A1 | -| A5 | CVE-2026-1839 | transformers | medium | 5.0.0rc3 | 引擎钉 4.52.1(且 5.x 是大版本破坏性升级) | `Trainer` 代码执行:**全仓无 HF `Trainer` 用法**(`training/trainer.py` 里的 `LoRATrainer` 是我们自己的类,`grep from transformers import Trainer` 零命中) | 关闭理由:代码路径不存在 | -| A6 | CVE-2026-4372 | transformers | high | 5.3.0 | 同上 | 远程代码执行类:需要加载不可信仓库且 `trust_remote_code=True`。本仓权重是本地目录 + 人工核验 + SHA-256(`LOCAL_RULES.md` 禁区流程);唯一从 Hub 取物的是 `security/content_safety.py` 的 CLIP `AutoTokenizer.from_pretrained(model_name, revision=…)`(固定 revision、非攻击者可控) | 关闭理由:不接受用户指定模型源;保留复点 | -| A7 | CVE-2026-5241 | transformers | high | 5.5.0 | 同上 | LightGlue 等模型初始化路径 —— 本仓不加载该类模型 | 关闭理由:未使用组件 | -| A8 | CVE-2026-9856 | transformers | high | 5.10.0 | 同上 | `save_pretrained` 经 chat template 造成任意文件写:**全仓无 `save_pretrained` 调用**(音色保存写的是我们自己的目录) | 关闭理由:代码路径不存在 | -| P1 | CVE-2025-4565 | protobuf | high | 4.25.8 | **上界来源未证实**(见 §3a) | protobuf 在本仓只被 tensorboard / tensorboardX / modelscope / descript-audiotools 间接使用,服务端**不解析任何来自网络的 protobuf/JSON wire 数据**(`grep google.protobuf app/` 零命中) | 关闭理由:无攻击面;真修需先做一次 pip-compile 复算确认可解上界 | -| P2 | CVE-2026-0994 | protobuf | high | 5.29.6 | 同上(且跨两个大版本) | 同 P1 | 同 P1 | - -一句话:**10 个公告里没有一个能靠"升个版"现在就消掉** —— 4 个 ReDoS 被 IndexTTS 引擎的精确 pin 挡住, -4 个需要 transformers 5.x(大版本),2 个需要 protobuf 越过 `descript-audiotools<3.20` / -`modelscope<3.21` 两道上游上界。 - -## 1a. 两道扫描器的豁免号是从哪来的(2026-09-21 实测日志,不凭记忆) - -下界回到 4.52.x 后,两道门禁会各自报一批号。**两边的 id 命名体系不同**:Trivy 用 CVE, -pip-audit 用 PYSEC。下列号全部取自 PR #101 那一次 CI 的真实输出,不是推测: - -| 来源 | 号 | 报的版本 | 修复版本 | 对应本文 §1 | -|---|---|---|---|---| -| `docker-build.yml` Trivy(job 106215347457,`Total: 6 (HIGH: 6)`,同一包在两处 site-packages 各计一次) | CVE-2026-4372 | transformers 4.52.4 | 5.3.0 | A6 | -| 同上 | CVE-2026-5241 | transformers 4.52.4 | 5.5.0 | A7 | -| 同上 | CVE-2026-9856 | transformers 4.52.4 | 5.10.0 | A8 | -| `security.yml` pip-audit(job 106215347173) | PYSEC-2025-216、PYSEC-2026-198、PYSEC-2026-228、PYSEC-2026-229、PYSEC-2026-392 | transformers 4.52.4 | 见 §1 | A5–A8 组 | - -落到的地方:`.trivyignore.yaml`(3 条 CVE,**每条带 `expiration: 2026-12-31`**,到期自动重新变红) -与 `security.yml` 里 pip-audit 的 5 个 `--ignore-vuln`。 -`tests/test_dependency_consistency.py::test_accepted_risk_registers_stay_in_sync` 会双向核对: -豁免文件里的 id 必须能在本文档找到、必须有 expiration,反之新出现的号不会被静音。 +| A1 | CVE-2025-5197 | PYSEC-2026-1983 / GHSA-9356-575x-2w9m | MODERATE | 4.53.0 | `convert_tf_weight_name_to_pt_weight_name()` 正则灾难性回溯 | `grep -rl convert_tf_weight_name_to_pt_weight_name app/ scripts/` → **0 命中**;本仓不做 TF→PT 权重名转换 | 已接受风险(pip-audit 豁免) | +| A2 | CVE-2025-6051 | PYSEC-2026-1988 / GHSA-rcv9-qm8p-9p6j | MODERATE | 4.53.0 | `EnglishNormalizer.normalize_numbers()` 正则 | 同法 **0 命中**(`normalize_numbers` / `EnglishNormalizer` 各 0);文本前处理走 voxcpm / indextts 自带前端,不用 transformers normalizer | 已接受风险 | +| A3 | CVE-2025-6638 | PYSEC-2026-1981 / GHSA-59p9-h35m-wg4g | MODERATE | 4.53.0 | `MarianTokenizer.remove_language_code()` 正则 | `grep Marian app/ scripts/` → **0 命中**,本仓不加载 Marian 模型 | 已接受风险 | +| A4 | CVE-2025-6921 | PYSEC-2026-1980 / GHSA-4w7r-h757-3r74 | MODERATE | 4.53.0 | `AdamWeightDecay._do_use_weight_decay()` 处理用户可控正则 | **0 命中**;且是**训练期**优化器,本仓不发训练(`training/` 由 AST 门禁隔离) | 已接受风险 | +| A5 | CVE-2026-1839 | PYSEC-2026-2288 / GHSA-69w3-r845-3855 | MODERATE(AV:L/UI:R) | 5.0.0rc3 | `Trainer._load_rng_state()` 调 `torch.load()` 未带 `weights_only=True` | `grep "from transformers import Trainer"` → **0 命中**;`training/trainer.py` 的 `LoRATrainer` 是我们自己的类,不继承 HF `Trainer` | 已接受风险 | +| A6 | CVE-2026-4372 | PYSEC-2026-2289 / GHSA-29pf-2h5f-8g72 | **HIGH** | 5.3.0 | 恶意 `config.json` 里 `_attn_implementation_internal` 指向攻击者仓库,`AutoModelForCausalLM.from_pretrained()` 加载时 RCE | 前提是**加载别人给的模型目录**。本仓权重全部来自 `model/` 本地目录,入库前人工确认 + SHA-256 复验(`LOCAL_RULES.md` 禁区流程);唯一从 Hub 取物的是 `security/content_safety.py` 里固定 revision 的 CLIP tokenizer,模型名不接受用户输入 | 已接受风险;Trivy 侧另需 `.trivyignore.yaml` | +| A7 | CVE-2026-5241 | PYSEC-2026-2290 / GHSA-fgcw-684q-jj6r | **HIGH**(AV:N) | 5.5.0 | LightGlue 模型加载路径 `trust_remote_code` 失效 → 初始化期任意代码执行 | 本仓只加载 TTS 语音模型(voxcpm / indextts / funasr / zipenhancer),无视觉匹配模型;`grep LightGlue` → 0 | 已接受风险;Trivy 侧同上 | +| A8 | CVE-2026-9856 | PYSEC-2026-3929 / GHSA-xrqw-3rrv-vx5w | **HIGH**(AV:N) | 5.10.0 | `save_pretrained()` 经 chat template 造成路径穿越任意文件写 | `grep -rn save_pretrained app/ scripts/` → **0 命中**;音色保存写的是我们自己的目录 | 已接受风险;Trivy 侧同上 | +| A9 | CVE-2025-14920 | PYSEC-2025-211 / 无 GHSA | 未评(CVSS3.0 `AV:L/…/UI:R/C:H/I:H/A:H`) | **无** | Perceiver 原始 checkpoint 反序列化 | ZDI 系列,入口都是**人工执行权重转换器**:`grep -riE "convert_[a-z_]*original_checkpoint|PerceiverModel" app/ scripts/` → **0 命中** | 已接受风险(Dependabot 不开单) | +| A10 | CVE-2025-14921 | PYSEC-2025-212 / 无 GHSA | 同上 | **无** | Transformer-XL 原始 checkpoint 反序列化 | 同 A9(`TransformerXL` 0 命中) | 同 A9 | +| A11 | CVE-2025-14924 | PYSEC-2025-213 / 无 GHSA | 同上 | **无** | megatron_gpt2 反序列化 | 同 A9(`megatron` 0 命中) | 同 A9 | +| A12 | CVE-2025-14926 | PYSEC-2025-214 / 无 GHSA | 同上 | **无** | SEW `convert_config` 代码注入 | 同 A9(`SEWModel` 0 命中) | 同 A9 | +| A13 | CVE-2025-14927 | PYSEC-2025-215 / 无 GHSA | 同上 | **无** | SEW-D `convert_config` 代码注入 | 同 A9(`SEWD` 0 命中) | 同 A9 | +| A14 | CVE-2025-14928 | PYSEC-2025-216 / 无 GHSA | 同上 | **无** | HuBERT `convert_config` 代码注入 | 同 A9(`HuBERT` 0 命中);ASR 侧走 funasr 自有权重格式 | 同 A9 | +| A15 | CVE-2025-14929 | PYSEC-2025-217 / 无 GHSA | 同上 | **无** | X-CLIP checkpoint 转换反序列化 | 同 A9(`XCLIP` 0 命中);CLIP 在本仓只做安全判定、且用 `AutoTokenizer` 不换 checkpoint | 同 A9 | +| A16 | CVE-2025-14930 | PYSEC-2025-218 / 无 GHSA | 同上 | **无** | GLM4 反序列化 | 同 A9;本仓无 LLM 对话模型 | 同 A9 | +| P1 | CVE-2025-4565 | — | high | 4.25.8 | protobuf wire 解析 | **上界来源未证实**(§3a);服务端 `grep google.protobuf app/` 零命中,无网络输入面 | 待 §3a 复算后再定 | +| P2 | CVE-2026-0994 | — | high | 5.29.6 | 同 P1 | 同 P1 | 同 P1 | + +一句话:**16 个 transformers 公告里没有一个能靠"升个版"现在就消掉** —— A1–A4 要 4.53(被 +IndexTTS 的精确 pin 挡住,§2),A5–A8 要 5.x 大版本,A9–A16 **上游根本没有修复版本**, +只能在 5.x 之后才可能出现。P1/P2 另说。 + +## 1a. 两道扫描器的豁免号是从哪来的(本轮实测,不凭记忆) + +下界回到 4.52.x 后,两道门禁各自报一批号,**口径不同所以数量差 5 倍**: + +| 门禁 | 过滤条件 | 看到的号 | 条数 | +|---|---|---|---| +| `docker-build.yml` / `docker-publish.yml` 的 Trivy | `severity: CRITICAL,HIGH` + `ignore-unfixed: true` | CVE-2026-4372 / 5241 / 9856(A6/A7/A8,唯一"HIGH 且有修复版本"的三条;同一包在 root 与 ttsuser 两处 site-packages 各计一次,所以 `Total: 6`) | 3 | +| `security.yml` 的 pip-audit | 无 severity、无 unfixed 过滤(它也没这两个开关) | §1 表 A1–A16 的全部 PYSEC 号 | 16 | + +run 35575129704 job 106255145288(pip-audit,"Found 22 known vulnerabilities, ignored 1") +与 run 35575129698 job 106255144801(Trivy)是这两行的出处;A9–A16 的 CVE↔PYSEC 对应、 +A1–A4 的"具体函数"列取自 `POST api.osv.dev/v1/query` +(`{"package":{"name":"transformers","ecosystem":"PyPI"},"version":"4.52.4"}`): +返回 24 条记录,按 CVE 去重后 16 个公告 —— 4.52.1 与 4.52.4 的集合**实测完全一致**。 + +**两次自己踩出来的纠正,留在这里当反例:** + +1. 上一版这里写的 5 个 PYSEC 号有 **4 个是假的**:`PYSEC-2026-198 / 228 / 229 / 392` 是把真号 + `…1980 / …2288 / …2290 / …3929` 从中间截断了。pip-audit 按**全号精确匹配**,所以那次 + `--ignore-vuln` 5 条里只豁免掉 1 条,CI 照红("ignored 1")。教训:号要从结构化输出里取, + 不能从表格里目抄 —— 表格列宽会把号截断。 +2. Trivy 的豁免我原先写成 `ignorefile: ".trivyignore.yaml"`,而 trivy-action v0.36.0 的合法输入 + 叫 **`trivyignores`**(没有 `ignorefile`)。写错**不会**报错,只出一条 + `Unexpected input(s) 'ignorefile'` 警告然后照常变红 —— 也就是"以为豁免了"。 + 同理 `.trivyignore.yaml` 的 schema 是 `package: {name, version}`(`version` 单数、字符串), + 我原先写的 `versions: [...]` 不生效。这两条现在由 `tests/test_dependency_consistency.py` + 的 D4 守卫钉住(输入名与 schema 键各断言一次),不再靠下一次 CI 才发现。 + +另外两个由"版本绑死"暴露出来的事实,一并记清: + +3. **镜像里装的是 4.52.4,锁里钉的是 4.52.1** —— 见 §3b。`.trivyignore.yaml` 因此把 + `package.version` 绑在 **4.52.4**(被扫的那个产物),哪天解析结果变了豁免自动失效、门禁变红。 +4. GitHub 的 20 条 Dependabot 告警只覆盖 A1–A8 + P1/P2(GHSA 有记录的那 10 个), + **A9–A16 那 8 条 PYSEC-only 的永远不会开单**。所以 §5 那批 dismiss 做完也不代表登记完成, + pip-audit 那 16 条豁免才是完整账本。 + ## 2. 本次实测证据(为什么明知有告警还是不升) @@ -110,14 +146,43 @@ transformers 4.52.1 + tokenizers 0.21.0(引擎元数据要求的组合,最 结论:**"被谁挡住"目前只能标为未证实**,确切断链要靠一次 `pip-compile` 复算(属于 §B1 的 relock 工作, 需要访问索引、拉几百 MB 依赖)。在它做完之前,P1/P2 的处置只写"无攻击面",不写"被上游挡住"。 +## 3b. 顺手发现的一个已知缺口:镜像与便携包装的不是同一个 transformers 版本 + +本轮为了核对 Trivy 报的版本读了一遍 `Dockerfile`: + +``` +Dockerfile:33 COPY pyproject.toml requirements.txt ./ +Dockerfile:38 RUN python3.12 -m pip install --no-cache-dir --user -r requirements.txt +``` + +装的是**声明** `requirements.txt`(`transformers>=4.52.1,<4.53`),解析结果是 **4.52.4**; +而 `requirements-lock.txt` 钉的是 **4.52.1**(便携包 / 桌面包按锁装)。CI 的镜像扫描输出了 +`transformers-4.52.4.dist-info`,两边对不上,是被扫描的产物自己说的。 + +后果分两层: +1. **安全口径**:镜像里那 3 条 HIGH 的修复版本都在 5.x,4.52.1 与 4.52.4 的公告集合 + 实测相同(§1a 的 OSV 查询两个版本都跑过),所以**豁免结论不受影响**; +2. **可复现性**:同一份代码经两条分发路径装出两个不同的 transformers 补丁号, + "锁已验证"这件事对容器部署形态并不成立 —— 与 §6 末尾"这份锁从没被真装验证过"是同一类问题。 + +本轮**没有**改 `Dockerfile`(改成装锁会连带改变镜像里全部 90+ 个包的版本,属于 §B1 relock +之后才能做的动作),只是把 `.trivyignore.yaml` 的 `package.version` 绑在镜像真实版本上, +让"哪天版本变了 → 豁免失效 → 门禁变红"这条链路保持有效。 + ## 4. 复点条件(满足其一就重新评估对应告警) -1. IndexTTS 官方放开 `transformers` 精确 pin → 立刻可关 A1–A4(4.53+),届时再判 5.x 的 A5–A8。 -2. `descript-audiotools` / `modelscope` 放开 `protobuf<3.20 / <3.21` → 可关 P1–P2。 -3. 若本服务将来**监听非本机地址**或**接受用户指定模型名/HF 仓库**,A6/A8/P1/P2 的 +1. IndexTTS 官方放开 `transformers` 精确 pin(或我们改 vendored 拷贝适配)→ 抬到 4.53+ 立刻 + 可关 A1–A4;抬到 5.10+ 则 A1–A8 一次全消。**A9–A16 只能等它们各自出修复版本** + (OSV 现在给的是"无修复版本",所以只要停在 4.52.x,这 8 条就会一直在 pip-audit 里红着)。 +2. protobuf 的 P1/P2:先做 §3a 那次 `pip-compile` 复算确认真正的阻断者,再谈放开哪道上界; + 本文第一版写的"被 `descript-audiotools<3.20` / `modelscope<3.21` 挡住"**尚未证实**。 +3. 若本服务将来**监听非本机地址**或**接受用户指定模型名/HF 仓库**,A6/A7/A8/A9–A16/P1/P2 的 "无攻击面"前提立即失效,必须重判。当前证据:`config.yaml host: "127.0.0.1"`、 `run_server(ip="127.0.0.1")`。 -4. 若 `vllm_backend.py`(现在无人引用,其 `trust_remote_code` 默认 True)被接进入口,A6 立即升为必修。 +4. 若 `vllm_backend.py`(现在无人引用,其 `trust_remote_code` 默认 True)被接进入口,A6/A7 立即升为必修。 +5. **到期复审**:`.trivyignore.yaml` 三条的 `expiration` 都是 2026-12-31,到期那一步自动变红; + pip-audit 侧没有到期机制(它不支持 expiration),所以 A1–A16 的 16 个号靠本文 + 人守 —— + 2026-12-31 之前要么按条件 1 抬版本消掉,要么把这份表带着做一次显式再确认。 ## 5. 需要仓库所有者点头的动作 diff --git a/pyproject.toml b/pyproject.toml index b9930a27..d36f50c6 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -60,9 +60,13 @@ dependencies = [ # `indextts.infer_v2_5` / `infer_v2` 导入失败(两个引擎直接起不来,VoxCPM2 正常), # 而 4.52.1 + tokenizers 0.21.0 下三引擎真推理全通(2.5:214,040 B / RMS 6176; # 2.0:205,124 B / RMS 6926;VoxCPM2:230,148 B / RMS 4615)。 - # 代价:4.52.x 带着几个"要 5.x 才修"的 transformers 公告。已按"已接受风险"逐条登记在 - # docs/SECURITY_DEPENDABOT_TRIAGE.md,并在 pip-audit(security.yml)与 Trivy - # (.trivyignore.yaml)里绑理由豁免——**新出现的公告仍会让门禁变红**。 + # 代价(2026-09-21 按 api.osv.dev 实测,不是估算):4.52.x 带 **16 个** transformers 公告 + # —— A1–A4 要 4.53(被上面的引擎 pin 挡住)、A5–A8 要 5.x 大版本、A9–A16 这 8 条 + # **上游根本没有修复版本**。已按"已接受风险"逐条登记理由与可达性判据在 + # docs/SECURITY_DEPENDABOT_TRIAGE.md §1,并在 pip-audit(security.yml,16 个 PYSEC 号) + # 与 Trivy(.trivyignore.yaml,只剩 3 条,因为那两步带 severity:CRITICAL,HIGH + + # ignore-unfixed:true 过滤)里绑理由豁免——**新出现的公告仍会让门禁变红**。 + # 注意 pip-audit 没有到期机制,这 16 条靠文档 §4 的 2026-12-31 复审约定守着。 # 抬这个上界之前必须先确认 IndexTTS 侧已适配。 # tests/test_dependency_consistency.py 会核对:本声明与 requirements-lock.txt 的钉版互相满足。 "transformers>=4.52.1,<4.53", diff --git a/tests/test_dependency_consistency.py b/tests/test_dependency_consistency.py index 33ea6dcf..ff4fdc9e 100644 --- a/tests/test_dependency_consistency.py +++ b/tests/test_dependency_consistency.py @@ -20,6 +20,13 @@ `docs/SECURITY_DEPENDABOT_TRIAGE.md`;`security.yml` 里 pip-audit 的 `--ignore-vuln` 也必须逐条出现在同一份分诊文档。文档里没有依据的豁免 = 未登记的降级,判红。 +D4 ``test_ignore_files_use_the_keys_the_tools_actually_read`` —— 堵"以为豁免了"这一类: + trivy-action v0.36.0 的合法输入叫 `trivyignores`(**没有** `ignorefile`,写错只警告不报错), + `.trivyignore.yaml` 的 schema 是 `package: {name, version}`(`version` 是单数字符串, + 写成 `versions: [...]` 不生效)。这两个坑本轮都真踩过,各断言一次。 + 同时核对:pip-audit 豁免的 PYSEC 号集合必须与分诊文档 §1 表**完全相等**(双向), + 多一个是"未登记降级",少一个是"登记了却没真豁免"。 + 维护约定同 test_fe_be_consistency.py:每条守卫配 `*_is_not_vacuous` 变异自证。 """ @@ -44,6 +51,11 @@ _TRIAGE = _ROOT / "docs" / "SECURITY_DEPENDABOT_TRIAGE.md" _TRIVY_IGNORE = _ROOT / ".trivyignore.yaml" _SECURITY_WF = _ROOT / ".github" / "workflows" / "security.yml" +# 两道镜像扫描:同一个 .trivyignore.yaml 必须被它们各自正确接上 +_IMAGE_WFS = ( + _ROOT / ".github" / "workflows" / "docker-build.yml", + _ROOT / ".github" / "workflows" / "docker-publish.yml", +) def _norm(name: str) -> str: @@ -179,7 +191,7 @@ def test_accepted_risk_registers_stay_in_sync(): unlogged = [e["id"] for e in entries if e["id"] not in triage] ignores = _pip_audit_ignores() - assert len(ignores) >= 5, f"只解析到 {len(ignores)} 条 pip-audit 豁免,解析已失效" + assert len(ignores) >= 16, f"只解析到 {len(ignores)} 条 pip-audit 豁免,解析已失效" # pip-audit 用的是 PYSEC 号,与 Trivy 的 CVE 号不同源;两者都必须在分诊文档里有账 orphan_pysec = [p for p in ignores if p not in triage] @@ -199,3 +211,89 @@ def test_accepted_cves_are_actually_pinned_versions(cve: str): ids = {e["id"] for e in _trivy_ignore_entries()} assert cve in ids, f"{cve} 不在 .trivyignore.yaml 里(该文件应只登记 docker-build 实测报出的号)" assert cve in _TRIAGE.read_text(encoding="utf-8"), f"{cve} 没在分诊文档里留痕" + + +# --------------------------------------------------------------------------- +# D4 "以为豁免了"专项:工具真正读取的键名 + 豁免集与文档表完全相等 +# --------------------------------------------------------------------------- + +# trivy-action 的合法输入名(v0.36.0 实测的 valid inputs 里有这个、没有 ignorefile) +_TRIVY_INPUT = "trivyignores" + + +def _trivy_ignore_version_keys() -> tuple[int, int]: + """返回 (.trivyignore.yaml 里 `package.version` 正确写法的处数, `versions:` 错误写法的处数)。""" + text = _TRIVY_IGNORE.read_text(encoding="utf-8") + good = len(re.findall(r"^\s{6}version:\s*[\"']?[0-9]", text, re.M)) + bad = len(re.findall(r"^\s{6}versions:", text, re.M)) + return good, bad + + +def _triage_table_pysecs() -> set[str]: + """分诊文档 §1 表 A 组行里登记的 PYSEC 号集合。""" + rows = [ln for ln in _TRIAGE.read_text(encoding="utf-8").splitlines() if re.match(r"^\| A\d+ \|", ln)] + ids: set[str] = set() + for ln in rows: + ids.update(re.findall(r"PYSEC-[0-9]{4}-[0-9]{3,7}", ln)) + return ids + + +def test_trivy_ignore_hooked_up_with_the_keys_the_tools_read(): + """两条镜像扫描必须用 trivyignores 接清单,且清单本身的 schema 键名要对。""" + problems = [] + for wf in _IMAGE_WFS: + text = wf.read_text(encoding="utf-8") + if f"{_TRIVY_INPUT}:" not in text: + problems.append( + f"{wf.name} 没有 {_TRIVY_INPUT}: 输入 —— 写错名字(例如 ignorefile)只会出一条警告然后照常变红" + ) + if "ignorefile:" in text: + problems.append(f"{wf.name} 用了 ignorefile:,trivy-action v0.36.0 不认这个输入名(等于没接豁免)") + + good, bad = _trivy_ignore_version_keys() + if bad: + problems.append( + f".trivyignore.yaml 里有 {bad} 处 `versions:` —— schema 只读 `package.version`(单数字符串),复数写法不生效" + ) + if good < 3: + problems.append(f".trivyignore.yaml 只解析到 {good} 条带 `package.version` 的条目,本文件现有 3 条豁免") + assert not problems, "豁免实际未生效:\n " + "\n ".join(problems) + + +def test_pip_audit_ignore_set_equals_the_triage_table(): + """pip-audit 的豁免集合必须与分诊文档 §1 的 A 组表**完全相等**。 + + 多了 = 有豁免没登记理由;少了 = 文档登记了风险却漏了豁免(CI 直接红,或更糟: + 号写残缺了还自以为豁免掉了 —— 本轮真的发生过 4 个被截断的假号)。 + """ + registered = _triage_table_pysecs() + exempted = set(_pip_audit_ignores()) + assert len(registered) >= 16, f"分诊文档 §1 只解析到 {len(registered)} 个 PYSEC,表或解析已失效" + assert exempted == registered, ( + f"豁免集与分诊表漂移:只在豁免里(未登记理由){sorted(exempted - registered)};" + f"只在表里(登记了却没豁免){sorted(registered - exempted)}" + ) + + +def test_d3_d4_guards_are_not_vacuous(): + """变异自证:把三处"写错就静默失效"的地方改坏,本组守卫必须变红。""" + entries = _trivy_ignore_entries() + assert len(entries) == 3, "Trivy 豁免条目数对不上,D3 的解析已失效" + assert all("expiration:" in e["body"] for e in entries) + assert all("version:" in e["body"] and "versions:" not in e["body"] for e in entries) + + # 号残缺(截断成前缀)必须被"完全相等"抓到:本轮真实发生过 4 个被截断的假号 + registered = _triage_table_pysecs() + exempted = set(_pip_audit_ignores()) + assert exempted == registered + truncated = {pid[:-1] for pid in exempted} + assert truncated != registered, "把每个豁免号各截掉一位后仍相等,说明这条断言在空转" + assert truncated.isdisjoint(registered), "截断后的号必须一个都对不上分诊表,否则前缀假号抓不住" + + # 输入名写错必须被抓到 + for wf in _IMAGE_WFS: + text = wf.read_text(encoding="utf-8") + assert f"{_TRIVY_INPUT}:" in text, f"{wf.name} 现在就没接上豁免" + assert "ignorefile:" not in text, f"{wf.name} 里混进了 trivy-action 不认的输入名" + mutated = text.replace(f"{_TRIVY_INPUT}:", "ignorefile:") + assert f"{_TRIVY_INPUT}:" not in mutated, "判据不是恒真:把输入名换成错的那个之后必须失去匹配"