6
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

ECR Basic Scanを定期的に実行する

6
Posted at

はじめに

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
  }
}
6
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
6
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?