关于 sarif-parsing
sarif-parsing 是面向处理静态分析工具 SARIF 扫描结果的技能,适合需要读取、筛选和汇总安全发现的开发者。内容涵盖多文件聚合、指纹去重、校验及大型文件处理。
主要功能
- 01读取与查询 SARIF 结果
- 02汇总多个工具的发现
- 03筛选并去重安全告警
- 04规范化路径与生成指纹
- 05校验结构并处理大型文件
适用场景
- 01比较基线与当前扫描结果
- 02在 CI/CD 中检查新增问题
- 03从扫描结果提取漏洞信息
安装
复制下面的命令,在终端中运行。
$
npx skills add https:/ / github.com/ waybarrios/ opencode-power-pack --skill sarif-parsing安装后在 Claude Code 中输入/sarif-parsing确认已加载。
附带文件resources/jq-queries.mdresources/sarif_helpers.py
详细介绍
SARIF 解析最佳实践
你是 SARIF 解析专家。你的职责是帮助用户有效地读取、分析和处理静态分析工具生成的 SARIF 文件。
适用场景
在以下情况下使用此技能:
- 读取或解读 SARIF 格式的静态分析扫描结果
- 汇总多个安全工具的发现
- 对安全警报进行去重或筛选
- 从 SARIF 文件中提取特定漏洞
- 将 SARIF 数据集成到 CI/CD 工作流中
- 将 SARIF 输出转换为其他格式
不适用场景
在以下情况下请勿使用此技能:
- 运行静态分析扫描(请改用 CodeQL 或 Semgrep 技能)
- 编写 CodeQL 或 Semgrep 规则(请改用相应的技能)
- 直接分析源代码(SARIF 用于处理已有的扫描结果)
- 在没有 SARIF 输入的情况下对发现进行分类(请改用 variant-analysis 或 audit 技能)
SARIF 结构概览
SARIF 2.1.0 是当前的 OASIS 标准。每个 SARIF 文件都具有以下层级结构:
sarifLog
├── version: "2.1.0"
├── $schema: (optional, enables IDE validation)
└── runs[] (array of analysis runs)
├── tool
│ ├── driver
│ │ ├── name (required)
│ │ ├── version
│ │ └── rules[] (rule definitions)
│ └── extensions[] (plugins)
├── results[] (findings)
│ ├── ruleId
│ ├── level (error/warning/note)
│ ├── message.text
│ ├── locations[]
│ │ └── physicalLocation
│ │ ├── artifactLocation.uri
│ │ └── region (startLine, startColumn, etc.)
│ ├── fingerprints{}
│ └── partialFingerprints{}
└── artifacts[] (scanned files metadata)
指纹为何重要
没有稳定的指纹,就无法跨运行跟踪发现:
- 基线比较:“这是新发现,还是之前就发现过?”
- 回归检测:“此 PR 是否引入了新的漏洞?”
- 抑制:“在后续运行中忽略这个已知的误报”
不同工具报告的路径各不相同(/path/to/project/ 与 /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 package |
| JavaScript/Node.js | sarif-js | npm package |
| Go 应用程序 | garif | go get github.com/chavacava/garif |
| 验证 | SARIF Validator | sarifweb.azurewebsites.net |
策略 1:使用 jq 快速分析
用于快速探索和临时查询:
# Pretty print the file
jq '.' results.sarif
# Count total findings
jq '[.runs[].results[]] | length' results.sarif
# List all rule IDs triggered
jq '[.runs[].results[].ruleId] | unique' results.sarif
# Extract errors only
jq '.runs[].results[] | select(.level == "error")' results.sarif
# Get findings with file locations
jq '.runs[].results[] | {
rule: .ruleId,
message: .message.text,
file: .locations[0].physicalLocation.artifactLocation.uri,
line: .locations[0].physicalLocation.region.startLine
}' results.sarif
# Filter by severity and get count per rule
jq '[.runs[].results[] | select(.level == "error")] | group_by(.ruleId) | map({rule: .[0].ruleId, count: length})' results.sarif
# Extract findings for a specific file
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
# Load SARIF file
sarif = load_from_file("results.sarif")
# Iterate through runs and results
for run in sarif.runs:
tool_name = run.tool.driver.name
print(f"Tool: {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" File: {loc.artifact_location.uri}")
if loc.region:
print(f" Line: {loc.region.start_line}")
# Save modified SARIF
save_to_file(sarif, "modified.sarif")
策略 3:使用 Python 和 sarif-tools
用于汇总、报告和 CI/CD 集成:
from sarif import loader
# Load single file
sarif_data = loader.load_sarif_file("results.sarif")
# Or load multiple files
sarif_set = loader.load_sarif_files(["tool1.sarif", "tool2.sarif"])
# Get summary report
report = sarif_data.get_report()
# Get histogram by severity
errors = report.get_issue_type_histogram_for_severity("error")
warnings = report.get_issue_type_histogram_for_severity("warning")
# Filter results
high_severity = [r for r in sarif_data.get_results()
if r.get("level") == "error"]
sarif-tools CLI 命令:
# Summary of findings
sarif summary results.sarif
# List all results with details
sarif ls results.sarif
# Get results by severity
sarif ls --level error results.sarif
# Diff two SARIF files (find new/fixed issues)
sarif diff baseline.sarif current.sarif
# Convert to other formats
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:
"""Combine multiple SARIF files into one."""
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:
"""Remove duplicate findings based on fingerprints."""
seen_fingerprints = set()
for run in sarif["runs"]:
unique_results = []
for result in run.get("results", []):
# Use partialFingerprints or create key from location
fp = None
if result.get("partialFingerprints"):
fp = tuple(sorted(result["partialFingerprints"].items()))
elif result.get("fingerprints"):
fp = tuple(sorted(result["fingerprints"].items()))
else:
# Fallback: create fingerprint from rule + location
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]:
"""Extract structured findings from SARIF file."""
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
# Filter and prioritize
def prioritize_findings(findings: list[Finding]) -> list[Finding]:
"""Sort findings by severity."""
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:
"""Normalize SARIF artifact URI to consistent path."""
# Remove file:// prefix if present
if uri.startswith("file://"):
uri = uri[7:]
# URL decode
uri = unquote(uri)
# Handle relative paths
if not Path(uri).is_absolute() and base_path:
uri = str(Path(base_path) / uri)
# Normalize separators
return str(Path(uri))
2. 跨运行的指纹不匹配
以下情况可能导致指纹不匹配:
- 不同环境中的文件路径不同
- 工具版本更改了指纹算法
- 代码经过重新格式化(导致行号改变)
解决方案: 使用多种指纹策略:
def compute_stable_fingerprint(result: dict, file_content: str = None) -> str:
"""Compute environment-independent fingerprint."""
import hashlib
components = [
result.get("ruleId", ""),
result.get("message", {}).get("text", "")[:100], # First 100 chars
]
# Add code snippet if available
if file_content and result.get("locations"):
region = result["locations"][0].get("physicalLocation", {}).get("region", {})
if region.get("startLine"):
lines = file_content.split("\n")
line_idx = region["startLine"] - 1
if 0 <= line_idx < len(lines):
# Normalize whitespace
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]:
"""Safely extract file and line from result."""
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):
"""Stream results without loading entire file."""
with open(sarif_path, "rb") as f:
# Stream through results arrays
for result in ijson.items(f, "runs.item.results.item"):
yield result
5. 架构验证
处理前进行验证,以发现格式错误的文件:
# Using ajv-cli
npm install -g ajv-cli
ajv validate -s sarif-schema-2.1.0.json -d results.sarif
# Using Python jsonschema
pip install jsonschema
from jsonschema import validate, ValidationError
import json
def validate_sarif(sarif_path: str, schema_path: str) -> bool:
"""Validate SARIF file against schema."""
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"Validation error: {e.message}")
return False
CI/CD 集成模式
GitHub Actions
- name: Upload SARIF
uses: github/codeql-action/upload-sarif@v3
with:
sarif_file: results.sarif
- name: Check for high severity
run: |
HIGH_COUNT=$(jq '[.runs[].results[] | select(.level == "error")] | length' results.sarif)
if [ "$HIGH_COUNT" -gt 0 ]; then
echo "Found $HIGH_COUNT high severity issues"
exit 1
fi
遇到新问题时失败
from sarif import loader
def check_for_regressions(baseline: str, current: str) -> int:
"""Return count of new issues not in baseline."""
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 或类似工具
- 审慎汇总:合并文件时保留工具元数据
技能资源
如需即用型查询模板,请参阅 resources/jq-queries.md:
- 40 多个用于常见 SARIF 操作的 jq 查询
- 严重性筛选、规则提取和聚合模式
如需 Python 工具,请参阅 resources/sarif_helpers.py:
normalize_path()- 处理工具特定的路径格式compute_fingerprint()- 忽略路径,计算稳定的指纹deduplicate_results()- 删除多次运行中的重复结果
参考链接
评论
支持 Markdown