🦅

InspectorのECR継続スキャンで最新のコンテナイメージの脆弱性のみを検知する方法

2025/01/04に公開

はじめに

InspectorのECR継続スキャン[1]において、最新のコンテナイメージで検知した脆弱性は、基本的には古いコンテナイメージでも検知される。
理由としては、MWバージョンが最新のコンテナイメージ < 古いコンテナイメージとなることは基本的にはなく、最新のコンテナイメージで脆弱性を含んでいるMWバージョンは、古いコンテナイメージのMWバージョンでも同じ脆弱性を含んでいる可能性が高いからである。

InspectorのECR継続スキャンはレポジトリに保存されているコンテナイメージ全てを対象とする為、例えばコンテナイメージのライフサイクルを10世代としている場合、最新のコンテナイメージで検知した脆弱性は、残りの9個のコンテナイメージでも検知される可能性が高く、この場合は同じ脆弱性が10個重複して検知されてしまう。
実際に、あるシステムで後からInspectorを有効化した際、大量の脆弱性がコンテナイメージに含まれており、それ×全世代分で脆弱性が数千個通知されてくるという、阿鼻叫喚な経験を筆者は体験している。

これに対処する為?、InspectorのECR継続スキャンには、Push/Pullからの経過日数が何日以内のコンテナイメージをスキャン対象とするかの設定がある。

しかし、この設定は絶妙に使い辛い。
カンの良い方ならお分かりだと思うが、経過日数の設定値次第では最新のコンテナイメージがスキャンの対象から除外される可能性があることに加えて、必ず最新のコンテナイメージのみをスキャン対象とすることはできない。

この設定とは別に抑制ルールという設定があり、「最新のコンテナイメージ以外の脆弱性を抑止する」抑止ルールを設定すれば、最新のコンテナイメージ以外で検知した脆弱性のステータスをSuppressedにできるので、最新のコンテナイメージの脆弱性のみ(ステータスがActive)を検知することが可能。

しかし、これもカンの良い方ならお分かりだと思うが、ECSコンテナイメージのベストプラクティスである「イメージタグはイミュータブルにする」に従う場合、Push毎に最新のコンテナイメージのタグ名が変わってしまう為、抑止ルールで「最新のコンテナイメージ以外の脆弱性検知を抑止する」には、Push毎に抑止ルールの更新が必要となる。

前置きが長くなったが、本記事では、ベストプラクティスの「イメージタグはイミュータブルにする」に準拠しながら、抑止ルールで「最新のコンテナイメージ以外の脆弱性を抑止する」方法を紹介する。

構成図

設定手順

前提

  • Terraformでの設定を前提とする
  • Inspectorの有効化、ECRレポジトリの作成、CI/CD(コミットハッシュの先頭7文字をイメージタグ名に設定して、ECRにPush)の設定は既に終わっているものとする
  • ECRレポジトリは一つとする

抑止ルールの作成

抑止ルールを作成する。
フィルターの値は適当なものでOK(後でLambdaから更新するので)。

resource "awscc_inspectorv2_filter" "image_tag" {
  # Resource arguments
  name            = "ImageTag"
  description     = "Suppress findings for non-latest container images"
  filter_action   = "SUPPRESS"
  filter_criteria = {
    comparison = "NOT_EQUALS"
    value      = "sample"
  }
  # Meta arguments
  lifecycle {
    ignore_changes = [filter_criteria] # filter_criteriaの更新はLambdaから実施するので、変更を無視する
  }
}

抑止ルールを更新するLambdaの作成

抑止ルールを更新するLambdaを作成する。
EventrBidgeからトリガーするので、それ用の許可設定を実施する。

data "aws_iam_policy_document" "lambda" {
  statement {
    effect    = "Allow"
    actions   = ["inspector2:UpdateFilter"]
    resources = ["*"]
  }
}

module "lambda_function" {
  # Module source
  source  = "terraform-aws-modules/lambda/aws"
  version = "7.8.1"
  # Module arguments
  ## Basic information
  function_name                           = "lambda-update-inspector-filter"
  description                             = ""
  package_type                            = "Zip"
  create_package                          = true
  recreate_missing_package                = false
  create_current_version_allowed_triggers = false
  runtime                                 = "python3.12"
  handler                                 = "lambda_function.lambda_handler"
  source_path                             = "${path.module}/python"
  architectures                           = ["arm64"]
  create_role                             = true
  role_name                               = "lambda-role-update-inspector-filter"
  policy_name                             = "lambda-policy-update-inspector-filter"
  attach_policy_json                      = true
  policy_json                             = data.aws_iam_policy_document.lambda.json
  ## Advanced Setting
  tags = {
    Name = "lambda-update-inspector-filter"
  }
  cloudwatch_logs_retention_in_days = 7
  ## General Settings
  memory_size = 240
  timeout     = 60
  ## Permission
  allowed_triggers = {
    one = {
      principal  = "events.amazonaws.com"
      source_arn = "arn:aws:events:${data.aws_region.current.name}:${data.aws_caller_identity.current.account_id}:rule/update-inspector-filter"
    }
  }
}
import boto3

