GitHub Actions で Trivy のスキャンを定期実行し結果を Security Hub に取り込んでみた

GitHub Actions で Trivy のスキャンを定期実行し結果を Security Hub に取り込んでみた

Clock Icon2025.01.20

こんにちは!クラウド事業本部コンサルティング部のたかくに(@takakuni_)です。

みなさん Trivy 使っていますでしょうか。コンテナイメージや IaC など幅広く静的解析できる点が魅力的です。

たとえば、 PR を切った時に Trivy を使ってスキャンを行い、問題なければマージするなどの使い方があります。

今回はマージされた後にフォーカスし、定期的なコンテナイメージのスキャンについて考えてみます。

Amazon Inspector

コンテナイメージスキャンで言えば、比較よくで Amazon Inspector が出てきます。 Amazon Inspector の場合、新しい脆弱性が利用しているコンテナイメージに影響がある場合は再スキャンしてくれます。

Trivy の場合はそのような機能はないため、Cron など何かしらの定期実行が必要になります。

やってみた

そこで今回は GitHub Actions を利用し、ECS で利用しているコンテナイメージを定期的にスキャンしてみたいと思います。

Security Hub 統合

まずは Security Hub で Aqua(Trivy)からの Findings を受け取れるように設定します。統合から Aqua を検索し、結果の受け入れを行います。

2025-01-20 at 22.24.07-製品の統合  Security Hub  ap-northeast-1.png

コード

続いてワークフローの作成です。

作成したコードは以下になります。ステップバイステップで説明します。

name: Trivy Scan

on:
  # schedule:
  #   - cron: '0 0 * * *'
  workflow_dispatch:

jobs:
  get-images:
    name: Get Container Images
    runs-on: ubuntu-22.04
    permissions:
      id-token: write
      contents: read
    outputs:
      matrix: ${{ steps.set-matrix.outputs.matrix }}
    steps:
      - uses: aws-actions/configure-aws-credentials@v4
        with:
          role-to-assume: arn:aws:iam::123456789012:role/check-trivy-role
          aws-region: ap-northeast-1
      - name: Get container images
        id: set-matrix
        run: |
          # タスク一覧の取得
          TASKS=$(aws ecs list-tasks \
            --cluster hogehoge-cluster \
            --service-name hogehoge-service \
            --output json)
          TASK_ARN=$(echo $TASKS | jq -r '.taskArns[0]')

          # タスクの全コンテナイメージを取得
          TASK=$(aws ecs describe-tasks \
            --cluster hogehoge-cluster \
            --tasks $TASK_ARN \
            --output json)

          # コンテナイメージの配列を作成
          IMAGES=$(echo $TASK | jq -c '{images: [.tasks[0].containers[].image]}')
          echo "matrix=$IMAGES" >> $GITHUB_OUTPUT
  scan:
    needs: get-images
    name: Scan Images
    runs-on: ubuntu-22.04
    permissions:
      id-token: write
      contents: read
    strategy:
      matrix: ${{fromJson(needs.get-images.outputs.matrix)}}
      fail-fast: false # 1つのスキャンが失敗しても他を続行
    steps:
      - uses: aws-actions/configure-aws-credentials@v4
        with:
          role-to-assume: arn:aws:iam::123456789012:role/check-trivy-role
          aws-region: ap-northeast-1
      # ECR にログイン
      - name: Login to Amazon ECR
        id: login-ecr
        uses: aws-actions/amazon-ecr-login@v2
      - name: Login to Amazon ECR Public
        id: login-ecr-public
        uses: aws-actions/amazon-ecr-login@v2
        with:
          registry-type: public
        env:
          AWS_REGION: 'us-east-1'
      # Trivy でスキャン
      - name: Run Trivy vulnerability scanner
        uses: aquasecurity/trivy-action@master
        with:
          image-ref: ${{ matrix.images }}
          format: 'template'
          template: '@$HOME/.local/bin/trivy-bin/contrib/asff.tpl'
          output: 'trivy-results-${{ github.run_attempt }}.sarif'
        # env: # キャッシュした DB を使う場合
        #   TRIVY_SKIP_DB_UPDATE: true
        #   TRIVY_SKIP_JAVA_DB_UPDATE: true
      # Security Hub に結果をインポート
      - name: Import Findings to AWS Security Hub
        env:
          AWS_REGION: ap-northeast-1
          AWS_ACCOUNT_ID: 123456789012
        run: |
          # ASFF 形式に変換
          cat trivy-results-${{ github.run_attempt }}.sarif | jq '.Findings' > batch-import-findings.asff
          # アカウント ID を置換
          sed -i "s/\"AwsAccountId\": \"\"/\"AwsAccountId\": \"$AWS_ACCOUNT_ID\"/g" batch-import-findings.asff
          # Findings があるか確認
          total=$(jq 'length' batch-import-findings.asff)
          # Findings がない場合は処理をスキップ
          if [ $total -eq 0 ]; then
              echo "No Findings found in batch-import-findings.asff. Skipping processing."
              exit 0
          fi
          # 100件ずつ処理
          for ((i=0; i<total; i+=100)); do
              remaining=$((total - i))
              if [ $remaining -lt 100 ]; then
                  end=$((i + remaining))
              else
                  end=$((i + 100))
              fi
              # 処理実行
              jq ".[${i}:${end}]" batch-import-findings.asff > "report_${i}_${end}.asff"
              aws securityhub batch-import-findings --findings file://report_${i}_${end}.asff
          done

