mcpField
S
sarif-parsing

sarif-parsing

github.com/trailofbitsセキュリティ7.9k6.0k 回インストール今日更新
リポジトリ

sarif-parsing について

sarif-parsingは、SARIF形式の静的解析結果を扱うセキュリティ担当者向けのスキルです。重大度の解決、フィンガープリントによる重複排除、パス正規化、CI/CDでの回帰チェックを扱います。

主な機能

  1. 01SARIFスキャン結果の解析
  2. 02ルール既定値を使った重大度解決
  3. 03パス正規化とフィンガープリント計算
  4. 04検出結果の集約・絞り込み・重複排除
  5. 05ベースライン比較による回帰検出
  6. 06jqクエリとPythonヘルパーの利用

活用シーン

  1. 01複数のセキュリティツールの結果を統合
  2. 02脆弱性を絞り込み重大度順に並べ替え
  3. 03現在のスキャンをベースラインと比較
  4. 04CI/CDパイプラインで検出結果を確認

インストール

コマンドをコピーしてターミナルで実行します。

$npx skills add https://github.com/trailofbits/skills --skill sarif-parsing

インストール後、Claude Code で/sarif-parsingを入力して読み込まれたか確認します。

同梱ファイル

SKILL.md と一緒にインストールされる参考資料やスクリプト。

agents/
openai.yaml
assets/
trail-of-bits-mark.svg
resources/
jq-queries.md
sarif_helpers.py
test_sarif_helpers.py
resources/fixtures/
codeql-no-level.sarif
levels-on-results.sarif

詳細

SARIF の解析に関するベストプラクティス

あなたは SARIF 解析の専門家です。ユーザーが静的解析ツールの SARIF ファイルを効果的に読み取り、分析し、処理できるよう支援します。

使用する場面

次のような場合にこのスキルを使用してください:

  • SARIF 形式の静的解析スキャン結果を読み取る、または解釈する
  • 複数のセキュリティツールからの検出結果を集約する
  • セキュリティアラートの重複を排除する、またはフィルタリングする
  • SARIF ファイルから特定の脆弱性を抽出する
  • CI/CD パイプラインに SARIF データを統合する
  • 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)。

  1. "fail" 以外の kind(パスまたは notApplicable のレコード)の場合は "none"
  2. 設定されている場合は result.level
  3. 対応するルールの defaultConfiguration.level。ruleIndex を runs[].tool.driver.rules[] に対応付けます。ツールが ruleIndex を省略している場合は、ruleId を rules[].id と照合します
  4. SARIF のデフォルトである "warning"

このスキルでの重大度に関するクエリは、すべてこの解決方法を基準にします。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 クエリjqbrew install jq / apt install jq
Python スクリプト(シンプル)pysarifuv run --with pysarif python script.py
Python スクリプト(高度)sarif-toolsuv run --with sarif-tools python script.py
.NET アプリケーションSARIF SDKNuGet パッケージ
JavaScript/Node.jssarif-jsnpm パッケージ
Go アプリケーションgarifgo get github.com/chavacava/garif
検証SARIF Validatorsarifweb.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() は、この4つすべてを実行します。

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)

重要な原則

  1. 最初に検証する:処理前に SARIF の構造を確認します
  2. 重要度を解決し、result.level は参照しない:省略可能なフィールドであり、CodeQL は常にこれを省略します
  3. 省略可能な項目を処理する:多くのフィールドは省略可能です。防御的なアクセスを使用します
  4. パスを正規化する:ツールごとに報告方法が異なるため、早い段階で正規化します
  5. フィンガープリントを適切に扱う:安定した重複排除のため、複数の戦略を組み合わせます
  6. 大容量ファイルをストリーム処理する:100MB 以上のファイルには ijson などを使用します
  7. 慎重に集約する:ファイルを結合する際は、ツールのメタデータを保持します

スキルのリソース

すぐに使えるクエリテンプレートについては、{baseDir}/resources/jq-queries.md を参照してください。

  • よく使われる SARIF 操作向けの jq クエリ40種類以上
  • LEVEL_FN - フィルタリングクエリの起点となる重要度解決処理
  • 重要度によるフィルタリング、ルールの抽出、集約パターン

Python ユーティリティについては、{baseDir}/resources/sarif_helpers.py を参照してください。

  • resolve_level() - 結果またはその結果が継承するルールから重要度を取得します
  • normalize_path() - ツール固有のパス形式に対応します
  • compute_fingerprint() - ルール、正規化されたパス、行番号、メッセージを使用します
  • deduplicate() - 実行間の重複を削除します

2つの SARIF フィクスチャが {baseDir}/resources/fixtures にあります。1つはルールのみに重要度が設定され、もう1つは結果に重要度が設定されています。それぞれにエラーが1つだけ含まれているため、ゲートを信頼する前に、既知の期待値と照らし合わせて確認できます。

参考リンク

コメント