【CloudFormation】Security Hubに集約した検出結果を整形して通知する仕組みを実装してみた

Security Hub経由で集約した検出結果をいい感じのメッセージで通知するCloudFormationテンプレートを作ったよ。
2022.05.24

この記事は公開されてから1年以上経過しています。情報が古い可能性がありますので、ご注意ください。

はじめに

最近Security Hubでリージョン集約機能がアップデートにより実装されました。

【アップデート】AWS Security Hub が検出結果のリージョン集約に対応しました | DevelopersIO

このアップデートからAWSのセキュリティサービスで検出した結果をSecurity Hubに集約することで、これまで利用リージョン分展開していた通知の仕組みを1つのリージョンだけで実装することができるようになりました。

【小ネタ】GuardDuty 通知は Security Hub 経由で行うとリージョン集約ができて便利 | DevelopersIO

こうしてSecurity Hubに集めて検出結果を通知できるようになったわけですが、今回はもう一歩踏み込んで整形したメッセージで通知する仕組みを実装していきます。

本ブログで実装する通知の仕組みは、ほぼ弊社が提供しているセキュアアカウントの実装を元にしています。手軽に通知設定したい・まるっとセキュアなアカウントが欲しい方はセキュアアカウントを利用頂くと幸せになれるかもしれません。セキュアアカウントについて詳細に知りたい方は是非以下のブログをご参照ください。

できるようになること

Security Hubに集約した検出結果をCloudFormationスタックを2つ(または3つ)展開するだけで、以下のような通知をメールやSlackで受け取れます。

メールの通知例

Slackの通知例

Teamsの通知例

構成

検出した結果を全てSecurity Hubに集約、Eventルールで各サービスの検出結果を取得してStepFunctionsで整形という流れになっています。

通知先としてはメールとSlack、Teamsを想定しています。セキュリティサービス(Security HubやGuardDuty)以外の部分は基本的に実装は全てCloudFormationで行います。共通実装は必須、どの通知先を利用するかは任意です。もちろん両方利用しても構いません。

また、今回Security Hubに集約して通知する検出結果は以下3つのサービスを想定しています。

  • GuardDuty
  • Security Hub(AWS 基礎セキュリティのベストプラクティス v1.0.0)
  • IAM Access Analyzer

CloudFormationテンプレート

今回実装に利用するCloudFormationテンプレートです。(コード量が多いため折りたたんでいます)

共通機能は必須、通知先はお好みでご利用ください。

①共通機能(クリックすると展開されます)
AWSTemplateFormatVersion: "2010-09-09"
Description: "Set security alert shaping"
Parameters:
  Prefix:
    Type: String
    Default: cm
  GuardDutySeverities:
    Type: CommaDelimitedList
    Default: "LOW,MEDIUM,HIGH,CRITICAL"
  SecurityHubSeverities:
    Type: CommaDelimitedList
    Default: "LOW,MEDIUM,HIGH,CRITICAL,INFORMATIONAL"

