name: devsecops-practices description: DevSecOps方法论指南,涵盖左移安全、SAST/DAST/IAST集成、CI/CD管道中的安全门、漏洞管理工作流和安全冠军计划。 allowed-tools: Read, Glob, Grep, Task
DevSecOps实践
使用DevSecOps原则在软件开发生命周期中集成安全的全面指南。
何时使用此技能
- 实施左移安全实践
- 设置SAST工具(Semgrep、CodeQL、SonarQube)
- 配置DAST扫描(OWASP ZAP、Burp Suite)
- 在CI/CD管道中集成安全门
- 构建漏洞管理工作流
- 建立安全冠军计划
- 创建安全SDLC过程
快速参考
DevSecOps成熟度级别
| 级别 | 特征 | 关键实践 |
|---|---|---|
| 级别1:初始 | 手动安全审查,临时测试 | 基本漏洞扫描,安全培训 |
| 级别2:管理 | CI/CD中的自动化扫描,定义的过程 | SAST集成,安全门 |
| 级别3:定义 | 安全嵌入所有阶段,指标跟踪 | DAST/IAST,威胁建模,SLA |
| 级别4:测量 | 持续监控,基于风险的决策 | 完全自动化,安全仪表板 |
| 级别5:优化 | 预测性安全,持续改进 | AI辅助,混沌工程 |
安全测试类型
| 类型 | 何时 | 发现什么 | 工具 |
|---|---|---|---|
| SAST | 构建时间 | 代码漏洞,模式 | Semgrep, CodeQL, SonarQube |
| SCA | 构建时间 | 依赖漏洞 | Snyk, Dependabot, npm audit |
| DAST | 运行时 | 运行应用漏洞 | OWASP ZAP, Burp Suite |
| IAST | 运行时 | 结合SAST+DAST | Contrast, Seeker |
| Secrets | 提交时间 | 硬编码凭据 | Gitleaks, truffleHog |
按管道阶段的安全门
┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐
│ 提交 │───►│ 构建 │───►│ 测试 │───►│ 部署 │───►│ 生产 │
└────┬─────┘ └────┬─────┘ └────┬─────┘ └────┬─────┘ └────┬─────┘
│ │ │ │ │
▼ ▼ ▼ ▼ ▼
┌─────────┐ ┌─────────┐ ┌─────────┐ ┌─────────┐ ┌─────────┐
│秘密扫描 │ │SAST │ │DAST │ │容器扫描 │ │运行时安全│
│提交前扫描 │ │SCA │ │渗透测试 │ │配置检查 │ │监控 │
│ │ │许可证检查│ │IAST │ │ │ │ │
└─────────┘ └─────────┘ └─────────┘ └─────────┘ └─────────┘
SAST(静态应用安全测试)
Semgrep设置
# .github/workflows/semgrep.yml
name: Semgrep
on:
push:
branches: [main]
pull_request:
branches: [main]
jobs:
semgrep:
runs-on: ubuntu-latest
container:
image: semgrep/semgrep
steps:
- uses: actions/checkout@v5
- name: 运行Semgrep
run: semgrep scan --config auto --sarif --output semgrep.sarif
- name: 上传SARIF
uses: github/codeql-action/upload-sarif@v3
with:
sarif_file: semgrep.sarif
Semgrep规则配置
# .semgrep.yml
rules:
# SQL注入
- id: sql-injection
patterns:
- pattern-either:
- pattern: cursor.execute($QUERY % ...)
- pattern: cursor.execute($QUERY.format(...))
- pattern: cursor.execute(f"...")
message: "潜在SQL注入。使用参数化查询。"
severity: ERROR
languages: [python]
# 硬编码秘密
- id: hardcoded-password
pattern-regex: '(?i)(password|passwd|pwd)\s*=\s*["\'][^"\']{8,}["\']'
message: "检测到硬编码密码"
severity: ERROR
languages: [python, javascript, typescript]
# 不安全加密
- id: insecure-hash
patterns:
- pattern-either:
- pattern: hashlib.md5(...)
- pattern: hashlib.sha1(...)
message: "用于加密目的时使用SHA-256或更强的算法"
severity: WARNING
languages: [python]
CodeQL设置
# .github/workflows/codeql.yml
name: CodeQL分析
on:
push:
branches: [main]
pull_request:
branches: [main]
schedule:
- cron: '0 6 * * 1' # 每周
jobs:
analyze:
runs-on: ubuntu-latest
permissions:
security-events: write
strategy:
matrix:
language: [javascript, python]
steps:
- uses: actions/checkout@v5
- name: 初始化CodeQL
uses: github/codeql-action/init@v3
with:
languages: ${{ matrix.language }}
queries: +security-extended
- name: 构建(如果需要)
uses: github/codeql-action/autobuild@v3
- name: 执行分析
uses: github/codeql-action/analyze@v3
with:
category: "/language:${{ matrix.language }}"
SonarQube集成
# .github/workflows/sonarqube.yml
name: SonarQube分析
on:
push:
branches: [main]
pull_request:
branches: [main]
jobs:
sonarqube:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
with:
fetch-depth: 0 # 完整历史以准确归因
- name: SonarQube扫描
uses: sonarsource/sonarqube-scan-action@master
env:
SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
SONAR_HOST_URL: ${{ secrets.SONAR_HOST_URL }}
- name: 质量门检查
uses: sonarsource/sonarqube-quality-gate-action@master
timeout-minutes: 5
env:
SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
# sonar-project.properties
sonar.projectKey=my-project
sonar.organization=my-org
# 源路径
sonar.sources=src
sonar.tests=tests
# 排除
sonar.exclusions=**/node_modules/**,**/*.test.js,**/vendor/**
# 覆盖率
sonar.javascript.lcov.reportPaths=coverage/lcov.info
sonar.python.coverage.reportPaths=coverage.xml
# 安全热点审查
sonar.security.hotspots.review.priority=HIGH
DAST(动态应用安全测试)
OWASP ZAP集成
# .github/workflows/zap.yml
name: OWASP ZAP扫描
on:
push:
branches: [main]
schedule:
- cron: '0 2 * * 0' # 每周日2点
jobs:
zap-scan:
runs-on: ubuntu-latest
services:
app:
image: my-app:latest
ports:
- 8080:8080
steps:
- uses: actions/checkout@v5
- name: ZAP基线扫描
uses: zaproxy/action-baseline@v0.11.0
with:
target: 'http://localhost:8080'
rules_file_name: '.zap/rules.tsv'
- name: ZAP完整扫描
uses: zaproxy/action-full-scan@v0.9.0
with:
target: 'http://localhost:8080'
cmd_options: '-a -j'
- name: 上传报告
uses: actions/upload-artifact@v4
with:
name: zap-report
path: report_html.html
ZAP规则配置
# .zap/rules.tsv
# 规则ID 操作 描述
10010 忽略 # Cookie无HttpOnly标志(由框架处理)
10011 警告 # Cookie无安全标志
10015 失败 # 不完整或无缓存控制和Pragma
10016 警告 # Web浏览器XSS保护未启用
10017 失败 # 跨域JavaScript源文件包含
10019 失败 # Content-Type头缺失
10020 失败 # X-Frame-Options头未设置
10021 失败 # X-Content-Type-Options头缺失
10038 失败 # 内容安全策略头未设置
DAST在Docker Compose中
# docker-compose.security.yml
version: '3.8'
services:
app:
build: .
ports:
- "8080:8080"
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:8080/health"]
interval: 5s
timeout: 10s
retries: 5
zap:
image: ghcr.io/zaproxy/zaproxy:stable
depends_on:
app:
condition: service_healthy
volumes:
- ./zap-reports:/zap/wrk
command: >
zap-full-scan.py
-t http://app:8080
-r zap-report.html
-J zap-report.json
-x zap-report.xml
安全门
门配置
using System.Text.Json;
using System.Text.Json.Serialization;
/// <summary>
/// CI/CD管道中的安全门强制执行。
/// </summary>
public enum Severity { Critical, High, Medium, Low, Info }
public enum GateDecision { Pass, Warn, Fail }
/// <summary>
/// 安全门阈值的配置。
/// </summary>
public sealed record SecurityGateConfig
{
// SAST阈值
public int SastCriticalMax { get; init; } = 0;
public int SastHighMax { get; init; } = 0;
public int SastMediumMax { get; init; } = 5;
// SCA阈值
public int ScaCriticalMax { get; init; } = 0;
public int ScaHighMax { get; init; } = 2;
public double ScaCvssThreshold { get; init; } = 7.0;
// DAST阈值
public int DastCriticalMax { get; init; } = 0;
public int DastHighMax { get; init; } = 1;
// 秘密
public int SecretsAllowed { get; init; } = 0;
// 许可证限制
public IReadOnlyList<string> ForbiddenLicenses { get; init; } = ["GPL-3.0", "AGPL-3.0"];
}
/// <summary>
/// 安全扫描的聚合结果。
/// </summary>
public sealed record ScanResults(
Dictionary<string, int> SastFindings,
Dictionary<string, int> ScaFindings,
Dictionary<string, int> DastFindings,
int SecretsFound,
IReadOnlyList<string> Licenses);
/// <summary>
/// 评估CI/CD管道的安全门。
/// </summary>
public static class SecurityGateEvaluator
{
public static (GateDecision Decision, List<string> Reasons) Evaluate(
SecurityGateConfig config,
ScanResults results)
{
var reasons = new List<string>();
var decision = GateDecision.Pass;
// 检查SAST
if (results.SastFindings.GetValueOrDefault("critical", 0) > config.SastCriticalMax)
{
decision = GateDecision.Fail;
reasons.Add($"SAST: {results.SastFindings["critical"]} 个关键发现(最大: {config.SastCriticalMax})");
}
if (results.SastFindings.GetValueOrDefault("high", 0) > config.SastHighMax)
{
decision = GateDecision.Fail;
reasons.Add($"SAST: {results.SastFindings["high"]} 个高发现(最大: {config.SastHighMax})");
}
// 检查SCA
if (results.ScaFindings.GetValueOrDefault("critical", 0) > config.ScaCriticalMax)
{
decision = GateDecision.Fail;
reasons.Add($"SCA: {results.ScaFindings["critical"]} 个关键漏洞(最大: {config.ScaCriticalMax})");
}
// 检查秘密
if (results.SecretsFound > config.SecretsAllowed)
{
decision = GateDecision.Fail;
reasons.Add($"秘密: 检测到 {results.SecretsFound} 个秘密");
}
// 检查许可证
foreach (var license in results.Licenses)
{
if (config.ForbiddenLicenses.Contains(license))
{
decision = GateDecision.Fail;
reasons.Add($"许可证: 检测到禁止许可证 {license}");
}
}
// 警告(不失败但报告)
if (results.SastFindings.GetValueOrDefault("medium", 0) > config.SastMediumMax)
{
if (decision == GateDecision.Pass)
decision = GateDecision.Warn;
reasons.Add($"SAST: {results.SastFindings["medium"]} 个中等发现(阈值: {config.SastMediumMax})");
}
return (decision, reasons);
}
}
// 在CI中的使用(控制台应用入口点)
public static class SecurityGateCli
{
public static async Task<int> Main(string[] args)
{
var jsonPath = args.FirstOrDefault() ?? "scan-results.json";
var json = await File.ReadAllTextAsync(jsonPath);
var rawResults = JsonSerializer.Deserialize<RawScanResults>(json)!;
var results = new ScanResults(
rawResults.Sast ?? new(),
rawResults.Sca ?? new(),
rawResults.Dast ?? new(),
rawResults.Secrets,
rawResults.Licenses ?? []);
var config = new SecurityGateConfig();
var (decision, reasons) = SecurityGateEvaluator.Evaluate(config, results);
Console.WriteLine($"安全门: {decision.ToString().ToUpper()}");
foreach (var reason in reasons)
Console.WriteLine($" - {reason}");
return decision == GateDecision.Fail ? 1 : 0;
}
private sealed record RawScanResults(
[property: JsonPropertyName("sast")] Dictionary<string, int>? Sast,
[property: JsonPropertyName("sca")] Dictionary<string, int>? Sca,
[property: JsonPropertyName("dast")] Dictionary<string, int>? Dast,
[property: JsonPropertyName("secrets")] int Secrets,
[property: JsonPropertyName("licenses")] List<string>? Licenses);
}
GitHub Actions安全门
# .github/workflows/security-gate.yml
name: 安全门
on:
pull_request:
branches: [main]
jobs:
security-gate:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
# 运行所有安全扫描
- name: SAST - Semgrep
uses: semgrep/semgrep-action@v1
with:
config: auto
generateSarif: true
- name: SCA - npm audit
run: npm audit --json > npm-audit.json || true
- name: 秘密 - Gitleaks
uses: gitleaks/gitleaks-action@v2
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
# 聚合和评估
- name: 评估安全门
run: |
python scripts/security_gate.py \
--sast-results semgrep.sarif \
--sca-results npm-audit.json \
--secrets-results gitleaks.json
- name: 在PR上评论
if: always()
uses: actions/github-script@v7
with:
script: |
const fs = require('fs');
const report = fs.readFileSync('security-report.md', 'utf8');
github.rest.issues.createComment({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: context.issue.number,
body: report
});
秘密扫描
使用Gitleaks的提交前钩子
# .pre-commit-config.yaml
repos:
- repo: https://github.com/gitleaks/gitleaks
rev: v8.18.0
hooks:
- id: gitleaks
- repo: https://github.com/Yelp/detect-secrets
rev: v1.4.0
hooks:
- id: detect-secrets
args: ['--baseline', '.secrets.baseline']
Gitleaks配置
# .gitleaks.toml
title = "Gitleaks配置"
[extend]
useDefault = true
[[rules]]
id = "custom-api-key"
description = "自定义API密钥模式"
regex = '''(?i)api[_-]?key\s*[:=]\s*['"]?[a-zA-Z0-9]{32,}['"]?'''
tags = ["key", "api"]
[[rules]]
id = "custom-password"
description = "硬编码密码"
regex = '''(?i)(password|passwd|pwd)\s*[:=]\s*['"][^'"]{8,}['"]'''
tags = ["password"]
[allowlist]
description = "全局允许列表"
paths = [
'''\.gitleaks\.toml$''',
'''\.secrets\.baseline$''',
'''test/.*\.py$''',
'''.*_test\.go$''',
]
GitHub秘密扫描
# .github/workflows/secret-scanning.yml
name: 秘密扫描
on:
push:
branches: [main]
pull_request:
jobs:
gitleaks:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
with:
fetch-depth: 0
- name: Gitleaks
uses: gitleaks/gitleaks-action@v2
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
trufflehog:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
with:
fetch-depth: 0
- name: TruffleHog
uses: trufflesecurity/trufflehog@main
with:
extra_args: --only-verified
漏洞管理工作流
漏洞跟踪
/// <summary>
/// 漏洞管理工作流自动化。
/// </summary>
public enum VulnStatus
{
New,
Triaged,
InProgress,
Resolved,
AcceptedRisk,
FalsePositive
}
public enum VulnSeverity { Critical = 1, High = 2, Medium = 3, Low = 4 }
/// <summary>
/// 具有生命周期管理的跟踪漏洞。
/// </summary>
public sealed class Vulnerability
{
public required string Id { get; init; }
public string? CveId { get; init; }
public required string Title { get; init; }
public required string Description { get; init; }
public required VulnSeverity Severity { get; init; }
public required double CvssScore { get; init; }
public required string AffectedComponent { get; init; }
public required string AffectedVersion { get; init; }
public string? FixedVersion { get; init; }
// 跟踪
public VulnStatus Status { get; set; } = VulnStatus.New;
public string? Assignee { get; set; }
public DateTime DiscoveredDate { get; init; } = DateTime.UtcNow;
public DateTime? DueDate { get; set; }
public DateTime? ResolvedDate { get; set; }
public List<string> Notes { get; } = [];
}
/// <summary>
/// 管理漏洞生命周期与SLA跟踪。
/// </summary>
public sealed class VulnerabilityManager
{
private static readonly IReadOnlyDictionary<VulnSeverity, int> SlaDays = new Dictionary<VulnSeverity, int>
{
[VulnSeverity.Critical] = 7,
[VulnSeverity.High] = 30,
[VulnSeverity.Medium] = 90,
[VulnSeverity.Low] = 180,
};
private readonly Dictionary<string, Vulnerability> _vulnerabilities = new();
public void AddVulnerability(Vulnerability vuln)
{
// 基于SLA自动设置截止日期
vuln.DueDate ??= vuln.DiscoveredDate.AddDays(
SlaDays.GetValueOrDefault(vuln.Severity, 90));
_vulnerabilities[vuln.Id] = vuln;
}
public void Triage(string vulnId, string assignee, VulnStatus status = VulnStatus.Triaged)
{
if (_vulnerabilities.TryGetValue(vulnId, out var vuln))
{
vuln.Status = status;
vuln.Assignee = assignee;
vuln.Notes.Add($"{DateTime.UtcNow:O}: 分配给 {assignee}");
}
}
public void Resolve(string vulnId, string resolution, VulnStatus status = VulnStatus.Resolved)
{
if (_vulnerabilities.TryGetValue(vulnId, out var vuln))
{
vuln.Status = status;
vuln.ResolvedDate = DateTime.UtcNow;
vuln.Notes.Add($"{DateTime.UtcNow:O}: 已解决 - {resolution}");
}
}
public void AcceptRisk(string vulnId, string justification, string approver)
{
if (_vulnerabilities.TryGetValue(vulnId, out var vuln))
{
vuln.Status = VulnStatus.AcceptedRisk;
vuln.Notes.Add($"{DateTime.UtcNow:O}: 风险已接受,批准人 {approver} - 理由: {justification}");
}
}
public IEnumerable<Vulnerability> GetOverdue()
{
var now = DateTime.UtcNow;
return _vulnerabilities.Values.Where(v =>
v.Status is not (VulnStatus.Resolved or VulnStatus.AcceptedRisk or VulnStatus.FalsePositive) &&
v.DueDate.HasValue &&
v.DueDate.Value < now);
}
public VulnerabilityMetrics GetMetrics()
{
var vulns = _vulnerabilities.Values.ToList();
return new VulnerabilityMetrics(
Total: vulns.Count,
Open: vulns.Count(v => v.Status is VulnStatus.New or VulnStatus.Triaged or VulnStatus.InProgress),
Resolved: vulns.Count(v => v.Status == VulnStatus.Resolved),
Overdue: GetOverdue().Count(),
BySeverity: Enum.GetValues<VulnSeverity>().ToDictionary(
sev => sev.ToString(),
sev => vulns.Count(v => v.Severity == sev)),
MttrDays: CalculateMttr(vulns));
}
private static double CalculateMttr(List<Vulnerability> vulns)
{
var resolved = vulns
.Where(v => v.Status == VulnStatus.Resolved && v.ResolvedDate.HasValue)
.ToList();
if (resolved.Count == 0) return 0.0;
var totalDays = resolved.Sum(v => (v.ResolvedDate!.Value - v.DiscoveredDate).TotalDays);
return totalDays / resolved.Count;
}
}
public sealed record VulnerabilityMetrics(
int Total,
int Open,
int Resolved,
int Overdue,
Dictionary<string, int> BySeverity,
double MttrDays);
安全冠军计划
计划结构
# 安全冠军计划
## 角色和职责
### 安全冠军
- 开发团队中的嵌入式安全倡导者
- 安全问题的第一联系人
- 参与安全培训并分享知识
- 审查安全关键代码更改
- 为团队分类安全发现
### 时间承诺
- 10-20%的工作时间用于安全活动
- 每周安全站会(30分钟)
- 每月安全培训(2小时)
- 每季度安全深度探讨(4小时)
## 选择标准
- 在团队中工作1年以上
- 对安全感兴趣
- 良好的沟通技巧
- 与同事的技术可信度
## 培训路径
1. **第1个月**:安全基础
- OWASP Top 10
- 安全编码基础
- 公司安全政策
2. **第2个月**:工具和流程
- SAST/DAST工具使用
- 安全门流程
- 漏洞管理
3. **第3个月**:高级主题
- 威胁建模
- 安全架构审查
- 事件响应基础
## 指标
- 冠军审查发现的漏洞
- 安全培训完成率
- 修复发现的时间
- 安全文化调查分数
安全清单
开发前
- [ ] 新功能的威胁模型已完成
- [ ] 安全需求已文档化
- [ ] 安全设计模式已识别
- [ ] 安全冠军已分配
开发中
- [ ] 启用提交前钩子(秘密、代码检查)
- [ ] IDE中集成SAST
- [ ] 遵循安全编码指南
- [ ] 安全关键代码由冠军审查
部署前
- [ ] 所有安全门已通过
- [ ] SAST发现已处理
- [ ] SCA漏洞已解决或接受
- [ ] DAST扫描已完成
- [ ] 安全审查已批准
部署后
- [ ] 启用运行时安全监控
- [ ] 计划漏洞扫描
- [ ] 更新事件响应计划
- [ ] 收集安全指标
参考
- SAST工具:参见
references/sast-tools.md获取详细工具配置 - 安全门:参见
references/security-gates.md获取门实现 - 漏洞工作流:参见
references/vulnerability-workflow.md获取完整工作流
相关技能
secure-coding- 安全开发实践supply-chain-security- 依赖和SBOM管理threat-modeling- 威胁识别和缓解
最后更新: 2025-12-26