AWS ECRでイメージスキャンした結果をSlackに通知する
AWSのECRではDockerイメージを自動でスキャンすることができます。しかしスキャンした結果は勝手には通知してくれません。そこでEventBridgeを使ってSlackに通知するAWS CDKコード例を紹介します。CDKですが、内容は他のツールでも流用できると思います。
また、EventBridgeとSNSを組み合わせたり、Lambdaを組み合わせたりする方法が他の記事で紹介されていますが、実はEventBridge単体でSlackのIncomingWebhookにAPIリクエストして通知できます。
サンプルコード
いきなりサンプルコードを紹介します。EventBridgeで作成する通知は2種類です。
- 脆弱性がHIGH / CRITICALが1件以上あるアラートモード
- 脆弱性のHIGH / CRITICALがない通常モード
// NOTIFY_SLACK_INCOMING_URL 環境変数にSlack IncomingWebhookのURLをセットする
import {
Stack,
StackProps,
SecretValue,
} from "aws-cdk-lib";
import { Construct } from "constructs";
import * as events from "aws-cdk-lib/aws-events";
import { PolicyDocument, Role, ServicePrincipal } from "aws-cdk-lib/aws-iam";
export class SampleStack extends Stack {
private destination: events.ApiDestination;
private execRole: Role;
constructor(scope: Construct, id: string, props?: StackProps) {
super(scope, id, props);
this.main();
}
main() {
// 共通で利用するSlack送信先定義を行う
this.createDestination();
// 実行ロールを作成する
this.createExecRole();
// アラート用のルールを作成する
this.buildAlertRule();
// 緊急性の脆弱性が見つからなかったルールを作成する
this.buildOkRule();
}
private buildAlertRule() {
const eventPattern = {
source: ["aws.ecr"],
"detail-type": ["ECR Image Scan"],
detail: {
"scan-status": ["COMPLETE"],
"finding-severity-counts": {
$or: [
{
CRITICAL: [{ numeric: [">", 0] }],
},
{
HIGH: [{ numeric: [">", 0] }],
},
],
},
},
};
const text = `❌ ECRにpushされたイメージの脆弱性スキャンが終わりました
• スキャン日時: <time>
• リポジトリ名: <repository>
• Critical: *<Critical>件*
• High: *<High>件*
詳しくは詳細ページを確認してください
https://ap-northeast-1.console.aws.amazon.com/ecr/repositories/private/${process.env.CDK_DEFAULT_ACCOUNT}/<repository>/_/image/<imageSha>/scan-results`.replace(
/\n/g,
"\\n",
);
this.buildEventBridgeRule("NotifyImageScanResultAlert", eventPattern, text);
}
private buildOkRule() {
const eventPattern = {
source: ["aws.ecr"],
"detail-type": ["ECR Image Scan"],
detail: {
"scan-status": ["COMPLETE"],
"finding-severity-counts": {
CRITICAL: [{ exists: false }],
HIGH: [{ exists: false }],
},
},
};
const text = `✅ ECRにpushされたイメージの脆弱性スキャンが終わりました
• スキャン日時: <time>
• リポジトリ名: <repository>
脆弱性は確認されませんでした`.replace(/\n/g, "\\n");
this.buildEventBridgeRule("NotifyImageScanResultOk", eventPattern, text);
}
private createDestination() {
const connection = new events.Connection(this, "SlackConnection", {
authorization: events.Authorization.apiKey(
"dummy-key",
SecretValue.unsafePlainText("dummy"),
),
});
this.destination = new events.ApiDestination(
this,
"SlackNotifyImageScanResult",
{
connection,
endpoint: process.env.NOTIFY_SLACK_INCOMING_URL || "",
httpMethod: events.HttpMethod.POST,
},
);
}
private createExecRole() {
this.execRole = new Role(this, "EventBridgeExecRole", {
assumedBy: new ServicePrincipal("events.amazonaws.com"),
inlinePolicies: {
inlinePolicies: PolicyDocument.fromJson({
Version: "2012-10-17",
Statement: [
{
Effect: "Allow",
Action: "events:InvokeApiDestination",
Resource: "*",
},
],
}),
},
});
}
private buildEventBridgeRule(
name: string,
eventPattern: events.EventPattern,
message: string,
) {
const inputTransformerProperty: events.CfnRule.InputTransformerProperty = {
inputTemplate: `{"text":"${message}"}`,
inputPathsMap: {
time: "$.time",
repository: "$.detail.repository-name",
imageSha: "$.detail.image-digest",
Critical: "$.detail.finding-severity-counts.CRITICAL",
High: "$.detail.finding-severity-counts.HIGH",
},
};
const target: events.CfnRule.TargetProperty = {
arn: this.destination.apiDestinationArn,
id: "SlackNotifyImageScanResult",
inputTransformer: inputTransformerProperty,
roleArn: this.execRole.roleArn,
};
// L1レベルでないと細かい設定ができない(トランスフォーマーなど)
new events.CfnRule(this, name, {
eventPattern,
name,
targets: [target],
});
}
}
結果で通知内容を変更する
EventBridgeの入力テンプレートには、初期値を設定することができない&条件分岐ができないため、脆弱性レポートの結果次第でSlackに通知する内容を切り替えることはできません。
素直にEventBridgeのルールを別で用意する必要があります。ルールのトリガーとなるイベントパターンでフィルタリングすることができるので、ここでアラートか通常かを分岐させます。
以下のイベントパターンは finding-severity-counts.CRITICAL
と finding-severity-counts.HIGH
のキーが存在しないときに脆弱性がないと判断させています。
const eventPattern = {
source: ["aws.ecr"],
"detail-type": ["ECR Image Scan"],
detail: {
"scan-status": ["COMPLETE"],
"finding-severity-counts": {
CRITICAL: [{ exists: false }],
HIGH: [{ exists: false }],
},
},
逆に脆弱性がある判断は CRITICAL: [{ numeric: [">", 0] }]
のようにします。ここの詳しい書き方はドキュメントを参照してください。
Slack IncomingWebhook URL登録
EventBridgeから直接WebAPIをリクエストすることができます。が、大前提として認証を求めるAPIじゃないと登録ができません。しかし普通のIncomingWebhook URLは単なるURLです。そこでダミーのリクエストヘッダを登録することで回避しています。
const connection = new events.Connection(this, "SlackConnection", {
authorization: events.Authorization.apiKey(
"dummy-key",
SecretValue.unsafePlainText("dummy"),
),
});
動作確認用のDockerイメージ
脆弱性があるDockerイメージがないと動作確認ができません。僕はとりあえず古いNode.jsイメージを利用しました。
FROM node:16.0.0-buster-slim
逆に脆弱性がないサンプルイメージは軽量なAlpineを利用しました。
FROM alpine
これらのイメージをECRにプッシュして動作確認をすると良いでしょう。
Discussion