Resources:
  #整形用ステートマシン
  SecurityAlertShapingStateMachine:
    Type: AWS::StepFunctions::StateMachine
    Properties:
      Definition:
        {
          "Comment": "A description of my state machine",
          "StartAt": "Choice Service",
          "States":
            {
              "Choice Service":
                {
                  "Type": "Choice",
                  "Choices":
                    [
                      {
                        "Variable": "$.detail.findings[0].ProductName",
                        "StringEquals": "GuardDuty",
                        "Next": "GuardDuty Choice Severity",
                      },
                      {
                        "Variable": "$.detail.findings[0].ProductName",
                        "StringEquals": "Security Hub",
                        "Next": "Security Hub Choice Severity",
                      },
                      {
                        "Variable": "$.detail.findings[0].ProductName",
                        "StringEquals": "IAM Access Analyzer",
                        "Next": "AccessAnalyzer Sharping",
                      },
                    ],
                  "Default": "Fail",
                },
              "Security Hub Choice Severity":
                {
                  "Type": "Choice",
                  "Choices":
                    [
                      {
                        "Or":
                          [
                            {
                              "Variable": "$.detail.findings[0].Severity.Label",
                              "StringEquals": "LOW",
                            },
                            {
                              "Variable": "$.detail.findings[0].Severity.Label",
                              "StringEquals": "INFORMATIONAL",
                            },
                          ],
                        "Next": "Security Hub Severity Low",
                      },
                      {
                        "Variable": "$.detail.findings[0].Severity.Label",
                        "StringEquals": "MEDIUM",
                        "Next": "Security Hub Severity Middle",
                      },
                      {
                        "Or":
                          [
                            {
                              "Variable": "$.detail.findings[0].Severity.Label",
                              "StringEquals": "HIGH",
                            },
                            {
                              "Variable": "$.detail.findings[0].Severity.Label",
                              "StringEquals": "CRITICAL",
                            },
                          ],
                        "Next": "Security Hub Severity High",
                      },
                    ],
                  "Default": "Security Hub Severity None",
                },
              "Security Hub Severity None":
                {
                  "Type": "Pass",
                  "Next": "Security Hub Sharping",
                  "Result": "不明",
                  "ResultPath": "$.SeverityName",
                },
              "Security Hub Severity Low":
                {
                  "Type": "Pass",
                  "Next": "Security Hub Sharping",
                  "Result": "低",
                  "ResultPath": "$.SeverityName",
                },
              "Security Hub Severity Middle":
                {
                  "Type": "Pass",
                  "Next": "Security Hub Sharping",
                  "Result": "中",
                  "ResultPath": "$.SeverityName",
                },
              "Security Hub Severity High":
                {
                  "Type": "Pass",
                  "Next": "Security Hub Sharping",
                  "Result": "高",
                  "ResultPath": "$.SeverityName",
                },
              "GuardDuty Choice Severity":
                {
                  "Type": "Choice",
                  "Choices":
                    [
                      {
                        "Or":
                          [
                            {
                              "Variable": "$.detail.findings[0].Severity.Label",
                              "StringEquals": "LOW",
                            },
                            {
                              "Variable": "$.detail.findings[0].Severity.Label",
                              "StringEquals": "INFORMATIONAL",
                            },
                          ],
                        "Next": "GuardDuty Severity Low",
                      },
                      {
                        "Variable": "$.detail.findings[0].Severity.Label",
                        "StringEquals": "MEDIUM",
                        "Next": "GuardDuty Severity Middle",
                      },
                      {
                        "Or":
                          [
                            {
                              "Variable": "$.detail.findings[0].Severity.Label",
                              "StringEquals": "HIGH",
                            },
                            {
                              "Variable": "$.detail.findings[0].Severity.Label",
                              "StringEquals": "CRITICAL",
                            },
                          ],
                        "Next": "GuardDuty Severity High",
                      },
                    ],
                  "Default": "GuardDuty Severity None",
                },
              "GuardDuty Severity None":
                {
                  "Type": "Pass",
                  "Next": "GuardDuty Sharping",
                  "Result": "不明",
                  "ResultPath": "$.SeverityName",
                },
              "GuardDuty Severity Low":
                {
                  "Type": "Pass",
                  "Next": "GuardDuty Sharping",
                  "Result": "低",
                  "ResultPath": "$.SeverityName",
                },
              "GuardDuty Severity Middle":
                {
                  "Type": "Pass",
                  "Next": "GuardDuty Sharping",
                  "Result": "中",
                  "ResultPath": "$.SeverityName",
                },
              "GuardDuty Severity High":
                {
                  "Type": "Pass",
                  "Next": "GuardDuty Sharping",
                  "Result": "高",
                  "ResultPath": "$.SeverityName",
                },
              "GuardDuty Sharping":
                {
                  "Type": "Pass",
                  "Next": "EventBridge PutEvents",
                  "Parameters":
                    {
                      "Subject.$": "States.Format('緊急度: {} GuardDutyセキュリティアラート Account: {}', $.SeverityName, $.detail.findings[0].AwsAccountId)",
                      "Message.$": "States.Format('以下の脅威を検知しました。  \n意図したものであるか確認してください。  \n検出タイプ: {}  \n詳細: {}  \nリージョン: {}  \n推奨事項URL: https://docs.aws.amazon.com/ja_jp/guardduty/latest/ug/guardduty_finding-types-active.html   \nコンソールURL: {}', $.detail.findings[0].Types[0], $.detail.findings[0].Description, $.detail.findings[0].Region, $.detail.findings[0].SourceUrl)",
                    },
                },
              "Security Hub Sharping":
                {
                  "Type": "Pass",
                  "Parameters":
                    {
                      "Subject.$": "States.Format('緊急度: {} Security Hubセキュリティアラート Account: {}', $.SeverityName, $.account)",
                      "Message.$": "States.Format('Security Hubでセキュリティ上好ましくない設定を検知しました。  \n意図したものであるか確認してください。  \n検知内容: {}  \nリソース種類: {}  \nリソースID: {}  \n詳細: {}  \nリージョン: {}  \n推奨事項URL: {}   \nコンソールURL: https://{}.console.aws.amazon.com/securityhub/home?region={}#/standards/aws-foundational-security-best-practices-1.0.0/{}', $.detail.findings[0].Title, $.detail.findings[0].Resources[0].Type, $.detail.findings[0].Resources[0].Id, $.detail.findings[0].Description,$.detail.findings[0].Region, $.detail.findings[0].ProductFields.RecommendationUrl, $.detail.findings[0].Region, $.detail.findings[0].Region, $.detail.findings[0].ProductFields.ControlId)",
                    },
                  "Next": "EventBridge PutEvents",
                },

              "AccessAnalyzer Sharping":
                {
                  "Type": "Pass",
                  "Parameters":
                    {
                      "Subject.$": "States.Format('IAM Access Analyzerセキュリティアラート Account: {}', $.detail.findings[0].AwsAccountId)",
                      "Message.$": "States.Format('以下リソースが外部共有されています。  \n意図した設定か確認してください。  \nリソース種類: {}  \nリソース名: {}  \nリージョン: {}  \n詳細: {}  \nコンソールURL: {}', $.detail.findings[0].Resources[0].Type, $.detail.findings[0].Resources[0].Id,$.detail.findings[0].Region,$.detail.findings[0].Description,$.detail.findings[0].SourceUrl)",
                    },
                  "Next": "EventBridge PutEvents",
                },
              "EventBridge PutEvents":
                {
                  "Type": "Task",
                  "Resource": "arn:aws:states:::events:putEvents",
                  "Parameters":
                    {
                      "Entries":
                        [
                          {
                            "Detail":
                              {
                                "Subject.$": "$.Subject",
                                "Message.$": "$.Message",
                              },
                            "DetailType": "Sharped Findings",
                            "EventBusName": !Ref SecurityAlertAggregatorBus,
                            "Source": "custom.securityalert.stepfunctions",
                          },
                        ],
                    },
                  "End": True,
                },
              "Fail": { "Type": "Fail" },
            },
        }
      RoleArn: !GetAtt SecurityAlertShapingStateMachineRole.Arn
      StateMachineName: !Sub ${Prefix}-sharping-security-alert-machine

  #整形用ステートマシン用ロール
  SecurityAlertShapingStateMachineRole:
    Type: AWS::IAM::Role
    Properties:
      AssumeRolePolicyDocument:
        {
          "Version": "2012-10-17",
          "Statement":
            [
              {
                "Effect": "Allow",
                "Principal": { "Service": "states.amazonaws.com" },
                "Action": "sts:AssumeRole",
              },
            ],
        }
      Policies:
        - PolicyName: !Sub ${Prefix}-shaping-statemachine-role-policy
          PolicyDocument:
            {
              "Version": "2012-10-17",
              "Statement":
                [
                  {
                    "Effect": "Allow",
                    "Action": ["events:PutEvents"],
                    "Resource": !GetAtt SecurityAlertAggregatorBus.Arn,
                  },
                ],
            }
      RoleName: !Sub ${Prefix}-shaping-statemachine-role

  #整形後の送信先イベントバス
  SecurityAlertAggregatorBus:
    Type: AWS::Events::EventBus
    Properties:
      Name: !Sub ${Prefix}-security-alert-aggregator-bus

  GuardDutyEventRule:
    Type: AWS::Events::Rule
    Properties:
      Name: !Sub ${Prefix}-security-alert-guardduty-finding-rule
      EventPattern:
        source: ["aws.securityhub"]
        detail-type: ["Security Hub Findings - Imported"]
        detail:
          findings:
            ProductName: ["GuardDuty"]
            Severity:
              Label: !Ref GuardDutySeverities
            RecordState:
              - "ACTIVE"
            Workflow:
              Status:
                - "NEW"
      Targets:
        - Arn: !GetAtt SecurityAlertShapingStateMachine.Arn
          Id: !Sub ${Prefix}-security-alert-guardduty-finding-rule
          RoleArn: !GetAtt SecurityAlertEventRuleRole.Arn

  SecurityHubEventRule:
    Type: AWS::Events::Rule
    Properties:
      Name: !Sub ${Prefix}-security-alert-securityhub-finding-rule
      EventPattern:
        source:
          - "aws.securityhub"
        detail-type:
          - "Security Hub Findings - Imported"
        detail:
          findings:
            Compliance:
              Status:
                - "FAILED"
                - "WARNING"
                - "NOT_AVAILABLE"
            RecordState:
              - "ACTIVE"
            Workflow:
              Status:
                - "NEW"
            ProductFields:
              StandardsArn:
                - "arn:aws:securityhub:::standards/aws-foundational-security-best-practices/v/1.0.0"
            Severity:
              Label: !Ref SecurityHubSeverities
      Targets:
        - Arn: !GetAtt SecurityAlertShapingStateMachine.Arn
          Id: !Sub ${Prefix}-security-alert-securityhub-finding-rule
          RoleArn: !GetAtt SecurityAlertEventRuleRole.Arn

  AccessAnalyzerEventRule:
    Type: AWS::Events::Rule
    Properties:
      Name: !Sub ${Prefix}-security-alert-access-analyzer-finding-rule
      EventPattern:
        source: ["aws.securityhub"]
        detail-type: ["Security Hub Findings - Imported"]
        detail:
          findings:
            ProductName: ["IAM Access Analyzer"]
            RecordState:
              - "ACTIVE"
            Workflow:
              Status:
                - "NEW"

      Targets:
        - Arn: !GetAtt SecurityAlertShapingStateMachine.Arn
          Id: !Sub ${Prefix}-security-alert-access-analyzer-finding-rule
          RoleArn: !GetAtt SecurityAlertEventRuleRole.Arn

  #Eventルール用ロール
  SecurityAlertEventRuleRole:
    Type: AWS::IAM::Role
    Properties:
      AssumeRolePolicyDocument:
        {
          "Version": "2012-10-17",
          "Statement":
            [
              {
                "Effect": "Allow",
                "Principal": { "Service": "events.amazonaws.com" },
                "Action": "sts:AssumeRole",
              },
            ],
        }
      Policies:
        - PolicyName: !Sub ${Prefix}-security-alert-eventrule-role-policy
          PolicyDocument:
            {
              "Version": "2012-10-17",
              "Statement":
                [
                  {
                    "Effect": "Allow",
                    "Action": ["states:StartExecution"],
                    "Resource": [!GetAtt SecurityAlertShapingStateMachine.Arn],
                  },
                ],
            }
      RoleName: !Sub ${Prefix}-security-alert-eventrule-role-policy

