InspectorのECR継続スキャンで最新のコンテナイメージの脆弱性のみを検知する方法
はじめに
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のネイティブな機能で、これを実現できれば良いんですけどね。
-
コンテナイメージのPush時ではなく、新たな脆弱性が出た時などに、既にPush済みのコンテナイメージに実施されるスキャン ↩︎
Discussion