はじめに

大量に発生する可能性があるSecurity Hubのセキュリティイベント検出結果を流量制御しながら

Teamsに通知させる構成を記載します。

TeamsのIncoming WebhookのAPI制限に引っかかり、上手く通知されなかった課題があり、

今回のアーキテクチャを作成しました。

概要

Security Hub及びSecurity Hub統合済の各AWSサービスのセキュリティイベントを

Teamsに通知するアーキテクチャと設定値について記載します。

Security Hubについて

Security Hubは以下のセキュリティルールが遵守されているかどうかを判断します。

このようなチェックを行うことにより、準備状況スコアを取得でき、注意の必要なアカウントおよびリソースを特定できます。

Security Hubに統合されたAWSサービスについて

Security Hubはセキュリティ基準だけではなく、以下のAWS サービスの統合を行うことで、

様々なセキュリティ違反を検知することができます。

  • Config:Config のマネージドおよびカスタムルールの評価
  • Firewall Manager:WAFルールが非準拠の結果やAWS Shield Advanced の攻撃が特定された場合の通知
  • GuardDuty:悪意のあるアクティビティや不正な行動の脅威を検出
  • Health :セキュリティ関連のアラートのみ検出
  • IAM Access Analyzer:セキュリティ上のリスクであるリソースやデータへの意図しないアクセスを検出
  • Inspector:脆弱性結果の検出

など

システム構成

  • 東京リージョンを基点として、大阪リージョン、バージニアリージョンのSecurity Hubをクロスリージョンで集約しています。
  • Security Hub及び統合AWSサービス※で発生したイベントをEventBridgeが拾いSQS経由でLambdaからTeams通知しています。
  • Teams通知失敗時にはメール通知を行います。
  • Teams通知成功時やメール通知成功した時のみSecurity Hubワークフローステータスを「通知済み」に変更します。
  • AWSサービス障害に備えてデッドレターキュー(以後DLQ)を設定しています。

※本記事では「GuardDuty」、「Config」、「IAM Access Analyzer」を定義します。

EventBridge-SQS-Lambdaの設定値

EventBridge

EventBridgeでSecurity Hubの各イベントを抽出するため、AWSサービス毎にEventBridgeを作成しています。

AWSサービス毎にEventBridgeのイベント内容やルール定義が異なるため、

EventBridgeはAWSサービス毎に作成することをお勧めします。(DLQ も同様)

今回の設定の検知基準は以下の通りです。WARNINGやLOWも検知したいなど要件があればカスタマイズください。

AWSサービス名 抽出ステータス 抽出条件
Security Hub セキュリティラベル CRITICAL、HIGH
ワークフローステータス NEW(新規)
コンプライアンスステータス PASSED以外
Config セキュリティラベル 全て※1
ワークフローステータス NEW(新規)
コンプライアンスステータス PASSED以外
GuardDuty セキュリティラベル CRITICAL、HIGH
ワークフローステータス NEW(新規)
コンプライアンスステータス EventBridgeのイベントに出力しないため定義なし
IAM Access Analyzer セキュリティラベル 全て※2
ワークフローステータス NEW(新規)
コンプライアンスステータス EventBridgeのイベントに出力しないため定義なし

※1 Configはデフォルト「MEDIUM」となる。

※2 IAM Access Analyzerは「LOW」でもクロスアカウント通知などがあり予期せぬ設定を検知するため全量としている。

SQS

