GuardDuty をリージョン集約して Slack 通知する方法について考えてみた

GuardDuty は SCP 等で制限していない限りは全リージョンで有効化することが推奨されます。
むしろ、積極的に利用していないリージョンでの検知こそ攻撃者のアクティビティによるものである可能性が高いとも言えます。
ただ、GuardDutyを有効化するだけではあまり意味が無く、重要な検知があった際は迅速に対応できるようにする必要があります。
となると、 GuardDuty 検知時の通知を整備したくなるのですが、 GuardDuty にはリージョン集約機能が無いので上手く設計しないと通知に利用するリソースが多くなってしまいます。
今回は Slack への通知を前提にして、リージョン集約しつつ通知する方法について改めて考えてみました。
Slack を前提に考える理由は、今まで私が Slack 通知を実装することが多かったからというだけです。
ただ、 Chatbot を使えれば条件は同じになると思うので、Microsoft Teams や Chime でも同じ話が言えると思います。

リージョン集約する方法

リージョン集約する方法として 2 パターン考えられます。

  1. Security Hub 連携を利用して、GuardDuty の検出結果を Security Hub に取り込んだ後に EventBridge で拾って SNS + Chatbot 経由で通知する
  2. EventBridge カスタムイベントバスに全リージョン分のイベントを集めてから、SNS + Chatbot 経由で通知する

GuardDuty の通知は Security Hub 経由で行うとリージョン集約ができるため、リージョンごとに EventBridge Rules 等を作成しなくて良くなります。
ただ、イベントが GuardDuty Finding から Security Hub Findings - Imported に変わるので扱うイベントが少し変わります。
また、 SNS をリージョン数分作成することもできるのですが、であれば EventBridge でリージョン集約した方が良いと思っているので考えないことにします。

前者のパターンについて解説したブログ

後者のパターンについて解説したブログ(通知先はメール)

結論としては Security Hub を利用しているのであれば Security Hub 連携で、使っていなければ EventBridge カスタムイベントバスで集約する形で良いと思います。
ですが、どちらを採用するかによってカスマイズしない場合の通知内容も変わるのでもう少し詳しくみていきます。

Security Hub 連携のパターン

マネージドな仕組みを少しでも多く使っていくならこちらになります。
Security Hub のリージョン集約先に EventBridge Rules + SNS を作成しつつ、 Chatbot の設定をすれば良いのでかなりシンプルですね。
ただ、この方法で一点気になっていたのは、通知文から GuardDuty の通知文であることが分かり辛くなることです。

GuardDuty のイベントを Security Hub  に取り込むと、Security Hub Findings - Imported というイベントになるので当たり前と言えば当たり前ですね。

