Claude Media
Claude CodeでDependabotアラートを読み取り専用で仕分ける手順

Claude CodeでDependabotアラートを読み取り専用で仕分ける手順

gh apiで取り出したDependabotアラートのJSONをclaude -pに渡し、実際に使っているか・どこから呼ばれるかを調べて対応順に並べます。Bashも編集も外した権限の切り方つきです。

Dependabotアラートが数十件たまると、深刻度の順に上から潰すだけでは時間が足りません。実際に効くのは「そのパッケージをコードが本当に使っているか」「脆弱な関数に呼び出し経路があるか」「直せる版が出ているか」の3点で、どれもリポジトリを読まないと分かりません。この読む作業をClaude Codeに任せ、判断と書き込みは人が持つ構成を、コマンドと権限設定つきで示します。

先に構成を書くと、アラートの取得はgh api、判定はclaude -p、アラートの状態変更は人、と3つに分けます。Claudeに渡す道具はRead・Grep・Globだけで、Bashも編集も持たせません。

取得はgh、判断はClaudeに分ける

Claudeにgh apiを叩かせる構成もありますが、ここでは取らない選択をします。理由は2つです。

  • ClaudeにBash(gh api *)を許可すると、ワイルドカードはgh api -X PATCH ...にも当たります。引数を絞るBashルールは崩れやすいと権限のドキュメントも警告しています。
  • アラートの一覧を先に取ってファイルにしておけば、Claudeは「渡されたJSONと手元のソースだけ」を材料にします。トークンはClaudeの手元に渡りません。

取得側は普通のシェルでgh apiを回し、判定側にはstdinでJSONを流し込みます。claude -pは標準入力を読めるので、ログをパイプで渡す使い方と同じ形です。入力の上限は10MBで、超えるとエラー終了します。後述のとおり先に項目を絞るので、通常は収まります。

必要な権限は読み取りだけ

GitHub側のトークンに必要な権限を先に確認します。リポジトリのアラート一覧エンドポイントについて、REST APIのドキュメントには次の条件が書かれています。

トークンの種類一覧の取得に必要なもの
fine-grained personal access token一覧の取得に必要なもの「Dependabot alerts」リポジトリ権限のread
personal access token(classic) / OAuthアプリ一覧の取得に必要なものsecurity_eventsスコープ(公開リポジトリだけならpublic_repo)

アラートの更新(dismissなど)は別のエンドポイントで、fine-grained tokenでは同じ「Dependabot alerts」権限のwriteが要ります。取得専用のトークンを作れば、うっかり更新系を叩く経路がそもそも無くなります。判定の自動化に使うトークンはreadのみにしておくと、スクリプトの誤りやシェルの打ち間違いがあっても、アラートは閉じられません。

対象を絞って取り出す

gh apiのGETでクエリを付けるには、--method GETを明示します。ドキュメントによると、パラメータを付けると既定ではPOSTに切り替わるためです。

gh api "repos/OWNER/REPO/dependabot/alerts" \
  --method GET -f state=open -f per_page=100 \
  --paginate --slurp \
  | jq 'add // []' > alerts-raw.json

per_pageの上限は100(既定30)です。--paginateは全ページを順に取り、--slurpは各ページの配列を外側の配列にまとめます。そこでaddで1本の配列に連結しています。

一覧エンドポイントには、仕分けに使える絞り込みがあります。

  • state: open、fixed、dismissed、auto_dismissedのカンマ区切り
  • severity: low / medium / high / critical
  • scope: runtime か development
  • ecosystemとpackage: 言語別・パッケージ別
  • has=patch: 修正版が出ているものだけ
  • sort=epss_percentage: 悪用される確率の高い順

最初の一括トリアージはstate=openだけで十分です。件数が多いリポジトリでは、severity=critical,highで取った分を先に回し、残りを後から別の実行にすると、1回の実行で読むソースが減ります。

jqで項目を絞ってから渡す

APIの1件には、アドバイザリ本文や参照URL、ユーザー情報など、判定に要らない項目が大量に入っています。そのまま渡すとコンテキストを食うだけなので、jqで必要な項目だけに縮めます。

jq 'map({
  number,
  package: .dependency.package.name,
  ecosystem: .dependency.package.ecosystem,
  manifest: .dependency.manifest_path,
  scope: .dependency.scope,
  relationship: .dependency.relationship,
  severity: .security_advisory.severity,
  ghsa: .security_advisory.ghsa_id,
  cve: .security_advisory.cve_id,
  summary: .security_advisory.summary,
  vulnerable: .security_vulnerability.vulnerable_version_range,
  patched: .security_vulnerability.first_patched_version.identifier,
  epss: .security_advisory.epss.percentage
})' alerts-raw.json > alerts.json