コンテナイメージの判別

現在利用しているコンテナイメージはどれかを考えます。

すべてのコンテナがプロジェクトのパイプライン上で回っていれば良いですが、サイドカーコンテナなど回してないものもあると仮定します。

そこで今回は実際の ECS タスクを見て起動しているコンテナイメージを判別するようにしてみました。

マトリックスを利用しているため、後続の scan ジョブは検出されたコンテナイメージ分、動くような書き方にしています。

jobs:
  get-images:
    name: Get Container Images
    runs-on: ubuntu-22.04
    permissions:
      id-token: write
      contents: read
    outputs:
      matrix: ${{ steps.set-matrix.outputs.matrix }}
    steps:
      - uses: aws-actions/configure-aws-credentials@v4
        with:
          role-to-assume: arn:aws:iam::123456789012:role/check-trivy-role
          aws-region: ap-northeast-1
      - name: Get container images
        id: set-matrix
        run: |
          # タスク一覧の取得
          TASKS=$(aws ecs list-tasks \
            --cluster hogehoge-cluster \
            --service-name hogehoge-service \
            --output json)
          TASK_ARN=$(echo $TASKS | jq -r '.taskArns[0]')

          # タスクの全コンテナイメージを取得
          TASK=$(aws ecs describe-tasks \
            --cluster hogehoge-cluster \
            --tasks $TASK_ARN \
            --output json)

          # コンテナイメージの配列を作成
          IMAGES=$(echo $TASK | jq -c '{images: [.tasks[0].containers[].image]}')
          echo "matrix=$IMAGES" >> $GITHUB_OUTPUT

スキャン

続いてスキャンジョブです。

レジストリ

コンテナイメージは ECR, ECR Public を想定したため、aws-actions/amazon-ecr-login@v2 を 2 回実行しています。

ECR Public の場合はリージョンを us-east-1 に変更しておきましょう。 次のエラーが発生します。

getaddrinfo ENOTFOUND api.ecr-public.ap-northeast-1.amazonaws.com
main.yaml
scan:
  needs: get-images
  name: Scan Images
  runs-on: ubuntu-22.04
  permissions:
    id-token: write
    contents: read
  strategy:
    matrix: ${{fromJson(needs.get-images.outputs.matrix)}}
    fail-fast: false # 1つのスキャンが失敗しても他を続行
  steps:
    - uses: aws-actions/configure-aws-credentials@v4
      with:
        role-to-assume: arn:aws:iam::123456789012:role/check-trivy-role
        aws-region: ap-northeast-1
    # ECR にログイン
