Docker Scout と GitHub の統合ワークフロー


前回の記事では、脆弱性スキャンの重要性、また、Docker Scout を使うことにより依存関係とそれに関連する脆弱性の概要がいかにわかりやすくなるかついて説明しました。

https://qiita.com/gtrekter/items/e15a862bd5b0c33069a9

この記事では、Dockerのコンテンツを本番ブランチと統合してDocker Hubに公開する前に、Dockerイメージを構築し脆弱性をスキャンするGitHub Workflowを作成することで、実際のDocker Scoutを学びましょう。 このプロセスにより、リリースされたアプリケーションの品質が高くなります。

# サンプル アプリケーションの作成

このチュートリアルでは、例として私の Hypnos アプリケーションコードを使用します。 これは、React.js と Yarn パッケージ マネージャを使用して構築された Web アプリケーションです。 まず、Dockerファイルを作成する必要があります。

```
FROM node:14-alpine
WORKDIR /app
COPY package.json ./
COPY yarn.lock ./
RUN yarn install --frozen-lockfile
COPY . .
EXPOSE 3000
CMD ["npm", "start"]
```

次に、node_modules が意図せず Docker イメージにコピーされないよう、 Docker に指示しましょう。 このフォルダはかなり大きくなる可能性があり、いずれにしてもイメージをビルドするのでコピーする必要はありません。 これを行うには、次の内容の .dockerignore ファイルを作成します:
```
node_modules
```
実際のGitHub Actionに移る前に、、イメージをローカルで構築して、すべてが期待どおりに動作するようにしましょう。
```
docker image build -t hypnos:1.0 .
```
最後に、新しく作成したイメージでコンテナを実行できます。
```
docker run -p 3000:3000 hypnos:1.0
```
# GitHub アクションを使用したビルドとスキャンの自動化

Docker のイメージが完成したので、Docker Scout を使用して脆弱性スキャンを自動化に集中しましょう。 Dockerは最近、Docker Scoutに特化したGitHubアクションをリリースしました。 ただし、この記事を書いている時点では、このアクションは(Docker Scout CLI でサポートされた)出力ファイルの作成をサポートしていません。 このため、このデモでは GitHub Action を使用しません。 代わりに、Docker Scout CLI を使用します。

## シークレットの作成

Docker Scout は、サブスクリプションベースのモデルで動作する独自の脆弱性データベースを使用します。 したがって、Docker Scout では、イメージをスキャンする前に ユーザーがDocker Hub に認証する必要があります。 これには、この情報を GitHub のシークレットに保存します。

- Docker Hub にアクセスし、アカウントでログインします。
- ユーザ名をクリックし、ドロップダウンから [Account Settings] を選択します。
- [Security] タブを選択し、[New Access Token] をクリックします。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3477222/f5be7a5c-20f0-d12e-a416-3e12c6d662a9.png)

- トークンの説明を入力し、そのアクセス権限を選択します。 その場合は、読み書きが必要になります。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3477222/f03d0234-332a-ae53-d6e5-d6afbfe12f04.png)

- Generateボタンをクリックし、トークンを保存してください。トークンは二度と表示されません。

次に、GitHub シークレットを作成し、Docker Hub トークンを保存しましょう。次に、別の GitHub シークレットを作成し、ユーザ名を保存しましょう。 今回、私はそれらをDOCKERHUB_TOKENとDOCKERHUB_USERとしました。

## GitHub ワークフローの作成

まず、トリガーを定義することで、ワークフローがトリガーされるタイミングを指定する必要があります。 この場合、メインブランチを対象としたプッシュまたはプル要求が発生したときにワークフローを起動させることにします。 その後、パイプラインでは、イベントの種類に応じてブロックを実行するための条件を使用します。
```
push:
  branches:
    - main
pull_request:
  types: [opened, synchronize, reopened, closed]
  branches:
    - main
```
次に、イメージの名前を含む環境変数を作成します。
```
env:
  DOCKER_IMAGE_NAME: hypnos:$(date +%s)
```
その後、Docker Scoutプラグインをインストールしてイメージをビルドします。

