GuardDutyの検出結果をBedrockで人間が読みやすい形にしてSlackに通知してみた

GuardDutyの検出結果をBedrockで人間が読みやすい形にしてSlackに通知してみた

Clock Icon2025.06.17

こんにちは、ゲームソリューション部のsoraです。
今回は、GuardDutyの検出結果をBedrockで読みやすくしてSlackに通知してみたことについて書いていきます。

構成

以下が今回作成する構成です。
GuardDutyの検出結果をEventBridge経由でLambdaに渡して、Bedrockで読みやすい形に変換、その後にSlackへ通知する流れです。

guardduty-bedrock-slack-01

先に通知したメッセージを貼ります。
guardduty-bedrock-slack-02

前提

Bedrockにて使用するモデルは有効化済みとします。

GuardDutyの有効化・設定

GuardDutyを使用するため、有効化していなければ有効化しておきます。
EventBridgeへの送信について、デフォルトでイベントバスdefaultへ送信する形になっています。
EventBridgeルールを作成することで他サービスへの通知が可能です。

guardduty-bedrock-slack-03

Terraformでのリソース作成(EventBridgeルール・Lambda)

AWSの他のリソースはTerraformで構築します。

EventBridgeルールで、GuardDutyからの以下のイベントをキャッチして、ターゲットであるLambdaに流します。

{
  "detail-type": ["GuardDuty Finding"],
  "source": ["aws.guardduty"]
}

Lambdaでは、AI用のプロンプトを記載してBedrockに通知内容を含めて呼び出します。
その結果として生成された文章を入れてSlackへ通知します。

import json
import boto3
import requests
import os
from datetime import datetime

def lambda_handler(event, context):
    try:
        print(f"Received event: {json.dumps(event)}")
        finding = event['detail']
        print(f"Processing finding: {finding.get('type', 'Unknown')}")

        # Bedrock client
        bedrock = boto3.client('bedrock-runtime')

        # AI用プロンプト作成 (Claude Sonnet 4向け最適化)
        prompt = f"""
あなたはAWSセキュリティの専門家です。以下のGuardDuty検知結果を分析し、技術者向けに日本語で要約してください:

## 検知内容
- **脅威タイプ**: {finding.get('type', 'Unknown')}
- **重要度**: {finding.get('severity', 'Unknown')}
- **対象リソース**: {finding.get('resource', {}).get('resourceType', 'Unknown')}
- **詳細説明**: {finding.get('description', 'No description')}

## 分析要求
以下の形式で回答してください:
**脅威概要**: [簡潔な説明]
**リスクレベル**: [重要度の説明]
**推奨対応**: [具体的なアクション]
"""

        # Bedrock呼び出し (Claude 3.5 Sonnet - 安定版)
        response = bedrock.invoke_model(
            modelId='anthropic.claude-3-5-sonnet-20240620-v1:0',
            body=json.dumps({
                "anthropic_version": "bedrock-2023-05-31",
                "max_tokens": 2000,
                "messages": [{"role": "user", "content": prompt}]
            })
        )

        ai_response = json.loads(response['body'].read())
        ai_explanation = ai_response['content'][0]['text']
        print(f"AI analysis completed. Response length: {len(ai_explanation)}")

        # Slack通知
        slack_payload = {
            "text": "GuardDuty脅威検知",
            "blocks": [
                {
                    "type": "header",
                    "text": {
                        "type": "plain_text",
                        "text": "GuardDuty脅威検知アラート"
                    }
                },
                {
                    "type": "section",
                    "text": {
                        "type": "mrkdwn",
                        "text": ai_explanation
                    }
                },
                {
                    "type": "context",
                    "elements": [
                        {
                            "type": "mrkdwn",
                            "text": f"検知時刻: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}"
                        }
                    ]
                }
            ]
        }

        print(f"Sending notification to Slack...")
        response = requests.post(
            os.environ['SLACK_WEBHOOK_URL'],
            json=slack_payload,
            headers={'Content-Type': 'application/json'}
        )
        print(f"Slack response status: {response.status_code}")

        return {
            'statusCode': 200,
            'body': json.dumps('Success')
        }

    except Exception as e:
        print(f"Error: {str(e)}")
        return {
            'statusCode': 500,
            'body': json.dumps(f'Error: {str(e)}')
        }

