AWS Lambdaを使ってAWS Security Hub CSPMの検出結果を日本語で通知する方法

皆さんこんにちは
気が付いたら誕生日まで残り一か月、どうも。いとさんです。

誕生日といえば、ホールケーキですね。
小さいときは丸ごと一つ食べるのが夢で、中学生の時に一度だけですが小さいホールケーキを丸ごと一つ買ってもらいました。
当時は運動部だったので余裕で食べきった記憶があります。今はアフタヌーンティーでおなかいっぱいです

さて今回はAWS Lambdaを使ってAWS Security Hub CSPMの検出結果を日本語で通知する方法について調査・実装してみました。

はじめに

従来、本サイトの運用ではAWS Security Hub CSPMで検出があった際にをAmazon EventBridge ⇒ Amazon SNS経由でメールで通知しておりました。
特に加工していなかったこともあり、以下のように検出結果がJSON形式で送られてきていました。

今回は、この通知を日本語でわかりやすく、かつ通知が完了したらNOTIFED(確認済み)にするように構築していこうと思います。

 

アーキテクチャ図

設定方法

前提として

  • AWSアカウントが有効
  • Security Hub CSPMが有効化済み
  • 通知先メールアドレスが決まっている
  • IAMユーザに十分な権限があること(Amazon SNS、Amazon Translate, Amazon EventBridgeの呼び出し)

上記の内容が設定されていることが必要です。

使用するIAMユーザーに十分な権限が設定されていな場合は、
IAM>ロール>ロールの作成から、
ユースケースをAWS Lambdaに設定し必要な権限を付与、ロールを作成します。

 

1. SNSトピックの作成

AWSコンソール → SNS → トピック → 「新しいトピックを作成」

名称例: securityhub-notify-jp
この時タイプはスタンダードに設定してください(サブスクリプションをEメールで設定するため)

作成されたトピックのarnを控えます(例:arn:aws:sns:ap-northeast-1:123456789012:securityhub-notify-jp)

2. サブスクリプションの作成

SNSが送られる方法を設定します。

サブスクリプション>サブスクリプションの作成

先ほど控えたトピックのarnを選択しプロトコルをEメールで設定します。
エンドポイントは送信先に設定したいメールアドレスを記入します。

以下のメールが送られてくるので『Confirm subscription』をクリックしステータスを確認済みにします。

 

3. Lambda関数(日本語化+SNS Publish)の作成

Lambda関数の設定を行います。
以下のように設定します。

関数名:(例:SecurityHubJapaneseNotify)
ランタイム: Python3.13
実行ロール: 既存のロールを使用するを選択し、SNS Publish、Translate権限のあるLambda関数用のIAMロールを選択します

コード例: 

import boto3
import os
from datetime import datetime

def lambda_handler(event, context):
    sns = boto3.client('sns')
    translate = boto3.client('translate')
    securityhub = boto3.client('securityhub')

    findings = event['detail'].get('findings', [])
    print("取得したfindings:", findings)

    if not findings:
        print("No findings detected.")
        return {'status': 'No findings'}

    for finding in findings:
        print("Processing finding:", finding.get('Id', 'No Id'))

        if finding.get('Workflow', {}).get('Status') != 'NEW':
            print("Skipping finding (not NEW):", finding.get('Id', 'No Id'))
            continue

        # Workflow Statusを NOTIFIED に変更
        sh_response = securityhub.batch_update_findings(
            FindingIdentifiers=[
            {
                'Id': finding['Id'],
                'ProductArn': finding['ProductArn']
            }
        ],
        Workflow={'Status': 'NOTIFIED'}
        )
        print("SecurityHub update response:", sh_response)

        # 2つ目の処理: 詳細な情報を含むSNS通知 (CRITICAL/HIGHのみ)
        en_severity = finding.get('FindingProviderFields', {}).get('Severity', {}).get('Label', 'UNKNOWN')
        if en_severity in ['CRITICAL', 'HIGH']:
            en_title = finding.get('Title', 'No Title')
            en_desc = finding.get('Description', 'No Description')
            en_region = event.get('region', '不明')
            en_account = event.get('account', '不明')
            en_time = finding.get('FirstObservedAt', event.get('time', '不明'))
            en_arn = finding.get('ProductFields', {}).get('Resources:0/Id', '不明')
            remediation = finding.get('Remediation', {}).get('Recommendation', {}).get('Text', '')

            jp_title = translate.translate_text(Text=en_title, SourceLanguageCode="en", TargetLanguageCode="ja")['TranslatedText']
            jp_desc = translate.translate_text(Text=en_desc, SourceLanguageCode="en", TargetLanguageCode="ja")['TranslatedText']
            jp_severity = '重大' if en_severity == 'CRITICAL' else '高'
            try:
                jp_time = datetime.strptime(en_time[:19], "%Y-%m-%dT%H:%M:%S").strftime("%Y年%m月%d日 %H時%M分%S秒")
            except Exception:
                jp_time = en_time
            jp_remediation = translate.translate_text(Text=remediation, SourceLanguageCode="en", TargetLanguageCode="ja")['TranslatedText'] if remediation else "対応情報なし"
            
            message_detailed = f"""【SecurityHub検出通知】

検出タイトル: {jp_title}
説明: {jp_desc}

重大度: {jp_severity}
発生時間: {jp_time}
リージョン: {en_region}
AWSアカウント: {en_account}
関連リソースARN: {en_arn}

推奨対応: {jp_remediation}

--
この通知はAWS Security Hubにより自動送信されました。
"""
            sns_response_detailed = sns.publish(
                TopicArn="トピックのARN", # 適切なARNに置き換えてください
                Subject="SecurityHub検出通知",
                Message=message_detailed
            )
            print("SNSへ日本語通知を配信しました\n", message_detailed)
        else:
            print(f"Severity {en_severity} is not CRITICAL or HIGH. Skipping detailed notification.")

    print("All findings processed.")
    return {'status': 'Complete'} 

