GitHub Actions で Trivy のスキャンを定期実行し結果を Security Hub に取り込んでみた
こんにちは!クラウド事業本部コンサルティング部のたかくに(@takakuni_)です。
みなさん Trivy 使っていますでしょうか。コンテナイメージや IaC など幅広く静的解析できる点が魅力的です。
たとえば、 PR を切った時に Trivy を使ってスキャンを行い、問題なければマージするなどの使い方があります。
今回はマージされた後にフォーカスし、定期的なコンテナイメージのスキャンについて考えてみます。
Amazon Inspector
コンテナイメージスキャンで言えば、比較よくで Amazon Inspector が出てきます。 Amazon Inspector の場合、新しい脆弱性が利用しているコンテナイメージに影響がある場合は再スキャンしてくれます。
Trivy の場合はそのような機能はないため、Cron など何かしらの定期実行が必要になります。
やってみた
そこで今回は GitHub Actions を利用し、ECS で利用しているコンテナイメージを定期的にスキャンしてみたいと思います。
Security Hub 統合
まずは Security Hub で Aqua(Trivy)からの Findings を受け取れるように設定します。統合から Aqua を検索し、結果の受け入れを行います。
コード
続いてワークフローの作成です。
作成したコードは以下になります。ステップバイステップで説明します。
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
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)に変換する必要があります。
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)
以下のドキュメントでは "@contrib/asff.tpl"
があるはずなのですが見つからず、上記で対応しました。
DB Cache
Trivy アクションでは脆弱性 DB が含まれたコンテナイメージを引っ張ってきます。Rate Limit に引っかからないよう、適切な頻度でキャッシュをしましょう。
# 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 をキャッシュするためのワークフローも用意されているためこちらも参考にしていただけると幸いです。
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
アカウント 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'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'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.
なお、この処理ができていない、リージョンが異なる等があった場合、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.
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
結果を確認
結果を確認してみます。うまく動いていますね。
Security Hub 側からも Aqua Security として Trivy のスキャン結果が閲覧できますね。
まとめ
以上、「GitHub Actions で Trivy のスキャンを定期実行し結果を Security Hub に取り込んでみた」でした。
いろんな技術のてんこ盛りでしたが、BatchImportFindings と ASFF の仕組みがわかれば他の統合も用意にできそうな気がしました。
このブログがどなたかの参考になれば幸いです。クラウド事業本部コンサルティング部のたかくに(@takakuni_)でした!