はじめに
ECRのBasic ScanはコンテナイメージをレポジトリにPushしたタイミングで実行されるため、開発が落ち着いてくるとPushするタイミングが減り、OSの脆弱性があまりチェックされてなくなってしまいます
手動での実行は可能ですが、いちいち手動でやるのもメンドウなのでEventBridge Schedulerで定期的に実行するようにした際のメモです
SSM Documentの作成
ECR Basic Scanを実行するSSM Documentを作成します
スキャン対象となるものはTag付けされているもので、以前のスキャン実行から1週間以上経過しているものとしています
(手動スキャンは24時間に1回しか実行できませんが、休日をはさむと対象になるのであえて1週間にしてます)
resource "aws_ssm_document" "ecr_basic_scan" {
name = "StartEcrBasicScan"
document_format = "YAML"
document_type = "Automation"
content = <<EOT
description: start ecr basic scan
schemaVersion: '0.3'
assumeRole: '{{ AutomationAssumeRole }}'
parameters:
AutomationAssumeRole:
type: String
EcrRepositoryName:
type: String
mainSteps:
- name: StartEcrBasicScan
action: 'aws:executeScript'
inputs:
Runtime: python3.11
Handler: start_ecr_basic_scan
InputPayload:
EcrRepositoryName: '{{ EcrRepositoryName }}'
Script: |
import boto3
from datetime import datetime, timezone
client = boto3.client('ecr')
def start_ecr_basic_scan(event, context):
repository_name = event.get('EcrRepositoryName')
out = {
"EcrRepositoryName": repository_name,
"ImageTags": []
}
try:
res = client.list_images(
repositoryName=repository_name,
filter={
'tagStatus': 'TAGGED',
}
)
for image in res['imageIds']:
scan_res = client.describe_image_scan_findings(
repositoryName=repository_name,
imageId=image
)
dt = datetime.now(timezone.utc) - scan_res['imageScanFindings']['imageScanCompletedAt']
if dt.days >= 7:
client.start_image_scan(
repositoryName=repository_name,
imageId=image
)
out['ImageTags'].append(image['imageTag'])
except Exception as e:
errorMsg = str(e)
raise Exception(f"Could not start basic scan on {repository_name}, error = '{errorMsg}'")
return out
outputs:
- Name: EcrRepositoryName
Selector: $.Payload.EcrRepositoryName
Type: String
- Name: ImageTags
Selector: $.Payload.ImageTags
Type: StringList
EOT
}
IAM Roleの作成
EventBridge Schedulerで実行するためのIAM Roleを作成します
data "aws_iam_policy_document" "ecr_scan_role" {
statement {
actions = ["sts:AssumeRole"]
effect = "Allow"
principals {
type = "Service"
identifiers = [
"scheduler.amazonaws.com",
"ssm.amazonaws.com",
]
}
}
}
data "aws_iam_policy_document" "ecr_scan_policy" {
statement {
actions = [
"ssm:StartAutomationExecution",
]
effect = "Allow"
resources = [
"arn:aws:ssm:ap-northeast-1:XXXXXXXXXXXX:automation-execution/*",
aws_ssm_document.ecr_basic_scan.arn
]
}
statement {
actions = [
"ecr:ListImages",
"ecr:DescribeImageScanFindings",
"ecr:StartImageScan",
]
effect = "Allow"
resources = [
"arn:aws:ecr:ap-northeast-1:XXXXXXXXXXXX:repository/*"
]
}
}
resource "aws_iam_role" "ecr_scan" {
name = "ecr-scan"
path = "/"
assume_role_policy = data.aws_iam_policy_document.ecr_scan_role.json
}
resource "aws_iam_role_policy" "ecr_scan" {
name = "start-ecr-basic-scan"
role = aws_iam_role.ecr_scan.id
policy = data.aws_iam_policy_document.ecr_scan_policy.json
}
EventBridgeからSSM DocumentでAssumeRoleする必要があるため、自分自身にAssumeRoleの権限を追加します
data "aws_iam_policy_document" "ecr_scan_pass_role" {
statement {
actions = [
"iam:PassRole",
]
effect = "Allow"
resources = [
aws_iam_role.ecr_scan.arn,
]
}
}
resource "aws_iam_role_policy" "ecr_scan_pass_role" {
name = "ecr-scan-pass-role"
role = aws_iam_role.ecr_scan.id
policy = data.aws_iam_policy_document.ecr_scan_pass_role.json
}
EventBridge Schedulerの作成
EventBridge Schedulerにて ssm:startAutomationExecution APIで作成したSSM Documentを実行します
resource "aws_scheduler_schedule" "ecr_scan" {
name = "ecr-scan"
state = "ENABLED"
flexible_time_window {
mode = "OFF"
}
schedule_expression = "cron(0 1 ? * * *)"
target {
arn = "arn:aws:scheduler:::aws-sdk:ssm:startAutomationExecution"
role_arn = aws_iam_role.ecr_scan.arn
input = jsonencode({
DocumentName = aws_ssm_document.ecr_basic_scan.name,
Parameters = {
AutomationAssumeRole = [aws_iam_role.ecr_scan.arn],
EcrRepositoryName = ["ecr-scan-test"]
}
})
}
}
通知設定
ついでに結果をSNS経由でSlackに通知します
Criticalが1件以上、またはHighが10件以上であれば通知するようにしています
resource "aws_cloudwatch_event_rule" "ecr_basic_scan" {
name = "ecr-basic-scan"
event_pattern = jsonencode({
"source": [
"aws.ecr"
],
"detail-type": [
"ECR Image Scan"
],
"detail": {
"scan-status": [
"COMPLETE"
],
"finding-severity-counts": {
"$or": [
{
"CRITICAL": [{
"numeric": [">", 0]
}]
},
{
"HIGH": [{
"numeric": [">", 9]
}]
}
]
}
}
})
}
そのままだとSlackでの通知がそっけないので通知内容をカスタマイズ
Next Stepsにスキャン結果へのリンクを追加しています
resource "aws_cloudwatch_event_target" "ecr_basic_scan" {
rule = aws_cloudwatch_event_rule.ecr_basic_scan.name
arn = aws_sns_topic.topic.arn
input_transformer {
input_paths = {
time = "$.time",
repository = "$.detail.repository-name",
digest = "$.detail.image-digest",
tags = "$.detail.image-tags[0]",
critical = "$.detail.finding-severity-counts.CRITICAL",
high = "$.detail.finding-severity-counts.HIGH"
}
input_template = <<EOT
{
"version": "1.0",
"source": "custom",
"content": {
"textType": "client-markdown",
"title": ":exclamation: [<repository>] ECR Image Scan",
"description": "*Time*:\n <time>\n*ImageDigent*:\n <digest>\n*ImageTags*:\n <tags>\n*FindingSeverity*:\n CRITICAL: <critical>\n HIGH: <high>",
"nextSteps": [
"Refer to <https://ap-northeast-1.console.aws.amazon.com/ecr/repositories/private/XXXXXXXXXXXX/<repository>/_/image/<digest>/details?region=ap-northeast-1|the scan findings>"
]
},
"metadata": {
"enableCustomActions": false
}
}
EOT
}
}