こんにちは。
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 の管理画面から詳細な情報を確認することも可能です。