SARIF解析Skill sarif-parsing

SARIF解析技能用于读取、分析和处理静态分析工具的SARIF格式结果文件,支持聚合多个工具的发现、去重警报、提取特定漏洞,并可集成到CI/CD管道中。关键词:SARIF解析、静态分析、安全扫描、数据聚合、去重、CI/CD集成、指纹技术、路径规范化。

测试 0 次安装 0 次浏览 更新于 3/24/2026

名称:sarif解析 描述:解析、分析和处理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文件都有以下层次结构:

sarif日志
├── 版本:"2.1.0"
├── $schema:(可选,启用IDE验证)
└── 运行[](分析运行数组)
    ├── 工具
    │   ├── 驱动程序
    │   │   ├── 名称(必需)
    │   │   ├── 版本
    │   │   └── 规则[](规则定义)
    │   └── 扩展[](插件)
    ├── 结果[](发现)
    │   ├── 规则ID
    │   ├── 级别(错误/警告/注意)
    │   ├── 消息.文本
    │   ├── 位置[]
    │   │   └── 物理位置
    │   │       ├── 工件位置.URI
    │   │       └── 区域(起始行、起始列等)
    │   ├── 指纹{}
    │   └── 部分指纹{}
    └── 工件[](扫描文件元数据)

为什么指纹很重要

没有稳定的指纹,您无法跨运行跟踪发现:

  • 基线比较:“这是新发现还是我们以前见过?”
  • 回归检测:“这个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[] | {
  规则: .ruleId,
  消息: .message.text,
  文件: .locations[0].physicalLocation.artifactLocation.uri,
  行: .locations[0].physicalLocation.region.startLine
}' results.sarif

# 按严重性过滤并获取每个规则的计数
jq '[.runs[].results[] | select(.level == "error")] | group_by(.ruleId) | map({规则: .[0].ruleId, 计数: length})' results.sarif

# 提取特定文件的发现
jq --arg 文件 "src/auth.py" '.runs[].results[] | select(.locations[].physicalLocation.artifactLocation.uri | contains($文件))' results.sarif

策略2:使用pysarif的Python方法

用于编程访问和完整对象模型:

from pysarif import load_from_file, save_to_file

# 加载SARIF文件
sarif = load_from_file("results.sarif")

# 遍历运行和结果
for run in sarif.runs:
    工具名称 = run.tool.driver.name
    print(f"工具:{工具名称}")

    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:使用sarif-tools的Python方法

用于聚合、报告和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()

# 按严重性获取直方图
错误 = report.get_issue_type_histogram_for_severity("error")
警告 = report.get_issue_type_histogram_for_severity("warning")

# 过滤结果
高严重性 = [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", []):
            # 使用部分指纹或从位置创建键
            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:
    规则_id: str
    级别: str
    消息: str
    文件路径: Optional[str]
    起始行: Optional[int]
    结束行: Optional[int]
    指纹: 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(
                规则_id=result.get("ruleId", "unknown"),
                级别=result.get("level", "warning"),
                消息=result.get("message", {}).get("text", ""),
                文件路径=phys.get("artifactLocation", {}).get("uri"),
                起始行=region.get("startLine"),
                结束行=region.get("endLine"),
                指纹=next(iter(result.get("partialFingerprints", {}).values()), None)
            ))

    return findings

# 过滤和优先排序
def prioritize_findings(findings: list[Finding]) -> list[Finding]:
    """按严重性排序发现。"""
    严重性顺序 = {"error": 0, "warning": 1, "note": 2, "none": 3}
    return sorted(findings, key=lambda f: 严重性顺序.get(f.级别, 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

    组件 = [
        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):
                # 规范化空白
                组件.append(lines[line_idx].strip())

    return hashlib.sha256("".join(组件).encode()).hexdigest()[:16]

3. 数据缺失或不完整

SARIF允许许多可选字段。始终使用防御性访问:

def safe_get_location(result: dict) -> tuple[str, int]:
    """安全地从结果中提取文件和行。"""
    try:
        loc = result.get("locations", [{}])[0]
        phys = loc.get("physicalLocation", {})
        文件路径 = phys.get("artifactLocation", {}).get("uri", "unknown")
        行 = phys.get("region", {}).get("startLine", 0)
        return 文件路径, 行
    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)

关键原则

  1. 先验证:在处理前检查SARIF结构
  2. 处理可选字段:许多字段是可选的;使用防御性访问
  3. 规范化路径:工具以不同方式报告路径;尽早规范化
  4. 明智地使用指纹:结合多种策略以实现稳定去重
  5. 流式处理大文件:对于100MB+文件,使用ijson或类似工具
  6. 深思熟虑地聚合:合并文件时保留工具元数据

技能资源

对于即用查询模板,请参见{baseDir}/resources/jq-queries.md

  • 40多个jq查询,用于常见SARIF操作
  • 严重性过滤、规则提取、聚合模式

对于Python实用程序,请参见{baseDir}/resources/sarif_helpers.py

  • normalize_path() - 处理工具特定的路径格式
  • compute_fingerprint() - 忽略路径的稳定指纹
  • deduplicate_results() - 跨运行去除重复

参考链接