【Terraform】Inspector V2 の ECR 拡張スキャン結果を Slack に通知する

こんにちは。
ECR の拡張スキャン結果を Slack に通知するリソースを Terraform で書いてみました。
※ 通知内容は Lambda でパースするので、極論 EventBridge の Input Transformer は不要です。

/**
# NOTE: Enhanced-Scanning EventBridge
*/

// 拡張スキャン Finding 実行結果のイベントルール
resource "aws_cloudwatch_event_rule" "ecr_enhanced_scanning_finding_event_rule" {
  name        = "${var.project}-${var.environment}-ecr-enhanced-scanning-finding-event-rule"
  description = "${var.project}-${var.environment}-ecr-enhanced-scanning-finding-event-rule"

  event_pattern = <<EOF
{
  "source": ["aws.inspector2"],
  "detail-type": ["Inspector2 Finding"],
  "detail": {
    "status": ["ACTIVE"],
    "severity": ["MEDIUM", "HIGH", "CRITICAL"]
  }
}
EOF
}

// 拡張スキャン Finding 実行結果の SNS 通知ルール
resource "aws_cloudwatch_event_target" "ecr_enhanced_scanning_finding_event_target" {
  rule      = aws_cloudwatch_event_rule.ecr_enhanced_scanning_finding_event_rule.name
  target_id = "${var.project}-${var.environment}-ecr-enhanced-scanning-finding-notification"
  arn       = aws_sns_topic.ecr_enhanced_scanning_finding.arn

  // JSON を整形して出力
  input_transformer {
    input_paths = {
      resources = "$.resources",
      firstobservedat = "$.detail.firstObservedAt",
      lastobservedat = "$.detail.lastObservedAt",
      updatedat = "$.detail.updatedAt",
      title = "$.detail.title",
      status = "$.detail.status",
      severity = "$.detail.severity",
      inspectorscore   = "$.detail.inspectorScore",
      sourceurl   = "$.detail.packageVulnerabilityDetails.sourceUrl",
    }
    input_template = <<EOF
{
  "resources": <resources>,
  "firstObservedAt": <firstobservedat>,
  "lastObservedAt": <lastobservedat>,
  "updatedAt": <updatedat>,
  "title": <title>,
  "status": <status>,
  "severity": <severity>,
  "inspectorScore": <inspectorscore>,
  "sourceUrl": <sourceurl>
}
EOF
  }
}

/**
# NOTE: Enhanced-Scanning SNS
*/

// 拡張スキャン Finding 実行結果の SNS 通知トピック
resource "aws_sns_topic" "ecr_enhanced_scanning_finding" {
  name = "ecr-enhanced-scanning-finding-notification"
}

// 拡張スキャン Finding 実行結果の SNS 通知トピック ポリシー
resource "aws_sns_topic_policy" "ecr_enhanced_scanning_finding" {
  arn    = aws_sns_topic.ecr_enhanced_scanning_finding.arn
  policy = data.aws_iam_policy_document.eventbridge_to_sns_finding.json
}

// SNS Finding Publish ポリシー
data "aws_iam_policy_document" "eventbridge_to_sns_finding" {
  statement {
    effect  = "Allow"
    actions = ["SNS:Publish"]

    principals {
      type        = "Service"
      identifiers = ["events.amazonaws.com"]
    }

    resources = [aws_sns_topic.ecr_enhanced_scanning_finding.arn]
  }
}

/**
# NOTE: IAM Role For Enhanced-Scanning Lambda
*/

// Lambda role
resource "aws_iam_role" "enhanced_scanning_lambda_role" {
  name               = "${var.project}-${var.environment}-ecr-enhanced-scanning-role"
  assume_role_policy = <<EOF
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Principal": {
        "Service": "lambda.amazonaws.com"
      },
      "Action": "sts:AssumeRole"
    }
  ]
}
EOF
}

// Lambda 基本実行 ポリシー アタッチ
resource "aws_iam_role_policy_attachment" "lambda_execution" {
  role       = aws_iam_role.enhanced_scanning_lambda_role.name
  policy_arn = "arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole"
}

// CloudWatch ReadOnly ポリシー アタッチ
resource "aws_iam_role_policy_attachment" "lambda_to_cw" {
  role       = aws_iam_role.enhanced_scanning_lambda_role.name
  policy_arn = "arn:aws:iam::aws:policy/CloudWatchReadOnlyAccess"
}

// SSM パラメータ 読み込み ポリシー
resource "aws_iam_policy" "lambda_to_ssm" {
  name = "${var.project}-${var.environment}-ecr-enhanced-scanning-policy"
  path = "/"

  policy = <<EOF
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Action": [
        "ssm:GetParameters",
        "secretsmanager:GetSecretValue",
        "kms:Decrypt"
      ],
      "Resource": "*"
    }
  ]
}
EOF
}

