4. 依赖漏洞扫描实战

现代应用的代码量中,真正由团队自己编写的部分往往只占一小部分,其余大量能力来自 npm、Maven、PyPI、Go Module、Composer 等开源生态。开源组件带来效率的同时,也把上游漏洞直接引入到制品中。GitLab Dependency Scanning 的目标,就是在流水线中自动识别项目依赖清单,解析出组件及其版本,再与漏洞数据库比对,把存在已知漏洞的组件以报告形式暴露出来,并可按策略阻断合并或部署。

要理解它的工作方式,先要区分两个概念:依赖清单与锁文件。依赖清单(如 package.json、pom.xml、requirements.txt)声明了直接依赖及其版本范围;锁文件(如 package-lock.json、yarn.lock、poetry.lock、go.sum)则固化了整棵依赖树的精确版本。Dependency Scanning 优先读取锁文件,因为只有精确版本才能与漏洞库中的受影响版本区间做可靠匹配。如果项目只有清单没有锁文件,扫描器只能按声明范围推断,结果会带有不确定性,甚至出现漏报。

GitLab 提供两种接入方式。第一种是使用官方 CI/CD 组件或模板,在 .gitlab-ci.yml 中引入 Dependency-Scanning.gitlab-ci.yml 模板,它会自动添加 dependency_scanning 作业,并把结果以 SAST/依赖扫描报告的形式上传。第二种是使用 gemnasium 系列分析器镜像,在自定义作业中显式调用。下面给出一个基于模板的最小可用示例,适用于以 npm 为主的前端或 Node.js 项目:

include:
  - template: Security/Dependency-Scanning.gitlab-ci.yml

variables:
  DS_EXCLUDED_PATHS: "spec,test,tests,tmp,node_modules"
  DS_MAX_DEPTH: "3"
  DS_REMEDIATE: "true"

dependency_scanning:
  stage: test
  rules:
    - if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH
    - if: $CI_PIPELINE_SOURCE == "merge_request_event"

这段配置的关键点在于:DS_EXCLUDED_PATHS 排除测试与临时目录,避免扫描器把测试夹具中的依赖当成生产依赖;DS_MAX_DEPTH 控制依赖树解析深度,防止超深依赖导致作业超时;DS_REMEDIATE 开启后,扫描器会尝试给出可升级到的安全版本建议。作业只在默认分支和合并请求事件上运行,既保证主干持续可见,又能在 MR 阶段拦截新增漏洞。

扫描执行时,分析器会经历“发现清单 → 解析依赖树 → 提取组件坐标 → 查询漏洞库 → 生成报告”这条链路。以 npm 为例,分析器读取 package-lock.json,构建出包含直接依赖与传递依赖的完整列表,每个组件带有名称、版本、引入路径。随后它把组件坐标与 GitLab 维护的漏洞数据库(基于公开漏洞源聚合)比对,命中后记录漏洞 ID、严重级别、受影响版本区间、修复版本以及该组件是通过哪条依赖路径被引入的。最后生成 gl-dependency-scanning-report.json,并在 MR 页面以“安全扫描”标签展示。

GitLab Dependency Scanning 数据流与判定链路 ① 依赖输入 package-lock.json pom.xml / go.sum requirements.txt 锁文件优先,精确版本 ② 依赖树解析 直接依赖 + 传递依赖 组件名 / 版本 / 引入路径 DS_MAX_DEPTH 控制深度 排除 test / tmp 目录 ③ 漏洞库比对 组件坐标 vs 受影响区间 漏洞 ID / 严重级别 修复版本 / 引入路径 命中即记录 ④ 报告输出 gl-dependency- scanning-report.json MR 安全扫描标签 可阻断合并 漏洞命中后的判定与处置路径 命中漏洞 读取严重级别 Critical / High 阻断 MR 合并 要求升级或替换组件 Medium 记录并排期修复 不阻断当前流水线 Low / Info 纳入技术债清单 定期批量升级 修复动作 升级到修复版本 或替换为安全组件 判定依据:漏洞严重级别 + 组件是否在生产依赖路径 + 是否存在可用修复版本

报告生成后,真正决定治理效果的是“怎么读”和“怎么处置”。在 MR 的安全扫描面板中,每条漏洞会显示组件名、当前版本、漏洞 ID、严重级别和修复建议。判断优先级时,不能只看严重级别,还要看该组件是否位于生产依赖路径上。例如一个 lodash 的 High 漏洞,如果它只被 devDependencies 中的构建工具引入,实际运行时风险会低于被主应用直接引用的同类漏洞。GitLab 报告中的“引入路径”字段正是用来做这个判断的。

处置动作通常有三类。第一类是升级:如果漏洞库给出了修复版本,且升级不破坏兼容性,直接在清单中提升版本并重新生成锁文件即可。第二类是替换:当组件已停止维护或修复版本跨度太大时,改用功能等价的安全组件。第三类是接受风险:对于无法立即修复的 Low/Info 漏洞,可记录到技术债清单并设置复查时间,但不建议对 Critical/High 长期挂起。下面给出一个可直接参考的漏洞处置记录表结构,字段围绕依赖扫描的实际输出设计:

组件 当前版本 漏洞 ID 严重级别 引入路径 修复版本 处置方式 状态
lodash 4.17.15 CVE-2020-8203 High 直接依赖 4.17.19 升级 已修复
minimist 1.2.0 CVE-2020-7598 Medium 传递依赖(webpack) 1.2.5 升级父依赖 处理中
axios 0.21.1 CVE-2021-3749 High 直接依赖 0.21.4 升级 已修复
node-forge 0.10.0 CVE-2022-24771 Low 传递依赖(自签名工具) 1.3.0 接受风险 已记录

需要特别注意的是,依赖扫描的结果与锁文件质量强相关。如果团队在 CI 中执行 npm install 而不是 npm ci,锁文件可能被重新生成,导致扫描到的版本与本地开发不一致。建议在扫描作业前确保锁文件已提交且未被修改,并在 MR 中把锁文件变更与依赖扫描结果一起评审。此外,DS_EXCLUDED_PATHS 的配置要谨慎:排除过多可能漏掉真实依赖,排除过少则会把测试夹具中的漏洞噪声带入报告,影响团队对真实风险的判断。

如果项目使用多模块结构(如 Maven 多模块或 monorepo),建议为每个子模块单独配置扫描路径,或使用 DS_MAX_DEPTH 与排除规则组合,避免一个超大依赖树拖慢整条流水线。扫描作业本身通常只需几分钟,但依赖树极深时可能显著变慢,必要时可拆分为多个并行作业。

最后要建立“扫描—修复—验证”的闭环。修复后重新触发流水线,确认报告中对应漏洞消失,且没有引入新的漏洞。对于升级后出现的兼容性问题,应结合单元测试与集成测试一起判断,而不是仅凭扫描报告变绿就认为风险已消除。依赖漏洞治理不是一次性任务,而是伴随每次依赖变更持续进行的工程习惯。