+   - name: Login to Amazon ECR
+     id: login-ecr
+     uses: aws-actions/amazon-ecr-login@v2
+   - name: Login to Amazon ECR Public
+     id: login-ecr-public
+     uses: aws-actions/amazon-ecr-login@v2
+     with:
+       registry-type: public
+     env:
+       AWS_REGION: 'us-east-1'
    # Trivy でスキャン
    - name: Run Trivy vulnerability scanner
      uses: aquasecurity/trivy-action@master
      with:
        image-ref: ${{ matrix.images }}
        format: 'template'
        template: '@$HOME/.local/bin/trivy-bin/contrib/asff.tpl'
        output: 'trivy-results-${{ github.run_attempt }}.sarif'
      # env: # キャッシュした DB を使う場合
      #   TRIVY_SKIP_DB_UPDATE: true
      #   TRIVY_SKIP_JAVA_DB_UPDATE: true
    # Security Hub に結果をインポート
    - name: Import Findings to AWS Security Hub
      env:
        AWS_REGION: ap-northeast-1
        AWS_ACCOUNT_ID: 123456789012
      run: |
        # ASFF 形式に変換
        cat trivy-results-${{ github.run_attempt }}.sarif | jq '.Findings' > batch-import-findings.asff
        # アカウント ID を置換
        sed -i "s/\"AwsAccountId\": \"\"/\"AwsAccountId\": \"$AWS_ACCOUNT_ID\"/g" batch-import-findings.asff
        # Findings があるか確認
        total=$(jq 'length' batch-import-findings.asff)
        # Findings がない場合は処理をスキップ
        if [ $total -eq 0 ]; then
            echo "No Findings found in batch-import-findings.asff. Skipping processing."
            exit 0
        fi
        # 100件ずつ処理
        for ((i=0; i<total; i+=100)); do
            remaining=$((total - i))
            if [ $remaining -lt 100 ]; then
                end=$((i + remaining))
            else
                end=$((i + 100))
            fi
            # 処理実行
            jq ".[${i}:${end}]" batch-import-findings.asff > "report_${i}_${end}.asff"
            aws securityhub batch-import-findings --findings file://report_${i}_${end}.asff
        done

ASFF

後続で Security Hub へ結果を送信します。

Security Hub へ送信する場合は ASFF(AWS Security Finding Format)に変換する必要があります。

https://docs.aws.amazon.com/ja_jp/securityhub/latest/userguide/securityhub-findings-format.html

ASFF のフォーマットはテンプレートを利用して変換します。テンプレートは '@$HOME/.local/bin/trivy-bin/contrib/asff.tpl' を利用します。

以下の README から、パスを特定しました。

Output template (@$HOME/.local/bin/trivy-bin/contrib/gitlab.tpl, @$HOME/.local/bin/trivy-bin/contrib/junit.tpl)

https://github.com/aquasecurity/trivy-action/blob/master/README.md#inputs

以下のドキュメントでは "@contrib/asff.tpl" があるはずなのですが見つからず、上記で対応しました。

https://trivy.dev/v0.17.2/integrations/aws-security-hub/

DB Cache

Trivy アクションでは脆弱性 DB が含まれたコンテナイメージを引っ張ってきます。Rate Limit に引っかからないよう、適切な頻度でキャッシュをしましょう。

main.yaml
    # Trivy でスキャン
    - name: Run Trivy vulnerability scanner
      uses: aquasecurity/trivy-action@master
      with:
        image-ref: ${{ matrix.images }}
        format: 'template'
        template: '@$HOME/.local/bin/trivy-bin/contrib/asff.tpl'
        output: 'trivy-results-${{ github.run_attempt }}.sarif'
+     env: # キャッシュした DB を使う場合
+       TRIVY_SKIP_DB_UPDATE: true
+       TRIVY_SKIP_JAVA_DB_UPDATE: true

また、DB をキャッシュするためのワークフローも用意されているためこちらも参考にしていただけると幸いです。

https://github.com/aquasecurity/trivy-action?tab=readme-ov-file#cache

Security Hub に結果をインポート

jq による整形

スキャン結果を Security Hub に結果をインポートします。インポート時は .Findings キーを抜いたバリューのみインポートする必要があるため再度データを整形します。

cat trivy-results-${{ github.run_attempt }}.sarif | jq '.Findings' > batch-import-findings.asff

The findings are formatted for the API with a key of Findings and a value of the array of findings. In order to upload via the CLI the outer wrapping must be removed being left with only the array of findings. The easiest way of doing this is with the jq library using the command

https://github.com/aquasecurity/trivy/blob/main/docs/tutorials/integrations/aws-security-hub.md

アカウント ID を置換

インポートする際に ECR Public のイメージを利用している場合、以下のように AwsAccountId が抜け落ちます。

