mcpField
S
sarif-parsing

sarif-parsing

github.com/waybarriosセキュリティ54648 回インストール2 日前に更新
リポジトリ

sarif-parsing について

sarif-parsingは、静的解析ツールのSARIF形式の結果を扱う開発者向けスキルです。検索や複数結果の集約、重複排除、検証、大容量ファイルの処理方法を扱います。

主な機能

  1. 01SARIF結果の読み込みと検索
  2. 02複数ツールの検出結果の集約
  3. 03セキュリティアラートの絞り込みと重複排除
  4. 04パスの正規化とフィンガープリントの計算
  5. 05スキーマ検証と大容量ファイル処理

活用シーン

  1. 01ベースラインと現在のスキャン結果の比較
  2. 02CI/CDで新たな問題を確認
  3. 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 クエリを実行するjqbrew install jq / apt install jq
Python スクリプト(シンプル)pysarifpip install pysarif
Python スクリプト(高度)sarif-toolspip install sarif-tools
.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

# 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: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:
        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: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 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)

主な原則

  1. 最初に検証する:処理の前に SARIF の構造を確認します
  2. オプション項目に対応する:多くのフィールドはオプションです。防御的なアクセスを使用します
  3. パスを正規化する:ツールによって報告されるパスは異なります。早い段階で正規化します
  4. フィンガープリントを適切に扱う:安定した重複排除のために、複数の戦略を組み合わせます
  5. 大容量ファイルをストリーム処理する:100MB 以上のファイルには ijson などを使用します
  6. 慎重に集約する:ファイルの結合時にはツールのメタデータを保持します

スキルリソース

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

  • 一般的な SARIF 操作用の jq クエリ 40 件以上
  • 重要度によるフィルタリング、ルールの抽出、集計パターン

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

  • normalize_path() - ツール固有のパス形式を処理します
  • compute_fingerprint() - パスを無視した安定したフィンガープリントを計算します
  • deduplicate_results() - 実行間の重複を削除します

参考リンク

コメント