名称: 软件供应链安全 描述: 软件供应链安全指南,涵盖SBOM生成、SLSA框架、依赖扫描、SCA工具,以及防护依赖混淆和typosquatting等供应链攻击。 允许工具: 读取, 全局查找, 搜索, 任务
软件供应链安全
软件供应链安全的全面指南,包括依赖管理、SBOM生成、漏洞扫描和供应链攻击防护。
何时使用此技能
- 生成软件物料清单(SBOM)
- 实施SLSA框架合规
- 设置依赖漏洞扫描
- 防护依赖混淆攻击
- 配置锁文件和完整性验证
- 使用Sigstore进行代码签名
- 验证软件来源
- 使用OpenSSF Scorecard评估项目安全性
快速参考
供应链攻击类型
| 攻击类型 | 描述 | 预防措施 |
|---|---|---|
| 依赖混淆 | 攻击者发布恶意包,使用内部包名 | 命名空间范围、私有注册表 |
| Typosquatting | 恶意包使用相似名称(如lodash vs 1odash) |
锁文件、仔细审查、工具 |
| 维护者被攻陷 | 合法包被劫持 | 固定版本、验证签名 |
| 构建系统攻击 | CI/CD管道被攻陷 | SLSA合规、密封构建 |
| 恶意依赖 | 新依赖包含恶意软件 | SCA扫描、SBOM审查 |
SLSA级别快速参考
| 级别 | 要求 | 防护 |
|---|---|---|
| SLSA 1 | 构建过程文档 | 基本透明度 |
| SLSA 2 | 认证来源、托管构建 | 构建后篡改 |
| SLSA 3 | 强化构建平台、不可伪造来源 | 构建期间篡改 |
| SLSA 4 | 两人审查、密封构建 | 内部威胁 |
按生态系统的必备工具
| 生态系统 | 漏洞扫描 | 锁文件 | SBOM生成 |
|---|---|---|---|
| npm/Node.js | npm audit, Snyk |
package-lock.json |
@cyclonedx/cyclonedx-npm |
| Python | pip-audit, Safety |
requirements.txt + 哈希, poetry.lock |
cyclonedx-python |
| Go | govulncheck, Snyk |
go.sum |
cyclonedx-gomod |
| .NET | dotnet list package --vulnerable |
packages.lock.json |
CycloneDX NuGet |
| Java/Maven | OWASP Dependency-Check | pom.xml 带版本 |
cyclonedx-maven-plugin |
| Rust | cargo audit |
Cargo.lock |
cargo-cyclonedx |
SBOM(软件物料清单)
SBOM格式
| 格式 | 标准 | 最佳用途 |
|---|---|---|
| CycloneDX | OASIS | 安全重点,支持VEX |
| SPDX | Linux Foundation | 许可证合规,法律 |
| SWID | ISO/IEC 19770-2 | 软件资产管理 |
CycloneDX SBOM生成
Node.js:
# 安装CycloneDX CLI
npm install -g @cyclonedx/cyclonedx-npm
# 生成SBOM
cyclonedx-npm --output-file sbom.json
cyclonedx-npm --output-file sbom.xml --output-format xml
Python:
# 安装CycloneDX
pip install cyclonedx-bom
# 从requirements.txt生成
cyclonedx-py requirements -i requirements.txt -o sbom.json --format json
# 从Poetry生成
cyclonedx-py poetry -o sbom.json --format json
# 从pip环境生成
cyclonedx-py environment -o sbom.json
.NET:
# 安装CycloneDX工具
dotnet tool install --global CycloneDX
# 生成SBOM
dotnet CycloneDX myproject.csproj -o sbom.json -j
Go:
# 安装cyclonedx-gomod
go install github.com/CycloneDX/cyclonedx-gomod/cmd/cyclonedx-gomod@latest
# 生成SBOM
cyclonedx-gomod mod -json -output sbom.json
CI/CD中的SBOM
# GitHub Actions - 生成和上传SBOM
名称: 生成SBOM
触发:
发布:
类型: [已发布]
任务:
sbom:
运行环境: ubuntu-latest
步骤:
- 使用: actions/checkout@v5
- 名称: 生成SBOM
使用: CycloneDX/gh-node-module-generatebom@v1
参数:
输出: sbom.json
- 名称: 上传SBOM到发布
使用: actions/upload-release-asset@v1
参数:
upload_url: ${{ github.event.release.upload_url }}
asset_path: sbom.json
asset_name: sbom.json
asset_content_type: application/json
- 名称: 提交到Dependency Track
运行: |
curl -X POST \
-H "X-Api-Key: ${{ secrets.DTRACK_API_KEY }}" \
-H "Content-Type: multipart/form-data" \
-F "project=${{ github.repository }}" \
-F "bom=@sbom.json" \
"${{ secrets.DTRACK_URL }}/api/v1/bom"
漏洞扫描
npm/Node.js
# 内置审计
npm audit
npm audit --json > audit-results.json
npm audit fix # 自动修复可能之处
# 检查过时包
npm outdated
# 在CI中使用better-npm-audit
npx better-npm-audit audit --level moderate
Python
# pip-audit(推荐)
pip install pip-audit
pip-audit
pip-audit --fix # 自动修复
pip-audit -r requirements.txt
pip-audit --format json > audit.json
# Safety(替代)
pip install safety
safety check
safety check -r requirements.txt
.NET
# 内置漏洞检查
dotnet list package --vulnerable
dotnet list package --vulnerable --include-transitive
# 输出为JSON用于CI
dotnet list package --vulnerable --format json > vulnerabilities.json
Go
# govulncheck(官方Go工具)
go install golang.org/x/vuln/cmd/govulncheck@latest
govulncheck ./...
govulncheck -json ./... > vuln.json
Rust
# cargo-audit
cargo install cargo-audit
cargo audit
cargo audit --json > audit.json
cargo audit fix # 自动修复(使用cargo-audit-fix)
锁文件和完整性
锁文件最佳实践
using System.Security.Cryptography;
using System.Text.Json;
using System.Text.Json.Serialization;
/// <summary>
/// 锁文件验证工具,用于供应链安全。
/// </summary>
public static class LockFileVerification
{
/// <summary>
/// 验证npm package-lock.json完整性哈希。
/// </summary>
public static Dictionary<string, PackageIntegrityResult> VerifyNpmIntegrity(string packageLockPath)
{
var json = File.ReadAllText(packageLockPath);
var lockData = JsonSerializer.Deserialize<NpmPackageLock>(json)!;
var results = new Dictionary<string, PackageIntegrityResult>();
foreach (var (name, info) in lockData.Packages ?? new())
{
if (string.IsNullOrEmpty(name)) continue; // 根包
if (!string.IsNullOrEmpty(info.Integrity))
{
var parts = info.Integrity.Split('-', 2);
results[name] = new PackageIntegrityResult(
HasIntegrity: true,
Algorithm: parts[0]);
}
else
{
results[name] = new PackageIntegrityResult(HasIntegrity: false, Algorithm: null);
}
}
return results;
}
/// <summary>
/// 验证NuGet packages.lock.json完整性。
/// </summary>
public static Dictionary<string, PackageIntegrityResult> VerifyNuGetLockFile(string lockFilePath)
{
var json = File.ReadAllText(lockFilePath);
var lockData = JsonSerializer.Deserialize<NuGetPackagesLock>(json)!;
var results = new Dictionary<string, PackageIntegrityResult>();
foreach (var (framework, dependencies) in lockData.Dependencies ?? new())
{
foreach (var (packageName, info) in dependencies)
{
var key = $"{packageName}@{info.Resolved}";
results[key] = new PackageIntegrityResult(
HasIntegrity: !string.IsNullOrEmpty(info.ContentHash),
Algorithm: !string.IsNullOrEmpty(info.ContentHash) ? "SHA512" : null);
}
}
return results;
}
}
public sealed record PackageIntegrityResult(bool HasIntegrity, string? Algorithm);
public sealed record NpmPackageLock(
[property: JsonPropertyName("packages")] Dictionary<string, NpmPackageInfo>? Packages);
public sealed record NpmPackageInfo(
[property: JsonPropertyName("integrity")] string? Integrity);
public sealed record NuGetPackagesLock(
[property: JsonPropertyName("dependencies")] Dictionary<string, Dictionary<string, NuGetDependencyInfo>>? Dependencies);
public sealed record NuGetDependencyInfo(
[property: JsonPropertyName("resolved")] string? Resolved,
[property: JsonPropertyName("contentHash")] string? ContentHash);
pip哈希验证
# requirements.txt带哈希(最安全)
requests==2.31.0 \
--hash=sha256:58cd2187c01e70e6e26505bca751777aa9f2ee0b7f4300988b709f44e013003f \
--hash=sha256:942c5a758f98d790eaed1a29cb6eefc7ffb0d1cf7af05c3d2791656dbd6ad1e1
certifi==2024.2.2 \
--hash=sha256:dc383c07b76109f368f6106eee2b593b04a011ea4d55f652c6ca24a754d1cdd1 \
--hash=sha256:922820b53db7a7257ffbda3f597266d435245903d80737e34f8a45ff3e3230d8
自动生成哈希
# pip-tools用于哈希生成
pip install pip-tools
# 生成带哈希的需求文件
pip-compile --generate-hashes requirements.in -o requirements.txt
# Poetry带哈希导出
poetry export --format requirements.txt --with-hashes > requirements.txt
依赖混淆防护
私有注册表配置
npm (.npmrc):
# 将包范围限定到私有注册表
@mycompany:registry=https://npm.mycompany.com/
//npm.mycompany.com/:_authToken=${NPM_TOKEN}
# 始终使用精确版本
save-exact=true
Python (pip.conf):
[global]
index-url = https://pypi.mycompany.com/simple/
extra-index-url = https://pypi.org/simple/
trusted-host = pypi.mycompany.com
[install]
# 优先私有包
prefer-binary = true
预防措施:
using System.Net.Http.Json;
using System.Text.Json.Serialization;
/// <summary>
/// 依赖混淆检测和预防工具。
/// </summary>
public sealed class DependencyConfusionChecker(HttpClient httpClient)
{
/// <summary>
/// 检查内部NuGet包名是否存在于nuget.org上。
/// </summary>
public async Task<Dictionary<string, ConfusionCheckResult>> CheckNuGetConfusionAsync(
IEnumerable<string> internalPackages,
CancellationToken cancellationToken = default)
{
var results = new Dictionary<string, ConfusionCheckResult>();
foreach (var package in internalPackages)
{
try
{
var response = await httpClient.GetAsync(
$"https://api.nuget.org/v3/registration5-semver1/{package.ToLowerInvariant()}/index.json",
cancellationToken);
if (response.IsSuccessStatusCode)
{
var registration = await response.Content.ReadFromJsonAsync<NuGetRegistration>(
cancellationToken: cancellationToken);
var latestVersion = registration?.Items?.LastOrDefault()?.Upper;
results[package] = new ConfusionCheckResult(
ExistsPublicly: true,
PublicVersion: latestVersion,
Risk: ConfusionRisk.High,
Recommendation: "在nuget.org上注册占位包或使用包前缀保留");
}
else if (response.StatusCode == System.Net.HttpStatusCode.NotFound)
{
results[package] = new ConfusionCheckResult(
ExistsPublicly: false,
PublicVersion: null,
Risk: ConfusionRisk.Low,
Recommendation: "考虑注册占位包");
}
}
catch (Exception ex)
{
results[package] = new ConfusionCheckResult(
ExistsPublicly: false,
PublicVersion: null,
Risk: ConfusionRisk.Unknown,
Recommendation: $"检查失败: {ex.Message}");
}
}
return results;
}
/// <summary>
/// 生成用于NuGet包保留的占位.csproj。
/// </summary>
public static string GeneratePlaceholderProject(
string packageId,
string description = "内部包 - 非公开使用")
{
return $"""
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>netstandard2.0</TargetFramework>
<PackageId>{packageId}</PackageId>
<Version>0.0.1</Version>
<Description>{description}</Description>
<Authors>安全团队</Authors>
<PackageTags>占位;内部;保留</PackageTags>
<IncludeSymbols>false</IncludeSymbols>
<IncludeSource>false</IncludeSource>
</PropertyGroup>
</Project>
""";
}
}
public sealed record ConfusionCheckResult(
bool ExistsPublicly,
string? PublicVersion,
ConfusionRisk Risk,
string Recommendation);
public enum ConfusionRisk { 低, 中, 高, 未知 }
public sealed record NuGetRegistration(
[property: JsonPropertyName("items")] List<NuGetCatalogPage>? Items);
public sealed record NuGetCatalogPage(
[property: JsonPropertyName("upper")] string? Upper);
使用Sigstore进行代码签名
Sigstore概述
Sigstore使用OIDC身份提供无密钥签名:
# 安装cosign
# macOS
brew install cosign
# Linux
curl -O -L "https://github.com/sigstore/cosign/releases/latest/download/cosign-linux-amd64"
chmod +x cosign-linux-amd64
sudo mv cosign-linux-amd64 /usr/local/bin/cosign
签名容器镜像
# 使用无密钥签名(OIDC)
cosign sign ghcr.io/myorg/myimage:v1.0.0
# 使用密钥签名
cosign generate-key-pair
cosign sign --key cosign.key ghcr.io/myorg/myimage:v1.0.0
# 验证签名
cosign verify ghcr.io/myorg/myimage:v1.0.0 \
--certificate-identity=ci@myorg.com \
--certificate-oidc-issuer=https://github.com/login/oauth
签名Python包
# 安装sigstore
pip install sigstore
# 签名包
python -m sigstore sign dist/mypackage-1.0.0.tar.gz
# 验证签名
python -m sigstore verify identity \
--cert-identity ci@myorg.com \
--cert-oidc-issuer https://github.com/login/oauth \
dist/mypackage-1.0.0.tar.gz
签名npm包
# npm来源(自npm 9.5.0内置)
npm publish --provenance
# 验证来源
npm audit signatures
OpenSSF Scorecard
运行Scorecard
# 安装scorecard
# macOS
brew install scorecard
# 运行在GitHub仓库
scorecard --repo=github.com/myorg/myproject
# 运行特定检查
scorecard --repo=github.com/myorg/myproject \
--checks=漏洞,依赖更新工具,固定依赖
# 输出为JSON
scorecard --repo=github.com/myorg/myproject --format=json > scorecard.json
Scorecard的GitHub Action
名称: Scorecard分析
触发:
推送:
分支: [main]
计划:
- cron: '0 6 * * 1' # 每周一
权限:
security-events: 写
id-token: 写
contents: 读
actions: 读
任务:
analysis:
运行环境: ubuntu-latest
步骤:
- 使用: actions/checkout@v5
- 名称: 运行Scorecard
使用: ossf/scorecard-action@v2
参数:
results_file: results.sarif
results_format: sarif
publish_results: true
- 名称: 上传到安全选项卡
使用: github/codeql-action/upload-sarif@v3
参数:
sarif_file: results.sarif
Scorecard检查解释
| 检查 | 测量内容 | 如何改进 |
|---|---|---|
| 漏洞 | 依赖中的已知漏洞 | 启用Dependabot,修复漏洞 |
| 依赖更新工具 | 自动化依赖更新 | 启用Dependabot/Renovate |
| 固定依赖 | CI使用固定依赖 | 固定操作版本,使用哈希 |
| 令牌权限 | 最小CI令牌权限 | 使用最低权限令牌 |
| 分支保护 | 主分支保护 | 要求审查,状态检查 |
| 代码审查 | PR需要审查 | 启用必要审查 |
| 签名发布 | 发布被签名 | 使用Sigstore/GPG签名 |
| 二进制工件 | 仓库包含二进制文件 | 移除二进制,使用发布 |
安全清单
发布前清单
- [ ] 为发布生成SBOM
- [ ] 运行漏洞扫描(npm audit、pip-audit等)
- [ ] 验证所有依赖有锁文件条目
- [ ] 检查依赖混淆风险
- [ ] 使用Sigstore签名发布工件
- [ ] 运行OpenSSF Scorecard
- [ ] 验证来源生成已启用
仓库安全
- [ ] 启用Dependabot或Renovate
- [ ] 配置分支保护规则
- [ ] 固定CI/CD操作版本带哈希
- [ ] 使用最小令牌权限
- [ ] 启用秘密扫描
- [ ] 为安全文件配置代码所有者
依赖管理
- [ ] 在所有项目中使用锁文件
- [ ] 启用完整性哈希验证
- [ ] 为内部包配置私有注册表
- [ ] 在公共注册表上注册占位包
- [ ] 添加新依赖前审查
- [ ] 监控typosquatting尝试
参考
- SBOM生成: 见
references/sbom-generation.md获取高级SBOM工作流 - SLSA框架: 见
references/slsa-levels.md获取实施指南 - 攻击防护: 见
references/dependency-attacks.md获取详细攻击模式
相关技能
安全编码- 安全开发实践DevSecOps实践- CI/CD安全集成容器安全- 容器镜像签名和扫描
最后更新: 2025-12-26