こんにちは。たかやまです。
こちらのブログのようにGuardDutyはSlackに通知することができますが、今回はこのGuardDutyの通知を重要度別にチャンネルへ飛ばす実装をしたいと思います。
構成
構成
Slackイメージ図
設定方法
構築はCDKを利用しています。リポジトリはこちらです。
SNS
2つのSlackチャンネルに連携するために、スタンダードのSNS トピックを2つ用意します。
- Severity Highを通知するトピック
- Severity Middleを通知するトピック
サブスクリプションはこのあとのチャットボットとの紐付けで設定されるため、この段階では設定不要です。
cdk(クリックで展開)
lib/sns-stack.ts
import { Stack, StackProps } from "aws-cdk-lib";
import { Construct } from "constructs";
import * as sns from "aws-cdk-lib/aws-sns";
export interface props extends StackProps {
projectName: String;
envName: String;
}
export class SnsStack extends Stack {
public readonly snsTopicForHigh: sns.ITopic;
public readonly snsTopicForMiddleLow: sns.ITopic;
constructor(scope: Construct, id: string, props: props) {
super(scope, id, props);
/**
* Create a SNS for the eventbridge
*/
this.snsTopicForHigh = new sns.Topic(this, `${props.projectName}-${props.envName}-for-guardduty-high`, {
topicName: `${props.projectName}-${props.envName}-for-guardduty-high`,
displayName: `${props.projectName}-${props.envName}-for-guardduty-high`,
});
this.snsTopicForMiddleLow = new sns.Topic(this, `${props.projectName}-${props.envName}-for-guardduty-middle-low`, {
topicName: `${props.projectName}-${props.envName}-for-guardduty-middle`,
displayName: `${props.projectName}-${props.envName}-for-guardduty-middle`,
});
}i
}
Lambda
重要度別にGuardDutyイベントを振り分ける処理をLambdaで行います。
単に重要度だけで分けるのであれば、EventBridgeのフィルター機能が利用可能です。
今回は後述するイベントのカスタマイズもしたいため、Lambdaを使っていきます。
Lambdaはseverity
の値で通知するSNSトピックを振り分ける処理を行っています。
重要度レベル | Severity |
---|---|
[High] (高) | 8.9 - 7.0 |
[Medium] (中) | 6.9 - 4.0 |
[Low] (低) | 3.9 - 1.0 |
lambda_function.py
import json
import os
import boto3
SNS_TOPIC_ARN_HIGH = os.environ["SNS_TOPIC_ARN_HIGH"]
SNS_TOPIC_ARN_MIDDLE_LOW = os.environ["SNS_TOPIC_ARN_MIDDLE_LOW"]
client = boto3.client("sns")
def lambda_handler(event, context):
message = json.dumps(event)
print(type(event["detail"]["severity"]))
if event["detail"]["severity"] >= 7:
client.publish(TopicArn=SNS_TOPIC_ARN_HIGH, Message=message)
else:
client.publish(TopicArn=SNS_TOPIC_ARN_MIDDLE_LOW, Message=message)
cdk(クリックで展開)
lib/lambda-stack.ts
import * as path from "path";
import { Stack, StackProps } from "aws-cdk-lib";
import { Construct } from "constructs";
import * as iam from "aws-cdk-lib/aws-iam";
import * as lambda from "aws-cdk-lib/aws-lambda";
import * as sns from "aws-cdk-lib/aws-sns";
export interface props extends StackProps {
projectName: String;
envName: String;
snsTopicForHigh: sns.ITopic;
snsTopicForMiddleLow: sns.ITopic;
}
export class LambdaStack extends Stack {
public readonly lambdaFunction: lambda.IFunction;
constructor(scope: Construct, id: string, props: props) {
super(scope, id, props);
/**
* Create a Role
*/
const role = new iam.Role(this, `${props.projectName}-${props.envName}-role`, {
assumedBy: new iam.ServicePrincipal("lambda.amazonaws.com"),
});
role.addManagedPolicy(iam.ManagedPolicy.fromAwsManagedPolicyName("AmazonSNSFullAccess"));
role.addManagedPolicy(
iam.ManagedPolicy.fromManagedPolicyArn(
this,
`${props.projectName}-${props.envName}-lambdabasic`,
"arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole"
)
);
/**
* Create a Lambda
*/
this.lambdaFunction = new lambda.Function(this, `${props.projectName}-${props.envName}-lambda`, {
functionName: `${props.projectName}-${props.envName}-lambda`,
runtime: lambda.Runtime.PYTHON_3_9,
handler: "lambda_function.lambda_handler",
environment: {
SNS_TOPIC_ARN_HIGH: props.snsTopicForHigh.topicArn,
SNS_TOPIC_ARN_MIDDLE_LOW: props.snsTopicForMiddleLow.topicArn,
},
code: lambda.Code.fromAsset(path.join(__dirname, "./assets")),
role: role,
});
}
}
EventBridge
GuardDutyが発生させたイベントはEventBridge経由でLambdaに連携していきます。
イベントのフィルターはLambdaで実施するので、EventBridgeはシンプルなイベントパターン設定となります。
ターゲットは作成したLambdaを指定します。
cdk(クリックで展開)
/lib/indeeventbridge-stack.ts
import { Stack, StackProps } from "aws-cdk-lib";
import { Construct } from "constructs";
import * as events from "aws-cdk-lib/aws-events";
import * as targets from "aws-cdk-lib/aws-events-targets";
import * as lambda from "aws-cdk-lib/aws-lambda";
export interface props extends StackProps {
projectName: String;
envName: String;
lambdaFunction: lambda.IFunction;
}
export class EventBridgeStack extends Stack {
constructor(scope: Construct, id: string, props: props) {
super(scope, id, props);
/**
* Create a EventBridge
*/
const rule = new events.Rule(this, `${props.projectName}-${props.envName}-events-rule`, {
ruleName: `${props.projectName}-${props.envName}-events-rule`,
eventPattern: {
source: ["aws.guardduty"],
detailType: ["GuardDuty Finding"],
},
targets: [new targets.LambdaFunction(props.lambdaFunction)],
});
}
}
ChatBot
初期設定
Slackの設定がなければ、手動で設定していきます。
slackを利用している場合リダイレクトされるので許可します。
ワークスペースが作成されます。
Cfn/CDKで作成する場合、ここで表示されるワークスペースIDを利用する形になります。
あとは、チャンネルIDと先程作成したSNSトピックを紐付けます。
cdk(クリックで展開)
/lib/chatbot-stack.ts
import { Stack, StackProps } from "aws-cdk-lib";
import { Construct } from "constructs";
import * as chatbot from "aws-cdk-lib/aws-chatbot";
import * as sns from "aws-cdk-lib/aws-sns";
export interface props extends StackProps {
projectName: String;
envName: String;
snsTopicForHigh: sns.ITopic;
snsTopicForMiddleLow: sns.ITopic;
slackWorkspaceId: string;
slackChannelId1: string;
slackChannelId2: string;
}
export class ChatBotStack extends Stack {
constructor(scope: Construct, id: string, props: props) {
super(scope, id, props);
/**
* Create a Chatbot
*/
const slackChannel1 = new chatbot.SlackChannelConfiguration(this, `${props.projectName}-${props.envName}-guardduty-high`, {
slackChannelConfigurationName: "guardduty-high",
slackWorkspaceId: props.slackWorkspaceId,
slackChannelId: props.slackChannelId1,
notificationTopics: [props.snsTopicForHigh],
});
const slackChannel2 = new chatbot.SlackChannelConfiguration(this, `${props.projectName}-${props.envName}-guardduty-middle-low`, {
slackChannelConfigurationName: "guardduty-middle-low",
slackWorkspaceId: props.slackWorkspaceId,
slackChannelId: props.slackChannelId2,
notificationTopics: [props.snsTopicForMiddleLow],
});
}
}
テスト
設定が完了したら、GuardDutyの結果サンプルの生成
を使って通知が行くか試します。
設定が問題なければ、最初のイメージ図のようなかたちで通知がいくかと思います。
通知のカスタマイズ
Lambdaを利用した理由は、通知のカスタマイズをしたいためです。
今回はAWSが定義している重要度とは別に特定の通知を重要度HighのSlackに通知したい場合を想定してカスタマイズしてみます。
例として、重要度Mediumの通知Exfiltration:S3/ObjectRead.Unusual
を重要度Highのslackチャンネルに通知してみたいと思います。
サンプルコードはこちらです。
lambda_function.py
import json
import os
import boto3
SNS_TOPIC_ARN_HIGH = os.environ["SNS_TOPIC_ARN_HIGH"]
SNS_TOPIC_ARN_MIDDLE_LOW = os.environ["SNS_TOPIC_ARN_MIDDLE_LOW"]
client = boto3.client("sns")
def lambda_handler(event, context):
high_list = ["Exfiltration:S3/ObjectRead.Unusual"]
message = json.dumps(event)
if event["detail"]["severity"] >= 7 or event["detail"]["type"] in high_list:
client.publish(TopicArn=SNS_TOPIC_ARN_HIGH, Message=message)
else:
client.publish(TopicArn=SNS_TOPIC_ARN_MIDDLE_LOW, Message=message)
再度結果サンプルの生成
を行うと、重要度Highのチャンネルに無事通知されていることを確認できました。
※severityの表記やアイコン色も変えたい場合は、コード内でseverityの値を書き換える
まとめ
重要度別にSlack通知を実装してみました。
Lambdaを使うことで重要度をカスタマイズできるので、独自の基準で重要度を管理したい場合にはぜひLambdaをご活用ください。
以上、たかやまでした。