{
  "version": "0",
  "id": "8e5622f9-d81c-4d81-612a-9319e7ee2506",
  "detail-type": "Security Hub Findings - Imported",
  "source": "aws.securityhub",
  "account": "123456789012",
  "time": "2019-04-11T21:52:17Z",
  "region": "us-west-2",
  "resources": ["arn:aws:securityhub:us-west-2::product/aws/macie/arn:aws:macie:us-west-2:123456789012:integtest/trigger/6294d71b927c41cbab915159a8f326a3/alert/f2893b211841"],
  "detail": {
    "findings": [{
      "SchemaVersion": "2018-10-08",
      "Id": "arn:aws:macie:us-west-2:123456789012:integtest/trigger/6214d71b927c41cbab015159a8f316a3/alert/f2893b211841467198cc1201e9031ee4",
      "ProductArn": "arn:aws:securityhub:us-west-2::product/aws/macie",
      "GeneratorId": "arn:aws:macie:us-west-2:123456789012:integtest/trigger/6214d71b927c41cbab015159a8f316a3",
      "AwsAccountId": "123456789012",
      "Types": ["Sensitive Data Identifications/Passwords/Google Suite Two-factor backup codes in S3"],
      "FirstObservedAt": "2019-04-11T21:52:15.900Z",
      "LastObservedAt": "2019-04-11T21:52:15.900Z",
      "CreatedAt": "2019-04-11T21:52:15.900Z",
      "UpdatedAt": "2019-04-11T21:52:15.900Z",
      "Severity": {
        "Product": 6,
        "Normalized": 15
      },
      "Confidence": 5,
      "Title": "Google Suite Two-Factor Backup Codes uploaded to S3",
      "Description": "Google Suite two-factor backup codes uploaded to S3....",
      "Remediation": {
        "Recommendation": {
          "Text": "v2 Release"
        }
      },
      "ProductFields": {
        "rule-arn": "arn:aws:macie:us-west-2:123456789012:trigger/6214d71b927c41cbab015159a8f316a3",
        "tags:0": "DATA_COMPLIANCE",
        "tags:1": "BASIC_ALERT",
        "themes:0/theme": "google_two_factor_backup",
        "themes:0/count": "1",
        "dlpRisk:0/risk": "8",
        "dlpRisk:0/count": "1",
        "owner:0/name": "vchin",
        "owner:0/count": "1",
        "aws/securityhub/FindingId": "arn:aws:securityhub:us-west-2::product/aws/macie/arn:aws:macie:us-west-2:123456789012:integtest/trigger/6214d71b927c41cbab015159a8f316a3/alert/f2893b211841467198cc1201e9031ee4",
        "aws/securityhub/SeverityLabel": "LOW",
        "aws/securityhub/ProductName": "Macie",
        "aws/securityhub/CompanyName": "Amazon"
      },
      "Resources": [{
        "Type": "AwsS3Bucket",
        "Id": "arn:aws:s3:::test-bucket-12",
        "Partition": "aws",
        "Region": "us-west-2"
      }],
      "RecordState": "ACTIVE",
      "WorkflowState": "NEW"
    }]
  }
}

ただし、2023/09 に Chatbot の通知内容をカスタマイズできるようになりました。

この方法を使えば Security Hub 経由の通知でも GuardDuty のものであることを明示できます。
試しに下記のような CloudFormation テンプレートを用意して、カスタマイズした通知を実装してみました。(Chatbot と Slack の連携は済んでいる前提です)

AWSTemplateFormatVersion: "2010-09-09"

Parameters:
  SlackWorkspaceId:
    Description: "Workspace Id for Slack"
    Type: String

  SlackChannelId:
    Description: "Channel Id in Slack for GuardDuty notification"
    Type: String