```
jobs:
  build:
    runs-on: ubuntu-latest
    steps:
    - uses: actions/checkout@v3

    - name: Install Docker Scout
      if: ${{ github.event_name == 'pull_request' }}
      run: |
       curl -fsSL https://raw.githubusercontent.com/docker/scout-cli/main/install.sh -o install-scout.sh
       sh install-scout.sh

    - name: Build the Docker image
      run: docker build . --file Dockerfile --tag $DOCKER_IMAGE_NAME
```

次に、Docker Scout アクションを追加し、コマンド ‘cves’ をトリガーします。このコマンドは、以前に作成したイメージ（ベース イメージを除く）で特定された CVE を表示し、結果を JSON ファイルに出力します。

```
- name: Run Docker Scout
  if: ${{ github.event_name == 'pull_request' }}
  run: |
    docker scout cves --ignore-base --format sarif --output hypnos.sarif.json
```

スキャン中に見つかった脆弱性の数に基づいて、出力変数を true または false に設定します。

```
- name: Check vulnerabilities
  id: check_vulnerabilities
  if: ${{ github.event_name == 'pull_request' }}
  run: |
    if [[ $(cat hypnos.sarif.json | jq '.runs[0].results | length') -gt 0 ]]; then
      echo -e "\e[31mThere were vulnerabilities in your Docker image. Check the comments on your PR to know more.\e[0m"
      echo "fail_workflow=true" >> "$GITHUB_OUTPUT"
    else
      echo "There were no vulnerabilities in your Docker image. Good job!"
      echo "fail_workflow=false" >> "$GITHUB_OUTPUT"
    fi
```

脆弱性の数がゼロを超える場合は、プルリクエストにコメントを作成します。 このコメントには、スキャンのルール、結果、および全体的な結果が含まれます。 その後、パイプラインを強制的に失敗させます。

```
- name: Create Comment
  if: ${{ github.event_name == 'pull_request' && steps.check_vulnerabilities.outputs.fail_workflow == 'true' }}
  uses: actions/github-script@v4
  with:
    github-token: ${{ secrets.GITHUB_TOKEN }}
    script: |
      const fs = require('fs');
      const body = `
      **JSON File Content:**
      \`\`\`
      ${fs.readFileSync('hypnos.sarif.json', 'utf8')}
      \`\`\`
      `;
      await github.issues.createComment({
        owner: context.repo.owner,
        repo: context.repo.repo,
        issue_number: context.issue.number,
        body: body
      });

- name: Fail Workflow
  if: ${{ github.event_name == 'pull_request' && steps.check_vulnerabilities.outputs.fail_workflow == 'true' }}
  run: exit 1
```

脆弱性が見つからない場合、ワークフローは正常に完了します。プルリクエストが承認されメインブランチにマージされたら、イメージに適切なタグを付けて Docker Hub にプッシュします。

```
- name: Tag the Docker image
  if: ${{ github.event_name == 'push' }}
  run: docker image tag $DOCKER_IMAGE_NAME ${{ secrets.DOCKERHUB_USERNAME }}/$DOCKER_IMAGE_NAME

- name: Publish the Docker image
  if: ${{ github.event_name == 'push' }}
  run: docker image push ${{ secrets.DOCKERHUB_USERNAME }}/$DOCKER_IMAGE_NAME 
```

このアプローチを採用し、Docker Scout を CI ワークフローに統合することで、開発プロセスの早い段階で脆弱性を積極的に特定することができます。これにより、迅速な修復が可能になり、リリースしたアプリケーションが高いレベルのセキュリティと品質を維持することができるようになります。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3477222/eaf1f233-6173-7c4d-9c56-7aa6c8d4fe78.png)

# 参考資料

- Hypnosアプリ:https://github.com/GTRekter/Hypnos
- ドッカースカウトアクション: https://github.com/docker/scout-action