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.jsonper_pageの上限は100(既定30)です。--paginateは全ページを順に取り、--slurpは各ページの配列を外側の配列にまとめます。そこでaddで1本の配列に連結しています。
一覧エンドポイントには、仕分けに使える絞り込みがあります。
state:open、fixed、dismissed、auto_dismissedのカンマ区切りseverity:low/medium/high/criticalscope:runtimeかdevelopmentecosystemと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つを調べさせることです。
- そのパッケージを、ソースが実際にimport・requireしているか
- 使っているなら、どのファイルのどこか(ファイル名と行番号つき)
- 修正版へ上げたときに、直接の依存か、他の依存経由かで手の入れ方が変わるか
判定の基準は、プロンプトにファイルとして固定しておきます。毎回口頭で指示すると、実行ごとに優先度の付け方がぶれます。
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が載っています。
gh auth statusで、使っているアカウントとトークンの種類を見る- fine-grained tokenなら、対象リポジトリが選ばれていて、「Dependabot alerts」がreadになっているかを見る
- 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を数件開いて、見立てが当たっているかを確かめてから、対象を広げてください。
参照した一次ソース
この記事の記述は、次の一次ソースと照合しています。更新時に参照先が変わった場合は一覧も更新します。