Help us understand the problem. What is going on with this article?

AWS Security Hubのカスタムアクションではリアルタイム通知はできない

More than 1 year has passed since last update.

タイトルのままなのですが、
それだけではあんまりですので以下に詳細を残します。

Security Hubって何?

AWS re:Invent 2018で発表されたAWS環境全体のセキュリティとコンプライアンスの状況を
確認できるサービスです。GuardDuty の脅威診断結果、Inspector のスキャン結果、Macie
による機密データの検出などAWSサービスはもちろん、パートナーツールからの出力結果も
取り込んで1つのダッシュボードで管理することができます。

https://aws.amazon.com/jp/security-hub/

ちなみに現在はPublic Preview中で無料で使用することができます。
2019/6/24 に正式リリースされました。

AWS Security Hub の一般提供が開始
https://aws.amazon.com/jp/about-aws/whats-new/2019/06/aws-security-hub-now-generally-available/

カスタムアクション

カスタムアクションはSecurity Hubで集約された検出結果(Fingings)を
CloudWatch Eventsに送信できる機能です。
これにより従来各サービス毎に設定していたSlackなどの通知についてもSecurity Hubで
集約できると思っていましたが、ここで勘違いをしていました。。。
Findings に結果が集約されるたびに CloudWatch Events にイベントが
送信されるわけではありません。
現状は Security Hub のマネージメントコンソールから選択した Findings を手動で
CloudWatch Events へ送信できる機能であると言えます。

CloudWatchイベントを使用したAWSセキュリティハブの自動化
https://docs.aws.amazon.com/securityhub/latest/userguide/securityhub-cloudwatch-events.html#securityhub-cwe-configure

現在のリリースでは、選択した Security Hubの検出結果と洞察の結果をCloudWatch Eventsに送信してさらに処理するようにSecurity Hubを設定できます。

現在のリリースでは という記載になっているので、今後のアップデートに期待します。

カスタムアクションの作成と設定

カスタムアクション自体の作成は簡単です。
Secuirty Hub コンソールのSettingsからCreate custom actionを選択します。
image.png

アクション名と詳細、アクションIDを入力して、OKを選択します。
例えばSlack通知用のアクションを作成するとして以下のように入力します。
image.png

作成したカスタムアクションのARNは以下のような形式になります。
arn:aws:securityhub:<region>:<account-id>:action/<custom-action-id>

カスタムアクションはCloudWatch Eventsにイベントを送信するだけになりますので
何かしらのアクションを実行するにはCloudWatch Eventsルールの作成が必要です。

ルールの作成ではイベントパターンを以下のようにJSONで直接編集します。

{
  "resources": [
    "arn:aws:securityhub:<region>:<account-id>:action/<custom-action-id>"
  ],
  "source": [
    "aws.securityhub"
  ]
}

ターゲットに任意のLambda関数やSNSトピックを指定することで
通知等の各種アクションを実行することができます。
image.png

イベントルールの作成後、Security Hub コンソールで任意でのFindingを選択し、
Actionsから作成したカスタムアクションを実行することができます。
image.png

参考: カスタムアクションを使用したSlackへの通知

例えばイベントターゲットでSlack通知を行うLambda関数を設定した場合は
以下のような結果になります。
image.png
繰り返しになってしますが、今のところSecurity Hubで確認したFindingやInsightに対して
手動でカスタムアクションを起動する形になります。

上記のSlack通知は、GitHubのaws-samplesにあがっている以下のサンプルをもとに
個人的に通知内容を一部変更&Python/YAMLで書き直しました。

https://github.com/aws-samples/aws-securityhub-to-slack

参考までにこちらに貼っておきます。

lambda_function.py
"""
This is a sample function to send AWS Security Hub Findings to a slack.

Environment variables:
    CHANNEL: Slack channel name
    MIN_SEVERITY_LEVEL: Minimum severity to notify
    WEBHOOK_URL: Incoming Webhook URL
"""

import json
import os
from datetime import datetime
from logging import getLogger, INFO
from urllib.request import Request, urlopen
from urllib.error import URLError, HTTPError
from dateutil.parser import parse

logger = getLogger()
logger.setLevel(INFO)