Outputs:
  SecurityAlertAggregatorBus:
    Value: !Ref SecurityAlertAggregatorBus
    Export:
      Name: security-alert-aggregator-bus
②メール通知用(クリックすると展開されます)
AWSTemplateFormatVersion: 2010-09-09
Description: "Set security alert mail"
Parameters:
  Prefix:
    Type: String
    Default: cm
  MailAddress:
    Description: Enter email address to send alert.
    Type: String
Resources:
  AlertEventRuleRole:
    Type: "AWS::IAM::Role"
    Properties:
      RoleName: !Sub ${Prefix}-security-alert-mail-eventrule-role
      AssumeRolePolicyDocument:
        Version: "2012-10-17"
        Statement:
          - Effect: "Allow"
            Principal:
              Service:
                - "events.amazonaws.com"
            Action:
              - "sts:AssumeRole"
  AlertEventRulePolicy:
    Type: AWS::IAM::ManagedPolicy
    Properties:
      ManagedPolicyName: !Sub ${Prefix}-security-alert-mail-eventrule-policy
      PolicyDocument:
        Version: "2012-10-17"
        Statement:
          - Effect: "Allow"
            Action:
              - "states:StartExecution"
            Resource: !Ref SendAlertMailMachine
      Roles:
        - !Ref AlertEventRuleRole
  AlertStateMachineRole:
    Type: "AWS::IAM::Role"
    Properties:
      RoleName: !Sub ${Prefix}-security-alert-mail-statemachine-role
      AssumeRolePolicyDocument:
        Version: "2012-10-17"
        Statement:
          - Effect: "Allow"
            Principal:
              Service:
                - "states.amazonaws.com"
            Action:
              - "sts:AssumeRole"
  AlertStateMachinePolicy:
    Type: AWS::IAM::ManagedPolicy
    Properties:
      ManagedPolicyName: !Sub ${Prefix}-security-alert-mail-statemachine-policy
      PolicyDocument:
        Version: "2012-10-17"
        Statement:
          - Effect: "Allow"
            Action:
              - "sns:Publish"
            Resource: !Ref SNSTopic
      Roles:
        - !Ref AlertStateMachineRole
  EventRule:
    Type: "AWS::Events::Rule"
    Properties:
      Name: !Sub ${Prefix}-security-alert-mail-rule
      EventBusName: !ImportValue security-alert-aggregator-bus
      Description: "Security alert"
      EventPattern: { "source": ["custom.securityalert.stepfunctions"] }
      Targets:
        - Arn: !Ref SendAlertMailMachine
          Id: IdSendAlertMailMachine
          RoleArn: !GetAtt AlertEventRuleRole.Arn
  SendAlertMailMachine:
    Type: AWS::StepFunctions::StateMachine
    Properties:
      StateMachineName: !Sub ${Prefix}-security-alert-mail-machine
      Definition:
        Comment: "Send Security alert mail machine"
        StartAt: SNSPublish
        States:
          SNSPublish:
            Type: Task
            Resource: "arn:aws:states:::sns:publish"
            Parameters:
              TopicArn: !Ref SNSTopic
              Subject.$: "$.detail.Subject"
              Message.$: "$.detail.Message"
            End: true
      RoleArn: !GetAtt AlertStateMachineRole.Arn
  SNSTopic:
    Type: "AWS::SNS::Topic"
    Properties:
      TopicName: !Sub ${Prefix}-security-alert-mail-topic
      KmsMasterKeyId: alias/aws/sns
  SNSSubscription:
    Type: "AWS::SNS::Subscription"
    Properties:
      Endpoint: !Ref MailAddress
      Protocol: email
      TopicArn: !Ref SNSTopic
  SNSTopicPolicy:
    Type: "AWS::SNS::TopicPolicy"
    Properties:
      PolicyDocument:
        Id: default_policy_ID
        Version: "2012-10-17"
        Statement:
          - Sid: default_statement_ID
            Effect: Allow
            Principal:
              AWS: "*"
            Action:
              - "SNS:GetTopicAttributes"
              - "SNS:SetTopicAttributes"
              - "SNS:AddPermission"
              - "SNS:RemovePermission"
              - "SNS:DeleteTopic"
              - "SNS:Subscribe"
              - "SNS:ListSubscriptionsByTopic"
              - "SNS:Publish"
              - "SNS:Receive"
            Resource: !Ref SNSTopic
            Condition:
              StringEquals:
                "AWS:SourceOwner": !Ref "AWS::AccountId"
          - Sid: AllowPublishFromStepFunctions
            Effect: Allow
            Principal:
              AWS: !GetAtt AlertStateMachineRole.Arn
            Action: "sns:Publish"
            Resource: !Ref SNSTopic
      Topics:
        - !Ref SNSTopic