// SSM パラメータ 読み込み ポリシー アタッチ
resource "aws_iam_role_policy_attachment" "lambda_to_ssm" {
  role       = aws_iam_role.enhanced_scanning_lambda_role.name
  policy_arn = aws_iam_policy.lambda_to_ssm.arn
}

// 環境変数暗号化 KMS
resource "aws_kms_key" "enhanced_scanning_lambda" {
  description             = "${var.project}-${var.environment}-ecr-enhanced-scanning-lambda-kms"
  deletion_window_in_days = 7
  enable_key_rotation     = true
  is_enabled              = true
}

// 環境変数暗号化 KMS Alias
resource "aws_kms_alias" "enhanced_scanning_lambda" {
  name          = "alias/${var.project}/${var.environment}/ecr_enhanced_scanning_lambda_kms_key"
  target_key_id = aws_kms_key.enhanced_scanning_lambda.id
}

Lambda

EventBridge から SNS に通知されたことをトリガーに、データをパースして Slack の API を叩く Lambda を用意します。
※ EventBridge から直接 Lambda を叩くことも可能です。
今回は AWS にてデフォルトで用意されている cloudwatch-alarm-to-slack-python を調整して使用します。

Lambda > 関数 > 関数の作成 > 設計図の使用 > cloudwatch-alarm-to-slack-python

・既存のロールを使用する で Terraform で作成したロールを選択

・SNS トピック で Terraform で作成した SNS トピックを選択

・環境変数 の kmsEncryptedHookUrl に Slack のペイロード URL を定義
※ https:// のスキーマは除く

・転送時の暗号化 を選択し、Terraform で作成した KMS キーで暗号化

※ 06/17 追記: Lambda も Terraform でデプロイする記事を投稿しました。

【Terraform】 ECR 拡張スキャン通知用の Lambda を自動デプロイしてみる

<CHANNEL_NAME> を通知する Slack ルームの名称に置換して下記コードに置き換えます。

import boto3
import json
import logging
import os

from base64 import b64decode
from urllib.request import Request, urlopen
from urllib.error import URLError, HTTPError
from urllib.parse import quote

ENCRYPTED_HOOK_URL = os.environ['kmsEncryptedHookUrl']

HOOK_URL = "https://" + boto3.client('kms').decrypt(
    CiphertextBlob=b64decode(ENCRYPTED_HOOK_URL),
    EncryptionContext={'LambdaFunctionName': os.environ['AWS_LAMBDA_FUNCTION_NAME']}
)['Plaintext'].decode('utf-8')

logger = logging.getLogger()
logger.setLevel(logging.INFO)


def lambda_handler(event, context):
    logger.info("Event: " + str(event))
    message = json.loads(event['Records'][0]['Sns']['Message'])
    logger.info("Message: " + str(message))

    resources = message['resources'][0]
    title = message['title']
    severity = message['severity']
    inspector_score = message['inspectorScore']
    source_url = message['sourceUrl']

    slack_message = {
        'channel': "#<CHANNEL_NAME>",
        'text': "<!here> \n ■ <https://ap-northeast-1.console.aws.amazon.com/ecr/repositories?region=ap-northeast-1 | *ECR*> : _スキャン結果に脆弱性が存在します。_ \n *Resources* : %s \n *Title* : %s \n *Severity* : %s \n *Inspector_score* : %s \n *Source_url* : %s \n" % (resources, title, severity, inspector_score, source_url)
    }


    logger.info("HOOK_URL: " + str(HOOK_URL))
    req = Request(HOOK_URL, json.dumps(slack_message).encode('utf-8'))
    try:
        response = urlopen(req)
        response.read()
        logger.info("Message posted to %s", slack_message['channel'])
    except HTTPError as e:
        logger.error("Request failed: %d %s", e.code, e.reason)
    except URLError as e:
        logger.error("Server connection failed: %s", e.reason)

動作検証

実際にコンテナイメージを ECR に push して動作を確認します。

aws ecr get-login-password --region ap-northeast-1 | docker login --username AWS --password-stdin xxxxxxxxxx.dkr.ecr.ap-northeast-1.amazonaws.com

DOCKER_BUILDKIT=1 docker build . -f Dockerfile -t src_app
docker tag src_app xxxxxxxxxx.dkr.ecr.ap-northeast-1.amazonaws.com/sanuki-inspection-repository
docker push xxxxxxxxxx.dkr.ecr.ap-northeast-1.amazonaws.com/sanuki-inspection-repository

下記のように Slack にメッセージが通知されれば成功です。
ECR の管理画面から詳細な情報を確認することも可能です。

投稿者: snk

保有資格 ・TOEIC 835 ・LPIC 304 ・AWS SAA/SAP ・GCP PCA ・IPA SC

コメントを残す

メールアドレスが公開されることはありません。 が付いている欄は必須項目です