关于 sarif-parsing
sarif-parsing 是面向静态分析结果处理的技能,帮助安全工程师读取、解析和整合 SARIF 文件。它涵盖严重性继承、指纹去重、路径规范化及 CI/CD 回归检查。
主要功能
- 01解析 SARIF 扫描结果
- 02按规则解析严重性
- 03规范化路径并计算指纹
- 04聚合、过滤和去重发现
- 05比较基线并检测回归
- 06提供 jq 查询与 Python 辅助工具
适用场景
- 01汇总多个安全工具的扫描结果
- 02筛选漏洞并按严重性排序
- 03比较当前扫描与基线以发现新问题
- 04在 CI/CD 中设置发现项检查
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ trailofbits/ skills --skill sarif-parsing安装后在 Claude Code 中输入/sarif-parsing确认已加载。
包含的文件
除 SKILL.md 外,安装时一起下载的参考资料和脚本。
agents/openai.yamlassets/trail-of-bits-mark.svgresources/jq-queries.mdsarif_helpers.pytest_sarif_helpers.pyresources/fixtures/codeql-no-level.sariflevels-on-results.sarif详细介绍
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
│ ├── ruleIndex (index into tool.driver.rules[])
│ ├── level (OPTIONAL, inherited from the rule when absent)
│ ├── message.text
│ ├── locations[]
│ │ └── physicalLocation
│ │ ├── artifactLocation.uri
│ │ └── region (startLine, startColumn, etc.)
│ ├── fingerprints{}
│ └── partialFingerprints{}
└── artifacts[] (scanned files metadata)
严重性并不总是记录在结果中
result.level 是可选的。CodeQL 会在每条结果中省略该字段,并将严重性记录在规则的 defaultConfiguration.level 中,由结果继承。若直接读取 result.level,无论 CodeQL 运行发现了多少错误,结果都会被评为干净,这会导致严重性门禁在仓库存在失败项时仍以退出码 0 结束。
按以下顺序解析严重性(SARIF 2.1.0 第 3.27.10 节):
kind不为"fail"(即通过或 notApplicable 记录),因此严重性为"none"- 存在时使用
result.level - 匹配规则的
defaultConfiguration.level:将ruleIndex与runs[].tool.driver.rules[]对应;若工具省略ruleIndex,则将ruleId与rules[].id匹配 "warning",即 SARIF 默认值
此技能中的所有严重性查询都从上述解析开始。在 jq 中,对应 {baseDir}/resources/jq-queries.md 中的 LEVEL_FN 定义;在 Python 中,对应 {baseDir}/resources/sarif_helpers.py 中的 resolve_level(result, run)。
指纹为何重要
没有稳定的指纹,就无法跨运行跟踪发现项:
- 基线比较:“这是新发现,还是之前就发现过?”
- 回归检测:“此 PR 是否引入了新的漏洞?”
- 抑制:“在后续运行中忽略这个已知的误报”
不同工具会报告不同的路径(/path/to/project/ 与 /github/workspace/),因此基于路径的匹配会失败。指纹会对内容(代码片段、规则 ID、相对位置)进行哈希处理,从而创建不受环境影响的稳定标识符。
工具选择指南
| 用例 | 工具 | 安装 / 运行 |
|---|---|---|
| 快速 CLI 查询 | jq | brew install jq / apt install jq |
| Python 脚本(简单) | pysarif | uv run --with pysarif python script.py |
| Python 脚本(高级) | sarif-tools | uv run --with sarif-tools python script.py |
| .NET 应用程序 | SARIF SDK | NuGet 包 |
| JavaScript/Node.js | sarif-js | npm 包 |
| 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
# Severity resolution, needed by every query below that filters on level.
# See resources/jq-queries.md for the annotated version.
LEVEL_FN='
def rule($run):
. as $r
| ($run.tool.driver.rules // []) as $rules
| (if ($r.ruleIndex | type) == "number" and $r.ruleIndex >= 0
then $rules[$r.ruleIndex] else null end)
// first($rules[] | select(.id == $r.ruleId))
// null;
def level($run):
. as $r
| if ($r.kind // "fail") != "fail" then "none"
else ($r.level // rule($run).defaultConfiguration.level // "warning") end;
'
# Extract errors only
jq "$LEVEL_FN"'.runs[] as $run | $run.results[] | select(level($run) == "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 "$LEVEL_FN"'[.runs[] as $run | $run.results[] | select(level($run) == "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:使用 pysarif 编写 Python 脚本
用于通过完整对象模型进行编程访问:
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:
# pysarif fills a missing result.level with "warning", so .level here is NOT the
# rule-inherited severity: a CodeQL error (no level on the result, severity on the
# rule) reads as "warning". Gate severity with Strategy 1's level() or with
# resolve_level() in resources/sarif_helpers.py, which resolve it from the rule.
print(f" {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:使用 sarif-tools 编写 Python 脚本
用于汇总、生成报告和集成 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 by severity. sarif-tools hands back raw result dicts, and a result's level may
# live on its rule, so resolve it against the run instead of reading r["level"].
from sarif_helpers import extract_findings, filter_by_level, load_sarif
high_severity = filter_by_level(extract_findings(load_sarif("results.sarif")), "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 sarif_helpers import deduplicate, extract_findings
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
unique = deduplicate(extract_findings(aggregate_sarif_files(["tool1.sarif", "tool2.sarif"])))
deduplicate() 优先使用工具提供的 fingerprints 或 partialFingerprints,如果没有,则对规则 ID、完整的规范化路径、行号和消息进行哈希处理。请将目录保留在键中:在 auth/login.py 和 admin/login.py 的同一行触发的同一规则属于两个不同的发现项,而仅使用基本文件名作为键会导致其中一个被丢弃。
策略 5:提取可操作数据
resources/sarif_helpers.py 仅使用标准库即可实现这一点。
extract_findings() 会返回 Finding 对象,其中的严重性已解析;filter_by_level()、sort_by_severity()、deduplicate() 和 diff_findings() 都可以处理这些对象:
from sarif_helpers import extract_findings, filter_by_level, load_sarif, sort_by_severity
findings = sort_by_severity(extract_findings(load_sarif("results.sarif")))
for f in filter_by_level(findings, "error"):
print(f"{f.file_path}:{f.start_line} [{f.level}] {f.rule_id}: {f.message}")
如果自己编写提取器,严重性是最容易悄无声息出错的部分:
def resolve_level(result: dict, run: dict) -> str:
"""Severity of a result: its own level, else its rule's default, else "warning"."""
if result.get("kind", "fail") != "fail":
return "none"
if result.get("level"):
return result["level"]
rules = run.get("tool", {}).get("driver", {}).get("rules", [])
index = result.get("ruleIndex")
rule = rules[index] if isinstance(index, int) and 0 <= index < len(rules) else next(
(r for r in rules if r.get("id") == result.get("ruleId")), {}
)
return rule.get("defaultConfiguration", {}).get("level") or "warning"
有些工具会在结果中携带 ruleIndex,另一些工具则只携带 ruleId。因此,如果解析器只采用一种方式进行关联,就会悄无声息地为另一类工具生成的每个结果返回默认值。
常见问题与解决方案
1. 路径规范化问题
不同工具报告路径的方式各异(绝对路径、相对路径、URI 编码),因此 file:///src/a%20b.py 和 src/a b.py 可能指向同一个文件。在比较或计算哈希之前,应去掉 file:// 方案、进行百分号解码、以基准路径为参照解析路径,并规范化分隔符:resources/sarif_helpers.py 中的 normalize_path() 会完成这四项操作。
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 # run via: uv run --with 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
uv run --with jsonschema python your_script.py # e.g. the function below
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: |
# select(.level == "error") counts zero on CodeQL output, which records severity on
# the rule instead. Resolve the level or the gate passes on a repo full of errors.
HIGH_COUNT=$(jq '
def rule($run):
. as $r
| ($run.tool.driver.rules // []) as $rules
| (if ($r.ruleIndex | type) == "number" and $r.ruleIndex >= 0
then $rules[$r.ruleIndex] else null end)
// first($rules[] | select(.id == $r.ruleId))
// null;
def level($run):
. as $r
| if ($r.kind // "fail") != "fail" then "none"
else ($r.level // rule($run).defaultConfiguration.level // "warning") end;
[.runs[] as $run | $run.results[] | select(level($run) == "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 结构
- 解析严重性,切勿读取
result.level:该字段是可选的,而且 CodeQL 总是会省略它 - 处理可选字段:许多字段都是可选的;应采用防御式访问
- 规范化路径:工具报告路径的方式各异,应尽早规范化
- 明智地使用指纹:结合多种策略,以实现稳定的去重
- 流式处理大文件:对于 100MB 以上的文件,使用 ijson 或类似工具
- 谨慎汇总:合并文件时保留工具元数据
技能资源
如需开箱即用的查询模板,请参阅 {baseDir}/resources/jq-queries.md:
- 40 多个用于常见 SARIF 操作的 jq 查询
LEVEL_FN——每个过滤查询都会从中开始的严重性解析逻辑- 严重性过滤、规则提取和汇总模式
如需 Python 工具,请参阅 {baseDir}/resources/sarif_helpers.py:
resolve_level()——从结果或其继承的规则中获取严重性normalize_path()——处理工具特有的路径格式compute_fingerprint()——规则、规范化路径、行号和消息deduplicate()——移除不同运行之间的重复项
{baseDir}/resources/fixtures 中有两个 SARIF 样例文件,一个仅在规则中包含严重性,另一个则在结果中包含严重性。每个文件都恰好包含一个错误,因此可以先用已知结果检查门禁,再正式使用。