Claude Media
Claude CodeでCVEの影響調査 — 呼び出し箇所まで複数リポジトリで仕分ける

Claude CodeでCVEの影響調査 — 呼び出し箇所まで複数リポジトリで仕分ける

CVEが公開された日に、GitHubのアドバイザリAPIで影響範囲を取り、依存の有無は機械で、呼び出し箇所はclaude -pで調べて「影響あり・なし・要確認」に仕分ける手順です。

CVEが公開された日に知りたいのは、自社のどのリポジトリが影響を受けるかです。しかも「その依存を入れているか」だけでは足りず、「脆弱な関数を呼んでいるか」まで分からないと、対応の優先度が決まりません。

この手順では作業を2段に分けます。アドバイザリの取得と依存の有無は、スクリプトで機械的に決めます。呼び出し箇所の調査だけを、Claude Codeの非対話実行(claude -p)に任せます。結果は「影響あり / 影響なし / 要確認」の3値で、リポジトリごとに1行の表にまとまります。

アドバイザリから「何を探すか」を取り出す

調査の入力は、アドバイザリの次の項目です。GitHubのグローバルアドバイザリAPIは、CVE番号を指定して1件引けます。

項目使い道
vulnerabilities[].package.ecosystem / name使い道どのエコシステムのどのパッケージか
vulnerable_version_range使い道影響を受けるバージョンの範囲
first_patched_version使い道修正済みの最初のバージョン
vulnerable_functions使い道脆弱な関数名の配列。呼び出し箇所の検索語になる

vulnerable_functions は配列ですが、値が null のアドバイザリもあります。関数名が分からないときの扱いは後で決めます。

curl -s -H "Authorization: Bearer $GITHUB_TOKEN" \
  -H "Accept: application/vnd.github+json" \
  "https://api.github.com/advisories?cve_id=CVE-YYYY-NNNNN" \
  | jq '.[0] | {ghsa_id, cve_id, severity, summary,
      vulnerabilities: [.vulnerabilities[] | {
        ecosystem: .package.ecosystem, name: .package.name,
        range: .vulnerable_version_range,
        fixed: .first_patched_version,
        functions: .vulnerable_functions}]}' \
  > advisory.json

CVE-YYYY-NNNNN は調べたいCVE番号に置き換えます。published=2026-10-27 のように日付を渡せば、その日に公開されたアドバイザリを一覧にもできます。ecosystem=npm などと組み合わせて絞ります。

当日の調査で引っかかりやすい点が1つあります。パラメータなしの一覧と cve_id 指定は、既定でGitHubがレビュー済みのアドバイザリだけを返します。type の種類には reviewed、unreviewed、malware があり、unreviewed のアドバイザリは type=unreviewed を付けないと出てきません。0件だったときは、CVEが存在しないと決めつけずに、この指定を足して引き直します。

3値の判定基準を先に決める

Claude Codeに判定させる前に、3つの値が何を意味するかを人間が決めておきます。ここが曖昧だと、リポジトリごとに基準が揺れます。

判定条件誰が決めるか
影響なし(依存なし)条件対象パッケージがロックファイルに存在しない誰が決めるかスクリプト
影響あり条件範囲内のバージョンが入っており、脆弱な関数の呼び出し箇所が見つかった誰が決めるかClaude Codeが根拠付きで提示し、人が確認
要確認条件範囲内のバージョンが入っているが、呼び出し箇所が見つからない、または判断できない誰が決めるか人

呼び出し箇所が見つからないときも「影響なし」とはしません。動的な呼び出しや設定ファイル経由の利用は、コードを読むだけでは見落とせるからです。「影響なし」を名乗れるのは、依存そのものが無いときだけにします。この線引きだけで、取りこぼしの危険な側の誤りを避けられます。

手順の全体像

手順

CVE当日の調査の流れ

  1. 1

    アドバイザリを取得する

    上の curl で advisory.json を作ります。全リポジトリで共通の入力です。

  2. 2

    依存の有無を機械で判定する

    各リポジトリのロックファイルを引き、対象パッケージが無ければその時点で「影響なし」にします。

  3. 3

    残ったリポジトリだけをclaude -pで調べる

    範囲内のバージョンと呼び出し箇所を調べ、JSONで返させます。

  4. 4

    結果を1つの表に集める

    リポジトリごとの判定と理由を jq で束ねます。

依存の有無はロックファイルで決める