③Slack通知用(クリックすると展開されます)
AWSTemplateFormatVersion: 2010-09-09
Description: "Set security alert slack"
Parameters:
  Prefix:
    Type: String
    Default: cm
  SlackChannelId:
    Description: Enter Slack Channel to send alert.
    Type: String
  BotUserOAuthToken:
    Description: Enter token for bot authentication.
    Type: String
Resources:
  AlertEventRuleRole:
    Type: "AWS::IAM::Role"
    Properties:
      RoleName: !Sub ${Prefix}-security-alert-slack-eventrule-role
      AssumeRolePolicyDocument:
        Version: "2012-10-17"
        Statement:
          - Effect: "Allow"
            Principal:
              Service:
                - "events.amazonaws.com"
            Action:
              - "sts:AssumeRole"
  AlertEventRulePolicy:
    Type: AWS::IAM::ManagedPolicy
    Properties:
      ManagedPolicyName: !Sub ${Prefix}-security-alert-slack-eventrule-policy
      PolicyDocument:
        Version: "2012-10-17"
        Statement:
          - Effect: "Allow"
            Action:
              - "events:InvokeApiDestination"
            Resource: !GetAtt EventApiDestination.Arn
      Roles:
        - !Ref AlertEventRuleRole
  EventRule:
    Type: AWS::Events::Rule
    Properties:
      Name: !Sub ${Prefix}-security-alert-slack-rule
      EventBusName: !ImportValue security-alert-aggregator-bus
      Description: "Security alert"
      EventPattern: { "source": ["custom.securityalert.stepfunctions"] }
      Targets:
        - Arn: !GetAtt EventApiDestination.Arn
          Id: IdSendAlertSlackApi
          RoleArn: !GetAtt AlertEventRuleRole.Arn
          InputTransformer:
            InputPathsMap:
              Message: "$.detail.Message"
              Subject: "$.detail.Subject"
            InputTemplate: !Sub '{"channel":"${SlackChannelId}","text":"<Subject>","blocks":[{"type":"header","text":{"type":"plain_text","text":"<Subject>"}},{"type":"divider"},{"type":"section","text":{"type":"mrkdwn","text":"<Message>"}}]}'
  EventApiDestination:
    Type: AWS::Events::ApiDestination
    Properties:
      Description: "Security alert ApiDestination"
      ConnectionArn: !GetAtt EventConnection.Arn
      HttpMethod: POST
      InvocationEndpoint: "https://slack.com/api/chat.postMessage"
      InvocationRateLimitPerSecond: 300
      Name: !Sub ${Prefix}-security-alert-slack-apidestination
  EventConnection:
    Type: AWS::Events::Connection
    Properties:
      AuthorizationType: API_KEY
      Description: "Security alert Connection"
      Name: !Sub ${Prefix}-security-alert-slack-connection
      AuthParameters:
        ApiKeyAuthParameters:
          ApiKeyName: Authorization
          ApiKeyValue: !Sub Bearer ${BotUserOAuthToken}
