AWS CDKでSecurity HubアラートをSlackに自動通知してみた
こんにちは!製造ビジネステクノロジー部の小林です。
前回の記事では、SecurityHubの通知をSNSトピック経由でメールに送信する実装をご紹介しました。今回は通知先にSlackチャンネルを追加する方法をご紹介します。
コミュニケーションツールとしてSlackを日常的に使用している方にとっては、メールよりもSlackの方がアラート通知に気づきやすく、より便利に活用できるかもしれません!
やってみた
Slack Incoming Webhookの設定
まずはSlack側の準備をしましょう!Slack API サイトにアクセスします。
Create New App」→「From scratch」を選択します。
アプリ名と使用するワークスペースを設定します。
左メニューから「Incoming Webhooks」を選択し、「Activate Incoming Webhooks」をオンにします。
「Add New Webhook to Workspace」をクリックし、通知を送信するチャンネルを選択します。
以上でSlack側の設定はOKです!Webhook URLが生成されました。このURLは後ほどSSMとLambdaで使用します。
AWS Systems Manager Parameter Storeにwebhook URLを保存
セキュリティを考慮して、Webhook URLはLambdaソースに直接記述せず、Parameter Storeに保存していきます。
AWS管理コンソールから「Systems Manager」に移動 → 「パラメータストア」 → 「パラメータの作成」を選択します。
下記の情報を入力していきます。入力後「パラメータの作成」を選択します。
- 名前: /security/slack-webhook-url
- 利用枠: 標準
- タイプ: 文字列
- 値: 先ほどSlackの設定で作成した、「Webhook URL」をコピペします。
パラメーターの作成が完了しました!SSMの設定はOKです。次はAWS CDKでSlackのWebhook URLにPOSTするLambdaを実装していきます。
Lambda関数の実装
まずは、Systems Manager Parameter Storeに保存した、webhook URLを取得してLambdaの環境変数に定義します。修正する内容は下記になります。
- SSM Parameter Store用に追加import文を追加
import * as ssm from 'aws-cdk-lib/aws-ssm';
- SSM Parameter StoreからSlack Webhook URLを取得
const slackWebhookUrl = ssm.StringParameter.fromStringParameterAttributes(this, 'SlackWebhookUrl',
{
parameterName: '/security/slack-webhook-url',
}).stringValue;
- Slack Webhook URLをLambda関数の環境変数に追加
SLACK_WEBHOOK_URL: slackWebhookUrl, // Slack Webhook URLを環境変数として追加
ソース全体
import * as cdk 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 sns from 'aws-cdk-lib/aws-sns';
import * as subscriptions from 'aws-cdk-lib/aws-sns-subscriptions';
import { NodejsFunction } from 'aws-cdk-lib/aws-lambda-nodejs';
import * as path from 'path';
import * as lambda from 'aws-cdk-lib/aws-lambda';
import * as ssm from 'aws-cdk-lib/aws-ssm'; // SSM Parameter Store用に追加
export class SecurityAction extends cdk.Stack {
constructor(scope: Construct, id: string, props?: cdk.StackProps) {
super(scope, id, props);
// SNSトピック
const topic = new sns.Topic(this, 'SecurityTopic', {
displayName: '【開発環境】Security Hub Alert', // ここで表示名を設定
});
// メール購読
topic.addSubscription(
new subscriptions.EmailSubscription('pipipipiii0001@gmail.com')
);
// Slack Webhook URL(SSM Parameter Storeから取得)
// 注: 事前にParameter Storeに'/security/slack-webhook-url'という名前でURLを保存しておく必要があります
const slackWebhookUrl = ssm.StringParameter.fromStringParameterAttributes(this, 'SlackWebhookUrl',
{
parameterName: '/security/slack-webhook-url',
}).stringValue;
// Lambda関数
const handler = new NodejsFunction(this, 'SecurityHandler', {
entry: path.join(__dirname, '../lambda/index.ts'),
handler: 'handler',
runtime: lambda.Runtime.NODEJS_20_X,
bundling: {
forceDockerBundling: false,
externalModules: [
'aws-sdk',
],
},
environment: {
TOPIC_ARN: topic.topicArn,
SLACK_WEBHOOK_URL: slackWebhookUrl, // Slack Webhook URLを環境変数として追加
},
});
// SNS発行権限を付与
topic.grantPublish(handler);
// EventBridgeルール
const rule = new events.Rule(this, 'SecurityRule', {
eventPattern: {
source: ['aws.securityhub'],
detailType: ['Security Hub Findings - Imported'],
detail: {
findings: {
Severity: {
Label: ['CRITICAL', 'HIGH', 'MEDIUM'],
}
}
}
}
});
// ルールのターゲットとしてLambda関数を追加
rule.addTarget(new targets.LambdaFunction(handler));
}
}
Lambda関数でのSlack通知機能の実装
Lambda関数にSlack通知機能を追加します。この実装では、環境変数から取得したSlack Webhook URLに対して、セキュリティアラート情報をPOSTリクエストします。
import { SNSClient, PublishCommand } from '@aws-sdk/client-sns';
const snsClient = new SNSClient({});
export const handler = async (event: any): Promise<void> => {
const findings = event.detail.findings;
if (!findings || findings.length === 0) return;
const topicArn = process.env.TOPIC_ARN;
const slackWebhookUrl = process.env.SLACK_WEBHOOK_URL; // Slack Webhook URLを環境変数から取得
for (const finding of findings) {
await snsClient.send(new PublishCommand({
TopicArn: topicArn,
Subject: `Security Alert:開発環境 ${finding.Title.substring(0, 90)}`,
Message: `Severity: ${finding.Severity.Label}\nResource: ${finding.Resources[0]?.Id || 'Unknown'}`
}));
}
// Slackに通知を送信(今回は簡略化のためfindingをany型としています!本来は型定義を推奨します。)
if (slackWebhookUrl) {
try {
const slackMessage = {
text: `Security Alert:開発環境\n${findings.map((finding: any) => `*Title:* ${finding.Title}\n*Severity:* ${finding.Severity.Label}\n*Resource:* ${finding.Resources[0]?.Id || 'Unknown'}\n`).join('\n')}`,
};
const response = await fetch(slackWebhookUrl, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(slackMessage),
});
if (!response.ok) {
console.error('Failed to send message to Slack:', response.statusText);
}
} catch (error) {
console.error('Error sending to Slack:', error);
}
}
};
以上で実装は完了です!cdk deployでデプロイしたら、実際の動作を確認行なってみましょう。
動作確認
設定が正しく機能しているか確認するため、下記の手順でテストを実施してみます。
テスト手順
- Security Hubコンソールにアクセスし、重要度「MEDIUM」以上の検出結果を選択
- 選択したアラートのワークフローステータスを「通知済み」に変更
- アラートを手動で通知
結果
テスト実行後、Slackチャンネルに正常にSecurity Hubからのアラート通知が届きました。これにより、構築したアラート通知システムが正常に動作していることが確認できました!
おわりに
今回ご紹介したSlack連携によって、チームが日常的に目にするコミュニケーションツール上でセキュリティアラートを受け取ることができるようになります。これにより、セキュリティインシデントの発見から対応までのタイムラグを短縮し、より効果的に対処できるようになるかもしれません。皆様のAWS環境のセキュリティ向上に少しでもお役に立てれば幸いです。