SQSの設定値と考慮点を以下に記載します。

  • イベント発生日時はイベント内から抽出できるため、FIFOキューではなく標準キューで作成しています。
  • 標準キューを採用したもう一つの理由としては、DLQの再配信がコンソール上から容易に行えるためです。
  • その他の設定は原則AWSの推奨値を踏襲し、ロングポーリングにしてコストを抑える工夫をしています。

    ※ロングポーリングについては以下のURLをご確認ください。

    参考URL:

    https://docs.aws.amazon.com/ja_jp/AWSSimpleQueueService/latest/SQSDeveloperGuide/sqs-short-and-long-polling.html

     

    パラメータ名 パラメータ説明 設定値 設定可能な値 備考
    DelaySeconds キューにある新しいメッセージの配信を一定の秒数延期 1(秒) 0~900(15分) Teams通知最大限界件数が4件/1秒のため、メッセージ配信を1秒ずらすため
    MessageRetentionPeriod キューがメッセージを保持できる秒数。 1,209,600(秒) 60~

    1,209,600

    最大値を指定
    ReceiveMessageWaitTimeSeconds キューからメッセージを取得する際にメッセージがキューに入るまで待機する秒数。 20(秒) 1~20 ロングポーリングにすると空のメッセージ受信回数を減らせるため、SQSのコスト削減とパフォーマンス向上になるため20を設定
    RedrivePolicy-maxReceiveCount メッセージがDLQに移動する前にキューに配信される回数。
    メッセージのReceiveCountがキューのmaxReceiveCountを超えると、キューはメッセージをDLQに移動する
    5(回) 1~

    1000

    キューのDLQ設定。メッセージがDLQに送信される前に正常に処理されやすくするために5以上に設定(AWS公式にて推奨)
    RedrivePolicy-deadLetterTargetArn maxReceiveCountで指定した値を超えた後に キューがメッセージを移動するDLQのARN DLQのARN キューのDLQ設定。
    VisibilityTimeout メッセージがキューから配信された後、メッセージが利用できなくなる秒数。(可視性タイムアウト) 720(秒) 0~

    43,200(12時間)

    VisibilityTimeoutに指定した値が経過するとmaxReceiveCountの回数分処理を繰り返し、DLQに格納する。

    Lambda関数のタイムアウトの6倍以上に設定(AWS公式にて推奨)

    Lambda

    システム固有で変更する箇所を環境変数にしています。

    パラメータ名 説明 設定値 指定可能な値 備考
    Environment 関数の環境変数 HOOK_URL,

    TOPIC_ARN,

    SLEEP_TIME

    WebHookのURL、SNSTopicのARN、Lambda待機時間を変数化
    Timeout 関数がタイムアウトするまでの秒数 120(秒) ~900(15分) 上記の環境変数で定義する「Lambdaの待機時間」に収まる時間を指定。後述する「Teams Incoming Webhookの制限値」に収めるためには最大96秒のため切りよく2分で定義

    Lambdaイベントトリガー

    Lambda実行数のクォータを意識して最低値の2を指定しています。

    パラメータ名 説明 設定値 指定可能な値 備考
    BatchSize 1バッチあたりに含められるSQSメッセージの数(Lambda1つに対して渡すSQSメッセージの数) 1 0~

    10

    1つのLambdaで1つのSQSメッセージを処理するため
    EventSourceArn イベントトリガーとなるリソースARN SQSのARN
    ScalingConfigーMaximumConcurrency SQSが呼び出すことができるLambdaの同時実行数 2 2~

    1000

    Lambda同時実行数の制限があり同一AWSアカウント内の同一リージョン内でデフォルト1000となる。

    他のLambda動作に影響を最小化するため同時実行数を2とする

    流量制御の設定について

    下記表の赤字に記載している、「1800件/86400秒間」の件数を超過しなければAPI制限に引っかからず全てTeamsに通知されます。

    「1800件/86400秒間」を超過しない流量制御設定は後述のParameters定義の「Lambda待機時間(秒)」に「96」と入力します。

    今回「Lambda待機時間」を「96秒」に決定した観点としては以下となります。

    • 各アラーム毎にスレッド単位で会話するため、1つのイベントを1つのTeamsメッセージ(=1スレッド)とする。
    • 数分遅れるより確実に対応を検討できるようにしたいため、Teams投稿は速度よりも確実に投稿されることを優先する。

    24時間(86400秒)に1800件も通知が来るほどAWSリソースがなかったり、もう少し通知を早くしたい場合には、

    Lambda待機時間を以下の表を基にカスタマイズしてください。

    Teams Incoming Webhook API制限 Lambda同時実行 Lambda待機時間(秒)

    ※Lambdaの環境変数で指定

    Security Hub イベント発生件数 処理時間(秒)
    4件/1秒間 2 0.5 1 0.5
    60件/30秒間 2 1 60 30
    100件/3600秒間 2 72 100 3600
    1800件/86400秒間 2 96 1800 86400

    ※API制限を超過しない理由としては以下となります。

    「Lambdaの最大同時実行数」は2個で固定しているため、Security Hubイベントが1800件発生した際に、

    1800件 ÷ 2個のLambda ×96秒=86,400秒で捌いてTeamsへ通知するため、API制限内に収めることができます。

    参考:Teams Incoming WebhookのAPI制限

    Incoming WebhookのAPI制限については以下のURLをご確認ください。1つのIncoming Webhookに対しての制限となります。

    参考URL:

    https://learn.microsoft.com/ja-jp/microsoftteams/platform/webhooks-and-connectors/how-to/connectors-using?tabs=cURL

    CloudFormationテンプレート

    本記事で扱うコードを記載します。

    Parameters定義

    CloudFormationのParameters入力値

    ・SecurityHubTeamsWebHookUrl:Teams通知先のIncoming WebHookのURLを入力
    ・SleepTimeNotifySecurityhubFunc:Lambdaの待機時間(秒) ※96と入力することでAPI制限に引っかからなくなります。
    ・SecurityHubMailSNSArn:メール通知先のSNSトピックのARN
    ・SecurityHubIntegrationGuardDuty:GuardDuty設定
    ・SecurityHubIntegrationConfig:Config設定
    ・SecurityHubIntegrationAccessAnalyzer:IAM Access Analyzer設定

    CloudFormationテンプレートファイル

    AWSTemplateFormatVersion: 2010-09-09
    
    ### ------------------------------------------------------------
    ### Description Section
    ### ------------------------------------------------------------
    Description: SecurityHubNotifications
    
    ## ------------------------------------------------------------
    ## Parameters Section
    ## ------------------------------------------------------------
    Parameters:
      SecurityHubTeamsWebHookUrl:
        Type: String
        Description: |
          (Require) Enter WebHookUrl for Teams(Securityhub).
        Default: https://xxx.com
      SleepTimeNotifySecurityhubFunc:
        Type: String
        Description: |
          (Require) Enter SleepTime for NotifySecurityhubFunc.
        Default: 96
      SecurityHubMailSNSArn:
        Type: String
        Description: |
          (Require) Enter SNSArn.
        Default: arn:aws:sns:ap-northeast-1:XXXXXXXXXX:XXXXX
      SecurityHubIntegrationGuardDuty:
        Type: String
        Description: |
          Select SecurityHub ProductName
        AllowedValues:
          - GuardDuty
          - ""
        Default: GuardDuty
      SecurityHubIntegrationConfig:
        Type: String
        Description: |
          Select SecurityHub ProductName
        AllowedValues:
          - Config
          - ""
        Default: Config
      SecurityHubIntegrationAccessAnalyzer:
        Type: String
        Description: |
          Select SecurityHub ProductName
        AllowedValues:
          - IAM Access Analyzer
          - ""
        Default: IAM Access Analyzer
    
    ## ------------------------------------------------------------
    ## Conditions Section
    ## ------------------------------------------------------------
    Conditions:
      SecurityHubProductValue1: !Equals [!Ref SecurityHubIntegrationGuardDuty, "GuardDuty"]
      SecurityHubProductValue2: !Equals [!Ref SecurityHubIntegrationConfig, "Config"]
      SecurityHubProductValue3: !Equals [!Ref SecurityHubIntegrationAccessAnalyzer, "IAM Access Analyzer"]
    
    ### ------------------------------------------------------------
    ### Resources Section
    ### ------------------------------------------------------------
    Resources:
      # ------------------------------------------------------------#
      #  EventBridge & DeadLetterQueue
      # ------------------------------------------------------------#
      SecurityHubEventToSQSEvent:
        Type: AWS::Events::Rule
        Properties:
          Name: securityhubevent-sqs
          EventPattern:
            source:
              - "aws.securityhub"
              - "EventBridge.retry"
            detail-type:
              - "Security Hub Findings - Imported"
            detail:
              findings:
                AwsAccountId:
                  - !Sub ${AWS::AccountId}
                Compliance:
                  Status:
                    - FAILED
                    - WARNING
                    - NOT_AVAILABLE
                ProductName:
                  - "Security Hub"
                Severity:
                  Label:
                    - CRITICAL
                    - HIGH
                Workflow:
                  Status:
                    - NEW
                RecordState:
                  - ACTIVE
    
          Targets:
            - Arn: !GetAtt SqsSecurityHub.Arn
              Id: AWSSecurityHub_Queue
              DeadLetterConfig:
                Arn: !GetAtt SqsEventBridgeDlq.Arn
              RetryPolicy:
                MaximumEventAgeInSeconds: 120
                MaximumRetryAttempts: 30
    
      GuardDutyEventToSQSEvent:
        Condition: SecurityHubProductValue1
        Type: AWS::Events::Rule
        Properties:
          Name: securityhub-guarddutyevent-sqs
          EventPattern:
            source:
              - "aws.securityhub"
              - "EventBridge.retry"
            detail-type:
              - "Security Hub Findings - Imported"
            detail:
              findings:
                AwsAccountId:
                  - !Sub ${AWS::AccountId}
                ProductName:
                  - !Sub ${SecurityHubIntegrationGuardDuty}
                Severity:
                  Label:
                    - CRITICAL
                    - HIGH
                Workflow:
                  Status:
                    - NEW
                RecordState:
                  - ACTIVE
    
          Targets:
            - Arn: !GetAtt SqsSecurityHub.Arn
              Id: AWSSecurityHub_Queue
              DeadLetterConfig:
                Arn: !GetAtt SqsGuardDutyEventBridgeDlq.Arn
              RetryPolicy:
                MaximumEventAgeInSeconds: 120
                MaximumRetryAttempts: 30
    
      ConfigEventToSQSEvent:
        Condition: SecurityHubProductValue2
        Type: AWS::Events::Rule
        Properties:
          Name: securityhub-configevent-sqs
          EventPattern:
            source:
              - "aws.securityhub"
              - "EventBridge.retry"
            detail-type:
              - "Security Hub Findings - Imported"
            detail:
              findings:
                AwsAccountId:
                  - !Sub ${AWS::AccountId}
                Compliance:
                  Status:
                    - FAILED
                    - WARNING
                    - NOT_AVAILABLE
                ProductName:
                  - !Sub ${SecurityHubIntegrationConfig}
                Workflow:
                  Status:
                    - NEW
                RecordState:
                  - ACTIVE
    
          Targets:
            - Arn: !GetAtt SqsSecurityHub.Arn
              Id: AWSSecurityHub_Queue
              DeadLetterConfig:
                Arn: !GetAtt SqsConfigEventBridgeDlq.Arn
              RetryPolicy:
                MaximumEventAgeInSeconds: 120
                MaximumRetryAttempts: 30
    
      IAMAccessAnalyzerEventToSQSEvent:
        Condition: SecurityHubProductValue3
        Type: AWS::Events::Rule
        Properties:
          Name: securityhub-iamaccessanalyzerevent-sqs
          EventPattern:
            source:
              - "aws.securityhub"
              - "EventBridge.retry"
            detail-type:
              - "Security Hub Findings - Imported"
            detail:
              findings:
                AwsAccountId:
                  - !Sub ${AWS::AccountId}
                ProductName:
                  - !Sub ${SecurityHubIntegrationAccessAnalyzer}
                Workflow:
                  Status:
                    - NEW
                RecordState:
                  - ACTIVE
    
          Targets:
            - Arn: !GetAtt SqsSecurityHub.Arn
              Id: AWSSecurityHub_Queue
              DeadLetterConfig:
                Arn: !GetAtt SqsIAMAccessAnalyzerEventBridgeDlq.Arn
              RetryPolicy:
                MaximumEventAgeInSeconds: 120
                MaximumRetryAttempts: 30
    
      #Eventbridge DeadLetterQueue
      SqsEventBridgeDlq:
        Type: AWS::SQS::Queue
        Properties:
          QueueName: sqs-securityhubevent-eventbridge-dlq-01
          DelaySeconds: 0
          MaximumMessageSize: 262144
          MessageRetentionPeriod: 1209600
          ReceiveMessageWaitTimeSeconds: 20
          VisibilityTimeout: 30
    
      SqsEventBridgeDlqPolicy:
        Type: AWS::SQS::QueuePolicy
        Properties:
          Queues:
            - !Ref SqsEventBridgeDlq
          PolicyDocument:
            Statement:
              - Effect: Allow
                Principal:
                  Service: events.amazonaws.com
                Action: SQS:SendMessage
                Resource: !GetAtt SqsEventBridgeDlq.Arn
                Condition:
                  ArnEquals:
                    aws:SourceArn: !GetAtt SecurityHubEventToSQSEvent.Arn
    
      SqsGuardDutyEventBridgeDlq:
        Condition: SecurityHubProductValue1
        Type: AWS::SQS::Queue
        Properties:
          QueueName: sqs-securityhub-guarddutyevent-eventbridge-dlq-01
          DelaySeconds: 0
          MaximumMessageSize: 262144
          MessageRetentionPeriod: 1209600
          ReceiveMessageWaitTimeSeconds: 20
          VisibilityTimeout: 30
    
      SqsGuardDutyEventBridgeDlqPolicy:
        Condition: SecurityHubProductValue1
        Type: AWS::SQS::QueuePolicy
        Properties:
          Queues:
            - !Ref SqsGuardDutyEventBridgeDlq
          PolicyDocument:
            Statement:
              - Effect: Allow
                Principal:
                  Service: events.amazonaws.com
                Action: SQS:SendMessage
                Resource: !GetAtt SqsGuardDutyEventBridgeDlq.Arn
                Condition:
                  ArnEquals:
                    aws:SourceArn: !GetAtt GuardDutyEventToSQSEvent.Arn
    
      SqsConfigEventBridgeDlq:
        Condition: SecurityHubProductValue2
        Type: AWS::SQS::Queue
        Properties:
          QueueName: sqs-securityhub-configevent-eventbridge-dlq-01
          DelaySeconds: 0
          MaximumMessageSize: 262144
          MessageRetentionPeriod: 1209600
          ReceiveMessageWaitTimeSeconds: 20
          VisibilityTimeout: 30
    
      SqsConfigEventBridgeDlqPolicy:
        Condition: SecurityHubProductValue2
        Type: AWS::SQS::QueuePolicy
        Properties:
          Queues:
            - !Ref SqsConfigEventBridgeDlq
          PolicyDocument:
            Statement:
              - Effect: Allow
                Principal:
                  Service: events.amazonaws.com
                Action: SQS:SendMessage
                Resource: !GetAtt SqsConfigEventBridgeDlq.Arn
                Condition:
                  ArnEquals:
                    aws:SourceArn: !GetAtt ConfigEventToSQSEvent.Arn
    
      SqsIAMAccessAnalyzerEventBridgeDlq:
        Condition: SecurityHubProductValue3
        Type: AWS::SQS::Queue
        Properties:
          QueueName: sqs-securityhub-iamaccessanalyzerevent-eventbridge-dlq-01
          DelaySeconds: 0
          MaximumMessageSize: 262144
          MessageRetentionPeriod: 1209600
          ReceiveMessageWaitTimeSeconds: 20
          VisibilityTimeout: 30
    
      SqsIAMAccessAnalyzerEventBridgeDlqPolicy:
        Condition: SecurityHubProductValue3
        Type: AWS::SQS::QueuePolicy
        Properties:
          Queues:
            - !Ref SqsIAMAccessAnalyzerEventBridgeDlq
          PolicyDocument:
            Statement:
              - Effect: Allow
                Principal:
                  Service: events.amazonaws.com
                Action: SQS:SendMessage
                Resource: !GetAtt SqsIAMAccessAnalyzerEventBridgeDlq.Arn
                Condition:
                  ArnEquals:
                    aws:SourceArn: !GetAtt IAMAccessAnalyzerEventToSQSEvent.Arn
    
      # ------------------------------------------------------------#
      #  SQS & DeadLetteQueue
      # ------------------------------------------------------------#
      SqsSecurityHub:
        Type: AWS::SQS::Queue
        Properties:
          QueueName: sqs-securityhubevent-lambda-01
          RedrivePolicy:
            deadLetterTargetArn: !GetAtt SqsSecurityHubDlq.Arn
            maxReceiveCount: 5
          DelaySeconds: 1
          MaximumMessageSize: 262144
          MessageRetentionPeriod: 1209600
          ReceiveMessageWaitTimeSeconds: 20
          VisibilityTimeout: 720
    
      SqsSecurityHubPolicy:
        Type: AWS::SQS::QueuePolicy
        Properties:
          Queues:
            - !Ref SqsSecurityHub
          PolicyDocument:
            Statement:
              - Effect: Allow
                Principal:
                  Service: events.amazonaws.com
                Action: SQS:SendMessage
                Resource: !GetAtt SqsSecurityHub.Arn
                Condition:
                  ArnEquals:
                    aws:SourceArn:
                      - !GetAtt SecurityHubEventToSQSEvent.Arn
                      - !GetAtt GuardDutyEventToSQSEvent.Arn
                      - !GetAtt ConfigEventToSQSEvent.Arn
                      - !GetAtt IAMAccessAnalyzerEventToSQSEvent.Arn
              - Effect: Allow
                Principal:
                  Service: sqs.amazonaws.com
                Action:
                  - SQS:SendMessage
                  - SQS:ReceiveMessage
                Resource: !GetAtt SqsSecurityHub.Arn
                Condition:
                  ArnEquals:
                    aws:SourceArn: !GetAtt SqsSecurityHubDlq.Arn
    
      SqsSecurityHubDlq:
        Type: AWS::SQS::Queue
        Properties:
          QueueName: sqs-securityhubevent-lambda-dlq-01
          DelaySeconds: 0
          MaximumMessageSize: 262144
          MessageRetentionPeriod: 1209600
          ReceiveMessageWaitTimeSeconds: 20
          VisibilityTimeout: 30
    
      SqsSecurityHubDlqPolicy:
        Type: AWS::SQS::QueuePolicy
        Properties:
          Queues:
            - !Ref SqsSecurityHubDlq
          PolicyDocument:
            Statement:
              - Effect: Allow
                Principal:
                  Service: sqs.amazonaws.com
                Action:
                  - SQS:SendMessage
                  - SQS:ReceiveMessage
                Resource: !GetAtt SqsSecurityHubDlq.Arn
                Condition:
                  ArnEquals:
                    aws:SourceArn: !GetAtt SqsSecurityHub.Arn
      # ------------------------------------------------------------#
      #  Lambda
      # ------------------------------------------------------------#
      # SecurityHubEvent
      SecurityHubEventFunc:
        Type: AWS::Lambda::Function
        DependsOn: SecurityHubEventFuncLogs
        Properties:
          Code:
            ZipFile: |
              #impoort library
              import boto3
              import botocore
              import datetime
              import json
              import os
              import sys
              import time
    
              from botocore.exceptions import ClientError
              from urllib.error import HTTPError, URLError
              from urllib.request import Request, urlopen
    
              #client make
              securityhub = boto3.client('securityhub')
              sns = boto3.client('sns')
    
              #environ
              HOOK_URL = os.environ['TeamsWebHookUrl']
              TOPIC_ARN = os.environ['SNSTopicArn']
              SLEEP_TIME =  int(os.environ['SleepTime'])
                                              
              #######################################
              # Funtion:Teams notification
              #######################################
              def notification_teams(message):
                  msg = json.loads(message)
                  product_name = msg['detail']['findings'][0]['ProductName']
                  event_id = msg['detail']['findings'][0]['Id']
                  event_title = msg['detail']['findings'][0]["Title"]
                  event_description =  msg['detail']['findings'][0]['Description']
                  resource = msg['detail']['findings'][0]['Resources'][0]['Id']
                  account_id = msg['detail']['findings'][0]['AwsAccountId']
                  region = msg['detail']['findings'][0]['Region']
                  event_time = msg['detail']['findings'][0]['UpdatedAt']
                  
                  teams_msg = {
                      'title': "SecurityHub Event Nortify",
                      'text': "**ProductName:** %s<br>**AlarmID:** %s<br>**Title:** %s<br>**Description:** %s<br>**Resource:** %s<br>**AccountID:** %s<br>**Region:** %s<br>**AlarmTime:** %s" % (product_name, event_id, event_title, event_description, resource, account_id, region, parse_date(event_time))
                  }
                  req = Request(HOOK_URL, json.dumps(teams_msg).encode('utf-8'))
                  try:
                      print('======================== Notify a message to Teams ====================')
                      print("Notify Teams Message: " + str(teams_msg))
                      response = urlopen(req)
                      responsecheck = response.read()
                      if b'Webhook message delivery failed with error' in responsecheck:
                          print('ERROR:%s. ' % (responsecheck))
                          return False
                      print("Successfully posted Message to Teams")
                      return None
                  except HTTPError as e:
                      print("Request failed: %d %s" %(e.code, e.reason))
                      return False
                  except URLError as e:
                      print("Server connection failed: %s" % e.reason)
                      return False
    
              #######################################
              # Funtion:Securityhub workflowstatus change
              #######################################
              def change_securityhub_workflowstatus(SQS_message):
                  print('======================== Get Securityhub information from SQS message ====================')
                  Securityhub = json.loads(SQS_message)
                  Securityhub_Id = Securityhub['detail']['findings'][0]['Id']
                  Securityhub_ProductArn = Securityhub['detail']['findings'][0]['ProductArn']
                  print("Securityhub Id: %s \nSecurityhub Arn: %s"% (Securityhub_Id, Securityhub_ProductArn))
                  try:
                      print('======================== Change the Securityhub workflow status to Notified ====================')
                      print("Change to Notified Securityhub Id: %s \nChange to Notified Securityhub Arn: %s"% (Securityhub_Id, Securityhub_ProductArn))
                      securityhub.batch_update_findings(
                          FindingIdentifiers=[
                              {
                                  'Id': Securityhub_Id,
                                  'ProductArn': Securityhub_ProductArn,
                              },
                          ],
                          Workflow={
                              'Status': 'NOTIFIED'
                          },
                      )
                      print("Successfully changed Securityhub workflow status to Notified")
                      return None
                  except botocore.exceptions.ClientError as e:
                      print('ERROR:%s. ' % (e))
                      return False
    
    
              #######################################
              # Funtion:SNS notification
              #######################################
              def notification_sns(message):
                  msg = json.loads(message)
    
                  product_name = msg['detail']['findings'][0]['ProductName']
                  event_id = msg['detail']['findings'][0]['Id']
                  event_title = msg['detail']['findings'][0]["Title"]
                  event_description =  msg['detail']['findings'][0]['Description']
                  resource = msg['detail']['findings'][0]['Resources'][0]['Id']
                  account_id = msg['detail']['findings'][0]['AwsAccountId']
                  region = msg['detail']['findings'][0]['Region']
                  event_time = msg['detail']['findings'][0]['UpdatedAt']
    
                  sns_msg = "ProductName: " + product_name + "\n" + "AlarmID: " + event_id + "\n" + "Title: " + event_title + "\n" + "Description: " + event_description + "\n" + "Resource: " + resource + "\n" + "AccountID: " + account_id + "\n" + "Region: " + region + "\n" + "AlarmTime: " + parse_date(event_time)
                  subject = "SecurityHub Event Nortify"
    
                  try:
                      print('======================== Notify a message to SNS ====================')
                      print("Notify SNS Message: \n" + str(sns_msg))
                      sns.publish(TopicArn=TOPIC_ARN, Message=sns_msg, Subject=subject)
                      print("Successfully posted Message to SNS")
                      return None
                  except botocore.exceptions.ClientError as e:
                      print('ERROR:%s. ' % (e))
                      return False
                      
              #######################################
              # Funtion:Parse Date
              #######################################
              def parse_date(event_time, date_format='%Y/%m/%d %H:%M:%S'):
                  time = datetime.datetime.strptime(event_time, '%Y-%m-%dT%H:%M:%S.%f%z')
                  time += datetime.timedelta(hours=9)
                  return time.strftime(date_format)
                  
              #######################################
              # Main
              #######################################
              def lambda_handler(event, context): 
                  print('======================== MAIN ====================')
                  SQS_message = event['Records'][0]['body']
                  print("Securityhub Error : " + str(SQS_message))
                  print('======================== Securityhub Error notify to Teams ====================')
                  if notification_teams(SQS_message) is not None:
                      print('======================== Securityhub Error notify to SNS ====================')
                      if notification_sns(SQS_message) is not None:
                          time.sleep(SLEEP_TIME)
                          return False
    
                  print('======================== Change the Securityhub workflow status ====================')
                  if change_securityhub_workflowstatus(SQS_message) is not None:
                      time.sleep(SLEEP_TIME)
                      return False
    
                  time.sleep(SLEEP_TIME)
    
          FunctionName: securityhub-notify-func
          Handler: index.lambda_handler
          Role: !GetAtt LambdaforSecurityHubEventRole.Arn
          Runtime: python3.8
          MemorySize: 128
          EphemeralStorage:
            Size: 512
          Timeout: 120
          Environment:
            Variables:
              TeamsWebHookUrl: !Sub ${SecurityHubTeamsWebHookUrl}
              SNSTopicArn: !Sub ${SecurityHubMailSNSArn}
              SleepTime: !Sub ${SleepTimeNotifySecurityhubFunc}
    
      LambdaforSecurityHubEventRole:
        Type: AWS::IAM::Role
        Properties:
          RoleName: securityhub-notify-func-role
          AssumeRolePolicyDocument:
            Version: "2012-10-17"
            Statement:
              - Effect: "Allow"
                Principal:
                  Service:
                    - "lambda.amazonaws.com"
                Action:
                  - "sts:AssumeRole"
          Policies:
            - PolicyName: securityhub-notify-func-policy
              PolicyDocument:
                Version: "2012-10-17"
                Statement:
                  - Effect: "Allow"
                    Action:
                      - logs:CreateLogGroup
                      - logs:CreateLogStream
                      - logs:PutLogEvents
                    Resource: !GetAtt SecurityHubEventFuncLogs.Arn
                  - Effect: "Allow"
                    Action:
                      - sqs:ReceiveMessage
                      - sqs:DeleteMessage
                      - sqs:GetQueueAttributes
                    Resource: !GetAtt SqsSecurityHub.Arn
                  - Effect: "Allow"
                    Action:
                      - securityhub:BatchUpdateFindings
                    Resource: !Sub arn:aws:securityhub:${AWS::Region}:${AWS::AccountId}:hub/default
                  - Effect: "Allow"
                    Action:
                      - sns:Publish
                    Resource: !Sub ${SecurityHubMailSNSArn}
    
      SecurityHubEventFuncEventSourceMapping:
        Type: AWS::Lambda::EventSourceMapping
        Properties:
          BatchSize: 1
          Enabled: true
          EventSourceArn: !GetAtt SqsSecurityHub.Arn
          FunctionName: !GetAtt SecurityHubEventFunc.Arn
          FunctionResponseTypes:
            - ReportBatchItemFailures
          ScalingConfig:
            MaximumConcurrency: 2
    
      # ------------------------------------------------------------#
      #  CloudWatch Logs
      # ------------------------------------------------------------#
      SecurityHubEventFuncLogs:
        Type: AWS::Logs::LogGroup
        DeletionPolicy: Retain
        Properties:
          LogGroupName: /aws/lambda/securityhub-notify-func
          RetentionInDays: 90
    
     

    動作検証

    実際にTeamsへ通知させた結果を記載します。

    Security HubイベントのTeams通知

    各リージョンの各AWSサービスのイベントがTeamsに以下のように通知されます。

    Security Hubのステータス

    上記のTeams通知で検知したイベントのワークフローステータスが「NOTIFIED(通知済み)」に変更されていることがわかります。

    「NOTIFIED(通知済み)」に設定することで、何度も通知されることを防止できます。

    流量制御の動作確認

    今回は動作検証として、Lambda待機時間を72秒に指定して、Security Hub イベントを約120件流してみました。

    3600秒(1時間)以内に合計100件を超過するとAPI制限に達して、SNS経由でメールが通知されますが、

    流量を抑えているので、API制限に該当せず全てTeamsに通知されます。

    ※ただし、86400秒(24時間)以内に合計1800件を超過するとAPI制限に達します。

    Teams Incoming Webhook API制限 Lambda

    同時実行

    Lambda待機時間(秒)

    ※Lambdaの環境変数で指定

    Security Hub イベント発生件数 処理時間(秒)
    4件/1秒間 2 0.5 1 0.5
    60件/30秒間 2 1 60 30
    100件/3600秒間 2 72 100 3600
    1800件/86400秒間 2 96 1800 86400

    ◆CloudFormationのParameters入力値

    Lambda待機時間を72秒にしてデプロイします。

    ◆Security Hubイベントの投入

    合計117件イベントを投入します。

    ◆動作確認結果

    約120件のSecurity Hubイベントを約1時間15分かけてTeamsに通知されました。

    ## Lambdaのログから Teams通知成功のメッセージをカウント
    root@1922c7e1be9d:/infra# aws logs filter-log-events --log-group-name /aws/lambda/securityhub-notify-func  --start-time `TZ=Asia/Tokyo date --date='2023-03-24 20:45:00.000' +%s%3N` \
    --filter-pattern "Successfully posted Message to Teams" \
    --query "events[].[message]" \
    --output text | grep ^S | wc -l
    117
    root@1922c7e1be9d:/infra# 

    API上限に達した場合はメール通知されますが、その件数は0件のためAPI制限に引っかからずに通知できました。

    ## Lambdaのログから SNS(メール)通知成功のメッセージをカウント
    root@1922c7e1be9d:/infra# aws logs filter-log-events --log-group-name /aws/lambda/securityhub-notify-func --start-time `TZ=Asia/Tokyo date --date='2023-03-24 20:45:00.000' +%s%3N` \
    ---filter-pattern "Successfully posted Message to SNS" \
    --query "events[].[message]" \
    --output text | grep ^S | wc -l 
    0
    root@1922c7e1be9d:/infra# 

    異常系のパターンについて

    TeamsのIncoming Webhook API上限に達した場合の挙動

    API上限を超えると一時的にTeams通知ができなくなるため、以下のようにメールが届きます。

    EventBridgeのDLQ再配信

    AWSサービス障害やAWSのクォータを超える通知に該当しない限りはDLQには入りません。

    DLQからEventBridgeへイベントの再配信が必要な場合には、

    DLQからイベントを取得の上、再度EventBridgeへ配信する必要があります。

    参考URL:

    https://docs.aws.amazon.com/scheduler/latest/UserGuide/configuring-schedule-dlq.html

    SQSのDLQ再配信

    AWSサービス障害やAWSのクォータを超える通知に該当しない限りはDLQには入りません。

    DLQからSQSへイベントの再配信が必要になった場合には、

    以下の図の「DLQ再処理の開始」を実行することで、再度Teamsに通知されます。

    CloudWatch Alarmの推奨定義

    今回記載したCloudFormationに以下の検知項目を入れると更に保守性があがるため、設定を推奨します。

    ・EventBridgeやSQSに紐づけているDLQへメッセージが投入された時

    ・SQSのイベント滞留時(通知までに30分を超過する流量が入った場合)

    ・Lambdaの関数エラー

    参考:CloudWatch Alarmのサンプルコード

       # DLQ message投入※実際に定義する際にはEventBridgeのDLQも定義
        SQSSecurityHubEventDLQApproximateNumberOfMessagesVisibleAlarm:
          Type: AWS::CloudWatch::Alarm
          Properties:
            AlarmName: sqs-securityhubevent-dlq-alarm
            Namespace: AWS/SQS
            # MetricName: NumberOfMessagesReceived
            MetricName: ApproximateNumberOfMessagesVisible
            Statistic: Sum
            Period: 300
            Dimensions:
              - Value: !GetAtt SqsSecurityHubDlq.QueueName
                Name: QueueName
            Threshold: 0
            ComparisonOperator: GreaterThanThreshold
            EvaluationPeriods: 1
            DatapointsToAlarm: 1
            TreatMissingData: notBreaching
            AlarmActions:
              - !Sub ${SecurityHubMailSNSArn}
            OKActions:
              - !Sub ${SecurityHubMailSNSArn}
            InsufficientDataActions: []
        # SQSのイベント滞留
        SQSSecurityHubEventLambdaApproximateAgeOfOldestMessageOver1hourAlarm:
          Type: AWS::CloudWatch::Alarm
          Properties:
            AlarmName: sqs-securityhubevent-approximateageofoldestmessage-over1hour-alarm
            Namespace: AWS/SQS
            # MetricName: NumberOfMessagesReceived
            MetricName: ApproximateAgeOfOldestMessage
            Statistic: Maximum
            Period: 300
            Dimensions:
              - Value: !GetAtt SqsSecurityHub.QueueName
                Name: QueueName
            Threshold: 100
            ComparisonOperator: GreaterThanThreshold
            EvaluationPeriods: 1
            DatapointsToAlarm: 1
            TreatMissingData: notBreaching
            AlarmActions:
              - !Sub ${SecurityHubMailSNSArn}
            OKActions:
              - !Sub ${SecurityHubMailSNSArn}
            InsufficientDataActions: []
       ♯ Lambda関数エラー
        LambdaFuncErrorsAlarm:
          Type: AWS::CloudWatch::Alarm
          Properties:
            AlarmName: lambda-securityhub-notify-func-errors-alarm
            Namespace: AWS/Lambda
            MetricName: Errors
            Statistic: Sum
            Period: 300
            Dimensions:
              - Value: securityhub-notify-func
                Name: FunctionName
            Threshold: 0
            ComparisonOperator: GreaterThanThreshold
            EvaluationPeriods: 1
            DatapointsToAlarm: 1
            TreatMissingData: notBreaching
            AlarmActions:
              - !Sub ${SecurityHubMailSNSArn}
            InsufficientDataActions: []
      
      

       

      まとめ

      Security HubのセキュリティイベントをTeamsに通知する方法を今回記載しました。

       

      保守運用SEが検知しやすくなるためTeams通知を活用されているシステムも多いですが、

      利用してみた所、API制限が厳しく通知出来ない事象が何度か発生しました。

      本記事で記載した流量制御を行うアーキテクチャを実装することで通知漏れを防ぐことが出来ます。

       

      ただし、大規模システムでは検出するAWSサービスが数多く存在するため、大量にイベントが発生することが懸念されます。

      そのため、Teamsではなく弊社のグループ会社であるTISシステムサービス株式会社が提供している監視APIサービスや、

      その他監視サービスを活用することをご検討ください。

      監視サービスでもAPI制限はあるため、Teams通知のプログラムを置き換えることで、

      本記事のアーキテクチャを活用することができます。

       

      CloudWatch Alarmも同様に本アーキテクチャを応用して流量制限を行い通知する仕組みも作成していますので別途投稿します。

      これからSecurity Hubのセキュリティ監視の検討されている方や既存システムの改善を検討されている方に、

      本記事が少しでも参考になれば幸いです。

       

      参考記事