④Teams通知用(クリックすると展開されます)
AWSTemplateFormatVersion: 2010-09-09
Description: "Set security alert teams"
Parameters:
  Prefix:
    Type: String
    Default: cm
  TeamsWebhookUrl:
    Description: Enter Teams Channel to send alert.
    Type: String
Resources:
  AlertEventRuleRole:
    Type: "AWS::IAM::Role"
    Properties:
      RoleName: !Sub ${Prefix}-security-alert-teams-eventrule-role
      AssumeRolePolicyDocument:
        Version: "2012-10-17"
        Statement:
          - Effect: "Allow"
            Principal:
              Service:
                - "events.amazonaws.com"
            Action:
              - "sts:AssumeRole"
  AlertEventRulePolicy:
    Type: AWS::IAM::ManagedPolicy
    Properties:
      ManagedPolicyName: !Sub ${Prefix}-security-alert-teams-eventrule-policy
      PolicyDocument:
        Version: "2012-10-17"
        Statement:
          - Effect: "Allow"
            Action:
              - "events:InvokeApiDestination"
            Resource: !GetAtt EventApiDestination.Arn
      Roles:
        - !Ref AlertEventRuleRole
  EventRule:
    Type: AWS::Events::Rule
    Properties:
      Name: !Sub ${Prefix}-security-alert-teams-rule
      EventBusName: !ImportValue security-alert-aggregator-bus
      Description: "Security alert"
      EventPattern: { "source": ["custom.securityalert.stepfunctions"] }
      Targets:
        - Arn: !GetAtt EventApiDestination.Arn
          Id: IdSendAlertSlackApi
          RoleArn: !GetAtt AlertEventRuleRole.Arn
          InputTransformer:
            InputPathsMap:
              Message: "$.detail.Message"
              Subject: "$.detail.Subject"
            InputTemplate: !Sub '{"title": "<Subject>","text": "<Message>"}'
  EventApiDestination:
    Type: AWS::Events::ApiDestination
    Properties:
      Description: "Security alert ApiDestination"
      ConnectionArn: !GetAtt EventConnection.Arn
      HttpMethod: POST
      InvocationEndpoint: !Ref TeamsWebhookUrl
      InvocationRateLimitPerSecond: 300
      Name: !Sub ${Prefix}-security-alert-teams-apidestination
  EventConnection:
    Type: AWS::Events::Connection
    Properties:
      AuthorizationType: API_KEY
      Description: "Security alert Connection"
      Name: !Sub ${Prefix}-security-alert-teams-connection
      AuthParameters:
        # 本来は必要ないが、必須項目のためダミーの値を入力
        ApiKeyAuthParameters:
          ApiKeyName: key
          ApiKeyValue: value

