GitHub ActionsでプルリクエストされたDockerfileのみを対象に脆弱性スキャンを行う

GitHub ActionsでプルリクエストされたDockerfileのみを対象に脆弱性スキャンを行う

Clock Icon2025.05.12

お疲れさまです。とーちです。

複数のDockerfileを単一のGitHubリポジトリに保管しているプロジェクトで、GitHub Actionsを使ったCI/CDで変更されたDockerfileのみを対象に脆弱性スキャンを行いたいというケースに遭遇しました。

今回の記事では、GitHubでDockerfileをプルリクエストしたときに、対象のDockerfileの脆弱性スキャンをGitHub Actionsで自動で行い、問題がなければECRにpushする仕組みの構築方法について紹介します。

前提条件

まずGitHubリポジトリのディレクトリ構成ですが、以下のような形になっていると想定してください。

.
├── .github
│   └── workflows
│       └── image-pr-scan.yaml
└── images
    └── productA
        ├── imageA
        │   └── Dockerfile
        └── imageB
            └── Dockerfile

imagesディレクトリには複数のプロダクト用のコンテナイメージビルドのためのDockerfileが格納されています。このリポジトリにDockerfileをプルリクエストすると、対象のDockerfileをビルドして脆弱性スキャンを行い、問題がなければECRにpushする仕組みを構築したいというのが今回の目的です。

GitHub Actionsワークフローの実装

早速ですが、上記を実現するためのGitHub Actionsワークフローファイルを紹介します。

GitHub Actionsワークフローファイル
name: Image PR Sysdig Scan

on:
  pull_request:
    paths:
      - 'images/**/Dockerfile'
    branches:
      - main
      - dev