[
	{
		"SchemaVersion": "2018-10-08",
		"Id": "public.ecr.aws/aws-observability/aws-for-fluent-bit:stable (amazon 2 (Karoo))/CVE-2024-9681",
		"ProductArn": "arn:aws:securityhub:us-east-1::product/aquasecurity/aquasecurity",
		"GeneratorId": "Trivy/CVE-2024-9681",
		"AwsAccountId": "",
		"Types": ["Software and Configuration Checks/Vulnerabilities/CVE"],
		"CreatedAt": "2025-01-20T06:59:25.937518211Z",
		"UpdatedAt": "2025-01-20T06:59:25.937531987Z",
		"Severity": {
			"Label": "MEDIUM"
		},
		"Title": "Trivy found a vulnerability to CVE-2024-9681 in container public.ecr.aws/aws-observability/aws-for-fluent-bit:stable (amazon 2 (Karoo)), related to curl",
		"Description": "When curl is asked to use HSTS, the expiry time for a subdomain might\noverwrite a parent domain&#39;s cache entry, making it end sooner or later than\notherwise intended.\n\nThis affects curl using applications that enable HSTS and use URLs with the\ninsecure `HTTP://` scheme and perform transfers with hosts like\n`x.example.com` as well as `example.com` where the first host is a subdomain\nof the second host.\n\n(The HSTS cache either needs to have been populated manually or there needs to\nhave been previous HTTPS acc ..",
		"Remediation": {
			"Recommendation": {
				"Text": "More information on this vulnerability is provided in the hyperlink",
				"Url": "https://avd.aquasec.com/nvd/cve-2024-9681"
			}
		},
		"ProductFields": {
			"Product Name": "Trivy"
		},
		"Resources": [
			{
				"Type": "Container",
				"Id": "public.ecr.aws/aws-observability/aws-for-fluent-bit:stable (amazon 2 (Karoo))",
				"Partition": "aws",
				"Region": "us-east-1",
				"Details": {
					"Container": {
						"ImageName": "public.ecr.aws/aws-observability/aws-for-fluent-bit:stable (amazon 2 (Karoo))"
					},
					"Other": {
						"CVE ID": "CVE-2024-9681",
						"CVE Title": "curl: HSTS subdomain overwrites parent cache entry",
						"PkgName": "curl",
						"Installed Package": "8.3.0-1.amzn2.0.7",
						"Patched Package": "8.3.0-1.amzn2.0.8",
						"NvdCvssScoreV3": "6.5",
						"NvdCvssVectorV3": "CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:N/I:H/A:L",
						"NvdCvssScoreV2": "0",
						"NvdCvssVectorV2": ""
					}
				}
			}
		],
		"RecordState": "ACTIVE"
	},
	{
		"SchemaVersion": "2018-10-08",
		"Id": "public.ecr.aws/aws-observability/aws-for-fluent-bit:stable (amazon 2 (Karoo))/CVE-2024-9681",
		"ProductArn": "arn:aws:securityhub:us-east-1::product/aquasecurity/aquasecurity",
		"GeneratorId": "Trivy/CVE-2024-9681",
		"AwsAccountId": "",
		"Types": ["Software and Configuration Checks/Vulnerabilities/CVE"],
		"CreatedAt": "2025-01-20T06:59:25.93782688Z",
		"UpdatedAt": "2025-01-20T06:59:25.937836427Z",
		"Severity": {
			"Label": "MEDIUM"
		},
		"Title": "Trivy found a vulnerability to CVE-2024-9681 in container public.ecr.aws/aws-observability/aws-for-fluent-bit:stable (amazon 2 (Karoo)), related to libcurl",
		"Description": "When curl is asked to use HSTS, the expiry time for a subdomain might\noverwrite a parent domain&#39;s cache entry, making it end sooner or later than\notherwise intended.\n\nThis affects curl using applications that enable HSTS and use URLs with the\ninsecure `HTTP://` scheme and perform transfers with hosts like\n`x.example.com` as well as `example.com` where the first host is a subdomain\nof the second host.\n\n(The HSTS cache either needs to have been populated manually or there needs to\nhave been previous HTTPS acc ..",
		"Remediation": {
			"Recommendation": {
				"Text": "More information on this vulnerability is provided in the hyperlink",
				"Url": "https://avd.aquasec.com/nvd/cve-2024-9681"
			}
		},
		"ProductFields": {
			"Product Name": "Trivy"
		},
		"Resources": [
			{
				"Type": "Container",
				"Id": "public.ecr.aws/aws-observability/aws-for-fluent-bit:stable (amazon 2 (Karoo))",
				"Partition": "aws",
				"Region": "us-east-1",
				"Details": {
					"Container": {
						"ImageName": "public.ecr.aws/aws-observability/aws-for-fluent-bit:stable (amazon 2 (Karoo))"
					},
					"Other": {
						"CVE ID": "CVE-2024-9681",
						"CVE Title": "curl: HSTS subdomain overwrites parent cache entry",
						"PkgName": "libcurl",
						"Installed Package": "8.3.0-1.amzn2.0.7",
						"Patched Package": "8.3.0-1.amzn2.0.8",
						"NvdCvssScoreV3": "6.5",
						"NvdCvssVectorV3": "CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:N/I:H/A:L",
						"NvdCvssScoreV2": "0",
						"NvdCvssVectorV2": ""
					}
				}
			}
		],
		"RecordState": "ACTIVE"
	}
]