実装してみる

先ほど紹介したCloudFormationを使って通知の仕組みを実装してみます。

前提

以下が設定されていることを前提としています。

①共通機能の展開

まずは各サービスのイベント取得と整形する機能を展開します。Security Hubの集約先のリージョンに ①共通機能 のテンプレートを使ってスタックを作成しましょう。パラメータは利用環境に合わせて変更してください。

  • Prefix
    • 各リソースにつけるプレフィックス
    • デフォルトはcm
  • GuardDutySeverities
    • GuardDutyで通知する重要度のラベル
    • LOW,MEDIUM,HIGH,CRITICALから通知したい重要度を入力
    • デフォルトは全て通知
  • SecurityHubSeverities
    • Security Hubで通知する重要度のラベル
    • LOW,MEDIUM,HIGH,CRITICAL,INFORMATIONALから通知したい重要度を入力
    • デフォルトは全て通知

その他はデフォルトで作成して大丈夫です。スタックの作成が完了することを確認しましょう。

実装している内容を以下で簡単に説明していますが、とりあえず使えればいいという方は ②メール通知用の展開 まで読み飛ばしてください。

EventBridge ルール

Security Hubに集約された検出結果から、目的のサービスだけを取得するEventルールを作成しています。

IAM Access Analyzerの場合は、Security Hubからイベントを取得するようなイベントパターンを設定しています。

