【ITニュース解説】How to Scan Container Images for Vulnerabilities in GitHub Actions
2026年10月27日に「Dev.to」が公開したITニュース「How to Scan Container Images for Vulnerabilities in GitHub Actions」について初心者にもわかりやすく解説しています。
ITニュース概要
コンテナイメージの脆弱性をGitHub Actionsで自動スキャンし、安全にリリースする方法を解説。Trivyを使い、重大な脆弱性があればビルドを停止、PRで結果を表示する。早期発見とリスク管理で安全な開発を支援する。
ITニュース解説
コンテナイメージは現代のソフトウェア開発に不可欠な技術であり、アプリケーションを構成するすべての要素をまとめて提供する。しかし、この利便性の一方で、セキュリティ上のリスクも内包している。コンテナイメージは「ベースイメージ」と呼ばれる土台の上に構築され、このベースイメージには数百ものソフトウェアパッケージが含まれている。これらのパッケージの中には、すでに公開されている脆弱性(CVE)を抱えているものが存在する可能性がある。自分たちの書いたコードに問題がなくても、土台に脆弱性があれば、アプリケーション全体が危険にさらされるのだ。
このような既知の脆弱性は、自動的なスキャンを開発パイプライン(CI/CDパイプライン)に組み込んでいないと、開発者が気づかないうちに本番環境にデプロイされてしまう。その結果、後になってセキュリティインシデントが発生したり、外部のセキュリティ監査で指摘されたりして、問題が発覚するという事態になりかねない。そのため、事前に脆弱性を検出し、対処する仕組みを導入することが強く推奨される。
コンテナイメージの脆弱性スキャンツールには、Trivy (Aquasec)、Grype (Anchore)、Docker Scoutなどの無料オプションがある。これらはGitHub Actionsで利用可能だ。特にTrivyは、脆弱性情報のカバー範囲の広さ、更新頻度の速さ、そしてGoモジュール、pip、npm、CargoといったOS以外の言語固有のパッケージマニフェストもサポートしている点で優れている。Trivyは具体的に、Alpine、Debian、Red Hat、WolfiなどのOSパッケージ、requirements.txtやgo.sumといった言語固有のロックファイル、DockerfileやHelmチャートの誤った設定、さらには誤ってイメージレイヤーに組み込まれた機密情報(シークレット)などもスキャンできる。ローカルでgo.sumのような言語のマニフェストファイルをスキャンするだけでは、ベースイメージによってインストールされたパッケージの脆弱性を見落とす可能性があるが、イメージ全体をスキャンすることでそれらも捕捉できる点が重要だ。
GitHub ActionsにTrivyを組み込むには、リポジトリの.github/workflowsディレクトリにscan.ymlファイルを作成する。このワークフローは、mainブランチへのプッシュ時やプルリクエスト時に実行されるように設定する。ジョブはubuntu-latest環境で実行され、まずactions/checkout@v4でコードをチェックアウトする。次に、docker build -t app:${{ github.sha }} .コマンドで、現在のコミットハッシュをタグとして持つコンテナイメージをビルドする。
Trivyはスキャン実行時に脆弱性データベースをダウンロードするが、これは約35MBと大きい。毎回ダウンロードするとジョブの実行時間が大幅に増加し、AquasecのCDNの可用性にも依存してしまう。これを避けるため、actions/cache@v4を使ってTrivyの脆弱性データベースをキャッシュすることが推奨される。これにより、2回目以降の実行ではデータベースのダウンロードがスキップされ、高速化される。
次に、aquasecurity/trivy-action@0.28.0アクションでTrivyを実行する。image-ref: app:${{ github.sha }}でビルドしたイメージを指定し、format: tableで見やすい表形式で結果を出力する。ここで非常に重要なのがexit-code: "1"の設定である。この設定により、Trivyが指定された深刻度(CRITICAL, HIGHなど)の脆弱性を検出した場合、GitHub Actionsのジョブが強制的に失敗する。スキャン結果が報告されるだけでビルドが停止しない場合、そのレポートはほとんど読まれずに放置されることが多い。ビルドを失敗させることで、開発チームは脆弱性への対応を余儀なくされ、スキャンが実用的なものとなる。また、ignore-unfixed: trueも重要だ。これは、パッケージのメンテナーがまだ修正パッチをリリースしていない脆弱性を無視するためのもので、これがないと修正できない大量の警告が表示され、チームがスキャン結果全体を無視する習慣をつけてしまう可能性がある。severity: CRITICAL,HIGHは、スキャン対象とする脆弱性の深刻度を「CRITICAL(致命的)」と「HIGH(高)」に限定する設定である。
プルリクエストのレビュープロセスを支援するためには、SARIF(Static Analysis Results Interchange Format)形式の出力が非常に有効だ。SARIF形式で出力された結果は、プルリクエストの変更箇所に直接アノテーション(注釈)として表示され、レビュアーは問題のあるパッケージやバージョンを正確に特定できる。これには、Trivyアクションを再度実行し、format: sarifとoutput: trivy-results.sarifを指定する。その後、github/codeql-action/upload-sarif@v3アクションを使って、生成されたSARIFファイルをGitHubリポジトリにアップロードする。このアップロードステップにはif: always()という条件を追加することが不可欠である。Trivyが脆弱性を検出し、exit-code: "1"によって前のステップが失敗した場合、通常であれば後続のステップはスキップされる。if: always()がなければ、SARIFファイルはアップロードされず、プルリクエストにアノテーションが表示されなくなり、スキャンを実行した目的が果たせなくなる。SARIFによってアップロードされた脆弱性情報は、リポジトリの「Security」タブにある「Code scanning alerts」に表示され、各エントリは問題のあるパッケージのバージョン、修正済みバージョン、そしてCVEの詳細ページへのリンクを提供する。
すべての脆弱性がすぐに修正できるわけではない。アップストリームのパッケージには修正サイクルがあり、ベースイメージの更新が遅れることや、特定の脆弱性がアプリケーションの実際には使用しないコードパスにのみ影響する場合もある。このような「許容されるリスク」については、.trivyignoreファイルを使って特定のCVEを明示的に抑制できる。このファイルには、単にCVE IDを記述するだけでなく、その脆弱性が抑制される理由、トリアージを行った日付、そしていつまでに再評価するかという「レビュー期限」などを具体的に記述することが非常に重要だ。数ヶ月後に誰かがその脆弱性がなぜ抑制されているのかを尋ねたとき、単に「承認済み」とだけ書かれていても、その理由が現在も有効であるかどうかの判断材料がないためである。ベースイメージの更新や主要な依存関係のバージョンアップ時には、.trivyignoreのエントリを定期的に見直し、必要に応じて更新することが良い運用プラクティスとなる。レビュー期限を強制するCIチェックを別途設けることも可能だ。
より高度な制御が必要なチーム、例えばCRITICALな脆弱性のみでビルドをブロックし、HIGHな脆弱性はログに記録するもののビルドは失敗させないといった要件がある場合、TrivyのJSON出力を直接パースすることが可能である。Trivyアクションのステップでformat: jsonとoutput: trivy-results.jsonを指定してJSON形式の結果を出力し、そのJSONファイルをPythonスクリプトなどで読み込んで処理する。提供されたPythonスクリプトの例では、CRITICALとHIGHの脆弱性を抽出し、CRITICALが見つかった場合にビルドを失敗させる(終了コード1を返す)というロジックが示されている。このようなスクリプトを拡張することで、パッケージ名でフィルタリングしたり、.trivyignoreに既に記載されているCVEをスキップしたり、HIGHな脆弱性が見つかった際にSlack通知を送信して後続のチケット発行を促したりと、柔軟な対応が可能になる。
要約すると、自動化されたコンテナイメージスキャンは、既知の脆弱性が誰もチェックしないままデプロイされてしまうという、コンテナセキュリティにおける最も一般的な失敗を防ぐ有効な手段である。GitHub ActionsとTrivyを使ったここで紹介した設定は、短時間で導入でき、致命的な脆弱性が見つかった場合にはビルドを強制的に失敗させ、プルリクエストにはインラインで脆弱性情報を表示し、許容されるリスクを適切に管理するプロセスを提供する。多くのチームが陥る落とし穴は、「スキャンを導入した」ことに満足し、exit-code: "1"を設定しないことである。デプロイをブロックしないスキャンは、結局のところ誰にも読まれないレポートを生み出すだけだ。スキャンを効果的に機能させる唯一の方法は、ビルドを壊すことである。そうすることで、脆弱性への対応が開発プロセスに組み込まれ、セキュリティが時間の経過とともに維持される。