AwsAccountId は Findings 登録時にマストで必要な情報のため、 sed を利用してアカウント ID を置換します。

# アカウント ID を置換
sed -i "s/\"AwsAccountId\": \"\"/\"AwsAccountId\": \"$AWS_ACCOUNT_ID\"/g" batch-import-findings.asff

You must call BatchImportFindings using the account that is associated with the findings. The identifier of the associated account is the value of the AwsAccountId attribute for the finding.

https://docs.aws.amazon.com/securityhub/latest/partnerguide/guidelines-batchimportfindings.html

なお、この処理ができていない、リージョンが異なる等があった場合、Admin 権限がついていたとしても次のように AccessDeniedException で怒られ続けます。

takakuni@ trivy-scan % aws securityhub batch-import-findings --region us-east-1 --findings file://tmp-report.asff
Enter MFA code for arn:aws:iam::123456789012:mfa/takakuni:

An error occurred (AccessDeniedException) when calling the BatchImportFindings operation: User: arn:aws:sts::123456789012:assumed-role/takakuni/botocore-session-1737379070 is not authorized to perform: securityhub:BatchImportFindings

100 件ごとに登録

BatchImportFindings は Max 100 件まで登録できます。

Send the largest batch that you can. Security Hub accepts up to 100 findings per batch, up to 240 KB per finding, and up to 6 MB per batch.

https://docs.aws.amazon.com/securityhub/latest/partnerguide/guidelines-batchimportfindings.html

1 API あたりの登録件数が 100 件を超えると、次のようにエラーが発生します。

An error occurred (InvalidInputException) when calling the BatchImportFindings operation: Invalid parameter 'Findings'. Size '134' is greater than maximum value: 100.

そこで 100 件ずつ処理を分割するように設定します。

# 100件ずつ処理
for ((i=0; i<total; i+=100)); do
    remaining=$((total - i))
    if [ $remaining -lt 100 ]; then
        end=$((i + remaining))
    else
        end=$((i + 100))
    fi
    # 処理実行
    jq ".[${i}:${end}]" batch-import-findings.asff > "report_${i}_${end}.asff"
    aws securityhub batch-import-findings --findings file://report_${i}_${end}.asff
done

結果を確認

結果を確認してみます。うまく動いていますね。

2025-01-20 at 22.25.28-Trivy Scan · takakuni-classmethodtrivy-scan@d3da84c.png

Security Hub 側からも Aqua Security として Trivy のスキャン結果が閲覧できますね。

2025-01-20 at 22.26.53-検出結果  Security Hub  ap-northeast-1.png

まとめ

以上、「GitHub Actions で Trivy のスキャンを定期実行し結果を Security Hub に取り込んでみた」でした。

いろんな技術のてんこ盛りでしたが、BatchImportFindings と ASFF の仕組みがわかれば他の統合も用意にできそうな気がしました。

このブログがどなたかの参考になれば幸いです。クラウド事業本部コンサルティング部のたかくに(@takakuni_)でした!

Share this article

facebook logohatena logotwitter logo

© Classmethod, Inc. All rights reserved.