{
  "detail-type": ["Security Hub Findings - Imported"],
  "source": ["aws.securityhub"],
  "detail": {
    "findings": {
      "ProductName": ["IAM Access Analyzer"],
      "RecordState": ["ACTIVE"],
      "Workflow": {
        "Status": ["NEW"]
      }
    }
  }
}

GuardDutyは重要度のフィルタリングをするためにSeverityのラベルを追加しています。${GuardDutySeverities}はCloudFormationのインプットです。

{
  "detail-type": ["Security Hub Findings - Imported"],
  "source": ["aws.securityhub"],
  "detail": {
    "findings": {
      "ProductName": ["GuardDuty"],
      "RecordState": ["ACTIVE"],
      "Workflow": {
        "Status": ["NEW"]
      },
      "Severity": {
        "Label": ["${GuardDutySeverities}"]
      }
    }
  }
}

Security Hubの通知はAWS 基礎セキュリティのベストプラクティス v1.0.0のみ通知させたかったので、以下のようなパターンで設定しました。${SecurityHubSeverities}はCloudFormationのインプットです。

{
  "detail-type": ["Security Hub Findings - Imported"],
  "source": ["aws.securityhub"],
  "detail": {
    "findings": {
      "Compliance": {
        "Status": ["FAILED", "WARNING", "NOT_AVAILABLE"]
      },
      "RecordState": ["ACTIVE"],
      "ProductFields": {
        "StandardsArn": ["arn:aws:securityhub:::standards/aws-foundational-security-best-practices/v/1.0.0"]
      },
      "Workflow": {
        "Status": ["NEW"]
      },
      "Severity": {
        "Label": ["${SecurityHubSeverities}"]
      }
    }
  }
}

より具体的にフィルタリングの内容を知りたい方はこちらのブログが分かりやすく解説されています。

StepFunctions

Security Hubから取得したイベントを整形するステートマシンの定義です。見づらくてすみません。

ステートマシンの詳細な定義は細かい話になるので、できればCloudFormationテンプレートを確認して下さい。 上から簡単に説明すると各サービスごとに整形が異なるため、まず取得したイベントがどのサービスかで分岐してます。その後GuardDutyとSecurity Hubでは重要度のラベルから[高,中,低]の判別をして次のフローに渡しています。

あとは各サービスごとに整形、カスタムイベントバスに整形したタイトルと本文をプッシュしてます。これらは全てLambdaを使わずStepFunctionsのみで実装できます、すごいですね。