残した項目は、どれも次の判断に使います。

  • scopeとrelationship: 実行時の依存か開発用か、直接の依存か間接の依存か
  • manifest: どのパッケージ定義ファイルに出ているか。モノレポでは、どのサブディレクトリを読むかの手掛かりになります
  • vulnerableとpatched: 脆弱な範囲と、最初の修正版(無ければnull)
  • epss: 悪用される確率のパーセンテージ

first_patched_versionはnullになり得る項目です。修正版が無いアラートはアップデートでは閉じられないので、プロンプトで別扱いにします。relationshipもunknownやinconclusiveが返り得るため、「直接の依存でなければ優先度を下げる」といった単純な規則にはしません。

判定の観点と出力の形を決める

Claudeに任せるのは、アラートごとに次の3つを調べさせることです。

  1. そのパッケージを、ソースが実際にimport・requireしているか
  2. 使っているなら、どのファイルのどこか(ファイル名と行番号つき)
  3. 修正版へ上げたときに、直接の依存か、他の依存経由かで手の入れ方が変わるか

判定の基準は、プロンプトにファイルとして固定しておきます。毎回口頭で指示すると、実行ごとに優先度の付け方がぶれます。

stdin の JSON は Dependabot アラートの一覧です。
カレントディレクトリのソースを Read / Grep / Glob だけで調べ、
各アラートについて次を判定してください。
 
- usage: imported(ソースから読み込まれている)/ dev_only(テスト・
  ビルド用のみ)/ not_found(読み込みが見当たらない)/ unclear
- evidence: 根拠にしたファイル名と行番号。見つからなければ検索に
  使った語と範囲
- priority: 1 = imported かつ scope が runtime かつ patched あり
  2 = imported だが patched なし、または dev_only で severity が high 以上
  3 = dev_only / not_found で patched あり
  4 = それ以外
 
推測で usage を imported にしないこと。根拠の行番号を示せない場合は
unclear にしてください。ファイルは変更しないでください。

出力はスキーマで固定します。--json-schemaを付けると、検証済みのJSONがstructured_outputフィールドに入ります。

{
  "type": "object",
  "properties": {
    "alerts": {
      "type": "array",
      "items": {
        "type": "object",
        "properties": {
          "number": {"type": "integer"},
          "package": {"type": "string"},
          "usage": {"type": "string",
                    "enum": ["imported", "dev_only", "not_found", "unclear"]},
          "evidence": {"type": "string"},
          "priority": {"type": "integer", "enum": [1, 2, 3, 4]},
          "next_step": {"type": "string"}
        },
        "required": ["number", "package", "usage", "evidence",
                     "priority", "next_step"]
      }
    }
  },
  "required": ["alerts"]
}

evidenceを必須にしてあるのは、後で人が裏を取れるようにするためです。

権限を閉じて実行する

ここまでのファイルをtriage-prompt.txtとtriage-schema.jsonに保存し、リポジトリのルートで実行します。

claude -p "$(cat triage-prompt.txt)" \
  --permission-mode dontAsk \
  --allowedTools "Read" "Grep" "Glob" \
  --disallowedTools "Bash" "Edit" "Write" \
  --max-turns 40 \
  --output-format json \
  --json-schema "$(cat triage-schema.json)" \
  < alerts.json > triage-result.json

権限まわりの意図は次のとおりです。

  • --permission-mode dontAskは、確認が必要になる呼び出しを黙って拒否するモードです。作業ディレクトリ内の読み取りと、--allowedToolsで許可した呼び出しは動きます。無人実行で止まらないための指定です
  • --disallowedToolsに素のツール名を書くと、そのツールはClaudeの文脈から消えます。許可の漏れに頼らず、Bash・Edit・Writeを持たせないことを二重に確かめられます
  • --max-turnsは、調べ物が際限なく続くのを止める上限です。上限に達するとエラー終了します
  • CI向けに起動を軽くしたいときは--bareを足せますが、その場合はANTHROPIC_API_KEYが要ります。サブスクリプションのログインは使われません

アラートのsummaryはGitHubのアドバイザリ由来の文字列で、リポジトリの管理者が書いたものではありません。この構成では、仮に文面に指示めいた内容が混ざっていても、Claudeが使える道具は読み取りだけです。書き込みの被害を権限の側で止めておく、という考え方です。

