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 页面以“安全扫描”标签展示。
报告生成后,真正决定治理效果的是“怎么读”和“怎么处置”。在 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 的配置要谨慎:排除过多可能漏掉真实依赖,排除过少则会把测试夹具中的漏洞噪声带入报告,影响团队对真实风险的判断。
DS_MAX_DEPTH 与排除规则组合,避免一个超大依赖树拖慢整条流水线。扫描作业本身通常只需几分钟,但依赖树极深时可能显著变慢,必要时可拆分为多个并行作业。最后要建立“扫描—修复—验证”的闭环。修复后重新触发流水线,确认报告中对应漏洞消失,且没有引入新的漏洞。对于升级后出现的兼容性问题,应结合单元测试与集成测试一起判断,而不是仅凭扫描报告变绿就认为风险已消除。依赖漏洞治理不是一次性任务,而是伴随每次依赖变更持续进行的工程习惯。