def get_params(event, properties):
    """Slack message formatting"""
    channel = os.environ['CHANNEL']
    message = f"*Security Hub finding in {event['region']} for Account: {event['account']}*"
    title = event['detail']['findings'][0]['Title']
    console_url = 'https://console.aws.amazon.com/securityhub/'
    description = event['detail']['findings'][0]['Description']
    product = event['detail']['findings'][0]['ProductFields']['aws/securityhub/ProductName']
    last_seen = datetime.strftime(
        parse(event['detail']['findings'][0]['UpdatedAt']),
        '%Y-%m-%d %H:%M:%S %Z'
    )
    slack_message = {
        'username': 'AWS Security Hub',
        'channels': channel,
        'icon_emoji': ':securityhub:',
        'text': message,
        'attachments': [
            {
                'fallback': 'AWS Security Hub Findings Description.',
                'color': properties['color'],
                'title': title,
                'title_link': f"{console_url}home?region={event['region']}#/findings",
                'text': description,
                'fields': [
                    {'title': 'Product', 'value': product, 'short': True},
                    {'title': 'Severity', 'value': properties['label'], 'short': True},
                    {'title': 'Last Seen', 'value': last_seen, 'short': True}
                ]
            }
        ]
    }
    return slack_message

def get_properties(severity, min_severity_level):
    """Returns the label and color setting of severity"""
    if severity < 4.0:
        if min_severity_level != 'LOW':
            logger.info("Skip Notification: Minimum Severity Level is %s.", min_severity_level)
            return
        properties = {'label': 'Low', 'color': 'warning'}
    elif severity < 7.0:
        if min_severity_level == 'HIGH':
            logger.info("Skip Notification: Minimum Severity Level is HIGH.")
            return
        properties = {'label': 'Medium', 'color': 'warning'}
    else:
        properties = {'label': 'High', 'color': 'danger'}
    return properties

def lambda_handler(event, context):
    """AWS Lambda Function to send Security Hub Findings to slack"""
    result = 1
    properties = get_properties(
        event['detail']['findings'][0]['Severity']['Product'],
        os.environ['MIN_SEVERITY_LEVEL']
    )
    if properties:
        slack_message = get_params(event, properties)
        req = Request(os.environ['WEBHOOK_URL'], json.dumps(slack_message).encode('utf-8'))
        try:
            with urlopen(req) as res:
                res.read()
                logger.info("Message posted.")
        except HTTPError as err:
            logger.error("Request failed: %d %s", err.code, err.reason)
        except URLError as err:
            logger.error("Server connection failed: %s", err.reason)
        else:
            result = 0
    return result

以下はCloudWatch Events、Lambdaのデプロイ、IAMの設定をまとめておこなう
CloudFormationテンプレートです。Stack Setでも動作します。
現状カスタムアクションの作成がCloudFormationに対応していないため、
手動で作成する必要があります(Custom action ID: SendToSlack)

CFn_SecurityHubToSlack.yaml
AWSTemplateFormatVersion: '2010-09-09'
Description: AWS Security Hub Findings to Slack

Parameters: 
  IncomingWebHookURL: 
    Default: "https://hooks.slack.com/services/XXXXXX/YYYYY/REPLACE_WITH_YOURS"
    Description: "Your unique Incoming Web Hook URL from slack service"
    Type: String
  SlackChannel: 
    Default: "#alerts"
    Description: "The slack channel to send findings to"
    Type: String
  MinSeverityLevel: 
    Default: LOW
    Description: "The minimum findings severity to send to your slack channel (LOW, MEDIUM or HIGH)"
    Type: String
    AllowedValues: 
      - LOW
      - MEDIUM
      - HIGH

Resources:
# Create IAM Role
  LambdaExecutionRole:
    Type: "AWS::IAM::Role"
    Properties:
      AssumeRolePolicyDocument:
        Version: "2012-10-17"
        Statement:
          - Effect: Allow
            Principal:
              Service: lambda.amazonaws.com
            Action:
              - sts:AssumeRole
      Path: "/"
      Policies:
        - PolicyName: LambdaBasicExecution
          PolicyDocument:
            Version: '2012-10-17'
            Statement:
              - Effect: Allow
                Action:
                  - logs:CreateLogGroup
                  - logs:CreateLogStream
                  - logs:PutLogEvents
                Resource: arn:aws:logs:*:*:*