jobs:
  sysdig_scan:
    runs-on: ubuntu-22.04
    env:
      SYSDIG_SECURE_ENDPOINT: "https://us2.app.sysdig.com"
      AWS_REGION: ap-northeast-1
      ECR_REPOSITORY_NAME: <ECRリポジトリ名>
      ASSUME_ROLE: arn:aws:iam::${{ secrets.AWS_ACCOUNT_ID }}:role/<IAMロール名>
      SYSDIG_POLICY: sysdig-best-practices
    permissions:
      id-token: write
      contents: read
      pull-requests: read
      actions: read
    steps:
      - name: Checkout
        uses: actions/checkout@v4
        
      - name: Get changed Dockerfiles
        uses: dorny/paths-filter@v2
        id: dockerfile_changes
        with:
          filters: |
            dockerfiles:
              - 'images/**/Dockerfile'
          list-files: shell  # shellフォーマットに変更
      
      - name: Check for multiple Dockerfile changes
        id: check-dockerfile
        run: |
          # shellフォーマットで出力されたファイルリスト
          FILES=(${{ steps.dockerfile_changes.outputs.dockerfiles_files }})
          FILE_COUNT=${#FILES[@]}
          
          echo "Changed Dockerfiles: ${FILES[@]}"
          echo "Found $FILE_COUNT changed Dockerfile(s)"
          
          if [ "$FILE_COUNT" -gt 1 ]; then
            echo "::error::Multiple Dockerfiles modified in this PR. Please limit changes to a single Dockerfile per PR."
            echo "Modified Dockerfiles:"
            for file in "${FILES[@]}"; do
              echo "  - $file"
            done
            exit 1
          elif [ "$FILE_COUNT" -eq 1 ]; then
            DOCKERFILE=${FILES[0]}
            echo "dockerfile=$DOCKERFILE" >> $GITHUB_OUTPUT
            echo "Found single Dockerfile change: $DOCKERFILE"
          else
            echo "No Dockerfile changes detected."
            echo "dockerfile=" >> $GITHUB_OUTPUT
          fi

      - name: Configure AWS credentials
        if: steps.check-dockerfile.outputs.dockerfile != ''
        uses: aws-actions/configure-aws-credentials@v4
        with:
          role-to-assume: ${{ env.ASSUME_ROLE }}
          aws-region: ${{ env.AWS_REGION }}
          
      - name: Login to Amazon ECR
        if: steps.check-dockerfile.outputs.dockerfile != ''
        id: login-ecr
        uses: aws-actions/amazon-ecr-login@v2
        
      - name: Setup cache
        if: steps.check-dockerfile.outputs.dockerfile != ''
        uses: actions/cache@v4
        with:
          path: cache
          key: ${{ runner.os }}-cache-${{ hashFiles('**/sysdig-cli-scanner', '**/latest_version.txt', '**/db/main.db.meta.json', '**/scanner-cache/inlineScannerCache.db') }}
          restore-keys: ${{ runner.os }}-cache-
          
      - name: Download sysdig-cli-scanner if needed
        if: steps.check-dockerfile.outputs.dockerfile != ''
        run:  |
          curl -sLO https://download.sysdig.com/scanning/sysdig-cli-scanner/latest_version.txt
          mkdir -p ${GITHUB_WORKSPACE}/cache/db/
          if [ ! -f ${GITHUB_WORKSPACE}/cache/latest_version.txt ] || [ $(cat ./latest_version.txt) != $(cat ${GITHUB_WORKSPACE}/cache/latest_version.txt) ]; then
            cp ./latest_version.txt ${GITHUB_WORKSPACE}/cache/latest_version.txt
            curl -sL -o ${GITHUB_WORKSPACE}/cache/sysdig-cli-scanner "https://download.sysdig.com/scanning/bin/sysdig-cli-scanner/$(cat ${GITHUB_WORKSPACE}/cache/latest_version.txt)/linux/amd64/sysdig-cli-scanner"
            chmod +x ${GITHUB_WORKSPACE}/cache/sysdig-cli-scanner
          else
            echo "sysdig-cli-scanner latest version already downloaded"
          fi

      - name: Build Docker image
        if: steps.check-dockerfile.outputs.dockerfile != ''
        id: build
        env:
          ECR_REGISTRY: ${{ steps.login-ecr.outputs.registry }}
          ECR_REPOSITORY: ${{ env.ECR_REPOSITORY_NAME }}
          DOCKERFILE: ${{ steps.check-dockerfile.outputs.dockerfile }}
        run: |
          dir=$(dirname "$DOCKERFILE")
          imagename=$(basename "$dir")
          tag=pr-${{ github.event.pull_request.number }}
          full_image=$ECR_REGISTRY/$ECR_REPOSITORY:$imagename-$tag
          
          echo "Building image from $DOCKERFILE"
          docker build -t $full_image -f $DOCKERFILE $dir
          echo "image=$full_image" >> $GITHUB_OUTPUT

      - name: Scan Docker image
        if: steps.build.outcome == 'success'
        id: scan
        env:
          SECURE_API_TOKEN: ${{ secrets.SYSDIG_SECURE_API_TOKEN }}
          IMAGE: ${{ steps.build.outputs.image }}
        run: |
          echo "Scanning image with Sysdig CLI Scanner"
          ${GITHUB_WORKSPACE}/cache/sysdig-cli-scanner \
            --apiurl ${SYSDIG_SECURE_ENDPOINT} \
            $IMAGE \
            --console-log \
            --dbpath=${GITHUB_WORKSPACE}/cache/db/ \
            --policy ${SYSDIG_POLICY} \
            --cachepath=${GITHUB_WORKSPACE}/cache/scanner-cache/

      - name: Push Docker image
        if: steps.scan.outcome == 'success'
        env:
          IMAGE: ${{ steps.build.outputs.image }}
        run: |
          echo "Scan passed successfully. Pushing image to ECR..."
          docker push $IMAGE

ワークフローの重要ポイントを解説

各ポイントを詳しく説明していきます。

トリガー条件の設定

on:
  pull_request:
    paths:
      - 'images/**/Dockerfile'
    branches:
      - main
      - dev

まず上記の部分です。この設定では、プルリクエストが発生し、かつ、以下の条件がすべて満たされた場合にのみワークフローが実行されます

  1. images ディレクトリ内の任意の場所にある Dockerfile が変更された場合 (paths)
  2. プルリクエストの対象ブランチが main または dev である場合 (branches)

imagesディレクトリに絞って発火させることで不要なワークフロー実行を防いでいます。

変更されたDockerfileの検出

      - name: Get changed Dockerfiles
        uses: dorny/paths-filter@v2
        id: dockerfile_changes
        with:
          filters: |
            dockerfiles:
              - 'images/**/Dockerfile'
          list-files: shell  # shellフォーマットに変更

肝となる変更ファイルの抽出処理です。ここではdorny/paths-filter というActionを使っています。

有名なActionだと思うのでご存知の方も多いのではと思いますが、プルリクエスト等で変更されたファイルが何かをさっと洗い出せる便利なActionになっています。

filters:フィルター名:フィルター条件 という形で記載するようになっており、上記の場合は変更ファイルのパスが images/**/Dockerfile と一致するとdockerfilesフィルターにマッチしたということになります。このフィルター名を使って、後続ステップで一致したファイル名等を参照できます。

また「dorny/paths-filter」を使う場合、permissionsにも注意が必要です。「dorny/paths-filter」では、プルリクエストで変更されたファイルのリストを取得するので、プルリクエストの情報を読取る pull-requests: read の権限が必要になります。この権限が不足していると Resource not accessible by integrationというエラーが出ます。

list-files:はフィルターに一致するファイルのリスト表示を有効にするオプションです。jsonやcsv、shellなどの出力形式を選択でき、shellの場合はLinuxシェルでコマンドライン引数リストとして使用可能なスペース区切りのリストで、一致したファイルのリストを参照できます。

単一のDockerfile変更の確認

      - name: Check for multiple Dockerfile changes
        id: check-dockerfile
        run: |
          # shellフォーマットで出力されたファイルリスト
          FILES=(${{ steps.dockerfile_changes.outputs.dockerfiles_files }})
          FILE_COUNT=${#FILES[@]}
          
          echo "Changed Dockerfiles: ${FILES[@]}"
          echo "Found $FILE_COUNT changed Dockerfile(s)"
          
          if [ "$FILE_COUNT" -gt 1 ]; then
            echo "::error::Multiple Dockerfiles modified in this PR. Please limit changes to a single Dockerfile per PR."
            echo "Modified Dockerfiles:"
            for file in "${FILES[@]}"; do
              echo "  - $file"
            done
            exit 1
          elif [ "$FILE_COUNT" -eq 1 ]; then
            DOCKERFILE=${FILES[0]}
            echo "dockerfile=$DOCKERFILE" >> $GITHUB_OUTPUT
            echo "Found single Dockerfile change: $DOCKERFILE"
          else
            echo "No Dockerfile changes detected."
            echo "dockerfile=" >> $GITHUB_OUTPUT
          fi

ここでは、前ステップのdockerfile_changes の出力を使って単一のDockerfileのみが変更されていることを確認しています。複数のDockerfileの変更を許可すると、Dockerfileごとに脆弱性スキャンの成功・失敗が別れたときにどうするかといった問題が出ると思ったので、今回はこのような実装にしました。

${{ steps.dockerfile_changes.outputs.dockerfiles_files }} で「dorny/paths-filter」のdockerfiles フィルターに一致したファイルの一覧が取得できます。更にその結果を()で囲むことでスペース区切りのそれぞれの値をBashで扱える配列として変数に格納することができます。echo ${FILES[0]} と実行すると配列の0番目の値が参照できる形です。

FILES[@] で配列FILESの全要素を参照でき、${#FILES[@]} で配列の要素数を取得できます。要素数が1よりも多ければ複数のDockerfileが変更されているということなので、異常終了するというわけです。

その他の処理については特別むずかしいところはないと思います。なおSysdig CLIスキャナーによる脆弱性スキャン処理は以下のURL記載のコードを参考にさせてもらいました。また、Sysdig CLIスキャナーでは、--policyオプションを使用することで、Sysdig Secure上に定義された脆弱性スキャンポリシーに基づいて、対象イメージが設定された基準に合格しているかどうかを判定できます。

https://sysdig.jp/blog/image-scanning-github-actions/

実際に試してみる

それでは実際にプルリクエストを出して脆弱性スキャンが行われることを確認したいと思います。

事前準備

まずは、AWS上にECRとGitHub Actions用のIAMロールを作成します。作り方は色々な記事で説明されていると思うので割愛します。

次にGitHubリポジトリの設定をしていきます。リポジトリのSecretsにAWSアカウントIDとSysdig CLI Scannerを使用するためのSysdig SecureAPIトークンを登録します。変数名はそれぞれ以下の通りです。

  • SYSDIG_SECURE_API_TOKEN:Sysdig SecureAPIトークン
  • AWS_ACCOUNT_ID:AWSアカウントID

alt text

なお、Sysdig SecureAPIトークンの取得方法についてはSysdigの公式ドキュメントに詳しい手順が記載されていますので、そちらをご参照ください。

プルリクエストの作成

この状態で、以下のようなDockerfileをimages/productA/imageB/Dockerfile に作成します。単純なnginxのlatestイメージです。

FROM nginx:latest

この変更をdevブランチにpushし、プルリクエストとしてmainブランチへのマージを試みます。

ワークフローの実行結果

プルリクエストが作成されるとGitHub Actionsワークフローが起動しました。以下のように変更ファイルがちゃんと認識されています。

alt text

以下のように指定したポリシーで脆弱性スキャン結果が判定されています。今回は合格(PASSED)しました。

vscode-paste-1747061792539-xack0j5ryz.png

最終的にECRにpushされているのが確認できました。もちろん脆弱性スキャン結果の判定がNGだった場合にはそこでワークフロー処理が止まるので、Pushはされません。

alt text

まとめ

以上、GitHubで任意のDockerfileをプルリクエストしたときに対象のDockerfileの脆弱性スキャンをGitHub Actionsで自動で行ってみました。

この記事がどなたかの参考になれば幸いです。

以上、とーちでした。

Share this article

facebook logohatena logotwitter logo

© Classmethod, Inc. All rights reserved.