DevSecOps 安全左移:CI/CD 集成扫描策略,从 SAST/DAST 到供应链安全完整指南
去年我们有两次安全事件:一次是开发者在代码里硬编码了 AWS key(推到了 GitHub 公开仓库),一次是依赖包里发现了 log4j 漏洞(生产环境还在用旧版本)。这两件事让我意识到:安全检查不能只在上线前做,必须嵌入到开发的每一个环节。
于是我们重构了 CI/CD 流水线,加了 SAST、SCA、DAST 三层扫描,配合镜像扫描和密钥检测。三个月后,安全事件的 MTTR 从 4 小时降到 30 分钟,高危漏洞率下降了 80%。
这篇分享完整的集成策略和踩坑经验,所有工具组合均来自生产环境的真实部署。
安全扫描三层模型
开发阶段 构建阶段 部署阶段
│ │ │
▼ ▼ ▼
┌────────┐ ┌────────────┐ ┌────────────┐
│ SAST │ │ SCA │ │ DAST │
│(静态代码)│ │(依赖分析) │ │(运行时测试) │
└────────┘ └────────────┘ └────────────┘
│ │ │
▼ ▼ ▼
PR 阶段 构建阶段 Staging 阶段
(阻断高危) (阻断高危) (阻断高危)
SAST:静态应用安全测试
在代码提交前分析代码逻辑漏洞。
# .github/workflows/sast.yml
name: SAST Scan
on: [pull_request]
jobs:
semgrep:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: returntocorp/semgrep-action@v1
with:
config: 'p/default' # 默认规则集
severity: 'error' # 只报告 error 级别
fail_on_exit: true # 高危漏洞阻塞流水线
args: '--json --output=semgrep.json'
- name: Upload SARIF
uses: github/codeql-action/upload-sarif@v2
with:
sarif_file: semgrep.json
Semgrep vs Bandit vs GoSec: - Semgrep:多语言支持、规则可自定义、误报率低——首选 - Bandit:Python 专用,简单直接 - GoSec:Go 专用,Golang 项目必装
踩坑:Semgrep 的规则集太多,选 p/default 可能漏掉一些高危规则。建议基于团队的技术栈自定义规则:
# semgrep.yaml
rules:
- id: hard-coded-secret
patterns:
- pattern: $PASSWORD = "$$";
- pattern: api_key: "$$"
message: "硬编码密钥不允许出现在代码中"
severity: ERROR
languages: [python, go, java]
SCA:软件成分分析
扫描依赖包的安全漏洞。
# Trivy 镜像扫描(最常用)
- name: Trivy Image Scan
uses: aquasecurity/trivy-action@master
with:
image-ref: 'myregistry/my-app:latest'
format: 'sarif'
output: 'trivy-results.sarif'
severity: 'CRITICAL,HIGH'
exit-code: '1' # 高危漏洞阻塞流水线
# Trivy 依赖扫描(Go module)
- name: Trivy Dependency Scan
uses: aquasecurity/trivy-action@master
with:
scan-type: 'fs'
scan-ref: '.'
format: 'sarif'
output: 'trivy-deps.sarif'
severity: 'CRITICAL,HIGH'
exit-code: '1'
Trivy vs Snyk vs Grype: - Trivy:免费、全面(镜像+代码+配置)、OCI 兼容——生产首选 - Snyk:功能最全但付费贵、IDE 插件体验好 - Grype:仅镜像扫描,轻量快速
实际操作经验:Trivy 扫描速度很快(Go 镜像约 30 秒),但规则库更新有延迟——log4j 漏洞爆发时 Trivy 用了 6 小时才更新规则。解决方案:同时跑 Snyk(规则更新快但付费)做补充。
DAST:动态应用安全测试
部署后对运行中的服务进行安全测试。
# OWASP ZAP 基础扫描
- name: OWASP ZAP Scan
uses: zaproxy/[email protected]
with:
target: 'http://staging.example.com'
rules_file_name: '.zap/rules.tsv'
cmd_options: '-a' # 允许扫描所有风险等级
fail_action: true
allow_issue_writing: false
ZAP vs Burp Suite vs Nessus: - ZAP:免费、开源、CI 集成好——自动化扫描首选 - Burp Suite:功能最强但付费、适合人工渗透测试 - Nessus:漏洞扫描器,偏基础设施而非应用层
DAST 的限制:只能测试已部署的服务,且无法发现代码逻辑漏洞(如 SQL 注入需要输入向量才能触发)。DAST 的价值在于发现配置错误和运行时漏洞。
密钥检测:第一道防线
# GitLeaks 密钥泄露检测
- name: GitLeaks Scan
uses: gitleaks/gitleaks-action@v2
env:
GITLEAKS_LICENSE: ${{ secrets.GITLEAKS_LICENSE }}
# 或者用 trivy 的 secret scan
- name: Trivy Secret Scan
uses: aquasecurity/trivy-action@master
with:
scan-type: 'fs'
scan-ref: '.'
severity: 'CRITICAL,HIGH'
format: 'sarif'
output: 'trivy-secret.sarif'
GitLeaks 规则库:默认规则覆盖 200+ 种密钥格式(AWS、GCP、Azure、GitHub token、JWT、SSH key 等),基本够用。如果有内部密钥格式,可以自定义规则:
# .gitleaks.toml
[[rules]]
id = "internal-api-key"
description = "Internal API Key"
regex = '^IAK_[a-zA-Z0-9]{20,}$'
severity = "CRITICAL"
安全策略:阻塞 vs 通知
这是 DevSecOps 最核心的决策——扫描发现问题后,要不要阻塞流水线?
| 扫描类型 | 建议策略 | 理由 |
|---|---|---|
| SAST 高危(SQL 注入、RCE) | 阻塞 | 漏洞直接可被利用 |
| SAST 中危(信息泄露) | 通知 | 需要人工评估 |
| SCA 高危(已知 CVE) | 阻塞 | 有明确 exploit |
| SCA 低危(信息泄露) | 通知 | 取决于依赖链 |
| 密钥泄露 | 阻塞 | 零容忍 |
| DAST 高危 | 阻塞 | 运行时漏洞 |
| 镜像基础镜像漏洞 | 通知+升级建议 | 不是开发可控的 |
我们的实践: - PR 阶段:SAST + SCA + 密钥检测 → 高危阻塞 - 构建阶段:镜像扫描 → 高危阻塞 - Staging 阶段:DAST → 全阻塞 - 每周安全报告:所有漏洞汇总 → 通知安全团队
# 分级阻塞策略
- name: Check Security Results
if: always()
run: |
critical=$(jq '[.results[] | select(.Severity=="CRITICAL")] | length' trivy-results.sarif)
high=$(jq '[.results[] | select(.Severity=="HIGH")] | length' trivy-results.sarif)
if [ "$critical" -gt 0 ]; then
echo "::error::Critical vulnerabilities found: $critical"
exit 1
fi
if [ "$high" -gt 0 ]; then
echo "::warning::High vulnerabilities found: $high (will notify security team)"
fi
供应链安全:不被注意的盲区
Supply Chain 攻击是近两年安全事件的主旋律——npm 包被投毒、CI/CD token 被盗、构建环境被入侵。
防护策略
# 1. 锁定依赖版本(不使用 latest)
# package.json
"dependencies": {
"express": "4.18.2", # 精确版本,不用 ^4.18.2
"lodash": "4.17.21"
}
# 2. 校验依赖完整性
- name: Verify Dependencies
run: |
npm audit --production --audit-level=high
go list -m all | xargs go vet
# 3. 镜像签名验证(cosign)
- name: Verify Image Signature
run: |
cosign verify --key https://example.com/key.pub \
myregistry/my-app:latest
# 4. SBOM 生成(SPDX 格式)
- name: Generate SBOM
uses: anchore/sbom-action@v0
with:
path: .
format: spdx-json
output-file: sbom.spdx.json
SBOM(Software Bill of Materials) 是供应链安全的基础设施——它记录了你的软件用了哪些依赖、什么版本。一旦发生供应链攻击(如 log4j),可以快速定位受影响的服务。
安全债务管理:不让漏洞积累
关键策略:不是不让漏洞出现,而是建立漏洞追踪和清理机制。
# 每周安全报告(自动发送给安全团队和开发负责人)
name: Weekly Security Report
on:
schedule:
- cron: '0 9 * * 1' # 每周一早上 9 点
jobs:
security-report:
runs-on: ubuntu-latest
steps:
- name: Collect Vulnerabilities
run: |
# 从所有仓库收集高危漏洞
gh api repos/${{ github.repository }}/security-advisories \
--jq '.[] | select(.severity=="high")'
- name: Send Report
run: |
curl -X POST ${{ secrets.SLACK_WEBHOOK }} \
-d '{"text":"Weekly Security Report: ${{ steps.collect.outputs.vulns }} critical vulnerabilities"}'
安全债务看板:用 GitHub Issues 或 Jira 跟踪每个漏洞的修复进度,设置 SLA: - Critical:24 小时内修复 - High:72 小时内修复 - Medium:7 天内修复 - Low:下一个迭代处理
FAQ
Q: 安全扫描太慢拖慢 CI/CD 怎么办?
分层处理: 1. 快速扫描放 PR 阶段(密钥检测、简单 SAST,<30 秒) 2. 完整扫描放 nightly build(全量 SAST、DAST,10-30 分钟) 3. 增量扫描放构建阶段(只扫描变更的文件)
我们的经验:把完整扫描放到 nightly build,PR 阶段只做快速扫描,整体 CI 时间从 15 分钟增加到 18 分钟(可接受)。
Q: 开发者抱怨安全扫描阻断正常开发怎么办?
- 明确告知阻断原因:不要只说"Build Failed",要给出具体的漏洞信息和修复建议
- 提供一键修复:自动依赖升级、自动密钥替换
- 建立安全沙箱:允许开发者在本地跑扫描工具,提前发现问题
- 定期培训:让开发者理解"为什么这个漏洞危险",而不是"为什么我的 PR 被拒"
Q: 安全扫描结果误报率高怎么处理?
# 建立误报豁免机制
- name: Suppress False Positives
run: |
# 把已确认的误报加入 ignore 列表
echo '{"ignore":[{"id":"semgrep-123","reason":"已确认误报:这是测试代码"}]}' \
> .semgrepignore
关键原则:不是消除误报,而是管理误报。建立一个共享的误报知识库,让全团队都能看到哪些扫描结果是误报,避免重复判断。