Terraformのコードは以下です。
variables.tfterraform.tfvars、IAM用のjsonなどは本ブログでは省略します。

### provider ###
terraform {
  required_providers {
    aws = {
      source  = "hashicorp/aws"
      version = "6.0.0-beta3"
    }
  }
}
# AWSプロバイダーの定義
provider "aws" {
  region = "ap-northeast-1"
}

### Resource ###
# EventBridge Rule
resource "aws_cloudwatch_event_rule" "guardduty_findings" {
  name        = "guardduty-findings-rule"
  description = "Capture GuardDuty findings"

  event_pattern = jsonencode({
    source      = ["aws.guardduty"]
    detail-type = ["GuardDuty Finding"]
  })
}

# EventBridge Target
resource "aws_cloudwatch_event_target" "lambda_target" {
  rule      = aws_cloudwatch_event_rule.guardduty_findings.name
  target_id = "GuardDutyLambdaTarget"
  arn       = aws_lambda_function.guardduty_processor.arn
}

# Lambda Permission for EventBridge
resource "aws_lambda_permission" "allow_eventbridge" {
  statement_id  = "AllowExecutionFromEventBridge"
  action        = "lambda:InvokeFunction"
  function_name = aws_lambda_function.guardduty_processor.function_name
  principal     = "events.amazonaws.com"
  source_arn    = aws_cloudwatch_event_rule.guardduty_findings.arn
}

# IAM Role for Lambda
resource "aws_iam_role" "lambda_role" {
  name = "guardduty-processor-role"

  assume_role_policy = file("${path.module}/policies/lambda-assume-role-policy.json")
}

# IAM Policy for Lambda
resource "aws_iam_role_policy" "lambda_policy" {
  name = "guardduty-processor-policy"
  role = aws_iam_role.lambda_role.id

  policy = file("${path.module}/policies/lambda-execution-policy.json")
}

# Lambda Function Archive
data "archive_file" "lambda_zip" {
  type        = "zip"
  source_dir  = "${path.module}/lambda"
  output_path = "${path.module}/lambda_deployment.zip"
}

# Lambda Function
resource "aws_lambda_function" "guardduty_processor" {
  filename         = data.archive_file.lambda_zip.output_path
  function_name    = "guardduty-processor"
  role            = aws_iam_role.lambda_role.arn
  handler         = "lambda.lambda_handler"
  runtime         = "python3.9"
  timeout         = 60
  source_code_hash = data.archive_file.lambda_zip.output_base64sha256

  environment {
    variables = {
      SLACK_WEBHOOK_URL = var.slack_webhook_url
    }
  }

  depends_on = [
    aws_iam_role_policy.lambda_policy,
    aws_cloudwatch_log_group.lambda_logs,
  ]
}

# CloudWatch Log Group
resource "aws_cloudwatch_log_group" "lambda_logs" {
  name              = "/aws/lambda/guardduty-processor"
  retention_in_days = 7
}

動作確認

GuardDutyのサンプルFinding(テスト用の偽の脅威検知データ)を作成することで検知・通知を試してみます。
私は全種類でサンプルを作成しましたが、約360個のデータが出てテスト後にアーカイブにするのが手間だったため、何かを指定して作成した方が良いと思います。

# detector-idの取得
aws guardduty list-detectors

# 全種類のサンプルFinding生成
aws guardduty create-sample-findings \
  --detector-id 70c5e5972290759d4fb682357a2882ec

サンプルデータ作成後に以下のようにSlackに通知が来ました。
もしLambdaなどでエラーが出た際は、一度サンプルデータをアーカイブして、修正後に再度サンプルデータを作成し直すことで動作します。

guardduty-bedrock-slack-02

最後に

今回は、GuardDutyの検出結果をBedrockで読みやすくしてSlackに通知してみたことを記事にしました。
どなたかの参考になると幸いです。

Share this article

facebook logohatena logotwitter logo

© Classmethod, Inc. All rights reserved.