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

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

## 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](https://qiita-image-store.s3.amazonaws.com/0/125105/aa176987-6b70-aee0-0255-2190fa3147f7.png)

アクション名と詳細、アクションIDを入力して、OKを選択します。
例えばSlack通知用のアクションを作成するとして以下のように入力します。
![image.png](https://qiita-image-store.s3.amazonaws.com/0/125105/eb54784e-26dc-d7e5-22cd-21f96232ca42.png)

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

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

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

```json
{
  "resources": [
    "arn:aws:securityhub:<region>:<account-id>:action/<custom-action-id>"
  ],
  "source": [
    "aws.securityhub"
  ]
}
```
ターゲットに任意のLambda関数やSNSトピックを指定することで
通知等の各種アクションを実行することができます。
![image.png](https://qiita-image-store.s3.amazonaws.com/0/125105/ec312b00-10c1-3752-9c81-8f883ef2f18b.png)

イベントルールの作成後、Security Hub コンソールで任意でのFindingを選択し、
Actionsから作成したカスタムアクションを実行することができます。
![image.png](https://qiita-image-store.s3.amazonaws.com/0/125105/c98a4c58-3271-1901-fc5c-3316a1c045a8.png)

## 参考: カスタムアクションを使用したSlackへの通知
例えばイベントターゲットでSlack通知を行うLambda関数を設定した場合は
以下のような結果になります。
![image.png](https://qiita-image-store.s3.amazonaws.com/0/125105/3ed57e79-2c84-ce0c-5cac-6ffceec26e0e.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
```

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