执行摘要 #
软件供应链攻击已经从理论威胁演变为现实中最具破坏力的攻击向量之一。从 2020 年的 SolarWinds Orion 事件到 2021 年的 Log4j 零日漏洞(CVE-2021-44228),再到 2024 年的 XZ Utils 后门事件(CVE-2024-3094),供应链攻击展现了其"一次入侵,万物遭殃"的放大效应。本文将系统性地从软件物料清单(SBOM)的生成、依赖漏洞检测、CI/CD 集成到策略即代码,提供一套完整的供应链安全工程实践。
软件供应链攻击全景 #
攻击向量分类 #
软件供应链攻击可以根据注入点分为以下几个维度:
软件供应链攻击面
┌─────────────────────────────────────────────────────────────┐
│ │
│ ┌───────────┐ ┌───────────┐ ┌───────────────────┐ │
│ │ 上游依赖 │ │ 构建系统 │ │ 分发渠道 │ │
│ │ ┌────────┐│ │ ┌────────┐│ │ ┌────────────────┐│ │
│ │ │npm包投毒││ │ │CI/CD劫持││ │ │PyPI/镜像篡改 ││ │
│ │ │Go模块劫持││ │ │依赖混淆│ │ │ │Docker镜像注入 ││ │
│ │ │Typosquat││ │ │编译器后门││ │ │签名伪造 ││ │
│ │ └────────┘│ │ └────────┘│ │ └────────────────┘│ │
│ └───────────┘ └───────────┘ └───────────────────┘ │
│ │
│ ┌───────────┐ ┌───────────┐ ┌───────────────────┐ │
│ │ 开发工具 │ │ 基础设施 │ │ 第三方服务 │ │
│ │ │IDE插件 ││ │ │云配置 ││ │ │CDN劫持 ││ │
│ │ │VS扩展 ││ │ │TF模块 ││ │ │JS脚本注入 ││ │
│ │ │DevOps工具││ │ │容器基座││ │ │监控SDK投毒 ││ │
│ │ └────────┘│ │ └────────┘│ │ └────────────────┘│ │
│ └───────────┘ └───────────┘ └───────────────────┘ │
└─────────────────────────────────────────────────────────────┘经典案例时间线 #
SolarWinds Orion (2020):
攻击时间线:
2020-03: 攻击者入侵 SolarWinds 开发网络
↓
2020-06: Sunburst 后门植入 Orion 平台构建流程
↓
2020-10: 受影响版本 v2019.4 HF 5 至 v2020.2 HF 1 发布
↓
2020-12: FireEye 和 Microsoft 公开披露
受影响客户约 18,000 家组织Log4j 漏洞 (CVE-2021-44228):
2021-11-24: 安全意识研究员 chalk_joe 在 Twitter 首次公开
↓
2021-11-30: Apache 发布 Log4j 2.15.0 修复版本
↓
2021-12: 全球范围内大规模利用,Cloudflare 记录日均 1000+ 次攻击
↓
2022-01: CVE-2021-44832 和 CVE-2021-45046 相继披露
↓
影响范围: 全球估计 30,000+ 个 Java 项目直接/间接依赖XZ Utils 后门 (CVE-2024-3094):
2021-2023: 攻击者以 "Jia Tan" 身份逐步获取 XZ/liblzma 维护者信任
↓
2024-01: 恶意代码合入 v5.6.0 和 v5.6.1 发布版本
↓
2024-03-29: Microsoft 工程师 Andres Freund 发现异常 SSH 延迟
↓
影响: SSH sshd 通过 liblzma 性能退化约 2-3 倍
后门: 可绕过 SSH 认证,但需要特定 RSA 密钥配置SBOM 标准深度解析 #
SPDX(Software Package Data Exchange) #
SPDX 由 Linux Foundation 维护,是目前最广泛采用的 SBOM 标准。其核心数据模型包含以下关键实体:
- SPDX Document: 根元素,包含元数据和 SPDX ID
- Package: 每个软件包的描述(名称、版本、供应商、许可证)
- File: 构成包的源文件信息
- Relationship: 包与包、包与文件之间的关系(DEPENDS_ON、CONTAINS 等)
一个 SPDX JSON 格式的 SBOM 示例:
{
"spdxVersion": "SPDX-2.3",
"dataLicense": "CC0-1.0",
"SPDXID": "SPDXRef-DOCUMENT",
"name": "TechOrigin-API-Service-1.2.0",
"documentNamespace": "https://techorigin.dev/sbom/api-service-1.2.0",
"creationInfo": {
"created": "2024-06-05T09:00:00Z",
"creators": [
"Tool: syft-0.98.0",
"Organization: TechOrigin"
],
"licenseListVersion": "3.20"
},
"packages": [
{
"name": "log4j-core",
"SPDXID": "SPDXRef-Package-log4j-core-2.14.1",
"versionInfo": "2.14.1",
"supplier": "ORGANIZATION: Apache Software Foundation",
"downloadLocation": "https://repo1.maven.org/maven2/org/apache/logging/log4j/log4j-core/2.14.1/",
"filesAnalyzed": false,
"licenseConcluded": "Apache-2.0",
"externalRefs": [
{
"referenceCategory": "PACKAGE-MANAGER",
"referenceType": "purl",
"referenceLocator": "pkg:maven/org.apache.logging.log4j/log4j-core@2.14.1"
}
],
"checksums": [
{
"algorithm": "SHA-256",
"checksumValue": "c243901dae295b76eb5e081f34c08e6665d4e1e3c7ec7e9b3f1a4c1c5e6d7d8e"
}
]
},
{
"name": "spring-boot-starter-web",
"SPDXID": "SPDXRef-Package-spring-boot-starter-web-2.7.5",
"versionInfo": "2.7.5",
"supplier": "ORGANIZATION: Pivotal Software, Inc.",
"externalRefs": [
{
"referenceCategory": "PACKAGE-MANAGER",
"referenceType": "purl",
"referenceLocator": "pkg:maven/org.springframework.boot/spring-boot-starter-web@2.7.5"
}
]
}
],
"relationships": [
{
"spdxElementId": "SPDXRef-DOCUMENT",
"relatedSpdxElement": "SPDXRef-Package-log4j-core-2.14.1",
"relationshipType": "DESCRIBES"
},
{
"spdxElementId": "SPDXRef-Package-spring-boot-starter-web-2.7.5",
"relatedSpdxElement": "SPDXRef-Package-log4j-core-2.14.1",
"relationshipType": "DEPENDS_ON"
}
]
}CycloneDX #
CycloneDX 由 OWASP 推动,其设计理念更侧重于安全用例。与 SPDX 相比,CycloneDX 在以下方面有其独特优势:
- 内置 vulnerability 和 metadata 组件
- 对组件依赖图的更精确建模
- 支持 BOM 等级(BOM Level 1/2/3)
SWID(Software Identification Tags) #
ISO/IEC 19770-2 标准,由 NIST 推动,主要用于企业软件资产管理。SWID 标签通常由软件供应商在安装包中附带。
多语言 SBOM 生成实战 #
Go 项目 SBOM 生成 #
Go 模块天然支持 SBOM 生成。Go 1.18+ 引入了 debug/buildinfo 包,可以提取完整的依赖信息。
使用 syft(Anchore 开源工具)生成 Go 项目的 SBOM:
# 安装 syft
$ curl -sSfL https://raw.githubusercontent.com/anchore/syft/main/install.sh | sh -s -- -b /usr/local/bin
# 从 Go 模块目录生成 SPDX JSON
$ syft packages dir:. -o spdx-json > sbom.spdx.json
# 从 Go 二进制文件提取 SBOM
$ syft packages ./my-app -o spdx-json > binary-sbom.spdx.json
# 从 Go module 缓存生成
$ syft packages $(go env GOMODCACHE) -o cyclonedx-json > deps-sbom.cdx.jsonGo 项目也可以通过标准库编程方式生成 SBOM:
package main
import (
"debug/buildinfo"
"encoding/json"
"fmt"
"log"
"os"
)
type SBOMPackage struct {
Name string `json:"name"`
Version string `json:"version"`
Sum string `json:"sum"`
ReplacementName string `json:"replacement_name,omitempty"`
ReplacementVer string `json:"replacement_version,omitempty"`
Indirect bool `json:"indirect"`
}
type SBOM struct {
GoVersion string `json:"go_version"`
GoArch string `json:"go_arch"`
GoOS string `json:"go_os"`
Path string `json:"main_module_path"`
Deps []SBOMPackage `json:"dependencies"`
}
func GenerateSBOM(binaryPath string) (*SBOM, error) {
info, err := buildinfo.ReadFile(binaryPath)
if err != nil {
return nil, fmt.Errorf("failed to read build info: %w", err)
}
sbom := &SBOM{
GoVersion: info.GoVersion,
GoArch: info.GoArch,
GoOS: info.GoOS,
Path: info.Path,
Deps: make([]SBOMPackage, 0, len(info.Deps)),
}
for _, dep := range info.Deps {
pkg := SBOMPackage{
Name: dep.Path,
Version: dep.Version,
Sum: dep.Sum,
}
if dep.Replace != nil {
pkg.ReplacementName = dep.Replace.Path
pkg.ReplacementVer = dep.Replace.Version
}
sbom.Deps = append(sbom.Deps, pkg)
}
return sbom, nil
}
func main() {
if len(os.Args) < 2 {
log.Fatal("Usage: sbom-gen <binary-path>")
}
sbom, err := GenerateSBOM(os.Args[1])
if err != nil {
log.Fatalf("Error: %v", err)
}
encoder := json.NewEncoder(os.Stdout)
encoder.SetIndent("", " ")
if err := encoder.Encode(sbom); err != nil {
log.Fatalf("JSON encode error: %v", err)
}
}Python 项目 SBOM 生成 #
Python 生态中,pip-licenses、pip-audit 和 cyclonedx-bom 是三个关键工具:
# 使用 cyclonedx-bom 生成 CycloneDX 格式
$ pip install cyclonedx-bom
$ cyclonedx-bom -o sbom.cdx.json --format json
# 使用 pip-audit 检查已知漏洞
$ pip install pip-audit
$ pip-audit --format json > vulnerability-report.json
# 使用 pip-licenses 生成许可证清单
$ pip-licenses --format=json --with-authors --with-urls > licenses.json通过 Python 脚本自动生成 SBOM:
#!/usr/bin/env python3
"""
sbom_generator.py - Generate SBOM for Python projects
Supports requirements.txt, poetry.lock, and pipenv/Pipfile.lock
"""
import json
import os
import sys
import subprocess
from datetime import datetime, timezone
from pathlib import Path
from typing import Dict, List, Optional
class SBOMGenerator:
"""Generate SBOM from Python project dependencies."""
def __init__(self, project_root: str = "."):
self.project_root = Path(project_root)
self.packages: List[Dict] = []
def scan_pip(self) -> List[Dict]:
"""Scan installed packages via pip list --format=json."""
result = subprocess.run(
[sys.executable, "-m", "pip", "list", "--format=json"],
capture_output=True, text=True, check=True
)
pip_packages = json.loads(result.stdout)
packages = []
for pkg in pip_packages:
packages.append({
"name": pkg["name"],
"version": pkg["version"],
"purl": f"pkg:pypi/{pkg['name'].lower()}@{pkg['version']}",
"type": "library",
"scope": "required"
})
return packages
def scan_poetry(self) -> List[Dict]:
"""Scan dependencies from poetry.lock file."""
lock_path = self.project_root / "poetry.lock"
if not lock_path.exists():
return []
# Use toml library to parse poetry.lock
try:
import toml
except ImportError:
subprocess.run([sys.executable, "-m", "pip", "install", "toml"],
capture_output=True)
import toml
with open(lock_path) as f:
lock_data = toml.load(f)
packages = []
for pkg in lock_data.get("package", []):
packages.append({
"name": pkg["name"],
"version": pkg["version"],
"purl": f"pkg:pypi/{pkg['name'].lower()}@{pkg['version']}",
"type": "library",
"scope": "required",
"licenses": [pkg.get("license", "UNKNOWN")],
})
return packages
def scan_requirements(self) -> List[Dict]:
"""Parse requirements.txt for dependencies."""
req_path = self.project_root / "requirements.txt"
if not req_path.exists():
return []
packages = []
with open(req_path) as f:
for line in f:
line = line.strip()
if not line or line.startswith("#") or line.startswith("-"):
continue
# Parse package==version, package>=version, etc.
for sep in ["==", ">=", "<=", "~=", "!="]:
if sep in line:
name, version = line.split(sep, 1)
packages.append({
"name": name.strip(),
"version": version.strip(),
"purl": f"pkg:pypi/{name.strip().lower()}@{version.strip()}",
"type": "library",
})
break
else:
packages.append({
"name": line,
"version": "unspecified",
"purl": f"pkg:pypi/{line.lower()}",
"type": "library",
})
return packages
def generate_cyclonedx(
self, source: str = "pip", output_path: str = "sbom.cdx.json"
) -> str:
"""Generate CycloneDX 1.5 format SBOM."""
scanners = {
"pip": self.scan_pip,
"poetry": self.scan_poetry,
"requirements": self.scan_requirements,
}
scanner = scanners.get(source)
if not scanner:
raise ValueError(f"Unknown source: {source}")
self.packages = scanner()
bom = {
"bomFormat": "CycloneDX",
"specVersion": "1.5",
"version": 1,
"metadata": {
"timestamp": datetime.now(timezone.utc).isoformat(),
"tools": [
{
"name": "sbom-generator",
"version": "1.0.0",
"vendor": "TechOrigin",
}
],
"component": {
"name": self.project_root.name,
"version": "1.0.0",
"type": "application",
},
},
"components": self.packages,
"dependencies": [
{
"ref": f"pkg:pypi/{self.project_root.name.lower()}",
"dependsOn": [pkg["purl"] for pkg in self.packages],
}
],
}
output = json.dumps(bom, indent=2)
Path(output_path).write_text(output)
return output
if __name__ == "__main__":
project = sys.argv[1] if len(sys.argv) > 1 else "."
source = sys.argv[2] if len(sys.argv) > 2 else "pip"
output = sys.argv[3] if len(sys.argv) > 3 else "sbom.cdx.json"
gen = SBOMGenerator(project)
result = gen.generate_cyclonedx(source=source, output_path=output)
print(f"SBOM generated: {output} ({len(gen.packages)} packages)")Node.js 项目 SBOM 生成 #
# 使用 cyclonedx-node 生成
$ npx @cyclonedx/cyclonedx-cli --output-file sbom.cdx.json --format JSON
# 使用 npm ls 生成交付树
$ npm ls --all --json > dependency-tree.json
# 使用 npm audit 检查漏洞
$ npm audit --json > audit-results.json
# 使用 license-checker 生成许可证清单
$ npx license-checker --json --production > licenses.json依赖漏洞扫描实战 #
Grype #
Grype 是 Anchore 开发的开源漏洞扫描器,原生支持 SBOM 输入:
# 安装 grype
$ curl -sSfL https://raw.githubusercontent.com/anchore/grype/main/install.sh | sh -s -- -b /usr/local/bin
# 扫描容器镜像
$ grype my-app:latest -o json > grype-report.json
# 扫描目录(项目源码)
$ grype dir:. --config grype.yaml -o table
# 扫描 SBOM(使用已生成的 SBOM 文件)
$ grype sbom:sbom.spdx.json --fail-on high
# 配置忽略规则 (grype.yaml)Grype 配置文件 grype.yaml:
# grype.yaml
exclude:
- path: "/test/fixtures/**"
- vulnerability: "CVE-2023-9999" # 已评估为风险可接受的已知漏洞
match:
java:
search-upstream: true
golang:
search-local-copies-of-standard-library: true
db:
cache-dir: "/tmp/grype-db"
validate-age: true
max-allowed-built-age: 120h # 数据库超过 120 小时提示更新
output-template: |
====== 漏洞扫描报告 ======
项目: {{ .Distro }}
时间: {{ .Timestamp }}
严重: {{ .CriticalCount }}
高危: {{ .HighCount }}
中危: {{ .MediumCount }}
低危: {{ .LowCount }}Trivy #
Trivy 由 Aqua Security 开发,是功能最全面的漏洞扫描工具之一:
# 安装 trivy
$ wget -qO - https://aquasecurity.github.io/trivy-repo/deb/public.key | sudo apt-key add -
$ echo "deb https://aquasecurity.github.io/trivy-repo/deb $(lsb_release -sc) main" | sudo tee -a /etc/apt/sources.list.d/trivy.list
$ sudo apt update && sudo apt install trivy
# 扫描文件系统
$ trivy fs --security-checks vuln,config,secret --format json -o trivy-fs-report.json .
# 扫描容器镜像
$ trivy image --severity HIGH,CRITICAL --format table my-app:latest
# 扫描 Git 仓库
$ trivy repo https://github.com/org/repo.git --format sarif -o results.sarif
# 扫描 SBOM
$ trivy sbom sbom.cdx.json --exit-code 1 --severity HIGH,CRITICAL
# 扫描 IaC 配置
$ trivy config --format json deployment.yaml terraform/综合扫描脚本 #
将 Grype 和 Trivy 整合到一个自动化扫描流程中:
#!/usr/bin/env python3
"""
supply_chain_scanner.py - Unified supply chain vulnerability scanner
Integrates Grype and Trivy for comprehensive dependency scanning
"""
import json
import subprocess
import sys
from dataclasses import dataclass, field
from enum import Enum
from pathlib import Path
from typing import Dict, List, Optional
class Severity(Enum):
CRITICAL = "CRITICAL"
HIGH = "HIGH"
MEDIUM = "MEDIUM"
LOW = "LOW"
UNKNOWN = "UNKNOWN"
@dataclass
class Vulnerability:
id: str # CVE-2024-XXXX
severity: Severity
package: str
installed_version: str
fixed_version: Optional[str]
title: str
description: str
urls: List[str] = field(default_factory=list)
scanner: str = ""
class SupplyChainScanner:
"""Unified scanner for supply chain vulnerability detection."""
def __init__(self, target: str, target_type: str = "dir"):
self.target = target
self.target_type = target_type
self.vulnerabilities: List[Vulnerability] = []
def run_command(self, cmd: List[str]) -> str:
"""Run command and return stdout."""
result = subprocess.run(
cmd, capture_output=True, text=True,
timeout=300 # 5-minute timeout
)
if result.returncode not in (0, 1): # 1 = vulnerabilities found
print(f"Warning: {' '.join(cmd)} exited with {result.returncode}")
return result.stdout
def scan_with_grype(self) -> List[Vulnerability]:
"""Run Grype scan."""
cmd = ["grype", self.target, "--scope", "squashed", "-o", "json"]
output = self.run_command(cmd)
if not output:
return []
data = json.loads(output)
vulns = []
for match in data.get("matches", []):
vuln = match.get("vulnerability", {})
severity_str = vuln.get("severity", "UNKNOWN")
try:
severity = Severity(severity_str)
except ValueError:
severity = Severity.UNKNOWN
vulns.append(Vulnerability(
id=vuln.get("id", ""),
severity=severity,
package=match.get("artifact", {}).get("name", ""),
installed_version=match.get("artifact", {}).get("version", ""),
fixed_version=vuln.get("fix", {}).get("versions", [None])[0]
if vuln.get("fix", {}).get("versions") else None,
title=vuln.get("description", "")[:200],
description=vuln.get("description", ""),
urls=vuln.get("references", []),
scanner="grype"
))
self.vulnerabilities.extend(vulns)
return vulns
def scan_with_trivy(self) -> List[Vulnerability]:
"""Run Trivy scan."""
if self.target_type == "image":
cmd = ["trivy", "image", "--format", "json", self.target]
elif self.target_type == "sbom":
cmd = ["trivy", "sbom", "--format", "json", self.target]
else:
cmd = ["trivy", "fs", "--format", "json", self.target]
output = self.run_command(cmd)
if not output:
return []
data = json.loads(output)
vulns = []
for result in data.get("Results", []):
for vuln in result.get("Vulnerabilities", []):
severity_str = vuln.get("Severity", "UNKNOWN")
try:
severity = Severity(severity_str)
except ValueError:
severity = Severity.UNKNOWN
vulns.append(Vulnerability(
id=vuln.get("VulnerabilityID", ""),
severity=severity,
package=result.get("Title", result.get("Target", "")),
installed_version=vuln.get("InstalledVersion", ""),
fixed_version=vuln.get("FixedVersion"),
title=vuln.get("Title", ""),
description=vuln.get("Description", ""),
urls=vuln.get("References", []),
scanner="trivy"
))
self.vulnerabilities.extend(vulns)
return vulns
def deduplicate(self) -> List[Vulnerability]:
"""Deduplicate vulnerabilities across scanners."""
seen = set()
unique = []
for v in self.vulnerabilities:
key = (v.id, v.package)
if key not in seen:
seen.add(key)
unique.append(v)
return unique
def generate_report(self, output_path: str = "scan-report.json") -> Dict:
"""Generate comprehensive scan report."""
unique = self.deduplicate()
severity_counts = {s: 0 for s in Severity}
for v in unique:
severity_counts[v.severity] += 1
report = {
"scan_target": self.target,
"total_vulnerabilities": len(unique),
"severity_summary": {
k.value: v for k, v in severity_counts.items()
},
"vulnerabilities": [
{
"cve": v.id,
"severity": v.severity.value,
"package": v.package,
"installed": v.installed_version,
"fixed_in": v.fixed_version or "unfixed",
"description": v.title,
"scanner_source": v.scanner,
}
for v in sorted(unique, key=lambda x: list(Severity).index(x.severity))
],
}
Path(output_path).write_text(json.dumps(report, indent=2))
return report
if __name__ == "__main__":
target = sys.argv[1] if len(sys.argv) > 1 else "."
scanner = SupplyChainScanner(target)
print(f"[*] Scanning {target} with Grype...")
scanner.scan_with_grype()
print(f"[*] Scanning {target} with Trivy...")
scanner.scan_with_trivy()
report = scanner.generate_report()
print(f"\n[+] Scan complete: {report['total_vulnerabilities']} unique vulnerabilities found")
for sev, count in report["severity_summary"].items():
print(f" {sev}: {count}")GitHub Actions: 自动化 SBOM 生成与扫描 #
将 SBOM 生成和漏洞扫描集成到 CI/CD 管道中是实现供应链安全自动化的关键步骤。
# .github/workflows/supply-chain-security.yml
name: Supply Chain Security Scan
on:
push:
branches: [main, master]
pull_request:
branches: [main, master]
schedule:
# 每周日凌晨 2 点运行全量扫描
- cron: '0 2 * * 0'
workflow_dispatch:
permissions:
contents: write
security-events: write
packages: read
jobs:
sbom-generation:
name: Generate SBOM
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Set up Go
uses: actions/setup-go@v5
with:
go-version: '1.22'
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: '3.12'
- name: Install syft
run: |
curl -sSfL https://raw.githubusercontent.com/anchore/syft/main/install.sh | sh -s -- -b /usr/local/bin
syft version
- name: Generate SBOM for Go modules
run: |
syft packages dir:. -o spdx-json > sbom-go.spdx.json
echo "Go SBOM generated"
- name: Generate SBOM for Python deps
run: |
pip install cyclonedx-bom
cyclonedx-bom -o sbom-python.cdx.json --format json
echo "Python SBOM generated"
- name: Generate SBOM for Node.js deps
if: hashFiles('package.json') != ''
run: |
npx @cyclonedx/cyclonedx-npm --output-file sbom-node.cdx.json
echo "Node.js SBOM generated"
- name: Merge SBOMs
run: |
# Create a combined SBOM referencing all language-specific ones
cat > sbom-combined.cdx.json << SBOM_EOF
{
"bomFormat": "CycloneDX",
"specVersion": "1.5",
"version": 1,
"metadata": {
"timestamp": "$(date -u +%Y-%m-%dT%H:%M:%SZ)",
"tools": [{"name": "TechOrigin-SBOM-Merger", "version": "1.0.0"}]
},
"components": [],
"compositions": [
{
"ref": "main",
"dependsOn": ["go-sbom", "python-sbom", "node-sbom"],
"assemblies": ["go-sbom", "python-sbom", "node-sbom"]
}
]
}
SBOM_EOF
- name: Upload SBOM artifacts
uses: actions/upload-artifact@v4
with:
name: sbom-files
path: sbom-*.json
retention-days: 90
vulnerability-scan:
name: Vulnerability Scanning
needs: sbom-generation
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Download SBOM
uses: actions/download-artifact@v4
with:
name: sbom-files
- name: Install Grype
run: |
curl -sSfL https://raw.githubusercontent.com/anchore/grype/main/install.sh | sh -s -- -b /usr/local/bin
- name: Install Trivy
uses: aquasecurity/trivy-action@master
with:
trivy-config: trivy.yaml
- name: Run Grype SBOM Scan
run: |
grype sbom:sbom-go.spdx.json \
--fail-on high \
--output sarif \
-o grype-results.sarif \
|| true
- name: Run Trivy Scan
uses: aquasecurity/trivy-action@master
with:
scan-type: 'fs'
format: 'sarif'
output: 'trivy-results.sarif'
severity: 'CRITICAL,HIGH'
exit-code: '0'
- name: Upload SARIF to GitHub Security
uses: github/codeql-action/upload-sarif@v3
if: always()
with:
sarif_file: trivy-results.sarif
category: trivy
- name: Run pip-audit
if: hashFiles('requirements.txt') != '' || hashFiles('poetry.lock') != ''
run: |
pip install pip-audit
pip-audit --format json -o pip-audit-report.json || true
- name: Generate summary
if: always()
run: |
echo "## Supply Chain Security Report" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "| Tool | Status | Output |" >> $GITHUB_STEP_SUMMARY
echo "|------|--------|--------|" >> $GITHUB_STEP_SUMMARY
echo "| Grype | Complete | grype-results.sarif |" >> $GITHUB_STEP_SUMMARY
echo "| Trivy | Complete | trivy-results.sarif |" >> $GITHUB_STEP_SUMMARY
echo "| pip-audit | Complete | pip-audit-report.json |" >> $GITHUB_STEP_SUMMARY
policy-check:
name: Policy as Code Check
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Install Conftest
run: |
conftest_version="0.53.0"
wget -q "https://github.com/open-policy-agent/conftest/releases/download/v${conftest_version}/conftest_${conftest_version}_Linux_x86_64.tar.gz"
tar xzf "conftest_${conftest_version}_Linux_x86_64.tar.gz"
sudo mv conftest /usr/local/bin/
- name: Run OPA Policy Checks
run: |
conftest test -p policies/ sbom-combined.cdx.json构建产物与加密资源管理 #
在持续集成和部署流程中,构建产物(编译后的二进制、SBOM 文件、签名证书等)通常包含敏感信息,尤其在金融、政务等受监管行业中,这些产物必须进行加密存储。当团队需要访问加密的构建产物或共享密钥时,可靠的密码恢复机制对保障业务连续性至关重要。如果凭证丢失导致无法解密关键构建产物,项目的发布流程可能被中断。在此类场景下,像猫密网(Catpasswd)这样的工具可以帮助恢复加密构建产物和共享资源的访问密码,确保在凭证遗失时仍能维持业务的正常运转。这属于企业风险管理中的基本兜底措施。
in-toto 与 Sigstore:供应链证明链 #
in-toto 框架 #
in-toto 是 CNCF 下的供应链安全框架,通过声明性策略和签名证明(attestations)确保软件从源码到部署的完整性:
in-toto 证明链:
源码仓库 CI/CD 系统 制品仓库 部署
┌──────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐
│ │ commit │ │ build │ │ sign │ │
│ dev │───────▶│ builder │──────▶│ registry │─────▶│ k8s │
│ │ │ │ │ │ │ cluster │
└──────┘ │ + link │ │ + SBOM │ │ + policy │
│ record │ │ │ │ check │
└──────────┘ └──────────┘ └──────────┘in-toto link 记录示例:
{
"_type": "link",
"name": "build-step",
"byproducts": {
"build_log": ["Step 1/5: FROM golang:1.22-alpine...", "..."]
},
"command": ["go", "build", "-o", "app", "."],
"environment": {
"GOOS": "linux",
"GOARCH": "amd64",
"CGO_ENABLED": "0"
},
"materials": [
{
"main.go": {
"sha256": "a1b2c3d4e5f6..."
}
}
],
"products": [
{
"app": {
"sha256": "f1e2d3c4b5a6..."
}
}
],
"signatures": [
{
"keyid": "rsa:abcdef123456",
"sig": "MEUCIQD..."
}
]
}Sigstore / cosign #
Sigstore 提供了无需管理 PKI 的容器镜像签名方案:
# 安装 cosign
$ go install github.com/sigstore/cosign/v2/cmd/cosign@latest
# 使用 keyless 签名(通过 OIDC 认证)
$ cosign sign --yes myregistry.io/my-app:v1.2.0
Tlog entry created at index: 12345678
# 验证签名
$ cosign verify myregistry.io/my-app:v1.2.0 \
--certificate-identity=builder@techorigin.dev \
--certificate-oidc-issuer=https://accounts.google.com
# 附加 SBOM 到镜像
$ cosign attach sbom \
--sbom sbom.spdx.json \
myregistry.io/my-app:v1.2.0
# 附加 in-toto attestation
$ cosign attach attestation \
--predicate provenance.json \
myregistry.io/my-app:v1.2.0
# 验证 attestation
$ cosign verify-attestation \
--type slsaprovenance1 \
myregistry.io/my-app:v1.2.0策略即代码:OPA / Gatekeeper #
使用 OPA(Open Policy Agent)和 Gatekeeper 对供应链安全实施强制性策略控制。
OPA 策略规则 #
# policies/sbom_policy.rego
package sbom
import rego.v1
deny[msg] {
input.components[_].name == "log4j-core"
version := input.components[_].version
not is_safe_version(version)
msg := sprintf(
"CRITICAL: log4j-core version %s is vulnerable (CVE-2021-44228, CVE-2021-45046, CVE-2021-44832). Upgrade to 2.17.1+",
[version]
)
}
is_safe_version(v) {
# Log4j 2.17.1+ is safe
semver := split(v, ".")
major := to_number(semver[0])
minor := to_number(semver[1])
patch := to_number(semver[2])
major >= 2
minor > 17
}
is_safe_version(v) {
semver := split(v, ".")
major := to_number(semver[0])
minor := to_number(semver[1])
patch := to_number(semver[2])
major >= 2
minor == 17
patch >= 1
}
# 禁止使用未授权许可证的依赖
deny[msg] {
component := input.components[_]
not is_approved_license(component.licenses[_])
msg := sprintf(
"Policy violation: %s@%s uses unapproved license. Approved: Apache-2.0, MIT, BSD-2-Clause, BSD-3-Clause, ISC",
[component.name, component.version]
)
}
is_approved_license(lic) {
lic in ["Apache-2.0", "MIT", "BSD-2-Clause", "BSD-3-Clause", "ISC", "MPL-2.0"]
}
# 要求所有组件都有 PURL
warn[msg] {
component := input.components[_]
not component.purl
msg := sprintf(
"Warning: component %s@%s has no PURL. SBOM completeness is reduced.",
[component.name, component.version]
)
}Gatekeeper Constraint Template #
# constrainttemplate.yaml
apiVersion: templates.gatekeeper.sh/v1
kind: ConstraintTemplate
metadata:
name: k8sbomrequired
spec:
crd:
spec:
names:
kind: K8SBomRequired
targets:
- target: admission.k8s.gatekeeper.sh
rego: |
package k8sbomrequired
deny[msg] {
not input.review.object.metadata.annotations["sbom.techorigin.dev/verified"]
msg := "SBOM verification annotation is required for all deployments"
}
deny[msg] {
input.review.object.metadata.annotations["sbom.techorigin.dev/verified"] == "false"
msg := "Deployments must have a verified SBOM before deployment"
}# constraint.yaml
apiVersion: constraints.gatekeeper.sh/v1
kind: K8SBomRequired
metadata:
name: require-sbom-verification
spec:
match:
kinds:
- apiGroups: ["apps"]
kinds: ["Deployment"]
namespaces: ["production", "staging"]防御策略总结 #
| 防御层次 | 工具/技术 | 检测目标 |
|---|---|---|
| SBOM 生成 | syft, cyclonedx-bom | 完整的依赖清单 |
| 漏洞扫描 | grype, trivy, pip-audit | 已知 CVE |
| 签名验证 | cosign, sigstore | 镜像篡改 |
| 供应链证明 | in-toto, slsa | 构建过程完整性 |
| 策略控制 | OPA, Gatekeeper | 部署合规 |
| 密钥管理 | gitleaks, trufflehog | 密钥泄露 |
供应链安全不是一次性的任务,而是一个持续的过程。通过 SBOM 建立依赖可见性,通过自动化扫描实现风险检测,通过 in-toto 和 Sigstore 构建信任链,通过 OPA 实施强制策略——这四层防御体系构成了现代软件供应链安全的工程基石。