2. GitLab CI/CD与安全扫描基础
要在 GitLab 中把 SAST 用好,第一步不是急着打开某个扫描模板,而是先弄清楚 GitLab CI/CD 的运行时骨架:一次代码提交之后,究竟是谁在什么时机、按什么顺序、在什么环境里执行了哪些任务。只有把 Pipeline、Stage、Job、Runner 这几个概念和它们之间的依赖关系理顺,才能准确回答“SAST 应该挂在哪里、为什么挂在那里、挂错位置会怎样”。
一、从一次 push 到一条流水线
当开发者向 GitLab 仓库推送提交或发起合并请求(Merge Request)时,GitLab 会读取仓库根目录下的 .gitlab-ci.yml,据此生成一条 Pipeline(流水线)。Pipeline 是一次完整的持续集成执行实例,它由若干 Stage(阶段) 组成,每个 Stage 内部又包含一个或多个 Job(作业)。真正执行 Job 脚本的机器或容器,就是 Runner。
四者的关系可以这样理解:Pipeline 是“整场演出”,Stage 是“第几幕”,Job 是“某个具体节目”,Runner 是“上台表演的演员”。同一 Stage 内的 Job 默认并行执行,不同 Stage 之间按 stages 声明的顺序串行推进——只有前一个 Stage 的所有 Job 都成功,后一个 Stage 才会开始。这个“阶段串行、阶段内并行”的规则,是决定 SAST 挂载点的核心依据。
二、Job 与 Runner:谁在跑、跑在哪
Job 是流水线中可执行的最小单元,它由 script 定义要执行的命令,由 stage 指定所属阶段,由 tags 决定由哪类 Runner 承接。Runner 是安装在物理机、虚拟机或 Kubernetes 集群上的代理进程,它向 GitLab 注册后领取 Job 并执行。Runner 的 executor 类型(如 shell、docker、kubernetes)决定了 Job 的运行环境隔离方式。
这一点对 SAST 尤其关键:SAST 工具通常需要拉取镜像、下载规则库、读取源码并生成报告,对网络、磁盘和 CPU 有一定要求。如果把它交给一个资源受限的共享 Runner,扫描可能超时或被中断。因此实践中常为安全扫描配置带特定 tags 的专用 Runner,例如 tags: [sast, docker],让 sast-scan 这个 Job 精确落到预装了扫描器镜像的节点上。
三、SAST 为什么挂在 test 阶段
SAST 的本质是在不运行代码的前提下,对源代码做词法、语法、数据流和控制流分析,找出注入、硬编码密钥、不安全反序列化等缺陷。它不需要部署产物,但需要源码和依赖清单,因此最自然的挂载点是 test 阶段,与单元测试、依赖扫描并列。
把它放在 test 而不是 build 之前,是因为很多 SAST 工具需要先完成依赖解析或编译产物辅助分析;把它放在 deploy 之前,是因为安全门禁必须在代码进入预发或生产环境之前生效。如果 SAST 失败,Pipeline 会在 test 阶段终止,deploy 阶段的 Job 根本不会启动——这就是“安全左移”在流水线层面的具体体现。
四、一个可运行的最小示例
下面这段 .gitlab-ci.yml 展示了 build、test、deploy 三个阶段,其中 sast-scan 位于 test 阶段,使用 GitLab 官方 SAST 模板,并通过 tags 指定 Runner。示例中的镜像版本和规则集为教学假设,实际使用时请按官方文档替换为当前稳定版本。
stages:
- build
- test
- deploy
variables:
SAST_EXCLUDED_PATHS: "spec, test, tests, tmp"
build-job:
stage: build
tags: [docker]
script:
- echo "编译与打包"
- make build
# SAST 挂载点:test 阶段
include:
- template: Security/SAST.gitlab-ci.yml
sast:
stage: test
tags: [sast, docker]
variables:
SAST_GOSEC_LEVEL: "high"
unit-test:
stage: test
tags: [docker]
script:
- make test
deploy-staging:
stage: deploy
tags: [docker]
script:
- ./deploy.sh staging
only:
- main
这段配置执行后,可以在 Pipeline 页面看到三个阶段依次点亮。如果 sast Job 失败,deploy-staging 会显示为 skipped,而不是 failed——这是判断“安全门禁是否真正生效”的直接依据。同时,在合并请求页面会出现 Security 报告卡片,列出新增漏洞的数量和等级。
五、关键参数与观察点
| 配置项 | 作用 | SAST 场景下的建议 |
|---|---|---|
stages | 定义阶段顺序 | 确保 test 在 deploy 之前 |
stage | Job 所属阶段 | SAST Job 设为 test |
tags | 匹配 Runner | 为扫描 Job 指定专用标签 |
rules / only | 控制触发条件 | MR 和主分支都触发扫描 |
allow_failure | 是否允许失败 | 初期可设 true 观察,稳定后设 false 做门禁 |
artifacts | 保留产物 | 保留 gl-sast-report.json 供报告展示 |
allow_failure 长期设为 true,使安全门禁形同虚设。另一个高频问题是 Runner 未打 tags,Job 被随机调度到无扫描镜像的节点,表现为“找不到命令”或“镜像拉取失败”。