# 第二十九章:DevSecOps — 安全左移 > "安全不是开发完成后的检查站,而是贯穿整个流水线的守护者。" ```{mermaid} mindmap root((DevSecOps)) CI/CD 安全 SAST SCA DAST 镜像扫描 工具链 Semgrep Snyk Trivy OWASP ZAP IaC 安全 tfsec Checkov kubesec 文化 Security Champion 安全培训 安全门禁 ``` ## 29.1 DevSecOps 理念 DevSecOps 将安全融入 DevOps 的每个阶段: ```{mermaid} flowchart LR Code["🖊️ Code
───
Pre-commit Hook
Secret Detection
Linting"] SAST["🔍 SAST
───
Semgrep / CodeQL
Bandit / gosec
SpotBugs"] SCA["📦 SCA
───
Snyk / pip-audit
govulncheck
OWASP Dep-Check"] Build["🔨 Build
───
编译打包
SBOM 生成
签名验证"] ImageScan["🐳 镜像扫描
───
Trivy / Grype
漏洞检查
合规基线"] DAST["⚡ DAST
───
OWASP ZAP
Nuclei
API Fuzzing"] Deploy["🚀 Deploy
───
安全配置
IaC 扫描
准入控制"] Monitor["📊 Monitor
───
SIEM / WAF
运行时防护
告警响应"] Code --> SAST --> SCA --> Build --> ImageScan --> DAST --> Deploy --> Monitor style Code fill:#e1f5fe,stroke:#0288d1 style SAST fill:#f3e5f5,stroke:#7b1fa2 style SCA fill:#fff3e0,stroke:#ef6c00 style Build fill:#e8f5e9,stroke:#388e3c style ImageScan fill:#fce4ec,stroke:#c62828 style DAST fill:#fff8e1,stroke:#f9a825 style Deploy fill:#e0f2f1,stroke:#00695c style Monitor fill:#f1f8e9,stroke:#558b2f ``` ## 29.2 SAST — 静态应用安全测试 | 工具 | 语言支持 | 特点 | 开源 | |------|---------|------|------| | Semgrep | 多语言 | 自定义规则、快速 | ✅ | | CodeQL | 多语言 | GitHub 集成、深度分析 | ✅ | | SonarQube | 多语言 | 全面、企业级 | 部分 | | Bandit | Python | Python 专用 | ✅ | ```bash # Semgrep 扫描 semgrep scan --config auto . # 自定义规则示例 # rules/no-hardcoded-secrets.yaml rules: - id: hardcoded-password patterns: - pattern: password = "..." message: "Hardcoded password detected" severity: ERROR languages: [python] ``` ### 29.2.1 SAST/DAST/SCA 工具集成点 ```{mermaid} flowchart TB subgraph IDE["🖥️ IDE / 本地开发"] PreCommit["Pre-commit Hook
GitLeaks / Semgrep"] end subgraph CI["⚙️ CI 流水线"] direction TB SAST_CI["SAST 扫描
Semgrep / CodeQL / SpotBugs / gosec / Bandit"] SCA_CI["SCA 扫描
Snyk / pip-audit / govulncheck / OWASP Dep-Check"] Secret_CI["密钥扫描
GitLeaks / TruffleHog"] IaC_CI["IaC 扫描
Checkov / tfsec"] Image_CI["镜像扫描
Trivy / Grype"] SBOM_CI["SBOM 生成
Syft / CycloneDX"] end subgraph Staging["🧪 Staging 环境"] DAST_Stage["DAST 扫描
OWASP ZAP / Nuclei"] API_Fuzz["API Fuzzing
RESTler / Schemathesis"] end subgraph Prod["🏭 Production"] RASP["运行时防护
RASP / WAF"] SIEM["安全监控
SIEM / 告警"] end PreCommit -->|"git push"| SAST_CI SAST_CI --> SCA_CI --> Secret_CI --> IaC_CI --> Image_CI --> SBOM_CI SBOM_CI -->|"部署到 Staging"| DAST_Stage DAST_Stage --> API_Fuzz API_Fuzz -->|"安全门禁通过"| RASP RASP --> SIEM style IDE fill:#e3f2fd,stroke:#1565c0 style CI fill:#f3e5f5,stroke:#6a1b9a style Staging fill:#fff3e0,stroke:#e65100 style Prod fill:#e8f5e9,stroke:#2e7d32 ``` ### 29.2.2 SAST 工具对比 | 特性 | Semgrep | SonarQube | CodeQL | Bandit (Python) | gosec (Go) | SpotBugs (Java) | |------|---------|-----------|--------|-----------------|------------|-----------------| | **语言覆盖** | 30+ 语言 | 30+ 语言 | 12+ 语言 | Python | Go | Java/Kotlin | | **规则自定义** | ⭐⭐⭐ YAML DSL | ⭐⭐ Java API | ⭐⭐⭐ QL 语言 | ⭐⭐ Python 插件 | ⭐⭐ Go 插件 | ⭐⭐ Java 插件 | | **CI 集成** | ⭐⭐⭐ 原生支持 | ⭐⭐ 需要服务端 | ⭐⭐⭐ GitHub 原生 | ⭐⭐ CLI | ⭐⭐ CLI | ⭐⭐ Maven/Gradle | | **扫描速度** | 🚀 极快 | 🐢 较慢 | 🐢 较慢 | 🚀 快 | 🚀 快 | ⚡ 中等 | | **误报率** | 低 | 中 | 低 | 中 | 低 | 中 | | **数据流分析** | 基础 | 深度 | 深度 | 无 | 基础 | 深度 | | **开源** | ✅ 社区版 | 部分开源 | ✅ | ✅ | ✅ | ✅ | | **企业功能** | Semgrep Cloud | 商业版 | GitHub Advanced Security | — | — | — | | **适用场景** | 通用快速扫描 | 企业全面治理 | GitHub 深度集成 | Python 项目 | Go 项目 | Java/JVM 项目 | ## 29.3 SCA — 软件组成分析 ```bash # Snyk — 依赖漏洞扫描 snyk test snyk monitor # 持续监控 # pip-audit — Python 依赖审计 pip-audit # npm audit — Node.js 依赖审计 npm audit --audit-level=high ``` ## 29.4 DAST — 动态应用安全测试 ```bash # OWASP ZAP — 动态扫描 docker run -t zaproxy/zap-stable zap-baseline.py \ -t https://staging.example.com \ -r report.html # Nuclei — 快速漏洞扫描 nuclei -u https://staging.example.com -t cves/ ``` ## 29.5 IaC 安全 ```bash # tfsec — Terraform 安全扫描 tfsec . # Checkov — 多 IaC 框架支持 checkov -d . checkov --framework terraform,kubernetes # kubesec — Kubernetes 清单安全评分 kubesec scan deployment.yaml ``` ## 29.6 CI/CD 安全流水线 ### 29.6.1 GitHub Actions 完整安全 Pipeline ```yaml name: Security Pipeline on: [push, pull_request] jobs: sast: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - name: Semgrep SAST uses: returntocorp/semgrep-action@v1 with: config: auto - name: CodeQL Analysis uses: github/codeql-action/init@v3 with: languages: python, java, go - name: CodeQL Autobuild uses: github/codeql-action/autobuild@v3 - name: CodeQL Perform Analysis uses: github/codeql-action/analyze@v3 sca: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - name: Snyk SCA uses: snyk/actions/python@master env: SNYK_TOKEN: ${{ secrets.SNYK_TOKEN }} - name: OWASP Dependency-Check uses: dependency-check/Dependency-Check_Action@main with: project: 'my-project' path: '.' format: 'HTML' env: JAVA_HOME: /opt/jdk container-scan: runs-on: ubuntu-latest needs: [sast, sca] steps: - uses: actions/checkout@v4 - name: Build image run: docker build -t myapp:${{ github.sha }} . - name: Trivy scan uses: aquasecurity/trivy-action@master with: image-ref: myapp:${{ github.sha }} severity: HIGH,CRITICAL exit-code: 1 format: 'sarif' output: 'trivy-results.sarif' - name: Upload Trivy SARIF uses: github/codeql-action/upload-sarif@v3 with: sarif_file: 'trivy-results.sarif' - name: Generate SBOM uses: anchore/sbom-action@v0 with: image: myapp:${{ github.sha }} format: cyclonedx-json output-file: sbom.json secret-scan: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 with: fetch-depth: 0 - name: GitLeaks uses: gitleaks/gitleaks-action@v2 env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} iac-scan: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - name: Checkov uses: bridgecrewio/checkov-action@master with: directory: ./terraform dast: runs-on: ubuntu-latest needs: [container-scan] steps: - uses: actions/checkout@v4 - name: Deploy to staging run: | docker compose -f docker-compose.staging.yml up -d sleep 30 # 等待服务就绪 - name: OWASP ZAP Baseline Scan uses: zaproxy/action-baseline@v0.12.0 with: target: 'http://localhost:8080' rules_file_name: '.zap/rules.tsv' cmd_options: '-a' security-gate: runs-on: ubuntu-latest needs: [sast, sca, container-scan, secret-scan, iac-scan, dast] steps: - name: Evaluate Security Gate run: | echo "All security checks passed — deployment approved" ``` ### 29.6.2 GitLab CI 等效安全流水线 ```yaml # .gitlab-ci.yml stages: - test - sast - sca - build - container-scan - dast - security-gate - deploy variables: DOCKER_IMAGE: $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA # ── SAST ────────────────────────────────────────────── semgrep-sast: stage: sast image: returntocorp/semgrep script: - semgrep scan --config auto --sarif -o semgrep.sarif . artifacts: reports: sast: semgrep.sarif paths: - semgrep.sarif bandit-sast: stage: sast image: python:3.12-slim script: - pip install bandit[toml] - bandit -r src/ -f json -o bandit-report.json --severity-level medium || true artifacts: paths: - bandit-report.json gosec-sast: stage: sast image: securego/gosec:latest script: - gosec -fmt=json -out=gosec-report.json ./... artifacts: paths: - gosec-report.json rules: - exists: - go.mod spotbugs-sast: stage: sast image: maven:3.9-eclipse-temurin-21 script: - mvn compile com.github.spotbugs:spotbugs-maven-plugin:spotbugs -Dspotbugs.plugins.plugin.groupId=com.h3xstream.findsecbugs -Dspotbugs.plugins.plugin.artifactId=findsecbugs-plugin artifacts: paths: - target/spotbugsXml.xml rules: - exists: - pom.xml # ── SCA ─────────────────────────────────────────────── snyk-sca: stage: sca image: snyk/snyk:python-3.12 script: - snyk auth $SNYK_TOKEN - snyk test --severity-threshold=high --json-file-output=snyk-report.json artifacts: paths: - snyk-report.json pip-audit-sca: stage: sca image: python:3.12-slim script: - pip install pip-audit - pip-audit -r requirements.txt --format json --output pip-audit-report.json || true artifacts: paths: - pip-audit-report.json dependency-check-sca: stage: sca image: owasp/dependency-check:latest script: - /usr/share/dependency-check/bin/dependency-check.sh --project "my-project" --scan . --format JSON --out dependency-check-report --failOnCVSS 7 artifacts: paths: - dependency-check-report/ # ── Build & Container Scan ──────────────────────────── build-image: stage: build image: docker:24 services: - docker:24-dind script: - docker build -t $DOCKER_IMAGE . - docker push $DOCKER_IMAGE trivy-container-scan: stage: container-scan image: aquasec/trivy:latest needs: [build-image] script: - trivy image --severity HIGH,CRITICAL --exit-code 1 --format json --output trivy-report.json $DOCKER_IMAGE artifacts: paths: - trivy-report.json # ── DAST ────────────────────────────────────────────── zap-dast: stage: dast image: zaproxy/zap-stable needs: [trivy-container-scan] script: - zap-baseline.py -t $STAGING_URL -r zap-report.html -J zap-report.json artifacts: paths: - zap-report.html - zap-report.json # ── Security Gate ───────────────────────────────────── security-gate: stage: security-gate image: python:3.12-slim needs: - semgrep-sast - snyk-sca - trivy-container-scan - zap-dast script: - pip install json-query - | echo "=== Security Gate Evaluation ===" # 检查是否存在 Critical/High 漏洞 python3 scripts/security_gate.py \ --semgrep semgrep.sarif \ --trivy trivy-report.json \ --zap zap-report.json \ --fail-on critical,high # ── Deploy ──────────────────────────────────────────── deploy-production: stage: deploy needs: [security-gate] script: - echo "Deploying to production..." when: manual environment: name: production ``` ## 29.7 安全门禁策略 | 严重程度 | CI 行为 | 说明 | |---------|---------|------| | Critical | ❌ 阻断 | 必须修复才能合并 | | High | ❌ 阻断 | 必须修复或获得豁免 | | Medium | ⚠️ 警告 | 记录并计划修复 | | Low | ℹ️ 信息 | 记录,不阻断 | ### 29.7.1 安全门禁决策流程 ```{mermaid} flowchart TD Start(["🔍 安全扫描完成"]) --> Collect["汇总所有扫描报告
SAST + SCA + 镜像扫描 + DAST"] Collect --> HasCritical{"存在 Critical
漏洞?"} HasCritical -->|"是"| BlockCritical["❌ 阻断部署
通知安全团队
创建 P0 工单"] HasCritical -->|"否"| HasHigh{"存在 High
漏洞?"} HasHigh -->|"是"| CheckExemption{"已获得安全
团队豁免?"} CheckExemption -->|"否"| BlockHigh["❌ 阻断部署
要求修复或申请豁免"] CheckExemption -->|"是"| LogExemption["📝 记录豁免
设置修复截止日期"] LogExemption --> HasMedium HasHigh -->|"否"| HasMedium{"存在 Medium
漏洞?"} HasMedium -->|"是"| WarnMedium["⚠️ 警告
创建修复工单
SLA: 30 天"] HasMedium -->|"否"| HasLow{"存在 Low
漏洞?"} WarnMedium --> HasLow HasLow -->|"是"| InfoLow["ℹ️ 记录信息
纳入技术债务"] HasLow -->|"否"| Clean["✅ 无漏洞"] InfoLow --> Approve Clean --> Approve Approve(["✅ 允许部署"]) BlockCritical --> Review["安全团队评审"] BlockHigh --> Review Review --> Fixed{"漏洞已修复?"} Fixed -->|"是"| Rescan["重新扫描"] Fixed -->|"否"| Escalate["升级处理"] Rescan --> Start style BlockCritical fill:#ffcdd2,stroke:#c62828 style BlockHigh fill:#ffcdd2,stroke:#c62828 style WarnMedium fill:#fff9c4,stroke:#f9a825 style InfoLow fill:#e3f2fd,stroke:#1565c0 style Approve fill:#c8e6c9,stroke:#2e7d32 style Clean fill:#c8e6c9,stroke:#2e7d32 ``` ### 29.7.2 安全门禁策略模板 ```yaml # security-gate-policy.yaml # 安全门禁策略配置 — 定义各级别漏洞的处理方式 policy: name: "Default Security Gate Policy" version: "1.0" # ── 阻断规则 ────────────────────────────────────── blocking_rules: - severity: CRITICAL action: block message: "Critical 漏洞必须在合并前修复" notify: - security-team@example.com - oncall-slack-channel auto_create_ticket: true ticket_priority: P0 - severity: HIGH action: block exemption_allowed: true exemption_approvers: - security-lead - engineering-director exemption_max_days: 14 message: "High 漏洞需修复或获得豁免" # ── 警告规则 ────────────────────────────────────── warning_rules: - severity: MEDIUM action: warn sla_days: 30 auto_create_ticket: true ticket_priority: P2 - severity: LOW action: info sla_days: 90 auto_create_ticket: false # ── 扫描器配置 ──────────────────────────────────── scanners: sast: required: true tools: [semgrep, codeql] fail_on: [CRITICAL, HIGH] sca: required: true tools: [snyk, pip-audit] fail_on: [CRITICAL, HIGH] ignore_dev_dependencies: true container_scan: required: true tools: [trivy] fail_on: [CRITICAL, HIGH] base_image_policy: "only-approved" dast: required: false # 仅在 staging 部署后执行 tools: [zap] fail_on: [CRITICAL] secret_scan: required: true tools: [gitleaks] fail_on: [ANY] # 任何密钥泄露都阻断 iac_scan: required: true tools: [checkov] fail_on: [CRITICAL, HIGH] # ── 豁免管理 ────────────────────────────────────── exemptions: require_justification: true require_approval: true max_duration_days: 90 review_cadence: "weekly" audit_log: true ``` ## 29.8 Python DevSecOps 实例 ### 29.8.1 Bandit SAST 自定义规则配置 ```toml # pyproject.toml — Bandit 配置 [tool.bandit] # 排除测试目录 exclude_dirs = ["tests", "venv", ".tox"] # 跳过特定检查 skips = ["B101"] # 允许 assert(仅在测试中) # 设置严重级别阈值 severity = "medium" confidence = "medium" [tool.bandit.assert_used] skips = ["**/test_*.py", "**/conftest.py"] ``` ```yaml # .bandit.yaml — 高级自定义规则配置 # 自定义 Bandit profile profile: name: custom_security_profile include: - any_other_function_with_shell_equals_true # B604 - exec_used # B102 - hardcoded_password_string # B105 - hardcoded_password_funcarg # B106 - hardcoded_sql_expressions # B608 - jinja2_autoescape_false # B701 - request_without_timeout # B113 - snmp_insecure_version # B508 - sql_statements # B608 - ssl_with_bad_defaults # B503 - try_except_pass # B110 # 自定义黑名单调用 blacklist_calls: bad_name_sets: - pickle: qualnames: - pickle.loads - pickle.load - pickle.Unpickler - cPickle.loads - cPickle.load - cPickle.Unpickler message: "Pickle 反序列化可导致远程代码执行,请使用 json 替代" level: HIGH - yaml_unsafe: qualnames: - yaml.load - yaml.unsafe_load message: "yaml.load 不安全,请使用 yaml.safe_load" level: HIGH blacklist_imports: bad_import_sets: - telnet: imports: - telnetlib message: "Telnet 协议不加密,请使用 SSH" level: HIGH ``` ```bash # 运行 Bandit 扫描 bandit -r src/ -f json -o bandit-report.json --severity-level medium --confidence-level medium bandit -r src/ -f html -o bandit-report.html # HTML 报告 bandit -r src/ -f sarif -o bandit-report.sarif # SARIF 格式(CI 集成) ``` ### 29.8.2 Safety / pip-audit SCA 集成脚本 ```python #!/usr/bin/env python3 """ sca_scanner.py — Python SCA 集成扫描脚本 同时运行 pip-audit 和 safety,聚合结果并判断是否阻断。 """ import json import subprocess import sys from dataclasses import dataclass, field from enum import Enum from pathlib import Path class Severity(Enum): CRITICAL = "critical" HIGH = "high" MEDIUM = "medium" LOW = "low" UNKNOWN = "unknown" @property def weight(self) -> int: return {"critical": 4, "high": 3, "medium": 2, "low": 1, "unknown": 0}[ self.value ] @dataclass class Vulnerability: package: str installed_version: str fixed_version: str | None vuln_id: str severity: Severity description: str source: str # "pip-audit" or "safety" @dataclass class SCAReport: vulnerabilities: list[Vulnerability] = field(default_factory=list) errors: list[str] = field(default_factory=list) @property def critical_count(self) -> int: return sum(1 for v in self.vulnerabilities if v.severity == Severity.CRITICAL) @property def high_count(self) -> int: return sum(1 for v in self.vulnerabilities if v.severity == Severity.HIGH) def should_block(self, block_on: set[Severity] | None = None) -> bool: if block_on is None: block_on = {Severity.CRITICAL, Severity.HIGH} return any(v.severity in block_on for v in self.vulnerabilities) def run_pip_audit(requirements: str = "requirements.txt") -> list[Vulnerability]: """运行 pip-audit 并解析结果。""" vulns = [] try: result = subprocess.run( ["pip-audit", "-r", requirements, "--format", "json", "--output", "-"], capture_output=True, text=True, timeout=300, ) data = json.loads(result.stdout) if result.stdout else {"dependencies": []} for dep in data.get("dependencies", []): for vuln in dep.get("vulns", []): vulns.append( Vulnerability( package=dep["name"], installed_version=dep["version"], fixed_version=vuln.get("fix_versions", [None])[0], vuln_id=vuln["id"], severity=_parse_severity(vuln.get("severity", "unknown")), description=vuln.get("description", ""), source="pip-audit", ) ) except (subprocess.TimeoutExpired, FileNotFoundError) as e: print(f"[WARN] pip-audit 执行失败: {e}", file=sys.stderr) return vulns def run_safety(requirements: str = "requirements.txt") -> list[Vulnerability]: """运行 safety check 并解析结果。""" vulns = [] try: result = subprocess.run( ["safety", "check", "-r", requirements, "--json"], capture_output=True, text=True, timeout=300, ) data = json.loads(result.stdout) if result.stdout else [] for item in data: # safety JSON 格式: [package, affected, installed, description, vuln_id] if isinstance(item, list) and len(item) >= 5: vulns.append( Vulnerability( package=item[0], installed_version=item[2], fixed_version=None, vuln_id=str(item[4]), severity=Severity.UNKNOWN, description=item[3], source="safety", ) ) except (subprocess.TimeoutExpired, FileNotFoundError) as e: print(f"[WARN] safety 执行失败: {e}", file=sys.stderr) return vulns def _parse_severity(raw: str) -> Severity: try: return Severity(raw.lower()) except ValueError: return Severity.UNKNOWN def aggregate_report(requirements: str = "requirements.txt") -> SCAReport: """聚合多个 SCA 工具的扫描结果。""" report = SCAReport() report.vulnerabilities.extend(run_pip_audit(requirements)) report.vulnerabilities.extend(run_safety(requirements)) # 按 vuln_id 去重(保留严重级别更高的) seen: dict[str, Vulnerability] = {} for v in report.vulnerabilities: key = f"{v.package}:{v.vuln_id}" if key not in seen or v.severity.weight > seen[key].severity.weight: seen[key] = v report.vulnerabilities = list(seen.values()) report.vulnerabilities.sort(key=lambda v: v.severity.weight, reverse=True) return report def main() -> None: report = aggregate_report() print(f"\n{'='*60}") print(f" SCA 扫描报告 — 共发现 {len(report.vulnerabilities)} 个漏洞") print(f" Critical: {report.critical_count} | High: {report.high_count}") print(f"{'='*60}\n") for v in report.vulnerabilities: icon = {"critical": "🔴", "high": "🟠", "medium": "🟡", "low": "🔵"}.get( v.severity.value, "⚪" ) print(f" {icon} [{v.severity.value.upper()}] {v.package}=={v.installed_version}") print(f" ID: {v.vuln_id} | 来源: {v.source}") if v.fixed_version: print(f" 修复版本: {v.fixed_version}") print(f" {v.description[:120]}") print() # 输出 JSON 报告 report_path = Path("sca-report.json") report_path.write_text( json.dumps( { "total": len(report.vulnerabilities), "critical": report.critical_count, "high": report.high_count, "blocked": report.should_block(), "vulnerabilities": [ { "package": v.package, "version": v.installed_version, "vuln_id": v.vuln_id, "severity": v.severity.value, "source": v.source, } for v in report.vulnerabilities ], }, indent=2, ) ) print(f" 📄 报告已保存: {report_path}") if report.should_block(): print("\n ❌ 安全门禁: 存在 Critical/High 漏洞,阻断部署\n") sys.exit(1) else: print("\n ✅ 安全门禁: 通过\n") if __name__ == "__main__": main() ``` ### 29.8.3 安全测试报告聚合器 ```python #!/usr/bin/env python3 """ security_report_aggregator.py — 聚合多种安全扫描报告,生成统一仪表盘。 支持: Bandit (SAST), pip-audit (SCA), Trivy (Container), ZAP (DAST) """ import json import sys from dataclasses import dataclass, field from datetime import datetime, timezone from pathlib import Path @dataclass class Finding: tool: str category: str # SAST, SCA, Container, DAST severity: str title: str description: str location: str vuln_id: str = "" @dataclass class AggregatedReport: findings: list[Finding] = field(default_factory=list) generated_at: str = field( default_factory=lambda: datetime.now(timezone.utc).isoformat() ) def summary(self) -> dict[str, dict[str, int]]: result: dict[str, dict[str, int]] = {} for f in self.findings: cat = result.setdefault(f.category, {}) cat[f.severity] = cat.get(f.severity, 0) + 1 return result def parse_bandit(path: Path) -> list[Finding]: data = json.loads(path.read_text()) return [ Finding( tool="Bandit", category="SAST", severity=r["issue_severity"].lower(), title=r["issue_text"], description=f"CWE: {r.get('issue_cwe', {}).get('id', 'N/A')}", location=f"{r['filename']}:{r['line_number']}", vuln_id=r.get("test_id", ""), ) for r in data.get("results", []) ] def parse_trivy(path: Path) -> list[Finding]: data = json.loads(path.read_text()) findings = [] for result in data.get("Results", []): for vuln in result.get("Vulnerabilities", []): findings.append( Finding( tool="Trivy", category="Container", severity=vuln.get("Severity", "UNKNOWN").lower(), title=f"{vuln['PkgName']}:{vuln.get('InstalledVersion', '?')}", description=vuln.get("Title", ""), location=result.get("Target", ""), vuln_id=vuln.get("VulnerabilityID", ""), ) ) return findings def parse_zap(path: Path) -> list[Finding]: data = json.loads(path.read_text()) findings = [] risk_map = {"0": "info", "1": "low", "2": "medium", "3": "high"} for site in data.get("site", []): for alert in site.get("alerts", []): findings.append( Finding( tool="ZAP", category="DAST", severity=risk_map.get(alert.get("riskcode", "0"), "unknown"), title=alert.get("name", ""), description=alert.get("desc", "")[:200], location=alert.get("uri", ""), vuln_id=f"CWE-{alert.get('cweid', 'N/A')}", ) ) return findings def aggregate(report_dir: Path) -> AggregatedReport: report = AggregatedReport() parsers = { "bandit-report.json": parse_bandit, "trivy-report.json": parse_trivy, "zap-report.json": parse_zap, } for filename, parser in parsers.items(): path = report_dir / filename if path.exists(): report.findings.extend(parser(path)) print(f" ✅ 已解析: {filename}") else: print(f" ⏭️ 跳过(未找到): {filename}") return report def print_dashboard(report: AggregatedReport) -> None: summary = report.summary() total = len(report.findings) print(f"\n{'='*60}") print(f" 🛡️ 安全扫描聚合报告 | {report.generated_at}") print(f" 总发现: {total}") print(f"{'='*60}") for category, counts in summary.items(): print(f"\n 📂 {category}:") for sev in ["critical", "high", "medium", "low", "info"]: if sev in counts: icon = {"critical": "🔴", "high": "🟠", "medium": "🟡", "low": "🔵", "info": "⚪"}[sev] print(f" {icon} {sev.upper()}: {counts[sev]}") print(f"\n{'='*60}\n") def main() -> None: report_dir = Path(sys.argv[1]) if len(sys.argv) > 1 else Path(".") report = aggregate(report_dir) print_dashboard(report) output = report_dir / "aggregated-security-report.json" output.write_text( json.dumps( { "generated_at": report.generated_at, "total_findings": len(report.findings), "summary": report.summary(), "findings": [ { "tool": f.tool, "category": f.category, "severity": f.severity, "title": f.title, "location": f.location, "vuln_id": f.vuln_id, } for f in report.findings ], }, indent=2, ) ) print(f" 📄 聚合报告已保存: {output}") if __name__ == "__main__": main() ``` ## 29.9 Java DevSecOps 实例 ### 29.9.1 SpotBugs + Find Security Bugs 配置(Maven) ```xml 4.8.4 1.13.0 9.0.9 com.github.spotbugs spotbugs-maven-plugin ${spotbugs.version} Max Medium true true ${project.build.directory}/spotbugs spotbugs-exclude.xml com.h3xstream.findsecbugs findsecbugs-plugin ${findsecbugs.version} security-check verify check ``` ```xml ``` ### 29.9.2 OWASP Dependency-Check Maven 插件配置 ```xml org.owasp dependency-check-maven ${dependency-check.version} 7 HTML JSON SARIF ${project.build.directory}/dependency-check ${env.NVD_API_KEY} true dependency-check-suppression.xml dependency-check verify check ``` ```xml 误报: 此 CVE 不影响我们的使用方式 CVE-2023-XXXXX 已通过其他方式缓解 ^pkg:maven/com\.example/.*$ 4 ``` ### 29.9.3 Gradle 安全扫描任务 ```groovy // build.gradle — Gradle 安全扫描配置 plugins { id 'java' id 'com.github.spotbugs' version '6.0.9' id 'org.owasp.dependencycheck' version '9.0.9' } // ── SpotBugs + Find Security Bugs ──────────────────── spotbugs { effort = 'max' reportLevel = 'medium' excludeFilter = file('spotbugs-exclude.xml') } dependencies { spotbugsPlugins 'com.h3xstream.findsecbugs:findsecbugs-plugin:1.13.0' } tasks.withType(com.github.spotbugs.snom.SpotBugsTask) { reports { xml.required = true html.required = true sarif.required = true } } // ── OWASP Dependency-Check ─────────────────────────── dependencyCheck { failBuildOnCVSS = 7.0f formats = ['HTML', 'JSON', 'SARIF'] suppressionFile = 'dependency-check-suppression.xml' nvd { apiKey = System.getenv('NVD_API_KEY') ?: '' } analyzers { assemblyEnabled = false // 非 .NET 项目禁用 } } // ── 聚合安全扫描任务 ───────────────────────────────── tasks.register('securityScan') { group = 'verification' description = 'Run all security scans (SAST + SCA)' dependsOn 'spotbugsMain', 'dependencyCheckAnalyze' doLast { println '=== Security Scan Complete ===' println "SpotBugs report: ${project.buildDir}/reports/spotbugs/main.html" println "Dependency-Check report: ${project.buildDir}/reports/dependency-check-report.html" } } // 将安全扫描集成到 check 生命周期 check.dependsOn 'securityScan' ``` ```bash # 运行 Gradle 安全扫描 ./gradlew securityScan # 单独运行 SpotBugs ./gradlew spotbugsMain # 单独运行 Dependency-Check ./gradlew dependencyCheckAnalyze ``` ## 29.10 Go DevSecOps 实例 ### 29.10.1 gosec 配置和自定义规则 ```yaml # .gosec.yaml — gosec 配置文件 global: # 排除测试文件 exclude-dir: - vendor - testdata - mocks # 排除特定规则 exclude: - G104 # 未检查的错误(某些场景下可接受) # 设置严重级别 severity: medium confidence: medium # 输出配置 output: format: json file: gosec-report.json ``` ```bash #!/bin/bash # scripts/gosec-scan.sh — gosec 扫描脚本 set -euo pipefail REPORT_DIR="security-reports" mkdir -p "$REPORT_DIR" echo "=== gosec SAST 扫描 ===" # 运行 gosec,生成多种格式报告 gosec -fmt=json -out="${REPORT_DIR}/gosec-report.json" \ -severity=medium -confidence=medium \ -exclude-dir=vendor -exclude-dir=testdata \ ./... || GOSEC_EXIT=$? # 同时生成 SARIF 格式(用于 GitHub/GitLab 集成) gosec -fmt=sarif -out="${REPORT_DIR}/gosec-report.sarif" \ -severity=medium -confidence=medium \ -exclude-dir=vendor -exclude-dir=testdata \ ./... 2>/dev/null || true # 解析结果 if [ -f "${REPORT_DIR}/gosec-report.json" ]; then TOTAL=$(jq '.Issues | length' "${REPORT_DIR}/gosec-report.json") HIGH=$(jq '[.Issues[] | select(.severity == "HIGH")] | length' "${REPORT_DIR}/gosec-report.json") MEDIUM=$(jq '[.Issues[] | select(.severity == "MEDIUM")] | length' "${REPORT_DIR}/gosec-report.json") echo " 总发现: ${TOTAL}" echo " HIGH: ${HIGH} | MEDIUM: ${MEDIUM}" if [ "${HIGH}" -gt 0 ]; then echo " ❌ 存在 HIGH 级别问题,阻断构建" exit 1 fi fi echo " ✅ gosec 扫描通过" ``` ### 29.10.2 govulncheck 集成脚本 ```bash #!/bin/bash # scripts/govulncheck-scan.sh — Go 依赖漏洞扫描 set -euo pipefail REPORT_DIR="security-reports" mkdir -p "$REPORT_DIR" echo "=== govulncheck SCA 扫描 ===" # 安装 govulncheck(如果未安装) if ! command -v govulncheck &> /dev/null; then go install golang.org/x/vuln/cmd/govulncheck@latest fi # 运行 govulncheck govulncheck -json ./... > "${REPORT_DIR}/govulncheck-report.json" 2>&1 || VULN_EXIT=$? # 解析 JSON 结果 if [ -f "${REPORT_DIR}/govulncheck-report.json" ]; then # 提取被调用的漏洞(实际影响的) CALLED=$(jq '[.[] | select(.finding != null) | select(.finding.trace[0].function != null)] | length' \ "${REPORT_DIR}/govulncheck-report.json" 2>/dev/null || echo "0") echo " 实际被调用的漏洞: ${CALLED}" if [ "${CALLED}" -gt 0 ]; then echo "" echo " 受影响的漏洞详情:" jq -r '.[] | select(.finding != null) | select(.finding.trace[0].function != null) | " 🔴 \(.finding.osv) — \(.finding.trace[0].module)@\(.finding.trace[0].version)"' \ "${REPORT_DIR}/govulncheck-report.json" 2>/dev/null || true echo "" echo " ❌ 存在被调用的已知漏洞,阻断构建" exit 1 fi fi echo " ✅ govulncheck 扫描通过" ``` ```go // tools/vulncheck/main.go — 编程方式调用 govulncheck package main import ( "context" "encoding/json" "fmt" "os" "golang.org/x/vuln/scan" ) // VulnReport 漏洞报告 type VulnReport struct { TotalVulns int `json:"total_vulns"` CalledVulns int `json:"called_vulns"` Blocked bool `json:"blocked"` Details []VulnEntry `json:"details"` } // VulnEntry 单条漏洞 type VulnEntry struct { ID string `json:"id"` Module string `json:"module"` Package string `json:"package"` Function string `json:"function,omitempty"` Called bool `json:"called"` } func main() { ctx := context.Background() // 使用 govulncheck 的 scan 命令 cmd := scan.Command(ctx, "./...") cmd.Stdout = os.Stdout cmd.Stderr = os.Stderr if err := cmd.Start(); err != nil { fmt.Fprintf(os.Stderr, "govulncheck 启动失败: %v\n", err) os.Exit(1) } if err := cmd.Wait(); err != nil { fmt.Fprintf(os.Stderr, "发现漏洞,请检查报告\n") os.Exit(1) } fmt.Println("✅ 未发现已知漏洞") } func writeReport(report VulnReport, path string) error { data, err := json.MarshalIndent(report, "", " ") if err != nil { return err } return os.WriteFile(path, data, 0644) } ``` ### 29.10.3 golangci-lint 安全 Linter 配置 ```yaml # .golangci.yml — golangci-lint 安全相关配置 run: timeout: 5m skip-dirs: - vendor - testdata - mocks linters: enable: # ── 安全相关 linter ────────────────────────────── - gosec # Go 安全检查(等同于独立 gosec) - bodyclose # HTTP response body 未关闭 - noctx # HTTP 请求未传递 context - rowserrcheck # sql.Rows.Err() 未检查 - sqlclosecheck # sql.Rows 未关闭 - makezero # 切片初始化后 append 可能导致 bug - exportloopref # 循环变量引用导出 # ── 代码质量 linter ────────────────────────────── - errcheck # 未检查的错误 - govet # Go vet 检查 - staticcheck # 静态分析 - ineffassign # 无效赋值 - unused # 未使用的代码 linters-settings: gosec: # 包含所有规则 includes: - G101 # 硬编码凭据 - G102 # 绑定到所有接口 - G103 # unsafe 包使用 - G104 # 未检查的错误 - G106 # ssh.InsecureIgnoreHostKey - G107 # URL 作为 HTTP 请求参数 - G108 # /debug/pprof 自动暴露 - G109 # 整数溢出 - G110 # 解压炸弹 - G111 # 目录遍历 - G112 # 慢速 HTTP 攻击 - G114 # net/http 默认 ServeMux - G201 # SQL 查询拼接 - G202 # SQL 字符串格式化 - G203 # HTML 模板未转义 - G204 # 命令注入 - G301 # 目录权限过大 - G302 # 文件权限过大 - G303 # 可预测的临时文件 - G304 # 文件路径注入 - G305 # Zip Slip - G306 # 写入文件权限过大 - G401 # 弱加密算法 (MD5/SHA1) - G402 # TLS 不安全配置 - G403 # RSA 密钥过短 - G404 # 不安全的随机数 - G501 # 导入黑名单 crypto/md5 - G502 # 导入黑名单 crypto/des - G503 # 导入黑名单 crypto/rc4 - G504 # 导入黑名单 net/http/cgi - G505 # 导入黑名单 crypto/sha1 - G601 # 隐式内存别名 # 排除特定规则(如果需要) excludes: [] # 设置置信度 confidence: medium severity: medium config: # G101: 硬编码凭据检测的正则 G101: pattern: "(?i)(password|secret|token|api_key|apikey|access_key)\\s*=\\s*['\"][^'\"]{8,}" bodyclose: # 无额外配置 noctx: # 无额外配置 issues: # 不排除任何安全相关的 issue exclude-rules: - path: _test\.go linters: - gosec # 测试文件中放宽安全检查 severity: default-severity: warning rules: - linters: - gosec severity: error ``` ```bash # 运行安全相关 lint golangci-lint run --enable gosec,bodyclose,noctx,rowserrcheck,sqlclosecheck # 仅运行 gosec golangci-lint run --disable-all --enable gosec # 生成 JSON 报告 golangci-lint run --out-format json > golangci-lint-report.json ``` ## 29.11 漏洞管理 SLA | 严重程度 | CVSS 范围 | 修复 SLA | 升级时间 | 示例 | |---------|----------|---------|---------|------| | **Critical** | 9.0 – 10.0 | **24 小时** | 4 小时未响应 → VP | RCE、SQL 注入(无认证) | | **High** | 7.0 – 8.9 | **7 天** | 3 天未响应 → Director | 认证绕过、SSRF | | **Medium** | 4.0 – 6.9 | **30 天** | 14 天未响应 → Manager | XSS(存储型)、信息泄露 | | **Low** | 0.1 – 3.9 | **90 天** | 60 天未响应 → Tech Lead | 低风险信息泄露、版本暴露 | | **Info** | 0.0 | **下个季度** | 不升级 | 最佳实践建议 | **SLA 补充说明:** - **修复 SLA** 从漏洞确认(非误报)之日起计算 - **豁免申请** 必须在 SLA 到期前提交,需安全团队审批 - **SLA 违规** 纳入团队安全 KPI,季度安全评审中回顾 - **零日漏洞** 不受常规 SLA 约束,启动应急响应流程 ## 29.12 Security Champion 计划 每个团队指定 1–2 名 Security Champion: - 参加安全培训(每季度) - 审查团队的安全实践 - 推动安全工具的采用 - 作为安全团队和开发团队的桥梁 - 参与威胁建模和安全设计评审 ## 29.13 小结 - **DevSecOps** 将安全融入 CI/CD 的每个阶段 - **SAST**(Semgrep)在编码阶段发现漏洞 - **SCA**(Snyk)检测依赖中的已知漏洞 - **DAST**(ZAP)在运行时发现安全问题 - **IaC 扫描**(Checkov)确保基础设施配置安全 - **安全门禁** 根据严重程度决定阻断还是警告 - **Security Champion** 是安全文化落地的关键 - **多语言支持**:Python(Bandit + pip-audit)、Java(SpotBugs + Dependency-Check)、Go(gosec + govulncheck)各有成熟工具链 - **漏洞管理 SLA** 确保漏洞在规定时间内得到修复 - **安全门禁策略** 应可配置、可审计、支持豁免流程