コンテナの脆弱性スキャンで検出されたパッケージの実行時利用状況を確認する方法
公開日:2026年9月16日
はじめに
Trivyは、コンテナで実際には使われていないパッケージも含めて、大量の脆弱性を検出します。 実際に使われているパッケージの検出結果を優先して対応するには、各パッケージの利用状況を確認する必要があります。 Trivyの検出結果だけでは利用状況を確認できないため、実行時の情報が必要です。
この記事では、procfsで読み込みを観測したファイルをOSパッケージに紐づけ、Trivyの検出結果と照合できるかを検証した結果を扱います。 常駐プロセスの一部のパッケージでは照合できましたが、短命プロセスの見逃しや、言語パッケージを紐づけられない場合がありました。1 実際に使われたパッケージの全体を正解として把握できていないため、この方法で利用状況をどこまで網羅的に確認できるかは、まだ評価できていません。
この記事では、次のことを記載します。
- Trivyの検出結果だけでは分からないこと
- パッケージの読み込みを示す証拠から分かることと、分からないこと
- 配布イメージ、自作イメージ、実運用ホストで何を検証し、どこまで確認できたか
- 正解データと比較できた範囲と、網羅性について未評価の範囲
- 実行時証拠をKEV・EPSSと組み合わせる方法
- Dockerホストで同じ対応関係を手動確認する簡単な手順
Trivyの検出結果だけでは分からないこと
この記事では、検出結果の1件を、検出したソフトウェアと脆弱性情報を照合して得られる、パッケージ・インストール済みバージョン・CVEの組として扱います。 件数はCVE×パッケージで数えるため、同じパッケージに複数の検出結果が対応する場合があります。3 件数はこの組み合わせの数を、深刻度は報告された脆弱性の重大さを表します。
検出結果には、次の限界があります。
- そのパッケージがこのコンテナで実際に使われているかは分からない
- 脆弱な機能に到達したかは分からない
- KEVやEPSSを加えても、この環境で対象パッケージが使われているかは分からない
- CISAのKEVは実環境での悪用の証拠があるCVEを示す(KEV Catalogの基礎と使い方を参照)4
- FIRSTのEPSSは今後30日以内の悪用確率を推定する(EPSSの基礎とスコアの確認方法を参照)5
そのため、パッケージの実行時利用状況を確認するには、観測したプロセスと、このコンテナ内のパッケージおよびバージョンを紐づける情報(実行時証拠)が必要です。 パッケージの使用が確認できた場合は、KEV・EPSS・深刻度などで判断した緊急度は変えず、同じ緊急度の検出結果の中で先に対応する判断材料にします。 先に対応する理由が分かるよう、使用状況はKEV・EPSSによる評価とは分けて記録します。
「実行時に使われている」の意味
この記事でいう「実行時に使われている」とは、稼働中のプロセスがそのパッケージの実行ファイルまたは共有ライブラリを読み込んでいることです。
読み込みの有無はprocfsの/proc/<pid>/exeと/proc/<pid>/mapsで観測します。678
この定義で分かることは次のとおりです。
- サンプル取得時点で、プロセスがそのファイルを実行またはマッピングしていたこと
- そのファイルの所有パッケージとバージョンが、検出結果のパッケージとバージョンに一致すること
分からないことは次のとおりです。
- 脆弱な関数が実行されたか
- 読み込みは確認できても、どの関数が呼ばれたかは分からない
- 観測に現れなかったパッケージが使われていないこと
- 別のリクエストやワーカー、サンプル取得の合間に読み込まれる可能性がある
したがって、パッケージの使用が確認できた場合は、同じ緊急度の検出結果の中で先に対応する判断材料にします。 使用を確認できなかった場合でも、使われていないとは断定できないため、それを理由に優先度を下げることはしません。13
観測したパス・所有元・バージョンの扱いの詳細は、procfsで稼働中コンテナのプロセスをOSパッケージに紐づける方法を参照してください。
検証内容
検証したのは、稼働中プロセスのファイルを読み取り、所有元のOSパッケージとバージョンを特定し、同じイメージのTrivy検出結果に照合する一連の処理です。 また、使用を別途確認した一部のパッケージについて、収集プログラムが検出できたかを比較しました。
以下では、照合処理の動作と、正解を用意できた範囲での検出結果を分けて記載します。
- 対象
- 配布イメージは
nginx:1.27、postgres:16、nginx-unprivileged:1.27-alpine - 自作イメージは、特定の状況を再現する小さなプログラムを公式のベースイメージに追加したもので、cryptographyのwheel入り
python:3.12-slim、静的Goサーバー入りdebian:12-slim、短命プロセス・一時的なdlopenを再現するDebianベースのケース - 実運用は別のUbuntuホストの4コンテナ(静的Go、JDK同梱JVM、Node.js×2)をAppArmor有効・rootで1回計測
- 主環境はWSL2ホスト1台(Linux 6.6.114、Docker Engine 29.1.3)、Trivy v0.71を使用
- 配布イメージは
- 手順
- 稼働中コンテナのイメージIDをスキャン
docker topで得たホスト側PIDの/proc/<pid>/exeとmapsの実行可能なマッピングを定期観測- コンテナ内のdpkg・apkデータベースでファイルの所有元を特定し、パッケージ名・バージョンを検出結果と照合
- 条件と判定
- 基準はrootで30秒ごとに300秒間の10サンプル
- 短命プロセスの捕捉確認に、間隔・期間・開始タイミングを変えた7条件を比較
- 確認済みにはスキャン対象イメージとパッケージ名・バージョンの一致が必要で、所有元不明のパスは推測で割り当てない
- 正しさの確認
- 自作イメージはプログラム自身の利用記録と照合し、記録対象の見逃しと誤判定を確認
- 公式イメージは常駐プロセスの利用ファイルを手作業で照合し、見逃しだけを確認
- いずれも脆弱な処理そのものの実行は確認しない
各計測の内容は公開されている計測結果に、 収集手順は計測用ハーネスに記載されています。113 以下はこれらの環境と対象で確認した結果です。使用・未使用の正解を確定していない対象については、見逃しや誤った利用判定を評価していません。
配布イメージと自作イメージでの確認結果
まず、ファイルからパッケージを特定し、スキャン結果へ紐づける処理が成立するかを調べました。 次の表は、その処理について確認した内容です。利用パッケージをすべて検出できたことを示すものではありません。113
| 検証対象 | 検証したこと | 確認できたこと・できなかったこと |
|---|---|---|
| Debianベースのnginx・PostgreSQL | 読み込まれたファイルをdpkgの所有情報からスキャン結果へ照合できるか | 観測した実行ファイル・共有ライブラリの所有パッケージを特定し、対応する検出結果に紐づけられた |
| Alpineベースのnginx | apkの所有情報でも同じ照合ができるか | 観測したファイルをapkのパッケージ情報に紐づけ、対応する検出結果と照合できた |
| cryptographyのwheelを追加したPython | OSの所有情報でPython本体や拡張をパッケージに紐づけられるか | 観測したOSライブラリの一部は照合できたが、Python本体・拡張はOSパッケージの所有情報に含まれず、pipパッケージとの照合はできなかった |
| 静的リンクのGoサーバー | 実行ファイルから内包する依存関係を特定できるか | 実行ファイル自体は観測できたが、OSパッケージの所有元はなく、内包するGoモジュールもこの方法では特定できなかった |
nginxでは、読み込まれた共有ライブラリのパスをlibssl3やzlib1gに紐づけ、同じ名前・バージョンのパッケージに対するTrivyの検出結果を対応付けられました。
これは、観測したファイルから検出結果までの照合が成立した例です。
観測されなかったパッケージが未使用だったか、使用されたのに見逃したかは、この照合結果だけでは判断できません。
正解データとの比較範囲
収集プログラムとは別に使用を確認したパッケージについて、検出できたものと見逃したものを比較しました。 nginxは常駐プロセスの利用ファイルを手作業で確認し、自作ケースはプログラム自身の利用ログを使っています。
次の表は、rootで30秒ごとに300秒間観測した標準条件の結果です。 数える単位はパッケージであり、CVEや脆弱性の件数ではありません。 重大度を限定せず、Trivyに検出結果があるパッケージのうち、別途使用を確認した対象を比較しています。2
| 検証対象 | 正解データで使用を確認した比較対象 | 収集プログラムが検出した数 | 見逃した数 |
|---|---|---|---|
| nginxの常駐プロセス | libc6・libssl3・zlib1g・libcrypt1の4パッケージ |
4 | 0 |
| 短命プロセスの自作ケース | curl・gitの2パッケージ |
0 | 2 |
| 一時的なライブラリ読み込みの自作ケース | libsqlite3-0の1パッケージ |
1 | 0 |
nginxの手作業の確認対象は6パッケージですが、nginxとlibpcre2-8-0にはこのスキャンで脆弱性の検出結果がなく、表の比較対象は残る4パッケージです。
この4パッケージは捉えられましたが、比較対象以外の使用状況は確定していません。
短命プロセスのケースでは、実行したことが利用ログで分かっているcurlとgitを標準条件で見逃しました。
一時的な読み込みのケースではlibsqlite3-0を捉えられましたが、読み込み時間が異なる場合にも捉えられるとは限りません。
各条件の結果は後述します。
実運用ホストで確認した制約
静的Goサービス、JDK同梱JVM、Node.jsサービス2つが動くUbuntuホストでも、同じ照合処理を試しました。 AppArmor有効の環境で、rootとして30秒ごとに300秒間、1回計測しました。1
| 調べた対象 | 確認した制約 |
|---|---|
| Goモジュール・JAR・npmなどの言語パッケージ | OSパッケージの所有情報からは紐づけられなかった |
| HIGH/CRITICALの検出結果があるOSパッケージ | 対応するファイルの読み込みを観測できなかった。使用・未使用の正解は確定していない |
/usr/local/bin/node、同梱JDK、静的Go実行ファイル |
OSパッケージデータベースに所有情報がなかった |
このホストでは、観測した情報をHIGH/CRITICALの検出結果に紐づけられませんでした。 言語パッケージやOSパッケージデータベース外の実行ファイルには、別の対応付け方法が必要であることを確認しました。113 実際に使用されたパッケージの総数を把握していないため、この結果から実運用での再現率を求めることはできません。
この計測方法の限界
この方法では、短命なプロセスや一時的なライブラリの読み込みを見落とすことがあります。 また、言語パッケージ、静的リンクの依存関係、パッケージデータベース外の実行ファイルは、OSパッケージの所有情報では紐づけられません。
短命なプロセス
サンプリングは、実行時間が短く、サンプル取得時には終了しているプロセスを見落とします。 捕捉できるかは、観測間隔だけでなく開始タイミングにも左右されます。
- 5秒ごとに約0.1秒実行するcurlは、300秒間を10秒間隔で観測した30サンプルのうち1回だけ捕捉
- 基準の開始タイミングで300秒間を30秒間隔または60秒間隔で観測した場合、curlの捕捉は0回
- Gitは試した7条件すべてで捕捉0回1
これらはプロセスを捉えたサンプル数であり、実行回数に対する捕捉率ではなく、一般的な見逃し率も求められません。
一時的なライブラリの読み込み
dlopenによる一時的な読み込みは、サンプル取得の合間に始まって終わることがあります。
- SQLiteを7秒間読み込み、7秒間解放するテストでは、7条件すべてで利用を確認でき、基準条件(300秒間を30秒間隔で観測)では10サンプル中3回捕捉1
一時的な読み込みも観測できる場合があるため計測が必要ですが、読み込み時間がさらに短い場合の捕捉は保証しません。
言語パッケージ
Python拡張・Goモジュール・JAR・npmはOSパッケージの所有情報では識別できず、拡張機能のファイルが見えても、スキャナーが報告した言語パッケージとバージョンを特定できるわけではありません。 PythonやNode.jsの観測だけでも、その依存パッケージの利用確認にはなりません。13
- cryptographyのwheel入りPythonでは、pipパッケージの検出結果はOSパッケージの所有情報で紐づけられず、Python本体と拡張にもOSパッケージの所有元がなかった1
ディストリビューションがパッケージ化した言語ソフトウェアも、所有情報で特定できるのはディストリビューションのパッケージまでで、別途報告される言語パッケージの検出結果との対応には追加の紐づけが必要です。
静的リンク
静的リンクの実行ファイルには共有ライブラリのマッピングがなく、組み込まれた依存関係を個々のライブラリのパスと所有情報から検出結果へ紐づけられません。
- Debianベースの静的Goテストでは、実行ファイル自体は観測できたがOSパッケージデータベースに所有元がなく、検出結果への紐づけはできなかった13
紐づけられなかったことは、その実行ファイルに脆弱性がないことを意味しません。
パッケージデータベースに含まれない実行ファイル
OSパッケージデータベースに含まれない実行ファイルは、読み取り可能でも所有パッケージを特定できません。
- 実運用ホストの
/usr/local/bin/node、同梱JDK、静的Go実行ファイルはOSパッケージデータベースの対象外13
ファイル名やディレクトリから所有元を推測せず、対応関係が得られなかったことを、プロセス情報の読み取り失敗と区別して記録します。
対応の優先順位付けにおける実行時証拠の使い方
実行時にパッケージの使用が確認できた場合は、KEV・EPSS・深刻度などで判断した緊急度は変えず、同じ緊急度の検出結果の中で先に対応する判断材料にします。 一方、観測期間中に使用を確認できなかった場合でも、使われていないとは断定できないため、それを理由に優先度を下げることはしません。 使用状況は、KEV・EPSSによる評価とは分けて記録し、対応の判断に使います。
| 判断材料 | 分かること | 分からないこと |
|---|---|---|
| KEV | このCVEの悪用が確認されているか | このコンテナが対象パッケージを読み込むか4 |
| EPSS | 予測期間内に悪用される可能性はどの程度か | この特定のコンテナが侵害されるか5 |
| 実行時証拠 | この環境で、実行ファイルやマッピングを通じて該当バージョンのパッケージを観測できたか | 脆弱な関数に到達したか13 |
すべての検出結果を対応対象として保持し、procfsによる紐づけの記事と同じ次の分類で観測結果を添えます。
| 分類 | 意味 |
|---|---|
確認済み(confirmed) |
有効なサンプルで、実行ファイルまたはマッピングを、検出結果のOSパッケージ名とバージョンに紐づけられた |
不明(unknown) |
読み取りの失敗、メタデータの欠落、所有元の曖昧さ、識別情報やバージョンの問題により、信頼できる結論を出せなかった |
未観測(unobserved) |
有効な収集を行ったものの、利用を確認できる証拠が得られなかった場合で、言語パッケージの紐づけに未対応の場合も含む |
ここでのconfirmed・unknown・unobservedはこの計測での分類で、KestreLynxの稼働時の使用状況のin_use・not_observed・unavailableとは別のものです。
KestreLynxでは、使用状況の判定に未対応の言語エコシステムはunavailableになります。
分類は証拠の状態であり、深刻度ではありません。 観測時刻、成功したサンプル数、照合できなかった理由を併せて保持します。13
扱い方は次のとおりです。
- 確認済みのHIGHの検出結果には、緊急度を変えず、同じ緊急度の検出結果の中で先に調査するための環境固有の根拠が加わる
- KEVに登録されたCVEのHIGHの検出結果は、未観測でもサンプリングでの不在が悪用の証拠を打ち消すわけではないため、引き続き緊急性が高い
- 不明の検出結果は、既存の対応の優先順位を維持しながら、収集上の問題を調査する
- 未観測の言語パッケージの検出結果には、ランタイムが一度も読み込まなかったという判断ではなく、適切な紐づけの証拠が必要である
確認済み件数を修正対象の全部として示さず、「このインストール済みパッケージのこのバージョンが、この時刻に、このプロセスにマッピングされていた」という説明を検出結果に添えます。
自分のホストでの確認方法
スキャン結果のパッケージを稼働中プロセスが読み込んでいるかを、1つのコンテナで手動確認する手順です。
対象はDebianベースのコンテナで、ホスト側でdpkg-query、Trivy、jqを使えることを前提としています。
以下のコマンドは、Linux上のDocker Engineホストで実行します。 実行前に、プレースホルダーを実際の値に置き換えてください。
コンテナのプロセスを一覧表示し、返されたホスト側PIDを1つ選びます。9
そのPIDについて、実行可能なファイルのマッピングからパスを抽出します。 この式は、固定列を取り除いた後のパス名を保持します。8
sudo awk '$2 ~ /x/ && $5 != 0 && $6 ~ /^\// {
for (i = 0; i < 5; i++) sub(/^[^[:space:]]+[[:space:]]+/, "")
print
}' '/proc/<host-pid>/maps'
出力例:
プロセスのルート経由で、コンテナのdpkgデータベースを照会します。
この例では、データベースに記録されたlibcのパスを使います。
マッピング上の/usr/libのパスが、データベースでは/lib以下の項目に対応する場合があります。1011
sudo dpkg-query --admindir='/proc/<host-pid>/root/var/lib/dpkg' \
-S /lib/x86_64-linux-gnu/libc.so.6
最後に、そのコンテナが使用しているイメージIDをスキャンし、OSパッケージの検出結果を識別する情報をJSONから抽出します。12
trivy image --scanners vuln --format json '<running-image-id>' |
jq -r '.Results[] | select(.Class == "os-pkgs") |
.Vulnerabilities[]? |
[.PkgName, .InstalledVersion, .VulnerabilityID] | @tsv'
特定したパッケージをPkgNameと比較し、続いてコンテナのパッケージメタデータから取得したバージョンをInstalledVersionと照合します。
所有元の照会だけでは、バージョンの確認は完了しません。
これは手動での抜き取り確認です。
コンテナで観測されたパッケージについて結論を出す前に、コンテナ内の各プロセスで収集を繰り返します。
/usr統合によるパスの別名、シンボリックリンクの所有関係、権限、信頼できるバージョン照合については、
procfsによる紐づけの詳しい手順を参照してください。
参考資料
この記事の計測結果は2026年9月12日のものです。
- 公開開発ログに、計測結果とその限界を記録している
- これらは特定のホストとワークロードでの結果であり、対象全体の再現率や、未観測のパッケージが未使用であることの証明ではない
-
保存済みの計測結果
1-root-30-300-p0-r2、9-root-30-300-p0-r1、10-root-30-300-p0-r1のgtb.jsonとcsv_all/gt_b.csvに基づく。表の比較対象数・検出数・見逃し数は、それぞれpopulation・tp・fn。正解データの範囲と集計方法は計測プログラムのREADME(サンプリング計測の公開版)を参照。 ↩
KestreLynxは、稼働中のDocker・KubernetesコンテナのイメージをTrivyで定期的にスキャンし、前回からの変化と、深刻度・CISA KEV・EPSSに基づく優先度をSlackやWebhookへ通知するOSSです。Docker・Compose向けの修正計画と修正PR(実験的)で、更新候補の比較からGitHubへのPR作成まで修正を支援します(既定で無効)。マージ・再ビルド・デプロイは利用者が行います。