流水线触发策略:PR 触发、定时触发、手动触发、增量扫描与全量扫描、触发条件表达式编写

安全扫描流水线的触发策略直接决定了扫描的时效性、资源消耗以及对开发流程的侵入程度。本章将系统讲解四种核心触发方式、两种扫描模式的取舍原则,以及如何通过条件表达式精确定义触发规则,帮助你构建既快又省的自动化安全防线。

前置要求:已掌握基础 CI/CD 流水线配置(Jenkins / GitLab CI / GitHub Actions 任一种),理解分支策略和基础通配符。

2.1 触发策略分类与对比

根据扫描触发来源和时机,可将策略分为三类:

触发类型 驱动因素 典型场景 优势 劣势
PR(合并请求)触发 代码评审事件 提交 PR 时自动扫描新增代码 早发现、快反馈、强左移 PR 量大时可能阻塞流水线
定时触发 时间调度(cron) 每日凌晨全量扫描所有分支 覆盖遗漏分支,适合合规审计 存在扫描延迟,非实时
手动触发 运维/安全人员主动操作 紧急漏洞公告后立即重扫 高度灵活,按需执行 依赖人工,无法自动化

2.2 增量扫描 vs 全量扫描

扫描范围控制是资源调度的关键。两种模式的核心区别如下:

维度 增量扫描 全量扫描
扫描对象 仅本次变更的文件或依赖 整个仓库所有文件
执行时间 通常 1-5 分钟 根据仓库大小 10-60+ 分钟
适用触发 PR 触发、手动触发(快速) 定时触发、手动触发(彻底)
依赖缓存 需维护上次扫描的基线数据 无需基线,独立执行
风险 可能遗漏间接依赖漏洞 资源消耗高,重复扫描已检文件
注意:增量扫描必须搭配可靠的变更集检测机制。若使用 Git 的 git diff --name-only 确定变更文件,注意当分支通过 rebase 或 squash 合并时,diff 对象可能不准确。推荐结合 git merge-base 计算真实差异。

2.3 触发条件表达式编写

实际项目中通常需要组合多个条件才能精确定位有效触发。以下以 Jenkins Pipeline 和 GitLab CI 为例,展示条件表达式写法。

// Jenkinsfile 示例:仅当 PR 修改了 src/ 或 config/ 目录时执行安全扫描
if (env.CHANGE_ID) {  // 确认是 PR 上下文
    def files = sh(script: 'git diff --name-only origin/main HEAD', returnStdout: true).trim().split('\n')
    def scanPaths = ['src/', 'config/', 'Dockerfile', '*.yaml']
    def shouldScan = files.any { file ->
        scanPaths.any { path -> file.startsWith(path) || file == path }
    }
    if (shouldScan) {
        stage('Security Scan') {
            // 执行增量扫描逻辑...
        }
    }
}
# GitLab CI .gitlab-ci.yml 规则示例
rules:
  - if: '$CI_PIPELINE_SOURCE == "merge_request_event"'
    changes:
      - "src/**/*"
      - "config/**/*"
    when: always
  - if: '$CI_PIPELINE_SOURCE == "schedule"'
    when: always                  # 定时触发全量扫描
  - when: manual                  # 手动触发始终可用

关键要点:

  • 路径匹配:使用 glob 模式时注意 ** 与 * 的语义差异(单层 vs 递归)。
  • 变量引用:在 Jenkins 中通过 env. 获取环境变量,在 GitLab 中通过 $ 引用内置变量。
  • 合并条件:可使用逻辑运算符 && / || 组合分支、标签、文件变更等条件。
  • 回退策略:当条件表达式解析失败时,建议将 when 设回 never 或 manual,避免意外跳过重要扫描。
调试技巧:在 Jenkins 中使用 echo "Files changed: ${files}" 打印变更列表;在 GitLab CI 中可以通过 CI_JOB_RULES 变量查看规则匹配结果。将条件逻辑单独放在一个短周期 job 中,先验证输出再嵌入正式流水线。

2.4 增量扫描的依赖缓存管理

增量扫描必须保存上次扫描的“快照”,通常包括:

  1. 文件校验和:每个文件的 SHA256 值,用于判断内容是否变化。
  2. 依赖清单:go.sum、package-lock.json 等,记录每个依赖版本与哈希。
  3. 扫描结果标识:上次扫描出的漏洞 ID,避免重复告警。

典型实现方式(以 SAST 为例):

#!/bin/bash
# 增量扫描脚本片段
CACHE_DIR="./.scan-cache"
mkdir -p "$CACHE_DIR"

prev_hash=$(cat "$CACHE_DIR/checksum.txt" 2>/dev/null)
current_hash=$(find src/ -type f -exec sha256sum {} + | sort | sha256sum)

if [ "$prev_hash" != "$current_hash" ]; then
  echo "代码已变更,执行增量扫描"
  # 扫描所有未缓存的文件,逐一对比
  # 结果存入 $CACHE_DIR/result.json
  echo "$current_hash" > "$CACHE_DIR/checksum.txt"
else
  echo "无变更,跳过扫描"
fi
常见错误:
  • 忽略 git merge-base 导致 diff 范围错误(增量扫描扫描了未变更文件)。
  • 缓存路径在分布式 runner 中不存在(需用 stash 或卷挂载共享目录)。
  • 未清理过期缓存导致磁盘不足(建议每次全量扫描后重置缓存)。

2.5 完整触发决策流程

下图展示了从代码变更到扫描执行的完整决策树:

流水线触发决策流程 代码变更推送 触发类型? PR 触发 定时触发 手动触发 条件表达式评估 扫描模式? 增量扫描 全量扫描 仅路径匹配 跳过条件

2.6 部署与稳定性优化

将上述策略投入生产时,需关注以下事项:

  • 资源限制:为全量扫描设置超时(如 30 分钟),避免因仓库膨胀导致 Runner 挂死。
  • 重试机制:定时触发的全量扫描若失败,应自动重试 1 次;增量扫描失败则回退为全量扫描。
  • 日志审计:每次触发都应记录触发类型、扫描模式、条件表达式结果,便于事后溯源。
  • 并行限流:避免同一仓库的多个 PR 同时触发全量扫描,可配置互斥锁或队列。
性能优化建议:
  • 对于 PR 触发,默认使用增量扫描;仅当变更涉及核心依赖文件(如 requirements.txt)时升级为全量扫描。
  • 将缓存存储到共享卷(如 NFS)或对象存储,避免每次从 Git 重新拉取。
  • 使用 changes 规则代替语言级 diff 计算,可减少流水线启动开销(如 GitLab CI 原生支持)。