GitHub Actionsでセキュリティスキャン結果をPRにコメントする

開発チームのセキュリティ意識を高めるため、GitHub ActionsでDockerイメージのセキュリティスキャンを行い、その結果をプルリク(PR)のコメントとして自動的に追加するような仕組みをCIに組み込みました。

![スクリーンショット 2024-07-14 9.23.34.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/569054/0c31a11b-9f11-634b-6fdb-1a6b41f98aba.png)

## `scan-and-comment`ジョブの概要
このジョブは、コードのチェックアウト、Dockerイメージのビルド、セキュリティスキャン、結果の整形、そしてPRへのコメント投稿までの一連のステップを自動化します。Dockerイメージの脆弱性スキャンにはTrivyを使用しています。Trivyについての詳細は別記事で触れています。

https://qiita.com/suzuki0430/items/e53b5f73569313cec670

## `scan-and-comment`ジョブの詳細
以下が、`scan-and-comment`ジョブを追加した後のCIワークフロー全文です。

```yml
name: backend-ci
on:
  push:
    branches:
      - main
      - staging
      - develop
  pull_request:
    types:
      - opened
      - synchronize
      - reopened
    paths:
      - "backend/**"
  workflow_dispatch: #手動動作確認用

jobs:
  setup:
    ...略...

  test:
    ...略...

  lint:
    ...略...

  tsc:
    ...略...

  scan-and-comment:
    runs-on: ubuntu-latest
    if: github.event_name == 'pull_request' && github.event.action == 'opened'
    steps:
      - name: Checkout code
        uses: actions/checkout@v4

      # 本番環境で使用されるDockerイメージをビルドし、その後のステップで脆弱性スキャンを行う
      - name: Build Docker Image
        run: docker build --build-arg ENV=prod -t local-image:latest -f ./backend/Dockerfile.ecs ./backend

      - name: Scan image with Trivy
        uses: aquasecurity/trivy-action@0.24.0
        with:
          image-ref: local-image:latest
          format: "table"
          severity: "CRITICAL,HIGH"
          output: trivy-result.txt

      - name: Check Trivy result file
        run: cat trivy-result.txt

      - name: Format Trivy Scan Result
        run: |
          if [ -s trivy-result.txt ]; then
            echo -e "## Vulnerability Scan Results\n<details><summary>Details</summary>\n\n\`\`\`\n$(cat trivy-result.txt)\n\`\`\`\n</details>" > formatted-trivy-result.md
          else
            echo -e "## Vulnerability Scan Results\nNo vulnerabilities were detected." > formatted-trivy-result.md
          fi

      - name: Comment PR with Trivy scan results
        uses: marocchino/sticky-pull-request-comment@v2
        with:
          path: formatted-trivy-result.md

      - name: Clean up Trivy result file
        run: rm -f trivy-result.txt formatted-trivy-result.md
```

### 1. コードのチェックアウト
`if: github.event_name == 'pull_request' && github.event.action == 'opened'`により、このジョブはPRが新しく作成されたときにのみ実行されます(PRの同期や再開時にはジョブがトリガーされません)。

また、チェックアウトで最新のコードを取得します。

```yaml
scan-and-comment:
  runs-on: ubuntu-latest
  if: github.event_name == 'pull_request' && github.event.action == 'opened'
  steps:
    - name: Checkout code
      uses: actions/checkout@v4
```

### 2. Dockerイメージのビルド
本番環境と同じ設定でDockerイメージをビルドします。これにより、本番環境で発生する可能性のある脆弱性を検出できます。

```yaml
- name: Build Docker Image
  run: docker build --build-arg ENV=prod -t local-image:latest -f ./backend/Dockerfile.ecs ./backend
```
以下がビルドに使用する`Dockerfile`です。本番環境でプロダクションビルドを行なうようにしているため、ビルド時に本番環境用のパラメータ`ENV=prod`を指定して、セキュリティスキャンの精度を高めています。

また、`local-image:latest`は脆弱性スキャンのみに使用するDockerイメージで、任意の名前をつけています。

```Dockerfile
FROM node:20.15.0 AS builder

WORKDIR /app

COPY package*.json ./
COPY yarn.lock ./

# Huskyを無視して依存関係をインストール
# yarn buildのためにこちらでproductionモードは使用しない
RUN yarn install --frozen-lockfile --ignore-scripts

COPY . .

RUN yarn build

ARG ENV

# stg環境とprod環境の場合はnode_modulesを削除してproductionモードで再インストールする
RUN if [ "$ENV" = "prod" ] || [ "$ENV" = "stg" ]; then \
        rm -rf node_modules && yarn install --frozen-lockfile --ignore-scripts --production; \
    fi

FROM node:20.15.0-slim

WORKDIR /app

COPY --from=builder /app/dist ./dist
COPY --from=builder /app/node_modules ./node_modules
COPY --from=builder /app/package.json ./package.json

EXPOSE 8080

CMD ["node", "dist/main"]
```

### 3. Trivyによるイメージスキャン
Trivyを使用してイメージのセキュリティスキャンを実施し、結果をテキストファイルに保存します。

以下が使用するアクションです。

https://github.com/aquasecurity/trivy-action/tree/master/

```yaml
- name: Scan image with Trivy
  uses: aquasecurity/trivy-action@0.24.0
  with:
    image-ref: local-image:latest
    format: "table"
    severity: "CRITICAL,HIGH"
    output: trivy-result.txt

- name: Check Trivy result file
  run: cat trivy-result.txt
```

### 4. スキャン結果の整形
スキャンの結果に基づき、詳細な情報を含むコメントフォーマットを作成します。

```yaml
- name: Format Trivy Scan Result
  run: |
    if [ -s trivy-result.txt ]; then
      echo -e "## Vulnerability Scan Results\n<details><summary>Details</summary>\n\n\`\`\`\n$(cat trivy-result.txt)\n\`\`\`\n</details>" > formatted-trivy-result.md
    else
      echo -e "## Vulnerability Scan Results\nNo vulnerabilities were detected." > formatted-trivy-result.md
    fi
```

### 5. PRへのコメント投稿
フォーマットされたスキャン結果をPRにコメントとして投稿します。

```yaml
- name: Comment PR with Trivy scan results
  uses: marocchino/sticky-pull-request-comment@v2
  with:
    path: formatted-trivy-result.md
```

### 6. クリーンアップ
最後に使用したファイルを削除し、環境をクリーンな状態に保ちます。

```yaml
- name: Clean up Trivy result file
  run: rm -f trivy-result.txt formatted-trivy-result.md
```