跳转至

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 跑,把每次误报记下来,月底再调。

相关阅读