投稿日
AWS Security Hubに集約した検出結果を流量制御しながらTeamsに通知する仕組み
もくじ
- はじめに
- 概要
- Security Hubについて
- Security Hubに統合されたAWSサービスについて
- システム構成
- EventBridge-SQS-Lambdaの設定値
- EventBridge
- SQS
- Lambda
- Lambdaイベントトリガー
- 流量制御の設定について
- 参考:Teams Incoming WebhookのAPI制限
- CloudFormationテンプレート
- Parameters定義
- CloudFormationテンプレートファイル
- 動作検証
- Security HubイベントのTeams通知
- Security Hubのステータス
- 流量制御の動作確認
- ◆CloudFormationのParameters入力値
- ◆Security Hubイベントの投入
- ◆動作確認結果
- 異常系のパターンについて
- TeamsのIncoming Webhook API上限に達した場合の挙動
- EventBridgeのDLQ再配信
- SQSのDLQ再配信
- CloudWatch Alarmの推奨定義
- まとめ
- 参考記事
はじめに
大量に発生する可能性がある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:
パラメータ名 | パラメータ説明 | 設定値 | 設定可能な値 | 備考 |
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:
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のセキュリティ監視の検討されている方や既存システムの改善を検討されている方に、
本記事が少しでも参考になれば幸いです。
参考記事
- SecurityHub統合:https://docs.aws.amazon.com/ja_jp/securityhub/latest/userguide/securityhub-internal-providers.html
- SecurityHubクロスリージョン集約:https://docs.aws.amazon.com/ja_jp/securityhub/latest/userguide/finding-aggregation-enable.html
- Incoming Webhookの設定:https://learn.microsoft.com/ja-jp/microsoftteams/platform/webhooks-and-connectors/how-to/add-incoming-webhook?tabs=dotnet
- Incoming Webhookの制限:https://learn.microsoft.com/ja-jp/microsoftteams/platform/webhooks-and-connectors/how-to/connectors-using?tabs=cURL
- EventBridgeのDLQ再処理:https://zenn.dev/shirou/articles/retry_clouwatch_event
- SQSでのLambdaの使用:https://docs.aws.amazon.com/ja_jp/lambda/latest/dg/with-sqs.html
- SQS-Lambdaのロングポーリング:https://docs.aws.amazon.com/ja_jp/AWSSimpleQueueService/latest/SQSDeveloperGuide/sqs-short-and-long-polling.html
- EventBridgeのDLQの使用:https://docs.aws.amazon.com/ja_jp/eventbridge/latest/userguide/eb-rule-dlq.html
- EventBridgeのDLQ再配信:https://docs.aws.amazon.com/scheduler/latest/UserGuide/configuring-schedule-dlq.html