Edited at

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

アクション名と詳細、アクション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


以上です。

参考になれば幸いです。