第二十九章:DevSecOps — 安全左移
“安全不是开发完成后的检查站,而是贯穿整个流水线的守护者。”
mindmap
root((DevSecOps))
CI/CD 安全
SAST
SCA
DAST
镜像扫描
工具链
Semgrep
Snyk
Trivy
OWASP ZAP
IaC 安全
tfsec
Checkov
kubesec
文化
Security Champion
安全培训
安全门禁
29.1 DevSecOps 理念
DevSecOps 将安全融入 DevOps 的每个阶段:
flowchart LR
Code["🖊️ Code<br/>───<br/>Pre-commit Hook<br/>Secret Detection<br/>Linting"]
SAST["🔍 SAST<br/>───<br/>Semgrep / CodeQL<br/>Bandit / gosec<br/>SpotBugs"]
SCA["📦 SCA<br/>───<br/>Snyk / pip-audit<br/>govulncheck<br/>OWASP Dep-Check"]
Build["🔨 Build<br/>───<br/>编译打包<br/>SBOM 生成<br/>签名验证"]
ImageScan["🐳 镜像扫描<br/>───<br/>Trivy / Grype<br/>漏洞检查<br/>合规基线"]
DAST["⚡ DAST<br/>───<br/>OWASP ZAP<br/>Nuclei<br/>API Fuzzing"]
Deploy["🚀 Deploy<br/>───<br/>安全配置<br/>IaC 扫描<br/>准入控制"]
Monitor["📊 Monitor<br/>───<br/>SIEM / WAF<br/>运行时防护<br/>告警响应"]
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 专用 |
✅ |
# 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 工具集成点
flowchart TB
subgraph IDE["🖥️ IDE / 本地开发"]
PreCommit["Pre-commit Hook<br/>GitLeaks / Semgrep"]
end
subgraph CI["⚙️ CI 流水线"]
direction TB
SAST_CI["SAST 扫描<br/>Semgrep / CodeQL / SpotBugs / gosec / Bandit"]
SCA_CI["SCA 扫描<br/>Snyk / pip-audit / govulncheck / OWASP Dep-Check"]
Secret_CI["密钥扫描<br/>GitLeaks / TruffleHog"]
IaC_CI["IaC 扫描<br/>Checkov / tfsec"]
Image_CI["镜像扫描<br/>Trivy / Grype"]
SBOM_CI["SBOM 生成<br/>Syft / CycloneDX"]
end
subgraph Staging["🧪 Staging 环境"]
DAST_Stage["DAST 扫描<br/>OWASP ZAP / Nuclei"]
API_Fuzz["API Fuzzing<br/>RESTler / Schemathesis"]
end
subgraph Prod["🏭 Production"]
RASP["运行时防护<br/>RASP / WAF"]
SIEM["安全监控<br/>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 — 软件组成分析
# Snyk — 依赖漏洞扫描
snyk test
snyk monitor # 持续监控
# pip-audit — Python 依赖审计
pip-audit
# npm audit — Node.js 依赖审计
npm audit --audit-level=high
29.4 DAST — 动态应用安全测试
# 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 安全
# 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
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 等效安全流水线
# .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 安全门禁决策流程
flowchart TD
Start(["🔍 安全扫描完成"]) --> Collect["汇总所有扫描报告<br/>SAST + SCA + 镜像扫描 + DAST"]
Collect --> HasCritical{"存在 Critical<br/>漏洞?"}
HasCritical -->|"是"| BlockCritical["❌ 阻断部署<br/>通知安全团队<br/>创建 P0 工单"]
HasCritical -->|"否"| HasHigh{"存在 High<br/>漏洞?"}
HasHigh -->|"是"| CheckExemption{"已获得安全<br/>团队豁免?"}
CheckExemption -->|"否"| BlockHigh["❌ 阻断部署<br/>要求修复或申请豁免"]
CheckExemption -->|"是"| LogExemption["📝 记录豁免<br/>设置修复截止日期"]
LogExemption --> HasMedium
HasHigh -->|"否"| HasMedium{"存在 Medium<br/>漏洞?"}
HasMedium -->|"是"| WarnMedium["⚠️ 警告<br/>创建修复工单<br/>SLA: 30 天"]
HasMedium -->|"否"| HasLow{"存在 Low<br/>漏洞?"}
WarnMedium --> HasLow
HasLow -->|"是"| InfoLow["ℹ️ 记录信息<br/>纳入技术债务"]
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 安全门禁策略模板
# 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 自定义规则配置
# 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"]
# .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
# 运行 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 集成脚本
#!/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 安全测试报告聚合器
#!/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)
<!-- pom.xml — SpotBugs + Find Security Bugs 插件配置 -->
<project>
<properties>
<spotbugs.version>4.8.4</spotbugs.version>
<findsecbugs.version>1.13.0</findsecbugs.version>
<dependency-check.version>9.0.9</dependency-check.version>
</properties>
<build>
<plugins>
<!-- SpotBugs + Find Security Bugs -->
<plugin>
<groupId>com.github.spotbugs</groupId>
<artifactId>spotbugs-maven-plugin</artifactId>
<version>${spotbugs.version}</version>
<configuration>
<!-- 仅报告 Medium 及以上 -->
<effort>Max</effort>
<threshold>Medium</threshold>
<failOnError>true</failOnError>
<xmlOutput>true</xmlOutput>
<xmlOutputDirectory>${project.build.directory}/spotbugs</xmlOutputDirectory>
<!-- 排除文件 -->
<excludeFilterFile>spotbugs-exclude.xml</excludeFilterFile>
<plugins>
<plugin>
<groupId>com.h3xstream.findsecbugs</groupId>
<artifactId>findsecbugs-plugin</artifactId>
<version>${findsecbugs.version}</version>
</plugin>
</plugins>
</configuration>
<executions>
<execution>
<id>security-check</id>
<phase>verify</phase>
<goals>
<goal>check</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
</project>
<!-- spotbugs-exclude.xml — 排除规则 -->
<FindBugsFilter>
<!-- 排除测试代码 -->
<Match>
<Source name="~.*Test\.java"/>
</Match>
<!-- 排除生成代码 -->
<Match>
<Package name="~.*\.generated\..*"/>
</Match>
</FindBugsFilter>
29.9.2 OWASP Dependency-Check Maven 插件配置
<!-- pom.xml — OWASP Dependency-Check 插件 -->
<plugin>
<groupId>org.owasp</groupId>
<artifactId>dependency-check-maven</artifactId>
<version>${dependency-check.version}</version>
<configuration>
<!-- CVSS 7.0 及以上阻断构建 -->
<failBuildOnCVSS>7</failBuildOnCVSS>
<formats>
<format>HTML</format>
<format>JSON</format>
<format>SARIF</format>
</formats>
<outputDirectory>${project.build.directory}/dependency-check</outputDirectory>
<!-- NVD API Key(加速数据库更新) -->
<nvdApiKey>${env.NVD_API_KEY}</nvdApiKey>
<!-- 排除测试依赖 -->
<skipTestScope>true</skipTestScope>
<!-- 排除特定 CVE(已评估为误报) -->
<suppressionFiles>
<suppressionFile>dependency-check-suppression.xml</suppressionFile>
</suppressionFiles>
</configuration>
<executions>
<execution>
<id>dependency-check</id>
<phase>verify</phase>
<goals>
<goal>check</goal>
</goals>
</execution>
</executions>
</plugin>
<!-- dependency-check-suppression.xml — 误报抑制 -->
<suppressions xmlns="https://jeremylong.github.io/DependencyCheck/dependency-suppression.1.3.xsd">
<suppress>
<notes>误报: 此 CVE 不影响我们的使用方式</notes>
<cve>CVE-2023-XXXXX</cve>
</suppress>
<suppress>
<notes>已通过其他方式缓解</notes>
<packageUrl regex="true">^pkg:maven/com\.example/.*$</packageUrl>
<cvssBelow>4</cvssBelow>
</suppress>
</suppressions>
29.9.3 Gradle 安全扫描任务
// 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'
# 运行 Gradle 安全扫描
./gradlew securityScan
# 单独运行 SpotBugs
./gradlew spotbugsMain
# 单独运行 Dependency-Check
./gradlew dependencyCheckAnalyze
29.10 Go DevSecOps 实例
29.10.1 gosec 配置和自定义规则
# .gosec.yaml — gosec 配置文件
global:
# 排除测试文件
exclude-dir:
- vendor
- testdata
- mocks
# 排除特定规则
exclude:
- G104 # 未检查的错误(某些场景下可接受)
# 设置严重级别
severity: medium
confidence: medium
# 输出配置
output:
format: json
file: gosec-report.json
#!/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 集成脚本
#!/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 扫描通过"
// 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 配置
# .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
# 运行安全相关 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 确保漏洞在规定时间内得到修复
安全门禁策略 应可配置、可审计、支持豁免流程