ここはLLMに任せない部分です。npmなら npm ls に name@version-range を渡すと、そのパッケージに至る経路だけが出ます。直接の依存だけでなく推移的依存も見るには --all を付けます。--package-lock-only を付けると、node_modules を無視して package-lock.json だけを読みます。リポジトリをインストールしていなくても調べられる指定です。

mkdir -p out
while read -r repo; do
  name=$(basename "$repo")
  (cd "$repo" && npm ls "$PKG" --all --package-lock-only --json) \
    > "out/$name.deps.json" 2>/dev/null || true
done < repos.txt

PKG には advisory.json のパッケージ名を入れます。|| true は、依存が無いときの終了コードでループが止まらないようにするためです。判定は終了コードではなく、出力JSONの中身で行います。dependencies に該当パッケージが現れなければ、そのリポジトリは「影響なし(依存なし)」として表に載せ、次の段には送りません。

npm以外のエコシステムでは、それぞれのパッケージマネージャーが持つ同等のコマンドで置き換えます。方針は同じで、ロックファイルから推移的依存まで含めて引くことです。

呼び出し箇所をclaude -pで調べる

依存が残ったリポジトリだけを、claude -p で調べます。入力はパイプで渡せます。構造化した出力を得るには --output-format json と --json-schema を組み合わせます。

まず判定の型を schema.json に書きます。

{
  "type": "object",
  "properties": {
    "verdict": { "enum": ["affected", "not_affected", "needs_review"] },
    "installed_version": { "type": "string" },
    "call_sites": { "type": "array", "items": { "type": "string" } },
    "reason": { "type": "string" }
  },
  "required": ["verdict", "reason"]
}

実行は次のとおりです。リポジトリのルートで、アドバイザリをパイプで渡します。

cat advisory.json | claude --bare -p \
  "stdinはCVEのアドバイザリ。このリポジトリのロックファイルで
対象パッケージの実バージョンを調べ、vulnerable_functionsの
呼び出し箇所をソースから探せ。functionsがnullなら、
summaryを読んで関連しそうなAPIを列挙し、その呼び出しを探せ。
範囲外ならnot_affected、範囲内で呼び出しがあればaffected、
範囲内だが呼び出しが見つからない・判断できないならneeds_review。
call_sitesはファイル:行番号で書く。" \
  --tools "Read,Bash" --permission-mode dontAsk \
  --max-turns 15 --max-budget-usd 1.00 \
  --output-format json --json-schema "$(cat schema.json)" \
  > "out/$name.claude.json"

引数の役割は次のとおりです。

  • --bare: フック、プラグイン、MCPサーバー、CLAUDE.mdの自動読み込みを省き、どのマシンでも同じ条件で動かす。サブスクリプションのログインは使われないため、ANTHROPIC_API_KEY を環境変数に設定しておく
  • --tools "Read,Bash": 使える組み込みツールをこの2つに絞る
  • --permission-mode dontAsk: 許可の確認が必要になる呼び出しをすべて拒否する。grep や cat のような読み取り専用コマンドは確認なしで通るので、調査には足りる
  • --max-turns と --max-budget-usd: 1リポジトリあたりの上限。ターン数の上限に達すると、エラーで終了する。予算は見積もりに基づくため、わずかに超えることがある

--bare はリポジトリ側のCLAUDE.mdも読みません。調査に必要な背景は、すべてプロンプトとstdinで渡す形にしてあります。

結果を1つの表にまとめる

--json-schema を付けた実行では、構造化された出力が structured_output に入ります。ただし、Claudeが依頼を断った場合などは、このフィールドが無いまま実行が終わります。集計では、フィールドが無い、または実行がエラー終了したリポジトリを「要確認」に倒します。

