名称: sarif-parsing 描述: 解析、分析和处理SARIF(静态分析结果交换格式)文件。适用于读取安全扫描结果、聚合多个工具的发现、去重警报、提取特定漏洞,或将SARIF数据集成到CI/CD管道中。 允许工具:
- Bash
- Read
- Glob
- Grep
SARIF解析最佳实践
您是一名SARIF解析专家。您的角色是帮助用户有效读取、分析和处理来自静态分析工具的SARIF文件。
何时使用
在以下情况使用此技能:
- 读取或解释SARIF格式的静态分析扫描结果
- 聚合多个安全工具的发现
- 去重或过滤安全警报
- 从SARIF文件中提取特定漏洞
- 将SARIF数据集成到CI/CD管道中
- 将SARIF输出转换为其他格式
何时不使用
不要将此技能用于:
- 运行静态分析扫描(使用CodeQL或Semgrep技能替代)
- 编写CodeQL或Semgrep规则(使用它们各自的技能)
- 直接分析源代码(SARIF用于处理现有扫描结果)
- 在没有SARIF输入的情况下处理发现(使用变体分析或审计技能)
SARIF结构概述
SARIF 2.1.0是当前的OASIS标准。每个SARIF文件具有以下层次结构:
sarifLog
├── version: "2.1.0"
├── $schema: (可选,启用IDE验证)
└── runs[] (分析运行数组)
├── tool
│ ├── driver
│ │ ├── name (必需)
│ │ ├── version
│ │ └── rules[] (规则定义)
│ └── extensions[] (插件)
├── results[] (发现)
│ ├── ruleId
│ ├── level (error/warning/note)
│ ├── message.text
│ ├── locations[]
│ │ └── physicalLocation
│ │ ├── artifactLocation.uri
│ │ └── region (startLine, startColumn等)
│ ├── fingerprints{}
│ └── partialFingerprints{}
└── artifacts[] (扫描文件的元数据)
为什么指纹识别重要
没有稳定指纹,无法跟踪跨运行的发现:
- 基线比较:“这是新发现还是我们之前见过的?”
- 回归检测:“这个PR是否引入了新漏洞?”
- 抑制:“在未来的运行中忽略这个已知的误报”
工具报告不同路径(/path/to/project/ vs /github/workspace/),因此基于路径的匹配失败。指纹通过哈希内容(代码片段、规则ID、相对位置)创建稳定的标识符,无论环境如何。
工具选择指南
| 用例 | 工具 | 安装 |
|---|---|---|
| 快速CLI查询 | jq | brew install jq / apt install jq |
| Python脚本(简单) | pysarif | pip install pysarif |
| Python脚本(高级) | sarif-tools | pip install sarif-tools |
| .NET应用 | SARIF SDK | NuGet包 |
| JavaScript/Node.js | sarif-js | npm包 |
| Go应用 | garif | go get github.com/chavacava/garif |
| 验证 | SARIF验证器 | sarifweb.azurewebsites.net |
策略1:使用jq进行快速分析
用于快速探索和一次性查询:
# 美化打印文件
jq '.' results.sarif
# 计算总发现数
jq '[.runs[].results[]] | length' results.sarif
# 列出所有触发的规则ID
jq '[.runs[].results[].ruleId] | unique' results.sarif
# 仅提取错误
jq '.runs[].results[] | select(.level == "error")' results.sarif
# 获取带文件位置的发现
jq '.runs[].results[] | {
rule: .ruleId,
message: .message.text,
file: .locations[0].physicalLocation.artifactLocation.uri,
line: .locations[0].physicalLocation.region.startLine
}' results.sarif
# 按严重性过滤并获取每个规则的计数
jq '[.runs[].results[] | select(.level == "error")] | group_by(.ruleId) | map({rule: .[0].ruleId, count: length})' results.sarif
# 提取特定文件的发现
jq --arg file "src/auth.py" '.runs[].results[] | select(.locations[].physicalLocation.artifactLocation.uri | contains($file))' results.sarif
策略2:使用Python和pysarif
用于具有完整对象模型的编程访问:
from pysarif import load_from_file, save_to_file
# 加载SARIF文件
sarif = load_from_file("results.sarif")
# 遍历运行和结果
for run in sarif.runs:
tool_name = run.tool.driver.name
print(f"工具: {tool_name}")
for result in run.results:
print(f" [{result.level}] {result.rule_id}: {result.message.text}")
if result.locations:
loc = result.locations[0].physical_location
if loc and loc.artifact_location:
print(f" 文件: {loc.artifact_location.uri}")
if loc.region:
print(f" 行: {loc.region.start_line}")
# 保存修改后的SARIF
save_to_file(sarif, "modified.sarif")
策略3:使用Python和sarif-tools
用于聚合、报告和CI/CD集成:
from sarif import loader
# 加载单个文件
sarif_data = loader.load_sarif_file("results.sarif")
# 或加载多个文件
sarif_set = loader.load_sarif_files(["tool1.sarif", "tool2.sarif"])
# 获取总结报告
report = sarif_data.get_report()
# 按严重性获取直方图
errors = report.get_issue_type_histogram_for_severity("error")
warnings = report.get_issue_type_histogram_for_severity("warning")
# 过滤结果
high_severity = [r for r in sarif_data.get_results()
if r.get("level") == "error"]
sarif-tools CLI命令:
# 发现摘要
sarif summary results.sarif
# 列出所有结果及详细信息
sarif ls results.sarif
# 按严重性获取结果
sarif ls --level error results.sarif
# 比较两个SARIF文件(查找新/修复的问题)
sarif diff baseline.sarif current.sarif
# 转换为其他格式
sarif csv results.sarif > results.csv
sarif html results.sarif > report.html
策略4:聚合多个SARIF文件
当组合多个工具的结果时:
import json
from pathlib import Path
def aggregate_sarif_files(sarif_paths: list[str]) -> dict:
"""将多个SARIF文件合并为一个。"""
aggregated = {
"version": "2.1.0",
"$schema": "https://json.schemastore.org/sarif-2.1.0.json",
"runs": []
}
for path in sarif_paths:
with open(path) as f:
sarif = json.load(f)
aggregated["runs"].extend(sarif.get("runs", []))
return aggregated
def deduplicate_results(sarif: dict) -> dict:
"""基于指纹移除重复发现。"""
seen_fingerprints = set()
for run in sarif["runs"]:
unique_results = []
for result in run.get("results", []):
# 使用partialFingerprints或从位置创建键
fp = None
if result.get("partialFingerprints"):
fp = tuple(sorted(result["partialFingerprints"].items()))
elif result.get("fingerprints"):
fp = tuple(sorted(result["fingerprints"].items()))
else:
# 备用:从规则+位置创建指纹
loc = result.get("locations", [{}])[0]
phys = loc.get("physicalLocation", {})
fp = (
result.get("ruleId"),
phys.get("artifactLocation", {}).get("uri"),
phys.get("region", {}).get("startLine")
)
if fp not in seen_fingerprints:
seen_fingerprints.add(fp)
unique_results.append(result)
run["results"] = unique_results
return sarif
策略5:提取可操作数据
import json
from dataclasses import dataclass
from typing import Optional
@dataclass
class Finding:
rule_id: str
level: str
message: str
file_path: Optional[str]
start_line: Optional[int]
end_line: Optional[int]
fingerprint: Optional[str]
def extract_findings(sarif_path: str) -> list[Finding]:
"""从SARIF文件中提取结构化发现。"""
with open(sarif_path) as f:
sarif = json.load(f)
findings = []
for run in sarif.get("runs", []):
for result in run.get("results", []):
loc = result.get("locations", [{}])[0]
phys = loc.get("physicalLocation", {})
region = phys.get("region", {})
findings.append(Finding(
rule_id=result.get("ruleId", "unknown"),
level=result.get("level", "warning"),
message=result.get("message", {}).get("text", ""),
file_path=phys.get("artifactLocation", {}).get("uri"),
start_line=region.get("startLine"),
end_line=region.get("endLine"),
fingerprint=next(iter(result.get("partialFingerprints", {}).values()), None)
))
return findings
# 过滤和优先化
def prioritize_findings(findings: list[Finding]) -> list[Finding]:
"""按严重性排序发现。"""
severity_order = {"error": 0, "warning": 1, "note": 2, "none": 3}
return sorted(findings, key=lambda f: severity_order.get(f.level, 99))
常见陷阱和解决方案
1. 路径归一化问题
不同工具报告路径不同(绝对、相对、URI编码):
from urllib.parse import unquote
from pathlib import Path
def normalize_path(uri: str, base_path: str = "") -> str:
"""将SARIF工件URI归一化为一致路径。"""
# 如果存在,移除file://前缀
if uri.startswith("file://"):
uri = uri[7:]
# URL解码
uri = unquote(uri)
# 处理相对路径
if not Path(uri).is_absolute() and base_path:
uri = str(Path(base_path) / uri)
# 归一化分隔符
return str(Path(uri))
2. 跨运行的指纹不匹配
如果以下情况,指纹可能不匹配:
- 文件路径在环境间不同
- 工具版本更改了指纹算法
- 代码被重新格式化(改变行号)
解决方案: 使用多种指纹策略:
def compute_stable_fingerprint(result: dict, file_content: str = None) -> str:
"""计算环境无关指纹。"""
import hashlib
components = [
result.get("ruleId", ""),
result.get("message", {}).get("text", "")[:100], # 前100个字符
]
# 如果可用,添加代码片段
if file_content and result.get("locations"):
region = result["locations"][0].get("physicalLocation", {}).get("region", {})
if region.get("startLine"):
lines = file_content.split("
")
line_idx = region["startLine"] - 1
if 0 <= line_idx < len(lines):
# 归一化空白
components.append(lines[line_idx].strip())
return hashlib.sha256("".join(components).encode()).hexdigest()[:16]
3. 缺少或不完整数据
SARIF允许许多可选字段。始终使用防御性访问:
def safe_get_location(result: dict) -> tuple[str, int]:
"""安全地从结果中提取文件和行。"""
try:
loc = result.get("locations", [{}])[0]
phys = loc.get("physicalLocation", {})
file_path = phys.get("artifactLocation", {}).get("uri", "unknown")
line = phys.get("region", {}).get("startLine", 0)
return file_path, line
except (IndexError, KeyError, TypeError):
return "unknown", 0
4. 大文件性能问题
对于非常大的SARIF文件(100MB+):
import ijson # pip install ijson
def stream_results(sarif_path: str):
"""流式处理结果而不加载整个文件。"""
with open(sarif_path, "rb") as f:
# 流式处理结果数组
for result in ijson.items(f, "runs.item.results.item"):
yield result
5. 模式验证
在处理前验证以捕获格式错误的文件:
# 使用ajv-cli
npm install -g ajv-cli
ajv validate -s sarif-schema-2.1.0.json -d results.sarif
# 使用Python jsonschema
pip install jsonschema
from jsonschema import validate, ValidationError
import json
def validate_sarif(sarif_path: str, schema_path: str) -> bool:
"""根据模式验证SARIF文件。"""
with open(sarif_path) as f:
sarif = json.load(f)
with open(schema_path) as f:
schema = json.load(f)
try:
validate(sarif, schema)
return True
except ValidationError as e:
print(f"验证错误: {e.message}")
return False
CI/CD集成模式
GitHub Actions
- name: 上传SARIF
uses: github/codeql-action/upload-sarif@v3
with:
sarif_file: results.sarif
- name: 检查高严重性问题
run: |
HIGH_COUNT=$(jq '[.runs[].results[] | select(.level == "error")] | length' results.sarif)
if [ "$HIGH_COUNT" -gt 0 ]; then
echo "发现 $HIGH_COUNT 个高严重性问题"
exit 1
fi
对新问题失败
from sarif import loader
def check_for_regressions(baseline: str, current: str) -> int:
"""返回基线中未有的新问题计数。"""
baseline_data = loader.load_sarif_file(baseline)
current_data = loader.load_sarif_file(current)
baseline_fps = {get_fingerprint(r) for r in baseline_data.get_results()}
new_issues = [r for r in current_data.get_results()
if get_fingerprint(r) not in baseline_fps]
return len(new_issues)
关键原则
- 先验证:在处理前检查SARIF结构
- 处理可选字段:许多字段是可选的;使用防御性访问
- 归一化路径:工具报告路径不同;尽早归一化
- 明智指纹:结合多种策略实现稳定去重
- 流式大文件:对100MB+文件使用ijson或类似工具
- 深思熟虑聚合:合并文件时保留工具元数据
技能资源
对于现成查询模板,请参见{baseDir}/resources/jq-queries.md:
- 40多个常见SARIF操作的jq查询
- 严重性过滤、规则提取、聚合模式
对于Python实用程序,请参见{baseDir}/resources/sarif_helpers.py:
normalize_path()- 处理工具特定路径格式compute_fingerprint()- 忽略路径的稳定指纹deduplicate_results()- 移除跨运行重复项