流水线触发策略: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 增量扫描的依赖缓存管理
增量扫描必须保存上次扫描的“快照”,通常包括:
- 文件校验和:每个文件的 SHA256 值,用于判断内容是否变化。
- 依赖清单:
go.sum、package-lock.json等,记录每个依赖版本与哈希。 - 扫描结果标识:上次扫描出的漏洞 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 完整触发决策流程
下图展示了从代码变更到扫描执行的完整决策树:
2.6 部署与稳定性优化
将上述策略投入生产时,需关注以下事项:
- 资源限制:为全量扫描设置超时(如 30 分钟),避免因仓库膨胀导致 Runner 挂死。
- 重试机制:定时触发的全量扫描若失败,应自动重试 1 次;增量扫描失败则回退为全量扫描。
- 日志审计:每次触发都应记录触发类型、扫描模式、条件表达式结果,便于事后溯源。
- 并行限流:避免同一仓库的多个 PR 同时触发全量扫描,可配置互斥锁或队列。
性能优化建议:
- 对于 PR 触发,默认使用增量扫描;仅当变更涉及核心依赖文件(如
requirements.txt)时升级为全量扫描。 - 将缓存存储到共享卷(如 NFS)或对象存储,避免每次从 Git 重新拉取。
- 使用
changes规则代替语言级 diff 计算,可减少流水线启动开销(如 GitLab CI 原生支持)。