Claude Media
SBOM(CycloneDX)をOSV-Scannerで突合し脆弱性を仕分ける手順

SBOM(CycloneDX)をOSV-Scannerで突合し脆弱性を仕分ける手順

取引先から届いたCycloneDXのSBOMをOSV-Scannerにかけ、Claude Codeで修正版の有無と到達可能性の確認要否に仕分けて是正表にする手順です。SBOMだけでは決まらない点と取引先への確認事項も扱います。

取引先から届いたSBOM(ソフトウェア部品表)は、そのままでは「どの部品が危ないか」を教えてくれません。CycloneDX形式のSBOMをOSV-Scannerに渡せば、含まれる部品とバージョンを脆弱性データベースのOSVに突合できます。出てきた一覧をClaude Codeで「修正版あり」「修正版なし」「取引先に確認」の3つに仕分ければ、そのまま是正表の下書きになります。

先に押さえておく点が1つあります。SBOMが教えてくれるのは「何が入っているか」であり、「その脆弱な機能を製品が呼んでいるか」ではありません。後半で、この差をどう扱うかを書きます。

CycloneDXのSBOMとOSV-Scannerの役割

CycloneDXはOWASP FoundationとEcma Internationalが開発する部品表の仕様で、ECMA-424として標準化されています。仕様の現行バージョンは1.7(2025年10月21日リリース)で、JSON、XML、Protocol Buffersの3形式があります。

OSV-Scannerは、プロジェクトの依存関係の一覧をOSVデータベースの脆弱性情報に結びつけるツールです。処理は2段階です。

  1. パッケージの抽出: 対象から使っているパッケージの情報を取り出す
  2. 脆弱性の突合: 取り出した情報を既知の脆弱性データベースと照合する

SBOMを渡す場合、1段目の「抽出」をSBOMが肩代わりします。ソースコードもロックファイルも手元になくても、部品の一覧さえあれば2段目に進めるのが、取引先製品の確認にこの組み合わせが向く理由です。

SBOMを渡して実行する

インストールはHomebrewなら1行です。WindowsはScoopかWinGetに対応しています。

brew install osv-scanner

SBOMは、ファイル名が形式ごとの規則に沿っていれば自動で見つかります。CycloneDXで規則に挙がっているファイル名は次の4つです。

形式自動検出されるファイル名
JSON自動検出されるファイル名bom.json、*.cdx.json
XML自動検出されるファイル名bom.xml、*.cdx.xml

届いたファイルが vendor-product.json のような名前なら、vendor-product.cdx.json に付け替えるか、次のようにファイルを直接指定します。対象には、SPDXでも *.spdx.json などの名前が使えます。

# ディレクトリ内のSBOMを探して突合する
osv-scanner scan source -r ./vendor-sbom/
 
# ファイルを直接指定する
osv-scanner scan source -L ./vendor-sbom/product.cdx.json

注意点が2つあります。ひとつは、対応するのはPackage URL(purl)を使ったSBOMだという点です。部品名とバージョンだけでpurlが付いていないSBOMは、突合に必要な情報が足りません。もうひとつは、SBOMのファイル名を規則に合わせないと、何も拾われずに終わる場合があることです。何も見つからなかったときの終了コードは128で、「脆弱性なし」の0とは別です。

終了コードは次の4つだけ覚えておけば足ります。

終了コード意味
0意味パッケージはあったが、既知の脆弱性に該当しない
1意味パッケージがあり、脆弱性が見つかった
127意味一般的なエラー
128意味パッケージが見つからない(スキャン形式がファイルを拾えなかった)

CIに組み込むときは、1を「失敗」ではなく「要確認」として扱い、128だけを入力ミスとして止めると誤判定を避けられます。

結果を2つの形式で保存する

用途の違う出力を2つ取っておくと、あとの仕分けが楽になります。

osv-scanner scan source -L product.cdx.json \
  --format json > result.json || true
osv-scanner scan source -L product.cdx.json \
  --format markdown > table.md || true

脆弱性が見つかると終了コードが1になるので、シェルスクリプトでは || true を付けて後続の処理を止めないようにします。JSON形式は結果だけを標準出力に出し、ほかの出力は標準エラーに回るため、リダイレクトで安全にファイルへ落とせます。

読み分けは次のとおりです。

  • table.md(Markdown表): CVSS、エコシステム、パッケージ、修正バージョン、バージョン、出所のファイルが並ぶ。修正バージョンが -- なら、修正版がまだない
  • result.json: OSVの完全な脆弱性レコードが入る。同じ別名(CVEとGHSAなど)を持つ脆弱性は groups にまとめられ、source.type にはSBOMから取った結果であることを示す sbom が入る

JSONからパッケージ単位の一覧を取り出すには、次の jq が使えます。groups が空のパッケージは脆弱性がないので除いています。