カスタムイベントバスに集約されたイベントはメールやSlackへ通知されるのですが、通知先が環境によって異なるため①共通機能には含めていません。通知の実装は後述します。

②メール通知用の展開

メールで通知したい人向けです。他の通知先を利用する方は呼ばして頂いて構いません。

赤枠で囲った部分を設定していきます。

といっても②メール通知用のテンプレートでスタックを同じリージョンに作成するだけです。インプットは以下の通り。

  • Prefix
    • 各リソースにつけるプレフィックス
    • デフォルトはcm
  • MailAddress
    • 通知先のメールアドレス

上記のパラメータを入力したらそのままスタックを作成してください。

入力したメールアドレス宛にAmazon SNSからメールアドレスの登録確認として「AWS Notification - Subscription Confirmation」というタイトルのメールが届きます。本文中の「Confirm subscription」リンクを押して登録を完了してください。

これでメール通知設定は完了です。

③Slack通知用の展開

Slackで通知したい人向けです。他の通知先を利用する方は呼ばして頂いて構いません。

赤枠で囲った部分を設定していきます。

こちらも③Slack通知用のテンプレートでスタックを同じリージョンに作成します。インプットは以下の通り。

  • Prefix
    • 各リソースにつけるプレフィックス
    • デフォルトはcm
  • BotUserOAuthToken
    • 連携するBotアプリの認証情報
  • SlackChannelId
    • 連携するBotアプリが追加されているチャンネルID

BotUserOAuthTokenSlackChannelIdの取得方法については以下ブログをご参照ください。

スタックの作成が完了すればSlackの通知設定は完了です。

④Teams通知用の展開

Teamsで通知したい人向けです。他の通知先を利用する方は呼ばして頂いて構いません。

赤枠で囲った部分を設定していきます。

こちらも④Teams通知用のテンプレートでスタックを同じリージョンに作成します。インプットは以下の通り。

  • Prefix
    • 各リソースにつけるプレフィックス
    • デフォルトはcm
  • TeamsWebhookUrl
    • 連携するTeamsのWebhookUrl

TeamsWebhookUrlの取得方法については以下ブログの「Teams側の準備」をご参照ください。

スタックの作成が完了すればTeamsの通知設定は完了です。

通知の確認

実装が完了したので、試しにGuardDutyの検出結果が通知されるか確認してみます。バージニア北部で検知した結果を東京リージョンに実装した仕組みで通知してみます。

サンプルイベントでもいいのですが、通知量がすごい数になるのが嫌なのでこちらを使って疑似的に検知してみます。少し時間がかかりますが、簡単にお試し検知ができるのでおすすめです。

無事各通知先で通知を受け取ることができました。

マルチアカウントで通知機能を実装する場合

もし本ブログの通知機能をマルチアカウントに実装したい場合は、StackSetsを使って①共通機能を展開するのがおすすめです。実はStackSetsで利用することを想定して、あえてCloudFormationテンプレートで作成しています。

StackSetsはコンソールから利用してもいいのですが、個人的にはCDKを使って管理するとものすごく簡単に展開先のリージョンやOUを指定できるので是非利用してみてください。(サンプルコードは以下ブログ参照)

複数アカウントにまるっと実装するなら以下の流れが良いかなと思っています。

  1. GuardDutyやSecurity HubをOrganizations統合で有効化
  2. 共通機能(①)の実装はStackSetsでまとめて展開
  3. 通知先の設定(②、③、④)のみ各AWSアカウントのユーザーが実施

1と2をあらかじめ自動化しておくことで、3の通知設定だけをユーザーが行うだけで済みます。ユーザーが通知設定する部分はService Catalogをうまく活用することで効率化できるかもしれません。

是非このあたりの仕組み作りに本ブログを含め役立てて頂ければ幸いです。

おわりに

Security Hubに集約した検出結果を通知する仕組みをCloudFormationでまるっと作ってみました。長々と書きましたが前提部分が満たせていればCloudFormationのスタック作るだけなので気軽に試してみてください。中身を理解しながらアレンジできる方は運用に合わせてメッセージを変えたり、検知対象のAWSサービスを追加したり自由にどうぞ。

逆に管理するのが大変そうだなと思った方は、冒頭に紹介したセキュアアカウントの利用をご検討頂ければ幸いです。

参考