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 挂载点的核心依据。

GitLab CI/CD 流水线结构与 SAST 挂载点 Pipeline(一次流水线实例) Stage: build Job: compile 编译 / 打包 Job: unit-test 单元测试 阶段内并行 Stage: test Job: sast-scan SAST 静态扫描 Job: dependency 依赖漏洞扫描 SAST 挂载点 Stage: deploy Job: deploy-stg 部署预发环境 Job: deploy-prod 部署生产环境 手动触发 Stage: report Job: sast-report 生成安全报告 Job: notify 通知 / 归档 结果汇总 Runner 执行层 Shared Runner Docker executor 公共共享,适合通用 Job Specific Runner Shell / Docker executor 专用,可预装扫描工具 Group Runner Kubernetes executor 组级共享,弹性伸缩 Runner 标签 tags 匹配 决定 Job 落点 sast-scan 由 tags 匹配到专用 Runner 阶段串行推进 · 阶段内并行 · SAST 位于 test 阶段,先于 deploy 执行

二、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 根本不会启动——这就是“安全左移”在流水线层面的具体体现。

挂载点判断口诀:需要源码、不需要运行环境 → test 阶段;需要运行中的服务 → DAST,通常放在 deploy 之后;需要镜像和依赖清单 → 容器扫描,通常紧跟 build。

四、一个可运行的最小示例

下面这段 .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 之前
stageJob 所属阶段SAST Job 设为 test
tags匹配 Runner为扫描 Job 指定专用标签
rules / only控制触发条件MR 和主分支都触发扫描
allow_failure是否允许失败初期可设 true 观察,稳定后设 false 做门禁
artifacts保留产物保留 gl-sast-report.json 供报告展示
常见误区:把 SAST 放在 build 之前,导致依赖未解析、误报率升高;或把 allow_failure 长期设为 true,使安全门禁形同虚设。另一个高频问题是 Runner 未打 tags,Job 被随机调度到无扫描镜像的节点,表现为“找不到命令”或“镜像拉取失败”。