jq -r '.results[] | .source.path as $s | .packages[]
  | select((.groups // []) | length > 0)
  | [$s, .package.ecosystem, .package.name, .package.version,
     (.groups | map(.ids[0]) | join(","))] | @tsv' result.json

Claude Codeに仕分けさせる

ここからがClaude Codeの出番です。result.json と table.md があるディレクトリで起動し、仕分けの基準を CLAUDE.md に書いておきます。毎回プロンプトに書くより、基準を固定したほうが取引先ごとの表が揃います。

# SBOM突合の仕分けルール
 
- 入力は osv-scanner の result.json と table.md。読み取り専用で扱い、書き換えない
- 脆弱性ごとに次のどれか1つの区分をつける
  - A: 修正バージョンがあり、取引先に更新を依頼できる
  - B: 修正バージョンが `--`(修正版なし)
  - C: 修正版はあるが、製品がその機能を使っているか分からない
- 区分Cは「確認事項」の列に、取引先に聞く内容を1文で書く
- 重大度はCVSSの値をそのまま転記する。独自の点数をつくらない
- 判断の根拠がJSONにない項目は「不明」と書き、推測で埋めない

そのうえで、たとえば次のように頼みます。

result.json と table.md を読み、CLAUDE.md のルールで仕分けて
remediation.md に是正表をつくって。列は
区分 / パッケージ / バージョン / 脆弱性ID / CVSS / 修正バージョン / 確認事項。
最後に、件数を区分ごとに数えて検算して。

この頼み方には2つの狙いがあります。まず、Claude Codeに渡すのは機械が出した事実(result.json)だけにして、判断の列(区分と確認事項)だけを任せます。次に、最後に件数の検算を頼むことで、行の取りこぼしを自分で確かめさせます。区分ごとの件数の合計が、table.md の行数と一致するかは、人の目でも見ておく価値があります。

表の体裁や列の並べ替え、期限列の追加といった作業は、スキャナー出力のCSVを入口にした脆弱性スキャン結果を是正対応表にする手順と同じです。そちらは期限の置き方や検算の観点も詳しいので、表の仕上げに使えます。

到達可能性はSBOMだけでは決まらない

OSV-Scannerには、脆弱性のあるコードが実際に実行されるかを調べるcall analysisという機能があります。呼ばれていない脆弱性は「unexecuted」として分けて表示され、Goではすでに既定で有効、全言語に広げるときは --call-analysis=all を指定します。

ただし、この機能の説明は、Goならgovulncheckでソースコードを解析し、Rustならソースをコンパイルしてデバッグ情報を調べる、という前提で書かれています。SBOMは部品の一覧であってソースコードではないので、SBOMだけを渡す今回の入力で同じ仕分けができる、とは書かれていません。取引先製品のソースが手元にないなら、「呼ばれていない」かどうかは、こちらでは決められないと考えるのが安全です。

そこで、到達可能性の確認が要るものは、取引先に聞く項目として表に残します。CycloneDXには、この用途に合った入れ物があります。仕様のVulnerabilities節は、脆弱性そのものに加えて、その悪用可能性(exploitability)を伝えられると説明しており、VEX(脆弱性の悪用可能性に関する交換情報)の用途に向くとしています。取引先に依頼するときは、次のように書くと通りやすくなります。

  • 区分Cの脆弱性IDを一覧にして渡し、製品に影響があるかを返してもらう
  • 回答はCycloneDXのVEX形式、または各IDについて「影響あり / 影響なし / 調査中」と理由が分かる形で求める
  • 影響なしの場合は、呼ばれていない機能、無効化されている構成など、根拠を書いてもらう

返ってきた「影響なし」を丸ごと信じるのではなく、根拠の欄が埋まっているかを見ます。根拠のない回答は、区分Cのまま残します。

影響がないと判断した脆弱性を除外する

取引先から根拠付きの回答が得られ、自社で影響なしと判断した脆弱性は、設定ファイルの osv-scanner.toml で無視リストに入れられます。理由と期限を残せるので、定期的な再スキャンのたびに同じ項目が出る手間が消えます。

[[IgnoredVulns]]
id = "GHSA-xxxx-xxxx-xxxx"
ignoreUntil = 2027-01-31
reason = "取引先のVEX回答: 該当機能は製品で無効化されている(回答日 2026-10-20)"

指定したIDの別名にあたる脆弱性も、まとめて無視されます。CVEで書いてもGHSAで書いても同じグループが消えるので、意図しない除外が混じっていないかは、設定を追加したときに確認します。設定ファイルを置く場所を変えたいときは --config で指定します。

ignoreUntil は省略もできますが、SBOMの突合では期限を入れておくのが無難です。取引先が製品を更新すれば部品の構成が変わり、前回の「影響なし」が成り立たなくなる可能性があるからです。

取引先SBOMで起きやすいつまずき

  • 結果がゼロ件で、終了コードが128: ファイル名が規則に沿っていない、または対応していない形式。まずファイル名と形式を確かめる
  • 部品が思ったより少ない: SBOMが製品の全構成を載せているとは限らない。CycloneDXのCompositions節は、部品の一覧が完全か不完全かを宣言する仕組みを持つ。完全性の宣言がない、または不完全とされているSBOMは、結果が少なくても「脆弱性が少ない」とは読まない
  • 同じ脆弱性が複数行に出る: 別名ごとに別IDで載るため。groups で束ねて数える
  • 修正バージョンが --: 修正版がまだない。区分Bとして、緩和策(無効化、ネットワーク分離など)の有無を取引先に聞く

SBOMの突合は、取引先の製品に脆弱性が「ある可能性」を洗い出す作業です。ここで出た表は、確定した事実ではなく、問い合わせの順番を決める道具として使います。自社コードの脆弱性を見つけたいときは、ブランチ差分を対象にするClaude Codeのsecurity-reviewコマンドのほうが入口に合います。

定期運用に載せる

SBOMは製品の更新のたびに差し替わります。取引先ごとにディレクトリを分け、CLAUDE.md の仕分けルールをそのまま使い回せば、同じ手順で再実行できます。回すときは次の3点をそろえます。

  1. 受領日とバージョンをファイル名やコミットに残し、いつのSBOMの結果かを後から辿れるようにする
  2. osv-scanner.toml の reason に、回答の出所と日付を書いておく
  3. 終了コード128が出たら、結果ゼロとは扱わずに入力を確認する

OSVのデータは日々更新されるので、同じSBOMでも数週間後に再スキャンすると、新しい脆弱性が出てくることがあります。取引先へ依頼済みの項目も、再スキャンのたびに修正バージョンの欄が変わっていないかを見ます。

この記事を共有:XはてブLinkedIn