Resources:
  GuardDutyNotificationRule:
    Type: AWS::Events::Rule
    Properties:
      Description: GuardDuty Findings via Security Hub
      EventBusName: default
      Name: guardduty-notification-rule
      EventPattern:
        source:
          - "aws.securityhub"
        detail-type:
          - "Security Hub Findings - Imported"
        detail:
          findings:
            ProductName:
              - GuardDuty
            Workflow:
              Status:
                - NEW
            RecordState:
              - ACTIVE
      Targets:
        - Arn: !Ref GuardDutyNotificationTopic
          Id: "GuardDutyNotificationTopic"
          InputTransformer:
            InputPathsMap:
              "affectedResource": "$.detail.findings[0].Resources[0].Id"
              "accountId": "$.detail.findings[0].AwsAccountId"
              "description": "$.detail.findings[0].Description"
              "findingId": "$.detail.findings[0].Id"
              "region": "$.detail.findings[0].Resources[0].Region"
              "severity": "$.detail.findings[0].Severity.Label"
              "title": "$.detail.findings[0].Title"
              "firstObservedAt": "$.detail.findings[0].FirstObservedAt"
              "lastObservedAt": "$.detail.findings[0].LastObservedAt"
            InputTemplate: !Sub '{"version" : "1.0", "source": "custom", "content": {"textType": "client-markdown", "title": ":rotating_light: GuardDuty Finding | <region> | Account: <accountId>", "description": "*Title*\n <title>\n *Description*\n <description>\n *Severity*\n <severity>\n *Affected Resource*\n <affectedResource>\n *FirstObservedAt(UTC)*\n <firstObservedAt>\n *LastObservedAt(UTC)*\n <lastObservedAt>\n\n *AWSアカウントにログインして、下記リンクから詳細を確認して下さい*\n https://${AWS::Region}.console.aws.amazon.com/securityhub/home?region=us-east-1#/findings?search=Id%3D%255Coperator%255C%253AEQUALS%255C%253A<findingId>"}}'

  GuardDutyNotificationTopic:
    Type: AWS::SNS::Topic
    Properties:
      TopicName: guardduty-notification-topic

  GuardDutyNotificationTopicPolicy:
    Type: AWS::SNS::TopicPolicy
    Properties:
      Topics:
        - !Ref GuardDutyNotificationTopic
      PolicyDocument:
        Version: "2012-10-17"
        Statement:
          - Sid: "EventBridgeRule"
            Effect: "Allow"
            Principal:
              Service: "events.amazonaws.com"
            Action: "sns:Publish"
            Resource: !Ref GuardDutyNotificationTopic

  GuardDutyNotificationChatbotConfiguration:
    Type: AWS::Chatbot::SlackChannelConfiguration
    Properties:
      ConfigurationName: GuardDutyNotification
      IamRoleArn: !GetAtt ChatbotIamRole.Arn
      LoggingLevel: INFO
      SlackChannelId: !Ref SlackChannelId
      SlackWorkspaceId: !Ref SlackWorkspaceId
      SnsTopicArns:
        - !Ref GuardDutyNotificationTopic

  ChatbotIamRole:
    Type: AWS::IAM::Role
    Properties:
      RoleName: chatbot-role
      AssumeRolePolicyDocument:
        Version: 2012-10-17
        Statement:
          - Effect: Allow
            Principal:
              Service: chatbot.amazonaws.com
            Action: sts:AssumeRole
      Policies:
        - PolicyName: chatbot-policy
          PolicyDocument:
            Version: 2012-10-17
            Statement:
              - Effect: Allow
                Action:
                  - cloudwatch:Describe*
                  - cloudwatch:Get*
                  - cloudwatch:List*
                Resource:
                  - "*"

こんな感じで通知できます。

一旦、GuardDuty のイベントを直接 Slack 通知した際の通知文に寄せることを意識しました。
必要に応じて通知文をさらにカスタマイズするとより使いやすくなるかと思います。
付けているリンクは Security Hub の検出結果一覧に飛ぶ形ですが、ソース URL を押すことで GuardDuty のコンソールにも遷移可能です。

Security Hub 連携を利用した方法は、GuardDuty 以外のセキュリティ系サービスでもほぼ同じ EventBridge のパターンで通知を実装できるメリットもあります。
Security Hub を有効にしているのであれば Security Hub 経由の通知がおすすめです。

EventBridge カスタムイベントバスに集約するパターン

GuardDuty を利用しているものの、Security Hub を利用していない場合はこちらを使うことになります。

Chatobot をカスタマイズしなくても上記のように通知できるので、こちらの通知が好きな人はこのパターンを採用しても良いと思います。
この場合、 GuardDuty のコンソールへの直接のリンクが提供されます。
ちなみに GuardDuty のイベントを直接扱うと下記のようになります。