細かい指定の際にtypoが出来ないように気をつけます。

4. EventBridgeルールの設定

    1. AWSコンソール → Amazon EventBridge → ルール → 「ルールを作成」します
      名前例:SecurityHubFindingsJPNotify
      イベントバス:デフォルト
    2. イベントパターンを作成します
      ターゲットタイプ:「Lambda関数」 

      コード例          

      {
          "source": ["aws.securityhub"],
          "detail-type": ["Security Hub Findings - Imported"],
          "detail": {
              "findings": {
                  "Compliance": {
                      "Status": ["FAILED", "WARNING"]
                  },
                  "RecordState": ["ACTIVE"],
                  "Workflow": {
                      "Status": ["NEW"]
                  },
                  "Severity": {
                      "Label": ["HIGH", "CRITICAL", "MEDIUM", "LOW", "INFORMATIONAL"]
                  }
              }
          }
      }

    3. ターゲットを選択します
      ターゲットを選択:Lambda関数
      ターゲットの場所:このアカウントのターゲット
      関数:先ほど作成したLambda関数
      実行ロール:SecurityHub CSPMの検出結果を日本語化するために作成したロールを選択
    4. タグは必要に応じて設定します。
    5. レビューと作成を行いルールの作成を押下します。

5. 動作テスト・確認

該当Lambda関数へ移動し
以下のコードを使いテストします。

{
    "version": "0",
    "id": "test-id-12345",
    "detail-type": "Security Hub Findings - Imported",
    "source": "aws.securityhub",
    "account": "123456789012",
    "region": "ap-northeast-1",
    "time": "2025-09-04T05:58:40Z",
    "resources": [
        "arn:aws:securityhub:ap-northeast-1:123456789012:product/aws/securityhub/arn:aws:securityhub:ap-northeast-1:security-control/EC2.8/finding/finding-id-1"
    ],
    "detail": {
        "findings": [
            {
                "Id": "finding-id-1",
                "ProductArn": "arn:aws:securityhub:ap-northeast-1:123456789012:product/aws/securityhub",
                "Title": "Instance Metadata Service is not required.",
                "Description": "HttpTokens is set to optional.",
                "Workflow": { "Status": "NEW" },
                "FindingProviderFields": {
                    "Severity": {
                        "Label": "HIGH",
                        "Original": "HIGH"
                    }
                },
                "ProductFields": {
                    "Resources:0/Id": "arn:aws:ec2:ap-northeast-1:123456789012:instance/i-0123456789abcdef0"
                },
                "FirstObservedAt": "2025-09-04T05:57:40.923Z",
                "Remediation": {
                    "Recommendation": {
                        "Text": "Review instance metadata options and update configuration.",
                        "Url": "https://docs.aws.amazon.com/securityhub/latest/userguide/securityhub-controls.html"
                    }
                }
            }
        ]
    }
}

テストを押下し成功すると無事以下のメールが届きました。


メールに付属している以下の2つのリンクはサブスクリプション解除と問い合わせのリンクなので間違えて踏まないようにしましょう
(チームメンバーが悲しくなります)

失敗するとエラーが起きるので該当箇所を修正します。

後日確認してみると
セキュリティの都合上他の範囲をお見せすることは出来ませんが
SecurityHubの検証結果が確認作業を行わなくても確認済みになっておりました。(メールは2回目以降のものが多く送られてこなかった)

 

まとめ

初めてAWS Lambda関数やAmazon Eventbridgeを使いましたが、仕組みを理解すると意外と簡単に構築できました。
細かく出力情報を指定する場合はTypoがないかを都度テストし確認することが大事です。
次はサブスクリプション解除のリンクの踏み間違いを防ぐ方法について調査してみようと思います。

ではでは(@^^)/~~~

著者について

運営担当のものです
◼️認定資格
・AWS Certified Cloud Practitioner
現在All AWS Certifications Engineers 目指して奮闘中
かわいいものと擬人化が大好き

いとさんをフォローする

クラウドに強いによるエンジニアブログです。

SCSKクラウドサービス(AWS)は、企業価値の向上につながるAWS 導入を全面支援するオールインワンサービスです。AWS最上位パートナーとして、多種多様な業界のシステム構築実績を持つSCSKが、お客様のDX推進を強力にサポートします。

AWS
シェアする
タイトルとURLをコピーしました