こんにちは。
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 でデプロイする記事を投稿しました。
<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 の管理画面から詳細な情報を確認することも可能です。