def lambda_handler(event, context):
    inspector2 = boto3.client('inspector2', 'ap-northeast-1')

    try:
        image_tag = event['image_tag']
        filter_arn = event['filter_arn']
    except KeyError as e:
        error_message = f"Error: Missing required parameter: {str(e)}"
        print(error_message)

    try:  
        update_filter(inspector2, image_tag, filter_arn)
        print("Filter updated successfully")  
    except Exception as e:  
        error_message = f"Error while updating filter: {str(e)}"  
        print(error_message)  

def update_filter(inspector2, image_tag, filter_arn):
    response = inspector2.update_filter(
        action='SUPPRESS',
        filterArn=filter_arn,
        filterCriteria={
            'ecrImageTags': [
                {
                    'comparison': 'NOT_EQUALS',
                    'value': image_tag
                }
            ]
        }
    )

LambdaをトリガーするEventrBidgeRuleの作成

LambdaをトリガーするEventrBidgeRuleを作成する。
ECRレポジトリへのコンテナイメージのPushをトリガーとする。

module "eventbridge_lambda" {
  # Module source  
  source  = "terraform-aws-modules/eventbridge/aws"
  version = "3.8.0"
  # Module arguments
  create_bus  = false
  create_role = false
  rules = {
    "update-inspector-filter" = {
      event_pattern = jsonencode(
        {
          "source" : ["aws.ecr"],
          "detail-type" : ["ECR Image Action"],
          "detail" : {
            "action-type" : ["PUSH"]
          }
        }
      )
      enabled = true
    }
  }
  targets = {
    "update-inspector-filter" = [
      {
        name = "trigger-lambda"
        arn  = module.lambda_function.lambda_function_arn
        input_transformer = {
          input_paths = {
            image_tag = "$.detail.image-tag"
          }
          input_template = <<-EOT
            {
              "image_tag": <image_tag>, 
              "filter_arn": "${awscc_inspectorv2_filter.image_tag.arn}"
            }
          EOT 
        }
      }
    ]
  }
  tags = {
    Name = "update-inspector-filter-rule"
  }
}

CI/CDを発火させ、最新のコンテナイメージをECRレポジトリにPushする

ECRレポジトリにコンテナイメージがPushされる度に、Lambdaによって抑止ルールのフィルターの値が更新され、最新のコンテナイメージ以外の脆弱性が抑止される状態となる。

脆弱性を通知するEventrBidgeRuleとSNSトピックの作成

脆弱性を通知するEventrBidgeRuleとSNSトピックを作成する。
ステータスがACTIVEの脆弱性の検知をトリガーとする。

module "sns_topic_finding" {
  # Module source  
  source  = "terraform-aws-modules/sns/aws"
  version = "6.1.0"
  # Module arguments
  name                        = "inspector-finding"
  enable_default_topic_policy = false
  topic_policy_statements = {
    pub = {
      actions = ["sns:Publish"]
      principals = [{
        type        = "Service"
        identifiers = ["events.amazonaws.com"]
      }]
    }
  }
  tags = {
    Name = "inspector-finding"
  }
}

module "eventbridge_finding" {
  # Module source  
  source  = "terraform-aws-modules/eventbridge/aws"
  version = "3.8.0"
  # Module arguments
  create_bus                    = false
  create_role                   = false
  rules = {
    "inspector-finding" = {
      event_pattern = jsonencode(
        {
          "source" : ["aws.inspector2"],
          "detail-type" : ["Inspector2 Finding"],
          "detail" : {
            "status" : ["ACTIVE"]
          }
        }
      )
      enabled = true
    }
  }
  targets = {
    "inspector-finding" = {
      name = "send-inspector-finding-to-sns"
      arn  = module.sns_topic_finding.topic_arn
      input_transformer = {
        input_paths = {
          region             = "$.region"
          resources          = "$.resources[0]"
          time               = "$.time"
          accountId          = "$.detail.awsAccountId"
          severity           = "$.detail.severity"
          findingType        = "$.detail.type"
          findingDescription = "$.detail.description"
          vulnerabilityId    = "$.detail.packageVulnerabilityDetails.vulnerabilityId"
          sourceUrl          = "$.detail.packageVulnerabilityDetails.sourceUrl"
        }
        input_template = <<-EOT
          {
            "version": "1.0", 
            "source": "custom", 
            "content": {
              "textType": "client-markdown", 
              "title": ":amazon_inspector: Inspector Finding | <region> | Account: <accountId>",  
              "description": "*検出結果タイプ*\n<findingType>\n\n*重要度*\n<severity>\n\n*CVE*\n<sourceUrl>\n\n*検出時間*\n<time>(UTC)\n\n*アカウントID*\n<accountId>\n\n*リージョン*\n<region>\n\n*リソース*\n<resources>\n\n*説明*\n<findingDescription>", 
              "nextSteps": [
                "AWSアカウントにログインする",
                "Inspectorコンソールにアクセスして詳細を確認する"
              ]
            }
          }
        EOT 
      }
    }
  }
  tags = {
    Name = "inspector-finding-rule"
  }
}

さいごに

ベストプラクティスの「イメージタグはイミュータブルにする」に準拠しながら、最新のコンテナイメージの脆弱性のみを検知するには、必須なテクニックだと思ってます。
Inspectorのネイティブな機能で、これを実現できれば良いんですけどね。

脚注
  1. コンテナイメージのPush時ではなく、新たな脆弱性が出た時などに、既にPush済みのコンテナイメージに実施されるスキャン ↩︎

Discussion