
脆弱性管理とは、自社が使っているサーバー・コンテナ・ライブラリ・ネットワーク機器に含まれる既知の脆弱性を継続的に把握し、悪用される可能性と影響の大きさで優先順位を付けて修正や緩和を進める、終わりのない運用プロセスです。年に1回の「脆弱性診断」を受けることとは目的も頻度も異なります。
ここではCVE・CVSS・KEV・EPSSという4つの指標の読み方を押さえたうえで、オープンソースのスキャナーであるTrivyを使い、コンテナイメージ・稼働中のサーバー・IaC・シークレットのスキャンをGitHub Actionsに組み込み、見つかった脆弱性を「どれから、いつまでに直すか」を決めるトリアージ基準と、経営層への報告の形までを扱います。コマンドはUbuntu 24.04とTrivyの執筆時点の版で確認したものです。
この記事でわかること
- CVSSの点数だけで優先順位を決めてはいけない理由と、KEV・EPSSを組み合わせた判断ルール
- Trivyでコンテナイメージ・稼働中サーバー・Terraform・シークレットをスキャンするコマンドとオプションの意味
- SBOM(CycloneDX/SPDX)を生成・保管し、新しいCVEが出た日に「影響があるか」を即答する方法
- GitHub Actionsで「修正可能なCRITICAL/HIGHがあればビルドを止める」ワークフローの書き方
- 修正SLAの決め方、直せないときの受容手順、経営層に見せる指標
脆弱性管理と脆弱性診断の違い
| 観点 | 脆弱性管理 | 脆弱性診断(セキュリティ診断) |
|---|---|---|
| 目的 | 既知の脆弱性(CVE)を資産全体で継続的に減らす | 特定のシステムに未知の欠陥(設計ミス、実装バグ)がないかを専門家が探す |
| 頻度 | 毎日〜毎週、自動 | リリース前や年1回、手動が中心 |
| 対象 | OSパッケージ、ライブラリ、コンテナ、IaC、機器のファームウェア | Webアプリの認可制御、業務ロジック、プラットフォーム設定 |
| 見つかるもの | 「バージョンが古い」に起因する既知の問題 | 「作り込みが悪い」に起因する固有の問題(IDOR、認可漏れなど) |
| 主体 | 自社の運用チーム(ツールで自動化) | 外部の診断会社、または社内のセキュリティチーム |
両者は代替関係にありません。診断で「問題なし」と言われたシステムでも、翌週にライブラリの新しいCVEが公開されれば脆弱になります。逆にスキャナーは、ログイン済みユーザーが他人のIDを指定すれば他人のデータが見える、といった作り込みの欠陥を検出できません。順番としては脆弱性管理を先に回し、自社開発のWebアプリはリリース前に診断を受ける、という組み合わせが費用対効果の面で妥当です。
CVE・CVSS・KEV・EPSSの読み方
CVE(Common Vulnerabilities and Exposures)は脆弱性1件ごとに付く識別子で、CVE-2024-3094 のように「年-連番」の形をとります。番号は「登録された年」であって公開年や発見年ではなく、連番の大小に意味はありません。ベンダーや研究者のアドバイザリ、スキャナーの出力、パッチノートをつなぐ共通キーとして使います。
CVSS(Common Vulnerability Scoring System)は深刻度を0.0〜10.0で表す指標で、執筆時点ではv3.1とv4.0が併存しています。基本スコアはベクター文字列から計算され、たとえば AV:N/AC:L/PR:N/UI:N は「ネットワーク経由で・特別な条件なく・認証なしで・利用者の操作なしに」攻撃できることを意味し、この時点でほぼHigh以上になります。重要なのは、基本スコアが評価するのは「脆弱性そのものの性質」であって、「あなたの環境で実際に悪用されるか」ではないという点です。インターネットに面していないサーバーの9.8と、公開Webサーバーの7.5では、後者の方が急ぎです。
KEV(Known Exploited Vulnerabilities)は米CISAが公開する「実際に悪用が確認された脆弱性」のカタログで、掲載されているものは点数に関係なく最優先です。EPSS(Exploit Prediction Scoring System)はFIRSTが公開する「今後30日以内に悪用される確率」(0〜1)で、毎日更新されます。CVSSが「悪用されたらどれだけ痛いか」、EPSSが「悪用されそうか」、KEVが「もう悪用されている」を表すので、3つを重ねると優先順位が自然に決まります。
| CVSS基本スコア | 深刻度 | 単独での扱い |
|---|---|---|
| 9.0〜10.0 | Critical | 修正版があれば計画を待たずに当てる候補。ただし到達経路の確認が先 |
| 7.0〜8.9 | High | SLA内で修正。KEV掲載やEPSS高なら格上げ |
| 4.0〜6.9 | Medium | 定期的なベースイメージ更新に乗せる |
| 0.1〜3.9 | Low | 棚卸し対象。個別対応はしない |
OSパッケージについてはもう1つ注意があります。Ubuntu、Debian、Red Hatなどのディストリビューションは、NVDのCVSSとは別に自分たちの環境での深刻度を独自に評価しており、Trivyはディストリビューションの評価が存在する場合にそちらを表示します。同じCVEでも nginx:1.27(Debianベース)と nginx:1.27-alpine で深刻度が違って見えるのはこのためで、バグではありません。
資産の把握から始める
スキャンしていない資産の脆弱性は永遠にゼロ件のままです。最初にやるべきは「何を動かしているか」の一覧で、少なくとも次の4種類を洗い出します。稼働中のコンテナイメージ、それを作るリポジトリとDockerfile、物理・仮想サーバーのOS、そしてVPN装置やファイアウォールなどのアプライアンスです。最後のアプライアンスはTrivyでは見えませんが、ランサムウェアの侵入口として最も多いカテゴリなので、ベンダーのアドバイザリ購読を台帳に紐づけて管理します。
# Dockerホストで稼働中のイメージ一覧
docker ps --format '{{.Image}}' | sort -u
# Kubernetesクラスタで稼働中のイメージ一覧(全namespace)
kubectl get pods -A -o jsonpath='{range .items[*]}{range .spec.containers[*]}{.image}{"\n"}{end}{end}' | sort -u一覧には「インターネットに面しているか」「個人情報を扱うか」「担当者は誰か」の3列を必ず付けてください。この3列がないと後のトリアージで期限を決められません。台帳の作り方そのものはIT資産管理の始め方で扱っており、コンテナイメージも「資産」として同じ台帳に載せるのが運用しやすい形です。
Trivyのインストールとコンテナイメージのスキャン
Trivyは単一バイナリのスキャナーで、イメージを展開して /etc/os-release からOSを判定し、dpkg・apk・rpmのパッケージデータベースと、package-lock.json、Gemfile.lock、poetry.lock、Goバイナリに埋め込まれたモジュール情報などの言語依存関係を読み取り、脆弱性データベース(初回実行時にダウンロードされ、以後は自動更新)と突き合わせます。エージェントは不要で、スキャン対象に何もインストールしません。
# 執筆時点の公式aptリポジトリでの手順。最新は公式ドキュメントで確認
sudo apt-get install -y wget gnupg
wget -qO - https://aquasecurity.github.io/trivy-repo/deb/public.key | gpg --dearmor | sudo tee /usr/share/keyrings/trivy.gpg > /dev/null
echo "deb [signed-by=/usr/share/keyrings/trivy.gpg] https://aquasecurity.github.io/trivy-repo/deb generic main" | sudo tee /etc/apt/sources.list.d/trivy.list
sudo apt-get update && sudo apt-get install -y trivy
trivy --version
# Version: 0.xx.x
# Vulnerability DB:
# Version: 2
# UpdatedAt: 2026-09-22 06:12:41 +0000 UTC ← DBの鮮度。1日以上古ければ更新を疑う# 基本のスキャン
trivy image nginx:1.27
# 実務で使う形:修正版が存在するHIGH以上だけを表示し、見つかれば終了コード1
trivy image --severity HIGH,CRITICAL --ignore-unfixed --exit-code 1 nginx:1.27
# 後で集計するためにJSONで保存
trivy image --format json --output nginx-1.27.json nginx:1.27nginx:1.27 (debian 12.7)
========================
Total: 9 (HIGH: 8, CRITICAL: 1)
Library Vulnerability Severity Status Installed Version Fixed Version Title
libexpat1 CVE-2024-45491 CRITICAL fixed 2.5.0-1 2.5.0-1+deb12u1 expat: integer overflow in dtdCopy ...
zlib1g CVE-2023-45853 CRITICAL affected 1:1.2.13.dfsg-1 (なし) zlib: integer overflow in zipOpenNewFileInZip4_64 ...--ignore-unfixed は「ディストリビューションが修正版をまだ出していないもの」を除外します。修正版がない脆弱性は、今日あなたにできることが(パッケージの削除か緩和策しか)ないので、CIを止める根拠にはしません。ただし無視するのではなく、別途 --ignore-unfixed なしの結果を週次で保存し、修正版が出た週に拾えるようにします。上の例で Status が affected の行がそれで、will_not_fix(ディストリビューションが修正しないと決めたもの)とは区別されます。
OSパッケージ・IaC・シークレットのスキャン
Trivyはイメージ以外に、稼働中サーバーのルートファイルシステム、リポジトリのディレクトリ、IaCの設定ファイルも同じデータベースで評価できます。稼働中サーバーはコンテナ化していないVMやミニPCの棚卸しに使え、apt list --upgradable より「どのCVEに該当するか」まで分かる点が違います。
# 稼働中のサーバー自身(OSパッケージ)。/proc や /sys を読まないよう rootfs モードを使う
sudo trivy rootfs --severity HIGH,CRITICAL --ignore-unfixed /
# リポジトリ:依存関係の脆弱性 + 設定ミス + シークレットを一度に
trivy fs --scanners vuln,misconfig,secret ./my-app
# Terraform / Kubernetesマニフェスト / Dockerfile の設定ミスだけ
trivy config ./inframisconfig スキャナーは、Terraformの「S3バケットの公開設定」「セキュリティグループの 0.0.0.0/0 からのSSH許可」、Kubernetesマニフェストの「特権コンテナ」「リソース制限なし」、Dockerfileの「USER未指定でrootのまま実行」といった、脆弱性ではなく設定の問題を検出します。secret スキャナーはAWSアクセスキーやGitHubトークン、秘密鍵(-----BEGIN ... PRIVATE KEY-----)のパターンをファイル内容から探し、aws-access-key-id や private-key のようなルールIDで報告します。どちらも「コミットする前に手元で回す」使い方が最も効き、CIで見つかった時点ではそのシークレットは履歴に残っているため、失効と再発行が必要になります。
SBOMの生成と、それを保管しておく理由
SBOM(Software Bill of Materials)は「このソフトウェアに何が入っているか」の部品表で、CycloneDXとSPDXという2つの標準形式があります。Trivyはどちらの形式でも生成でき、生成したSBOMをイメージの代わりにスキャンできます。価値が出るのは新しいCVEが公開された日で、リリースごとのSBOMを保管してあれば、すべてのイメージを取得し直さなくても「どのバージョンのどのサービスが該当するか」を数分で答えられます。
# CycloneDX形式(依存関係グラフを含む。取引先への提出はこちらが多い)
trivy image --format cyclonedx --output myapp-1.4.2.cdx.json myapp:1.4.2
# SPDX形式(ライセンス情報の連携に向く)
trivy image --format spdx-json --output myapp-1.4.2.spdx.json myapp:1.4.2
# 保管しておいたSBOMを、最新の脆弱性DBで再評価する(イメージの取得は不要)
trivy sbom --severity HIGH,CRITICAL myapp-1.4.2.cdx.json形式の選び方は、取引先や顧客から要求があればその形式、なければCycloneDXで統一して構いません。SBOMはリリース成果物の1つとしてイメージと同じタグで保管し、少なくとも本番で動いているバージョンの分は消さないようにします。
CI(GitHub Actions)への組み込み
CIに入れる目的は2つあり、「新たに持ち込まれる脆弱性を止める」ことと「既に動いているものを定期的に見直す」ことです。前者はプルリクエストごとのスキャンで、修正版のあるCRITICAL/HIGHがあればビルドを失敗させます。後者はスケジュール実行で、コードに変更がなくても週に1回はmainブランチのイメージを評価します。
name: container-scan
on:
pull_request:
push:
branches: [main]
schedule:
- cron: '0 21 * * 0' # 毎週月曜 06:00 JST(UTCでは日曜 21:00)
permissions:
contents: read
security-events: write # SARIF を Security タブに送る場合のみ必要
jobs:
scan:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Build image
run: docker build -t myapp:${{ github.sha }} .
# アクションのバージョンは執筆時点の例。公式リポジトリのタグを確認し、可能ならコミットSHAで固定する
- name: Scan and fail on fixable CRITICAL/HIGH
uses: aquasecurity/[email protected]
with:
image-ref: myapp:${{ github.sha }}
format: table
severity: CRITICAL,HIGH
ignore-unfixed: true
exit-code: '1'
- name: Produce SARIF (all severities, for the Security tab)
if: always()
uses: aquasecurity/[email protected]
with:
image-ref: myapp:${{ github.sha }}
format: sarif
output: trivy.sarif
ignore-unfixed: true
- name: Upload SARIF
if: always()
uses: github/codeql-action/upload-sarif@v3
with:
sarif_file: trivy.sarif「意図的に見送る」脆弱性は .trivyignore.yaml に理由と期限付きで書きます。期限を過ぎると再び検出されるため、「一度無視したら永遠に無視」になりません。無視した項目は --show-suppressed を付けると出力に含まれるので、月次の棚卸しで確認できます。
vulnerabilities:
- id: CVE-2025-0000 # (例)対象のCVE
statement: "該当する機能(zipの書き込み)を使用していない。次回のベースイメージ更新で解消予定"
expired_at: 2026-12-31 # この日を過ぎると再び検出されるワークフローの基本構文やシークレットの扱いはGitHub Actionsで始めるCI/CDで扱っています。脆弱性データベースはコンテナレジストリ(GHCR)から取得されるため、多数のジョブが同時に走るとレート制限に当たることがあります。~/.cache/trivy を actions/cache で保持するか、環境変数 TRIVY_DB_REPOSITORY で公式のミラー(執筆時点では public.ecr.aws/aquasecurity/trivy-db など)を指定すると安定します。
トリアージ基準と修正SLA
スキャン結果は放置すると数百件に膨らみ、「全部直す」は最初の週で破綻します。必要なのは、誰が見ても同じ結論になる判断ルールです。次の表は目安で、日数は自社の変更管理の速さに合わせて調整してください。ただし「KEV掲載かつインターネットに面している」を最上位に置くことだけは変えないでください。
| 条件(上から順に判定) | 期限の目安 | 対応 |
|---|---|---|
| KEVに掲載、かつ資産がインターネットに面している | 72時間以内 | 計画外の緊急変更として適用。当てられないなら一時的に公開停止やWAFで遮断 |
| KEVに掲載(内部のみ)、またはEPSSが高い(例: 0.1以上)でCVSS High以上 | 7日以内 | 次の定例リリースを待たず個別に更新 |
| CVSS Critical/High で修正版あり | 30日以内 | ベースイメージ更新や依存関係の更新に乗せる |
| CVSS Medium で修正版あり | 90日以内 | 月次のベースイメージ更新で解消 |
| 修正版なし(affected) | 期限なし・週次で再評価 | 不要なパッケージなら削除。使わない機能なら .trivyignore.yaml に理由と期限を書いて受容 |
| Low | 対応しない | 件数の推移だけ記録 |
修正の手段で最も効くのはベースイメージの選び直しです。python:3.12(フルのDebian)を python:3.12-slim に替えるだけで、使っていないパッケージ由来の検出が大きく減ります。さらにアプリが必要とするランタイム以外を含まないディストロレス系のイメージにすれば、OSパッケージ由来の脆弱性はほぼ消えますが、シェルがないためデバッグ手順が変わるので、運用チームが慣れてから移行してください。「件数を減らすために放置する」より「入っているものを減らす」方が、監査でも説明しやすくなります。
Ubuntu/Debianはセキュリティ修正を上流の新バージョンにせず、同じバージョン番号のまま修正だけを取り込む「バックポート」で配布します。Trivyはディストリビューションのバージョン表記(2.5.0-1+deb12u1 のような形)を理解しているので誤検知しませんが、CVSSデータベースを直接引く自作スクリプトや一部のツールは「2.5.0だから脆弱」と誤判定します。逆に、pipやnpmで個別に入れたライブラリにはこのバックポートがないので、修正は必ずバージョンを上げる形になります。同じライブラリを使うなら、ディストリビューションのパッケージの方が修正の追従が楽です。
確認方法:スキャンが正しく回っているかを見る
# 1. DBが新しいか(UpdatedAt が24時間以内)
trivy --version | grep UpdatedAt
# 2. 終了コードがゲートとして機能しているか(0=検出なし、1=検出あり)
trivy image --severity CRITICAL --ignore-unfixed --exit-code 1 myapp:1.4.2 > /dev/null; echo "exit=$?"
# 3. 無視設定が意図通りか(抑止された項目を表示)
trivy image --show-suppressed --severity HIGH,CRITICAL myapp:1.4.2
# 4. 言語依存関係が見えているか("Node.js" "Python" などのターゲットが表に出るか)
trivy image --list-all-pkgs --format json myapp:1.4.2 | python3 -c 'import json,sys; [print(r["Type"], len(r.get("Packages",[]))) for r in json.load(sys.stdin)["Results"]]'4番目の確認で debian のような OS の行しか出ない場合、アプリの依存関係がスキャンされていません。ロックファイルがイメージに含まれていない(node_modules だけコピーしている)、あるいはビルド済みバイナリにメタデータがないケースで、この状態のままでは「脆弱性ゼロ」が「見えていないだけ」になります。
つまずきポイント
「failed to download vulnerability DB」「TOOMANYREQUESTS」
脆弱性DBの取得元であるコンテナレジストリのレート制限に当たっています。CIで並列にジョブを走らせたときに起きやすく、対処は ~/.cache/trivy のキャッシュを保持する、TRIVY_DB_REPOSITORY でミラーを指定する、あるいは社内に1台だけDBをダウンロードするジョブを置いて他は --skip-db-update でキャッシュを使う、の3つです。プロキシ配下では HTTPS_PROXY の設定漏れでも同じエラーになります。
「OS is not detected」と出て、OSパッケージが評価されない
scratch ベースや独自ビルドのイメージで /etc/os-release がないと、OSパッケージのスキャンは行われません。ディストロレス系はTrivyが専用の判定に対応しているので通常は問題ありませんが、自前で最小化したイメージでは言語依存関係だけが対象になります。この場合はビルド時に生成したSBOMをスキャンする形に切り替えます。
検出が数百件あって、どこから手を付けてよいかわからない
まず --ignore-unfixed --severity HIGH,CRITICAL で絞り、それでも多ければベースイメージを slim 系に替えてから再スキャンします。それだけで大半が消えるのが普通です。残ったものを上のトリアージ表に当てはめ、.trivyignore.yaml に期限付きで受容を書く。最初の月にやるのはこれだけで十分で、Mediumやunfixedは件数の推移を追うだけにします。
.trivyignore が効かない
ファイルはコマンドを実行したカレントディレクトリから読まれます。CIではチェックアウト先で実行されているか、--ignorefile で明示しているかを確認してください。YAML形式(.trivyignore.yaml)とプレーンテキスト形式(.trivyignore)は別ファイルで、前者にのみ expired_at や statement を書けます。効いているかは --show-suppressed で判定します。
レポートと経営層への説明
経営層に「CVEが312件あります」と伝えても判断はできません。見せるべき数字は4つに絞ります。(1)台帳にある資産のうちスキャンできている割合、(2)KEV掲載かつインターネットに面した資産の未対応件数(この数字だけは常にゼロを目指す)、(3)SLA内に修正できた割合、(4)Critical/Highの件数の月次推移です。1つ目が低いうちは他の数字に意味がないので、最初の四半期は「スキャン率を上げる」を目標にします。
予算や工数の依頼は「脆弱性が多いから」ではなく「ベースイメージを統一すれば毎月の対応が減る」「アプライアンスの更新を保守契約に含めれば侵入口が塞がる」という、作業量が減る話として出す方が通ります。脆弱性管理が最終的に何を防いでいるかはランサムウェア対策の整理と併せて説明すると、抽象的な「リスク」が「事業停止日数」に置き換わります。
よくある質問
CVSSが高ければ必ず対応すべき?
必ずではありません。CVSS基本スコアは脆弱性そのものの性質を表し、あなたの環境で到達できるか、悪用が確認されているかは含みません。KEV掲載やEPSSの高さ、インターネットに面しているかを組み合わせて優先順位を決め、修正版がないものは受容の理由と期限を記録します。ただし「CVSS Criticalで修正版あり」を30日以上放置する状態は、監査でも説明が難しくなります。
脆弱性診断サービスとの違いは?
診断サービスは専門家が特定のシステムの作り込み(認可漏れ、業務ロジックの欠陥)を探す一回性の作業で、スキャナーでは見つからない問題を対象にします。脆弱性管理は既知のCVEを資産全体で継続的に減らす運用です。両方が必要で、自社開発のWebアプリはリリース前に診断を、それ以外はスキャナーによる継続管理を、という分担が一般的です。
SBOMはなぜ必要?
新しい脆弱性が公開されたとき、「自社のどのシステムが該当するか」を、すべてを再スキャンせずに答えるためです。リリースごとのSBOMを保管していれば、CVEの対象パッケージ名でSBOMを検索するだけで影響範囲が分かります。取引先から提出を求められる場面も増えているので、CycloneDX形式で自動生成しておくと後から困りません。
まとめ
脆弱性管理は「資産の一覧→自動スキャン→判断ルールに沿ったトリアージ→修正と受容の記録」を回し続ける運用で、Trivyはこのうちスキャンの部分を無償で担えます。最初の1か月は、台帳を作り、--ignore-unfixed --severity HIGH,CRITICAL でCIにゲートを置き、ベースイメージをslim系に揃えるところまでで十分です。優先順位はCVSSだけで決めず、KEVとインターネット露出を最上位に置く。修正できないものは .trivyignore.yaml に理由と期限を残す。この2つを守れば、件数に振り回されない運用になります。