for f in out/*.claude.json; do
  name=$(basename "$f" .claude.json)
  jq -r --arg r "$name" '[$r,
    (.structured_output.verdict // "needs_review"),
    (.structured_output.reason // "structured_outputなし")]
    | @tsv' "$f"
done > summary.tsv
 
jq -s 'map(.total_cost_usd // 0) | add' out/*.claude.json

// 演算子は、左辺が無いときに右辺を使う指定です。ターン上限に達した実行はエラーで終わります。out/ に出力JSONが残らず summary.tsv に行が欠けたリポジトリは、依存あり・調査失敗として手で拾います。最後の行は total_cost_usd を合計し、1回の調査にいくらかかったかを見る集計です。この値はクライアント側の見積もりで、実際の請求額とは異なる場合があります。

返ってくるJSONの読み方

スキーマどおりに返った場合、structured_output は次のような形になります(スキーマに沿った書式の例で、実際の出力ではありません)。

{
  "verdict": "needs_review",
  "installed_version": "1.2.3",
  "call_sites": [],
  "reason": "範囲内のバージョンだが、対象関数の呼び出しは見つからない"
}

needs_review の行では、reason に「何を探して何が無かったか」が書かれているかを見ます。理由が空疎なものは、プロンプトの探し方の指示が足りないサインなので、検索語を足して再実行します。affected の行は、call_sites のファイルを開いて、実際に脆弱な使い方になっているかを人が読んで確かめます。

複数のリポジトリで回すときの注意

他人のリポジトリで -p を動かす前に

claude -p は信頼ダイアログを出しません。--bare を付けない実行では、リポジトリの設定ファイルにあるフックが動き、.mcp.json のサーバーにも確認なしで接続します。一方で、そのリポジトリが .claude/settings.json に書いた permissions.allow は、未信頼のフォルダーでは使われません。

自社リポジトリを一括で回す場面でも、手元にクローンしただけの外部リポジトリが混ざることがあります。上の --bare は、速度のためだけでなく、調査対象のリポジトリが仕込んだ設定を動かさないための指定でもあります。

許可の確認で止まらないようにする

誰も承認できない定期実行では、--permission-prompts none も使えます。確認が必要になる呼び出しを拒否し、Claudeに「承認できる人がいないので再試行しない」と伝えます。Claude Code v2.1.259以降の指定です。ここでは dontAsk がすでに同じ役割を果たすため、必須ではありません。

並列度と費用

リポジトリ数が多いなら、xargs -P などで並列に回せます。並列にすると費用も同時に積み上がるので、先に3〜5本で試し、total_cost_usd の平均から全体の見積もりを出します。上限を --max-budget-usd で刻んでおけば、1本の暴走で全体が膨らむことは避けられます。

判定の限界

3値のうち、人の確認なしに信用してよいのは「影響なし(依存なし)」だけです。Claudeが返した affected と needs_review は、確認の順番を決めるための下書きとして扱います。

  • call_sites に挙がったファイルと行は、開いて実在を確かめる。存在しない行番号が混ざる可能性を前提にしておく
  • vulnerable_functions が null のアドバイザリでは、関数名ではなく説明文から探す形になる。検索の精度が下がるので、needs_review が増える
  • バージョン範囲の比較は、ロックファイルの値とアドバイザリの範囲の文字列を突き合わせる作業になる。範囲の書式が複雑なときは、機械で比較するスクリプトに差し替えたほうが確実
  • モノレポでは、パッケージごとに別のロックファイルを持つことがある。リポジトリ単位でなく、ロックファイル単位で実行する

SBOM全体を突き合わせる別の入口は、SBOM(CycloneDX)をOSV-Scannerで突合する手順にあります。スキャナーを回して脆弱性を直すループはTrivyで脆弱性を直す手順、結果を是正対応表に整える作業は脆弱性スキャン結果の是正対応表が扱っています。今回のように特定のCVEを起点にして呼び出し箇所まで見る調査は、スキャナーの一覧とは別の使い方です。

非対話実行のオプション全般は、claude -pオプションの解説にまとまっています。

よくあるつまずき

  • アドバイザリが0件: レビュー前の可能性がある。type=unreviewed を付けて引き直す
  • 429(Too many requests)が返る: 呼び出しの間隔を空けて引き直す。一覧はページ単位(per_page は最大100)で取る
  • 判定が全部 needs_review になる: ターン上限が低すぎるか、--tools にReadが入っていない。1本だけ上限を緩めて動かし、原因を切り分ける
  • structured_output が無い: スキーマに合う出力を返せなかった実行。プロンプトを短くし、判定の条件を明文化する
  • 巨大な入力でエラー: パイプで渡せるstdinは10MBまで。アドバイザリの description は長くなりうるので、必要な項目だけに絞ってから渡す

参照した一次ソース

この記事の記述は、次の一次ソースと照合しています。更新時に参照先が変わった場合は一覧も更新します。

  1. code.claude.com/docs/en/headless
  2. docs.github.com/en/rest/security-advisories/global-advisories
  3. code.claude.com/docs/en/permissions
  4. code.claude.com/docs/en/cli-reference
  5. docs.npmjs.com/cli/v10/commands/npm-ls
この記事を共有:XはてブLinkedIn