{
  "version": "0",
  "id": "c8c4daa7-a20c-2f03-0070-b7393dd542ad",
  "detail-type": "GuardDuty Finding",
  "source": "aws.guardduty",
  "account": "123456789012",
  "time": "1970-01-01T00:00:00Z",
  "region": "us-east-1",
  "resources": [],
  "detail": {
    "schemaVersion": "2.0",
    "accountId": "123456789012",
    "region": "us-east-1",
    "partition": "aws",
    "id": "16afba5c5c43e07c9e3e5e2e544e95df",
    "arn": "arn:aws:guardduty:us-east-1:123456789012:detector/123456789012/finding/16afba5c5c43e07c9e3e5e2e544e95df",
    "type": "Canary:EC2/Stateless.IntegTest",
    "resource": {
      "resourceType": "Instance",
      "instanceDetails": {
        "instanceId": "i-05746eb48123455e0",
        "instanceType": "t2.micro",
        "launchTime": 1492735675000,
        "productCodes": [],
        "networkInterfaces": [{
          "ipv6Addresses": [],
          "privateDnsName": "ip-0-0-0-0.us-east-1.compute.internal",
          "privateIpAddress": "0.0.0.0",
          "privateIpAddresses": [{
            "privateDnsName": "ip-0-0-0-0.us-east-1.compute.internal",
            "privateIpAddress": "0.0.0.0"
          }],
          "subnetId": "subnet-d58b7123",
          "vpcId": "vpc-34865123",
          "securityGroups": [{
            "groupName": "launch-wizard-1",
            "groupId": "sg-9918a123"
          }],
          "publicDnsName": "ec2-11-111-111-1.us-east-1.compute.amazonaws.com",
          "publicIp": "11.111.111.1"
        }],
        "tags": [{
          "key": "Name",
          "value": "ssh-22-open"
        }],
        "instanceState": "running",
        "availabilityZone": "us-east-1b",
        "imageId": "ami-4836a123",
        "imageDescription": "Amazon Linux AMI 2017.03.0.20170417 x86_64 HVM GP2"
      }
    },
    "service": {
      "serviceName": "guardduty",
      "detectorId": "3caf4e0aaa46ce4ccbcef949a8785353",
      "action": {
        "actionType": "NETWORK_CONNECTION",
        "networkConnectionAction": {
          "connectionDirection": "OUTBOUND",
          "remoteIpDetails": {
            "ipAddressV4": "0.0.0.0",
            "organization": {
              "asn": -1,
              "isp": "GeneratedFindingISP",
              "org": "GeneratedFindingORG"
            },
            "country": {
              "countryName": "United States"
            },
            "city": {
              "cityName": "GeneratedFindingCityName"
            },
            "geoLocation": {
              "lat": 0,
              "lon": 0
            }
          },
          "remotePortDetails": {
            "port": 22,
            "portName": "SSH"
          },
          "localPortDetails": {
            "port": 2000,
            "portName": "Unknown"
          },
          "protocol": "TCP",
          "blocked": false
        }
      },
      "resourceRole": "TARGET",
      "additionalInfo": {
        "unusualProtocol": "UDP",
        "threatListName": "GeneratedFindingCustomerListName",
        "unusual": 22
      },
      "eventFirstSeen": "2017-10-31T23:16:23Z",
      "eventLastSeen": "2017-10-31T23:16:23Z",
      "archived": false,
      "count": 1
    },
    "severity": 5,
    "createdAt": "2017-10-31T23:16:23.824Z",
    "updatedAt": "2017-10-31T23:16:23.824Z",
    "title": "Canary:EC2/Stateless.IntegTest",
    "description": "Canary:EC2/Stateless.IntegTest"
  }
}

CloudFormation で実装してみます。
まず、下記テンプレートを集約先リージョンに展開します。

AWSTemplateFormatVersion: "2010-09-09"

Parameters:
  SlackWorkspaceId:
    Description: "Workspace Id for Slack"
    Type: String

  SlackChannelId:
    Description: "Channel Id in Slack for GuardDuty notification"
    Type: String