実行後はjqで優先度順の表にできます。structured_outputは、有効な出力が得られなかった実行では付かないため、使う前に存在を確かめます。

jq -r 'if .structured_output then
  .structured_output.alerts | sort_by(.priority)[]
  | [.priority, .number, .package, .usage, .next_step] | @tsv
else "no structured_output" end' triage-result.json

判定結果をそのまま信じない

この方法が出すのは、Grepと読解にもとづく「使われていそうか」の見立てです。静的な到達可能性解析ではありません。次の限界を前提に、結果の読み方を決めておきます。

見える範囲見えない範囲
ソース中のimport・requireと、その周辺の呼び出し見えない範囲動的ロード、リフレクション、設定ファイル経由で読まれる使い方
直接の依存としての使われ方見えない範囲間接の依存が内部で呼ぶ脆弱な関数
evidenceに書かれた行番号見えない範囲書かれていない箇所に潜む別の呼び出し

not_foundと出たアラートは「安全」ではなく「Grepでは見つからなかった」です。間接の依存で脆弱性が内側にある場合は、そもそもソースにimportが現れません。relationshipがtransitiveのアラートは、not_foundでも優先度を下げすぎないようにします。

人が確認するのは、全件ではなく次の2グループに絞れます。

  • priorityが1と2のアラート: evidenceの行番号を開いて、使われ方が合っているか見る
  • not_foundでseverityがcritical・highのもの: 別の読み込み経路が無いか確かめる

修正の実行は、別の作業に切り出せます。たとえば優先度1の1件ずつを対象に、更新と再スキャンのループを回す手順はTrivyを回して脆弱性を直す手順と同じ形で組めます。

アラートを閉じるのは人がやる

PATCH /repos/{owner}/{repo}/dependabot/alerts/{alert_number}で、アラートをdismissedにできます。このときdismissed_reasonが必須で、値はfix_started、inaccurate、no_bandwidth、not_used、tolerable_riskのいずれかです。dismissed_commentは任意で、280字までです。

not_foundの結果を見てnot_usedで閉じたくなりますが、ここは人が実行する側に残します。

  • not_usedは、上の限界の節のとおりGrepの結果だけでは言い切れません
  • 閉じた理由はアラートのコメントとして残るため、evidenceの行番号を貼ってから自分の名前で閉じると、後から経緯を追えます
  • 取得用のトークンはreadのみなので、自動化の側からは閉じられません

Claudeには閉じるコマンドの下書きまでを作らせる、くらいが無理のない線です。実行は自分のターミナルで、書き込み権限のあるトークンを使います。

取得で詰まったとき

gh apiが403や404を返したときは、リポジトリの設定を触る前に、どのトークンで叩いているかを確かめます。REST APIの一覧エンドポイントのステータスコード表には、200・304・400・403・404・422が載っています。

  1. gh auth statusで、使っているアカウントとトークンの種類を見る
  2. fine-grained tokenなら、対象リポジトリが選ばれていて、「Dependabot alerts」がreadになっているかを見る
  3. classic tokenなら、security_events(公開リポジトリのみならpublic_repo)が付いているかを見る

結果が空配列のときは、state=openで絞っているぶん、開いているアラートが無いだけの可能性があります。state=open,fixed,dismissedで一度広げて確かめられます。

Renovateが作る更新PRの検証は、この記事とは別の入口です。アラートの仕分けと、更新PRの検証を組み合わせる場合はRenovateとDependabotのPRを検証する運用を併用すると、アラートで見つけた更新をPRの側で受け止められます。claude -pそのものの権限や出力形式は、claude -pの使い方にまとまっています。

まとめ

この構成の肝は、取得・判定・書き込みの権限を別々の主体に分けた点です。取得はreadのトークン、判定は読み取りだけのClaude、閉じる操作は人。それぞれが越境できない形にしておくと、アラートの件数が増えても、誰が何を決めたかを追えます。

最初に回すのは、severity=critical,highのopenアラートで十分です。出てきたevidenceを数件開いて、見立てが当たっているかを確かめてから、対象を広げてください。

参照した一次ソース

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

  1. code.claude.com/docs/en/headless
  2. code.claude.com/docs/en/permissions
  3. docs.github.com/en/rest/dependabot/alerts
  4. cli.github.com/manual/gh_api
この記事を共有:XはてブLinkedIn