Skip to content

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),可以快速定位受影响的服务。

# GitHub Actions 自动生成 SBOM
- uses: anchore/sbom-action@v0
  with:
    path: .
    format: spdx-json

安全债务管理:不让漏洞积累

关键策略:不是不让漏洞出现,而是建立漏洞追踪和清理机制。

# 每周安全报告(自动发送给安全团队和开发负责人)
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: 开发者抱怨安全扫描阻断正常开发怎么办?

  1. 明确告知阻断原因:不要只说"Build Failed",要给出具体的漏洞信息和修复建议
  2. 提供一键修复:自动依赖升级、自动密钥替换
  3. 建立安全沙箱:允许开发者在本地跑扫描工具,提前发现问题
  4. 定期培训:让开发者理解"为什么这个漏洞危险",而不是"为什么我的 PR 被拒"

Q: 安全扫描结果误报率高怎么处理?

# 建立误报豁免机制
- name: Suppress False Positives
  run: |
    # 把已确认的误报加入 ignore 列表
    echo '{"ignore":[{"id":"semgrep-123","reason":"已确认误报:这是测试代码"}]}' \
      > .semgrepignore

关键原则:不是消除误报,而是管理误报。建立一个共享的误报知识库,让全团队都能看到哪些扫描结果是误报,避免重复判断。