Resources:
  GuardDutyAggregateEventBus:
    Type: AWS::Events::EventBus
    Properties:
      Name: guardguty-aggregate-eventbus

  GuardDutyNotificationRule:
    Type: AWS::Events::Rule
    Properties:
      Name: guardduty-notification-rule
      EventBusName: !Ref GuardDutyAggregateEventBus
      Targets:
        - Arn: !Ref GuardDutyNotificationTopic
          Id: "GuardDutyNotificationTopic"
      EventPattern:
        source:
          - "aws.guardduty"
        detail-type:
          - "GuardDuty Finding"

  GuardDutyNotificationTopic:
    Type: AWS::SNS::Topic
    Properties:
      TopicName: guardduty-notification-topic

  GuardDutyNotificationTopicPolicy:
    Type: AWS::SNS::TopicPolicy
    Properties:
      Topics:
        - !Ref GuardDutyNotificationTopic
      PolicyDocument:
        Version: "2012-10-17"
        Statement:
          - Sid: "EventBridgeRule"
            Effect: "Allow"
            Principal:
              Service: "events.amazonaws.com"
            Action: "sns:Publish"
            Resource: !Ref GuardDutyNotificationTopic

  GuardDutyNotificationRuleRole:
    Type: AWS::IAM::Role
    Properties:
      Path: "/service-role/"
      RoleName: guardduty-notification-rule-role
      AssumeRolePolicyDocument:
        Version: "2012-10-17"
        Statement:
          - Effect: "Allow"
            Principal:
              Service: "events.amazonaws.com"
            Action: "sts:AssumeRole"
      ManagedPolicyArns:
        - !Ref GuardDutyNotificationRulePolicy

  GuardDutyNotificationRulePolicy:
    Type: AWS::IAM::ManagedPolicy
    Properties:
      ManagedPolicyName: guardduty-notification-rule-policy
      Path: "/service-role/"
      PolicyDocument:
        Version: "2012-10-17"
        Statement:
          - Effect: "Allow"
            Action:
              - "events:PutEvents"
            Resource:
              - !GetAtt GuardDutyAggregateEventBus.Arn

  GuardDutyNotificationChatbotConfiguration:
    Type: AWS::Chatbot::SlackChannelConfiguration
    Properties:
      ConfigurationName: GuardDutyNotification
      IamRoleArn: !GetAtt ChatbotIamRole.Arn
      LoggingLevel: INFO
      SlackChannelId: !Ref SlackChannelId
      SlackWorkspaceId: !Ref SlackWorkspaceId
      SnsTopicArns:
        - !Ref GuardDutyNotificationTopic

  ChatbotIamRole:
    Type: AWS::IAM::Role
    Properties:
      RoleName: chatbot-role
      AssumeRolePolicyDocument:
        Version: 2012-10-17
        Statement:
          - Effect: Allow
            Principal:
              Service: chatbot.amazonaws.com
            Action: sts:AssumeRole
      Policies:
        - PolicyName: chatbot-policy
          PolicyDocument:
            Version: 2012-10-17
            Statement:
              - Effect: Allow
                Action:
                  - cloudwatch:Describe*
                  - cloudwatch:Get*
                  - cloudwatch:List*
                Resource:
                  - "*"

Outputs:
  GuardDutyAggregateEventBusARN:
    Description: "This value is used in the stack set parameters."
    Value: !GetAtt GuardDutyAggregateEventBus.Arn
    Export:
      Name: GuardDutyAggregateEventBusARN
  GuardDutyNotificationRuleRoleARN:
    Description: "This value is used in the stack set parameters."
    Value: !GetAtt GuardDutyNotificationRuleRole.Arn
    Export:
      Name: GuardDutyNotificationRuleRole

次に下記 EventBridge rules を展開します。
こちらは 各リージョンに展開する必要があるので StackSets を利用するのが便利です。

AWSTemplateFormatVersion: "2010-09-09"

Parameters:
  GuardDutyAggregateEventBusARN:
    Description: "Enter the value of the output of the receiver stack"
    Type: String
  GuardDutyNotificationRuleRoleARN:
    Description: "Enter the value of the output of the receiver stack"
    Type: String

Resources:
  GuardDutyNotificationRule:
    Type: "AWS::Events::Rule"
    Properties:
      Name: guardduty-notification-rule
      EventBusName: "default"
      Targets:
        - Arn: !Ref GuardDutyAggregateEventBusARN
          Id: "GuardDutyAggregateEventBus"
          RoleArn: !Ref GuardDutyNotificationRuleRoleARN
      EventPattern:
        source:
          - "aws.guardduty"
        detail-type:
          - "GuardDuty Finding"

そこまで構成が複雑になるわけではないので、無理に Security Hub を有効化するくらいであればこちらを採用すると良さそうです。

まとめ

どっちのパターンも便利です。
Security Hub を有効化しているかが一つ大きな判断基準になると思いますが、要件に合わせて選んでみて下さい。