SCA 接进流水线后,先解决这三件事¶
前两天都在代码层打转,今天换依赖。翻了一下流水线,SCA 这个 stage 从上线起就没红过,报告每周照常归档,看着挺健康。后来发现不是依赖干净,是它压根没有失败条件。
这篇只记三件必须先解决的事,按踩到的顺序排。
一、门禁的默认值等于没门¶
OWASP Dependency-Check 的 Maven 插件,官方文档里 failBuildOnCVSS 的默认值写着 11,后面还跟了一句解释:CVSS 分值范围是 0 到 10,所以默认情况下构建永远不会失败。
也就是说,只要不显式配这个值,插件会老老实实扫描、出报告、返回 0。那个绿了几周的 stage,原因就在这。
设成 7(High 及以上)是常见起点:
<!-- pom.xml -->
<plugin>
<groupId>org.owasp</groupId>
<artifactId>dependency-check-maven</artifactId>
<version>12.2.2</version>
<configuration>
<failBuildOnCVSS>7</failBuildOnCVSS>
<formats>
<format>HTML</format>
<format>SARIF</format>
</formats>
<suppressionFiles>
<suppressionFile>owasp-suppression.xml</suppressionFile>
</suppressionFiles>
<!-- 抑制规则失效时让构建失败,防止抑制表烂在仓库里 -->
<failBuildOnUnusedSuppressionRule>true</failBuildOnUnusedSuppressionRule>
</configuration>
</plugin>
命令行调用时长这样:
mvn -B verify org.owasp:dependency-check-maven:12.2.2:check \
-DnvdApiKey="$NVD_API_KEY" \
-DfailBuildOnCVSS=7
两个容易写错的地方:
- CLI 和 Maven/Gradle 的参数名不一样。CLI 是
--failOnCVSS(0~10 之间,命中就返回非 0),Maven / Gradle 插件叫failBuildOnCVSS。把 CLI 的写法搬到-D上,不会报错,只是静默不生效。 failBuildOnCVSS是「CVSS ≥ 阈值就失败」,不是「有漏洞就失败」。Maven 插件另有failBuildOnAnyVulnerability,默认 false,别混用。JUNIT 报告那条线还有个junitFailOnCVSS,默认 0,只影响 JUNIT 报告里的用例状态,跟构建失败是两码事。
顺带把 failBuildOnUnusedSuppressionRule 打开。抑制表里那条 CVE 修好之后,规则会变成「未使用」,这时候让构建红一次,逼着人去删,比三年后没人知道为什么忽略强。
二、NVD 数据源:为什么第一次跑那么久¶
第二个问题是耗时。第一次全量跑,日志停在拉 NVD 数据上很久,还伴着几次重试。查了 CLI 参数表才明白,不是网络差,是限流策略本身就慢:
| 参数 | 默认值 | 说明 |
|---|---|---|
--nvdApiDelay |
有 key 3500 ms,无 key 8000 ms | 每次调 NVD API 之间的等待 |
--nvdMaxRetryCount |
10 | 单次调用最大重试 |
--nvdApiResultsPerPage |
2000(上限 2000) | 单页记录数 |
--nvdValidForHours |
4 | 多久检查一次更新 |
--nvdApiKey |
— | 从 https://nvd.nist.gov/developers/request-an-api-key 申请 |
没有 API key 时每次请求之间要等 8 秒,NVD 数据全量分页拉下来,第一次跑到几十分钟一点都不奇怪。申请一个 key,等待降到 3.5 秒,这一步几乎是无成本的优化。
CI 里还得把数据目录缓存住,否则每个 job 都重来一遍:
dependency-check:
stage: test
image: maven:3.9-eclipse-temurin-21
variables:
NVD_DATA: "$CI_PROJECT_DIR/.nvd"
cache:
key: dc-nvd
paths: [.nvd/]
script:
- >
mvn -B org.owasp:dependency-check-maven:12.2.2:check
-DnvdApiKey="$NVD_API_KEY"
-DfailBuildOnCVSS=7
几个配套参数,按环境挑:
--updateonly 只更新本地 NVD 库、不扫描(适合做一个独立的定时 job)
--noupdate 扫描时不更新,直接用缓存(避免每个 PR 都去碰 NVD)
--purge 清空本地库,强制重建(库疑似损坏时用)
--nvdDatafeed 指向内网镜像的 nvdcve-{0}.json.gz,离线环境走这条
把「更新库」和「跑扫描」拆成两个 job 会更稳:nightly 更新一次,PR 里带 --noupdate 直接用缓存。数据源抖动就不会再把 PR 拖红。注意缓存别设成永久不过期,--nvdValidForHours 默认 4 小时是有道理的,隔太久的库会漏新 CVE。
三、锁文件、作用域、可达性¶
第三件事决定结果准不准。
没有锁文件,SCA 就是在猜。 Maven 的 pom、npm 的 package-lock.json、Go 的 go.mod 这类文件才带得准确的版本和传递依赖。官方 Maven 插件的 scanSet 默认就包含 package.json、package-lock.json、npm-shrinkwrap.json、Gopkg.lock、go.mod 这些路径——它显然知道光看源码目录不够。只扔一个 target/*.jar 进去,识别出来的 GAV 经常缺版本,漏报和误报一起来。
测试依赖默认不扫。 Maven 插件 skipTestScope 默认 true,Gradle 插件 skipTestGroups 默认 true。这是合理的:JUnit、Mockito 进不了制品。但如果你们的打包方式把 test scope 的东西也塞进了镜像,这个默认值就成了盲区,得自己确认一遍,别想当然。
只拦能修的。 无修复版本的 CVE,阻断也变不出补丁,只会让门禁长期红着直到被人关掉。Trivy 的 --ignore-unfixed 就是这个意思(官方释义是「只显示有修复版本的漏洞」);dependency-check 这边要靠抑制表加 until 期限临时接住,到期复审:
<!-- owasp-suppression.xml -->
<suppress until="2026-12-31Z">
<notes>仅测试作用域,不进入制品;上游暂无修复版本,SEC-2402 跟踪</notes>
<cve>CVE-2024-XXXXX</cve>
</suppress>
(上面那个编号是占位,别直接贴进真实抑制表。)
四、dependency-check 和 Trivy 各管一段¶
这两个不冲突,位置都不一样:
| 放在哪一步 | 覆盖 | 门禁参数 | |
|---|---|---|---|
| Dependency-Check | 构建阶段,贴着 Maven / Gradle | Java 生态为主,NVD / CVSS 粒度细,抑制表可审计 | failBuildOnCVSS / --failOnCVSS |
| Trivy | 镜像构建之后、推送之前 | OS 包 + 语言依赖 + IaC 配置 + 密钥,还能直接出 SBOM | --exit-code + --severity |
镜像那一刀用 Trivy,命令是从官方 CLI 文档里对出来的:
trivy image \
--severity CRITICAL,HIGH \
--ignore-unfixed \
--exit-code 1 \
--format sarif --output trivy.sarif \
"app:${GIT_SHA}"
# 顺手出一份 CycloneDX SBOM,新 CVE 爆出来时能快速反查受影响镜像
trivy image --format cyclonedx --output bom.cdx "app:${GIT_SHA}"
--exit-code 要显式给,Trivy 默认不会因为扫出问题就返回非 0,跟这轮开头 SonarQube 那个坑是同一类。--ignorefile 默认是 .trivyignore,抑制条目同样该带工单号和复审日期。漏洞库可以用 --download-db-only 单独预热,配合 CI 缓存,避免每个 job 都拉一次 DB。
五、验收¶
1. 在 pom 里临时引一个已知有 High 漏洞的旧版本,确认 job 红
2. 把 failBuildOnCVSS 改回 11(或不配),确认 job 又变绿 —— 反向证明门禁确实由它控制
3. 删掉抑制表里一条仍在使用的规则,确认 failBuildOnUnusedSuppressionRule 生效
4. 用 --noupdate 跑一次,确认离线缓存可用且耗时明显下降
5. 检查镜像扫描是不是跑在镜像构建之后、推送之前
第 2 条看着多余,但它是唯一能证明「不是碰巧绿」的动作。我这边就是靠它确认了之前几周的绿跟依赖质量没关系。
坑位表¶
| 现象 | 根因 | 处理 |
|---|---|---|
| 报告有漏洞,job 一直绿 | failBuildOnCVSS 默认 11 |
显式设 7 或按团队分级 |
CLI 里 -DfailOnCVSS 不生效 |
CLI 与 Maven/Gradle 参数名不同 | 插件用 failBuildOnCVSS |
| 首次跑几十分钟 | 无 NVD API key,请求间隔 8000 ms | 申请 key(降到 3500 ms)+ 缓存数据目录 |
| 每个 PR 都去拉 NVD | 没拆更新与扫描 | nightly --updateonly,PR 用 --noupdate |
| 漏报传递依赖 | 没有锁文件,或只扫了 jar 目录 | 把 lock 文件纳入 scanSet |
| 无修复版本 CVE 长期红灯 | 门禁没区分「可修」 | --ignore-unfixed / 带期限的抑制 |
| 抑制表三年没清 | 规则失效无人发现 | failBuildOnUnusedSuppressionRule |
还没定下来的¶
CVSS 7 这个阈值是不是合适还没结论。它把「High 但代码里根本走不到」的依赖也算进去,误报率得再观察一阵;真正想做的是可达性判断,但那要接运行时数据,不是这一轮能完成的。先按 7 跑,把每次误报记下来,月底再调。