
GitHub ActionsでプルリクエストされたDockerfileのみを対象に脆弱性スキャンを行う
お疲れさまです。とーちです。
複数の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
まず上記の部分です。この設定では、プルリクエストが発生し、かつ、以下の条件がすべて満たされた場合にのみワークフローが実行されます
images
ディレクトリ内の任意の場所にあるDockerfile
が変更された場合 (paths
)- プルリクエストの対象ブランチが
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上に定義された脆弱性スキャンポリシーに基づいて、対象イメージが設定された基準に合格しているかどうかを判定できます。
実際に試してみる
それでは実際にプルリクエストを出して脆弱性スキャンが行われることを確認したいと思います。
事前準備
まずは、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
なお、Sysdig SecureAPIトークンの取得方法についてはSysdigの公式ドキュメントに詳しい手順が記載されていますので、そちらをご参照ください。
プルリクエストの作成
この状態で、以下のようなDockerfileをimages/productA/imageB/Dockerfile
に作成します。単純なnginxのlatestイメージです。
FROM nginx:latest
この変更をdevブランチにpushし、プルリクエストとしてmainブランチへのマージを試みます。
ワークフローの実行結果
プルリクエストが作成されるとGitHub Actionsワークフローが起動しました。以下のように変更ファイルがちゃんと認識されています。
以下のように指定したポリシーで脆弱性スキャン結果が判定されています。今回は合格(PASSED)しました。
最終的にECRにpushされているのが確認できました。もちろん脆弱性スキャン結果の判定がNGだった場合にはそこでワークフロー処理が止まるので、Pushはされません。
まとめ
以上、GitHubで任意のDockerfileをプルリクエストしたときに対象のDockerfileの脆弱性スキャンをGitHub Actionsで自動で行ってみました。
この記事がどなたかの参考になれば幸いです。
以上、とーちでした。