# Create Lambda Function
  LambdaFunction: 
    Type: "AWS::Lambda::Function"
    Properties: 
      Handler: "index.lambda_handler"
      Role: !GetAtt LambdaExecutionRole.Arn
      Code: 
        ZipFile: |
          """
          This is a sample function to send AWS Security Hub Findings to a slack.

          Environment variables:
              CHANNEL: Slack channel name
              MIN_SEVERITY_LEVEL: Minimum severity to notify
              WEBHOOK_URL: Incoming Webhook URL
          """

          import json
          import os
          from datetime import datetime
          from logging import getLogger, INFO
          from urllib.request import Request, urlopen
          from urllib.error import URLError, HTTPError
          from dateutil.parser import parse

          logger = getLogger()
          logger.setLevel(INFO)

          def get_params(event, properties):
              """Slack message formatting"""
              channel = os.environ['CHANNEL']
              message = f"*Security Hub finding in {event['region']} for Account: {event['account']}*"
              title = event['detail']['findings'][0]['Title']
              console_url = 'https://console.aws.amazon.com/securityhub/'
              description = event['detail']['findings'][0]['Description']
              product = event['detail']['findings'][0]['ProductFields']['aws/securityhub/ProductName']
              last_seen = datetime.strftime(
                  parse(event['detail']['findings'][0]['UpdatedAt']),
                  '%Y-%m-%d %H:%M:%S %Z'
              )
              slack_message = {
                  'username': 'AWS Secuirty Hub',
                  'channels': channel,
                  'icon_emoji': ':securityhub:',
                  'text': message,
                  'attachments': [
                      {
                          'fallback': 'AWS Security Hub Findings Description.',
                          'color': properties['color'],
                          'title': title,
                          'title_link': f"{console_url}home?region={event['region']}#/findings",
                          'text': description,
                          'fields': [
                              {'title': 'Product', 'value': product, 'short': True},
                              {'title': 'Severity', 'value': properties['label'], 'short': True},
                              {'title': 'Last Seen', 'value': last_seen, 'short': True}
                          ]
                      }
                  ]
              }
              return slack_message

          def get_properties(severity, min_severity_level):
              """Returns the label and color setting of severity"""
              if severity < 4.0:
                  if min_severity_level != 'LOW':
                      logger.info("Skip Notification: Minimum Severity Level is %s.", min_severity_level)
                      return
                  properties = {'label': 'Low', 'color': 'warning'}
              elif severity < 7.0:
                  if min_severity_level == 'HIGH':
                      logger.info("Skip Notification: Minimum Severity Level is HIGH.")
                      return
                  properties = {'label': 'Medium', 'color': 'warning'}
              else:
                  properties = {'label': 'High', 'color': 'danger'}
              return properties

          def lambda_handler(event, context):
              """AWS Lambda Function to send Security Hub Findings to slack"""
              result = 1
              properties = get_properties(
                  event['detail']['findings'][0]['Severity']['Product'],
                  os.environ['MIN_SEVERITY_LEVEL']
              )
              if properties: 
                  slack_message = get_params(event, properties)
                  req = Request(os.environ['WEBHOOK_URL'], json.dumps(slack_message).encode('utf-8'))
                  try:
                      with urlopen(req) as res:
                          res.read()
                          logger.info("Message posted.")
                  except HTTPError as err:
                      logger.error("Request failed: %d %s", err.code, err.reason)
                  except URLError as err:
                      logger.error("Server connection failed: %s", err.reason)
                  else:
                      result = 0
              return result

      Runtime: "python3.6"
      MemorySize: 128
      Timeout: 3
      Environment:
        Variables:
          CHANNEL: !Ref SlackChannel
          MIN_SEVERITY_LEVEL: !Ref MinSeverityLevel
          WEBHOOK_URL: !Ref IncomingWebHookURL

# Create CloudWatch Events Rule
  EventRule:
    Type: "AWS::Events::Rule"
    Properties: 
      Description: "AWS Security Hub Findings to Slack"
      EventPattern:
        source: 
          - aws.securityhub
        resources: 
          - !Join
            - ""
            - - "arn:aws:securityhub:"
              - !Ref "AWS::Region"
              - ":"
              - !Ref "AWS::AccountId"
              - ":"
              - "action/custom/SendToSlack"
      Name: SecurityHub-to-Slack
      State: "ENABLED"
      Targets:
        - 
          Arn: !GetAtt LambdaFunction.Arn
          Id: "TargetFunctionV1"

# Create Invoke Lambda Permission
  PermissionForEventsToInvokeLambda: 
    Type: "AWS::Lambda::Permission"
    Properties: 
      FunctionName: !Ref LambdaFunction
      Action: "lambda:InvokeFunction"
      Principal: "events.amazonaws.com"
      SourceArn: !GetAtt EventRule.Arn

以上です。
参考になれば幸いです。

hayao_k
2019 & 2020 APN AWS Top Engineers / AWS Community Builder に選出いただきました。 掲載内容は個人の見解であり、所属する企業を代表するものではありません。
saison_information_systems
モード1(守りのIT)・モード2(攻めのIT)を兼ね備えたバイモーダル・インテグレーターとしてデータ連携プラットフォームのHULFTシリーズ, リンケージサービス, 流通ITサービス, フィナンシャルITサービスを提供します。
https://home.saison.co.jp/
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away