一句话总结
质量门禁是流水线里一道道"不达标就拦下"的自动关卡;
这一篇把它们装齐--从测试覆盖率,静态扫描(SAST),到依赖与镜像漏洞扫描(Trivy),SBOM 清单,镜像签名,层层守住"合进来的代码够好,推出去的镜像可信".
前置回顾
第 01 篇定义了质量门禁:阶段间的卡点,是"敢于自动化"的前提.第 02 篇我们用 GitHub Actions 让 npm test 当了第一道门.第 03 篇产出了可追溯的镜像.本篇要回答:除了"测试通过",还该卡哪些条件,才能让无人工审查的自动交付真正放心?这是阶段一的收尾,也是阶段二 GitOps 自动部署的安全底座.
为什么"测试通过"远远不够
很多人以为 CI 的门禁就是"跑单测".但测试只能证明功能符合预期,管不了下面这些同样能搞垮生产的问题:
测试全绿,但是:
- 新写的代码根本没被测试覆盖到 -- 测试"绿"得没意义
- 代码里有 SQL 拼接,存在注入漏洞 -- 功能正常,安全是个洞
- 引入的某个第三方库有已知高危 CVE -- 你的锅,但不是你写的
- 基础镜像 alpine 自带的某个包有漏洞 -- 镜像层面的风险
- 镜像在推送途中被人掉包 -- 部署的不是你构建的那个
这些问题有个共同点:功能测试发现不了,但都会变成生产事故或安全事件.质量门禁的设计,就是为每一类风险配一道专门的自动关卡.我们从"代码本身"由内向外,一层层往镜像和供应链推.
|
门禁一:测试覆盖率
覆盖率(Coverage)衡量"测试到底跑过了多少比例的代码".它堵的是"测试绿得没意义"这个洞--如果新增的 200 行代码一行都没被测到,那测试通过只能说明没被测的部分恰好没崩.
|
实践中更常用现成工具(如 SonarQube,Codecov)来设阈值,但原理就是这个 exit 1:不达标就让 Step 失败,流水线随之中止.这就是第 02 篇"失败即变红,阻止合并"机制的延续.
别盲目追求 100% 覆盖率
覆盖率是必要不充分的指标:100% 覆盖也可能断言写得毫无意义.而且强行追求高覆盖会逼出大量"为覆盖而覆盖"的无效测试.更实用的门禁是增量覆盖率--只要求"本次新增/改动的代码"覆盖率达标,而非整个项目,这样既守住新代码质量,又不会被历史欠债拖垮.
门禁二:SAST 静态应用安全测试
SAST(Static Application Security Testing,静态应用安全测试):不运行代码,直接扫描源码,找出危险的写法和已知的漏洞模式.它堵的是"功能正常但有安全洞"这一类.
典型能揪出的问题:SQL 注入(字符串拼接 SQL),命令注入,硬编码的密码/密钥,不安全的加密算法,路径穿越等.
SAST 像拼写检查器:它不理解你文章写得好不好(那是功能测试的事),但能机械地标出所有"拼错的词"--那些已知有风险的代码模式.
常用工具:CodeQL(GitHub 原生,在 Actions 里几行配置就能开),Semgrep,SonarQube.在 GitHub Actions 里启用 CodeQL:
|
SAST 之外还有 DAST
与 SAST(静态,看源码)相对的是 DAST(Dynamic,动态):把应用真正跑起来,像黑客一样发请求攻击它,从外部找漏洞.SAST 早(提交时就能扫),覆盖全(看得到所有代码),但有误报;DAST 准(真实攻击),但晚(要先部署),且只能测到跑起来的路径.两者互补,CI 阶段以 SAST 为主.
门禁三:依赖扫描
现代项目里,你自己写的代码可能只占 10%,其余 90% 是第三方依赖.这些依赖里的已知漏洞,就是你应用的漏洞--著名的 Log4j 漏洞就是这么炸全网的.
依赖扫描(也叫 SCA,Software Composition Analysis,软件成分分析)比对你的依赖清单和公开漏洞库(CVE),揪出"你用的某个库的某个版本有已知高危漏洞".
|
工具:GitHub 自带的 Dependabot(免费,自动提修复 PR),Snyk,以及下面要重点讲的 Trivy(它依赖扫描和镜像扫描都能干).
门禁四:用 Trivy 扫描镜像
前三道门守的是源码和依赖.但第 03 篇构建出的镜像,还包含基础镜像(如 alpine)自带的操作系统包--这些你没写,没直接引入,但同样可能有漏洞.需要直接扫"成品镜像".
Trivy(Aqua Security 出品)是云原生领域最流行的扫描器,一个工具扫多种目标:镜像里的 OS 包漏洞,应用依赖漏洞,配置错误,密钥泄露.在流水线里,它接在第 03 篇构建之后:
|
关键又是 exit-code: 1:扫到高危漏洞就让流水线变红,这个镜像根本不会被推到仓库,更不会上线.门禁的精髓始终是这一下"拦截".
扫描左移:越早扫越省事
注意上面是"先构建,扫过再推".理想情况下扫描还要更"左"(更靠近开发):本地提交前扫,PR 时扫,构建时扫,层层前置.这叫安全左移(Shift Left)--问题发现得越早,修复成本越低.等镜像都推到生产了才扫出高危漏洞,代价是指数级的.
门禁五:供应链安全 -- SBOM 与镜像签名
前面四道门解决"东西本身有没有问题".最后这道门解决一个更隐蔽的问题:我部署的这个镜像,真的是我那条可信流水线构建出来的吗?中途有没有被掉包?里面到底装了什么?这就是软件供应链安全(Software Supply Chain Security).
SBOM:软件物料清单
SBOM(Software Bill of Materials,软件物料清单) 是一份机器可读的清单,精确列出"这个镜像里到底装了哪些包,哪些版本".
SBOM 就像食品包装上的成分配料表.平时你可能不看,但一旦某种成分被曝出有问题(某个库爆出新漏洞),你能立刻翻配料表,精确查出"我哪些镜像含这个成分,受不受影响",而不用挨个把镜像拆开重新化验.
构建时用 Trivy 或 Syft 生成 SBOM,随镜像一起存档.等于给每个制品留了一份"出厂成分存根",出事时能秒级排查影响面.
镜像签名:证明"出身清白"
镜像从仓库拉取,在网络上传输,理论上可能被篡改或冒名顶替.镜像签名用密码学手段给镜像盖一个"防伪章":流水线在推送前用私钥签名,集群在部署前用公钥验签--验不过就拒绝运行.
云原生标准工具是 cosign(属于 Sigstore 项目):
|
这和第 02 篇的 OIDC 是一条线索
第 02 篇推荐用 OIDC 免长期密钥,本篇推荐签名免被篡改--它们共同指向一个现代安全理念:不靠"藏好一个长期秘密",而靠"每次都用短期,可验证的凭证证明身份".cosign 甚至支持"keyless"无密钥签名,直接用 OIDC 身份签发.整条供应链的信任,就是这样一环扣一环建立起来的.SLSA 框架(Supply-chain Levels for Software Artifacts)正是对这套"可追溯,可验证"实践的分级标准.
收尾:阶段一的完整 CI 流水线长什么样
把第 01-04 篇所有要点拼起来,一条成熟的 CI 流水线的骨架就清晰了:
|
到这里,流水线已经能自动,可信地把一次提交变成一个经过层层把关的镜像.正因为有这一整套门禁兜底,阶段二才敢让 GitOps 无人工介入地把镜像部署上线--门禁的严密程度,决定了你敢把自动化推进到哪一步.这正好呼应第 01 篇:门禁是敢于自动化的前提.
快速回顾
- 测试通过 ≠ 安全可上线:覆盖率,安全洞,依赖漏洞,镜像漏洞,被篡改,功能测试都管不了
- 门禁一 覆盖率:堵"测试绿得没意义",优先用增量覆盖率
- 门禁二 SAST:不运行代码,扫源码找危险写法(CodeQL),像拼写检查器
- 门禁三 依赖扫描(SCA):比对 CVE 库揪出第三方库的已知漏洞(Dependabot)
- 门禁四 Trivy:扫成品镜像里的 OS 包/依赖漏洞,
exit-code:1拦截,讲究安全左移 - 门禁五 供应链:SBOM 是"成分清单"便于排查影响面,cosign 签名证明"未被篡改,出身可信"
- 主线:门禁越严,越敢把自动化(直到无人工的 GitOps 部署)推得越远
这么多扫描,流水线会不会变得很慢?
会增加时间,但有应对:1. 并行跑(第 02 篇)--SAST,依赖扫描和单测互不依赖,可同时进行;2. 分级--PR 时跑快速扫描,合并到主干或发版前再跑全量深度扫描;3. 缓存扫描数据库.安全和速度的平衡,本身就是门禁设计的一部分,不必所有检查都卡在每一次提交上.
扫出几十个漏洞但短期修不完,难道流水线就一直红着?
现实中确实如此,所以工具都支持分级和豁免:只对 HIGH,CRITICAL 设硬门禁(第 04 篇示例),中低危先记录不拦截;对确认无法利用或已有缓解措施的特定 CVE,可以加到忽略清单(如 .trivyignore)并写明理由和期限.关键是有意识地决策每个漏洞"修,缓,忍",而不是直接关掉门禁假装看不见.
动手练习
- 覆盖率门禁验证:给第 02 篇的流水线加一个覆盖率统计,设一个阈值,故意让覆盖率不达标,确认门禁拦截.
- CodeQL SAST 扫描:在仓库开启 CodeQL,故意写一段拼接 SQL 的代码,看 SAST 能否在 PR 的 Security 标签里报出注入风险.
- Trivy 镜像扫描:本地用
trivy image <你第03篇推的镜像>扫一次,看看一个"普通"镜像里到底有多少 OS 层漏洞. - 基础镜像更新对比:把基础镜像从某个旧版本换成最新版,重新扫描对比漏洞数量变化--直观感受"及时更新基础镜像"为何重要.
- (进阶)SBOM 生成:用
trivy image --format spdx-json给镜像生成一份 SBOM,打开看看里面列了多少你"从没听说过"的包.