やりたいこと
以下のページで紹介されている改善後の構成で、
-
脆弱性が検出されると EventBridge から Lambda1 を起動し、DynamoDB に脆弱性情報を格納する。この時 DynamoDB の パーティションキーとソートキーで重複をチェックし、既にレコードが存在する場合はスキップし、レコードが存在しない場合は DynamoDB に格納する
-
EventBridge Scheduler で 1 日に 1 回だけLambda2 を起動し、DynamoDB から、まだ通知が行われていない脆弱性を取得し Slack へ通知する。Slack への通知が完了すると DynamoDB に格納されている脆弱性のステータスを NEW ⇨ NOTIFIED に更新する
この 2 つを行う。
実装の上での問題点
しかし、ここで問題点となる点は、1. においてパーティションキーを「RepositoryName」にするか、「Id」にするかである。
リポジトリ別 | イメージ別 | |
---|---|---|
パーティションキーを「RepositoryName」 | パーティションキーを「Id」 | |
'RepositoryName': 'tomcat' | サンプル | 'Id': 'arn:aws:ecr:ap-northeast-1:123456789012:repository/tomcat/sha256: cd1cbd81d01ab8d373f57e4f061575f519c585153a72badc30e9141ee53eb528' |
上記でいう tomcat のイメージを push してもパーティションキーが変わらないので、確かに通知を減らすことはできると思う。 | メリット | 例として v1.0.1 で脆弱性が検出され、v1.0.2 でパッチを当てたとする。この時、v1.0.2 で実際に脆弱性が検出されなければ改善されていることがわかる。 |
例として v1.0.1 で脆弱性が検出され、v1.0.2 でパッチを当てたとする。この時、v1.0.2 で実際に脆弱性が検出されなくなったかわからない。 | デメリット | イメージが毎回異なるのでイメージ毎に通知がされる。 |
そのため、実際には以下の対応なども併せて検討すると良いかと思います。
- 通知は本番環境のみとする(本番環境において頻繁にイメージを push することはないと想定されるため)
- 開発環境も通知したい場合は、CI/CD 化する時に前回のイメージとの差分がない場合はイメージを build,push は無闇に行わない
- ECR のライフサイクルポリシーを使用してイメージは 2 世代までしか残さないようにする
今回の検証ではパーティションキーを「Id」 とした場合で行う。そのため、イメージが毎回異なるのでイメージ毎に通知がされますが、2.の実装により通知は纏めて通知されます。
実際に脆弱性が改善されたかわからなくても良い方はパーティションキーを「Id」から「RepositoryName」に置き換えて実行してみると良いと思います。
設定
前提
- Inspector が有効になっており、脆弱性が検出可能な状態であること
- Security Hub で Inspector を受け入れる設定になっていること
- Step Functions の実装が完了していること。
- Step Functions で Security Hub のワークフローステータスを NOTIFIED に更新する
DynamoDB
DynamoDB テーブルを作成します。
{
"TableName": "inspector_dynamodb",
"AttributeDefinitions": [
{
"AttributeName": "resources_id",
"AttributeType": "S"
},
{
"AttributeName": "title",
"AttributeType": "S"
},
{
"AttributeName": "workflow_status",
"AttributeType": "S"
}
],
"KeySchema": [
{
"AttributeName": "resources_id",
"KeyType": "HASH"
},
{
"AttributeName": "title",
"KeyType": "RANGE"
}
],
"ProvisionedThroughput": {
"ReadCapacityUnits": 1,
"WriteCapacityUnits": 1
},
"GlobalSecondaryIndexes": [
{
"IndexName": "workflow_status-index",
"KeySchema": [
{
"AttributeName": "workflow_status",
"KeyType": "HASH"
}
],
"Projection": {
"ProjectionType": "ALL"
},
"ProvisionedThroughput": {
"ReadCapacityUnits": 1,
"WriteCapacityUnits": 1
}
}
],
"TableClass": "STANDARD",
"DeletionProtectionEnabled": false
}
aws dynamodb create-table --cli-input-json file://create-dynamodb-table.json
これにより、次の内容で DynamoDB テーブルが作成されます。(実際は、緊急度などその他の情報を必要かと思いますので、項目はもう少し多くなりますが、検証のため最小限にしています。)
- テーブル:inspector_dynamodb
- パーティションキー:resources_id
- ソートキー:title
- パーティションキー(グローバルセカンダリインデックス):workflow_status
Lambda
Lambda1、Lambda2 いずれも IAMロールに DynamoDB へのアクセス権限を付与することを忘れないでください。
Lambda1
やりたいこと 1. の実装です。
import boto3
def lambda_handler(event, context):
# イベントから必要な情報を抽出
finding = event['detail']['findings'][0]
resources_id = finding['Resources'][0]['Id']
title = finding['Title']
dynamodb = boto3.client('dynamodb')
# DynamoDBにアイテムを追加する前に重複をチェック
try:
response = dynamodb.get_item(
TableName='inspector_dynamodb',
Key={
'resources_id': {'S': resources_id},
'title': {'S': title}
}
)
# 既に同じパーティションキーとソートキーを持つレコードが存在する場合は登録をスキップ
if 'Item' in response:
print("Entry already exists, skipping...")
return
except dynamodb.exceptions.ResourceNotFoundException:
print("DynamoDB table does not exist.")
return
except Exception as e:
print(f"Error while querying DynamoDB: {e}")
return
# DynamoDBに新しいレコードを追加
try:
response = dynamodb.put_item(
TableName='inspector_dynamodb',
Item={
'resources_id': {'S': resources_id},
'title': {'S': title},
'workflow_status': {'S': 'NEW'},
}
)
print("Item added successfully:", response)
except Exception as e:
print(f"Error while adding item to DynamoDB: {e}")
Lambda2
やりたいこと 2. の実装です。
import json
import boto3
import urllib.parse
import urllib.request
# Slack Webhook URL
SLACK_WEBHOOK_URL = "https://hooks.slack.com/services/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"
def lambda_handler(event, context):
dynamodb = boto3.client('dynamodb')
table_name = 'inspector_dynamodb'
# フィルタリング条件
filter_expression = "#ws = :status"
expression_attribute_values = {":status": {"S": "NEW"}}
expression_attribute_names = {"#ws": "workflow_status"}
# workflow_statusがNEWのものをクエリ
response = dynamodb.scan(
TableName=table_name,
FilterExpression=filter_expression,
ExpressionAttributeValues=expression_attribute_values,
ExpressionAttributeNames=expression_attribute_names
)
# フィルタリングされたレコードがあるかどうかを確認
items = response.get('Items', [])
if items:
# workflow_status=NEWがある場合
message = "*新しい脆弱性が検出されました。:*\n"
for item in items:
record_info = f"リソースID: {item['resources_id']['S']}\nタイトル: {item['title']['S']}\n"
message += f"```{record_info}```\n"
notify_slack(message)
# workflow_statusをNEWからNOTIFIEDに更新
update_status(dynamodb, table_name, items)
else:
# workflow_status=NEWがない場合
message = "新しく脆弱性はありません。"
notify_slack(message)
def notify_slack(message):
payload = {'text': message}
data = json.dumps(payload).encode('utf-8')
req = urllib.request.Request(SLACK_WEBHOOK_URL, data=data, headers={'Content-Type': 'application/json'})
try:
with urllib.request.urlopen(req) as response:
print("Message sent to Slack successfully")
except urllib.error.HTTPError as e:
print("Failed to send message to Slack. Error:", e.code)
except urllib.error.URLError as e:
print("Failed to send message to Slack. Error:", e.reason)
def update_status(dynamodb, table_name, items):
for item in items:
key = {
'resources_id': item['resources_id'],
'title': item['title']
}
update_expression = "SET #ws = :status"
expression_attribute_values = {":status": {"S": "NOTIFIED"}}
expression_attribute_names = {"#ws": "workflow_status"}
try:
dynamodb.update_item(
TableName=table_name,
Key=key,
UpdateExpression=update_expression,
ExpressionAttributeValues=expression_attribute_values,
ExpressionAttributeNames=expression_attribute_names
)
print(f"Updated workflow_status to NOTIFIED for record: {key}")
except dynamodb.exceptions.ClientError as e:
print(f"Failed to update workflow_status for record {key}. Error: {e}")
EventBridge
EventBridge1
EventBridge1 の実装です。
イベントパターンは次の通りとし、ターゲットには Lambda1 を指定します。
{
"detail": {
"findings": {
"ProductName": ["Inspector"],
"RecordState": ["ACTIVE"],
"Severity": {
"Label": ["CRITICAL"]
},
"Workflow": {
"Status": ["NEW"]
}
}
},
"detail-type": ["Security Hub Findings - Imported"],
"source": ["aws.securityhub"]
}
EventBridge2
EventBridge2 の実装です。
スケジュールは次の通り日本時間で 9 時に実行されるようにし、ターゲットには Lambda2 を指定します。
0 9 * * ? *
分 時間 日 月 曜日 年
動作確認
動作確認1
動作確認のために ECR に v1.0.1 のイメージを push します。
docker build -t tomcat .
docker tag tomcat:latest 123456789012.dkr.ecr.ap-northeast-1.amazonaws.com/tomcat:v1.0.1
docker push 123456789012.dkr.ecr.ap-northeast-1.amazonaws.com/tomcat:v1.0.1
Security Hub から検出結果を確認します。
DynamoDB から項目を確認すると、レコードが登録されていることが確認できます。
念の為 Lambda1 のログも確認し、Item added successfully: でレスポンスが 200 で返却されていたら成功です。
Item added successfully:
{
'ResponseMetadata': {
'RequestId': 'OB40MRAHMRGFSUHDBLGLJMH077VV4KQNSO5AEMVJF66Q9ASUAAJG',
'HTTPStatusCode': 200,
'HTTPHeaders': {
'server': 'Server',
'date': 'Thu,
23 May 2024 16 : 33 : 41 GMT',
'content-type': 'application/x-amz-json-1.0',
'content-length': '2',
'connection': 'keep-alive',
'x-amzn-requestid': 'OB40MRAHMRGFSUHDBLGLJMH077VV4KQNSO5AEMVJF66Q9ASUAAJG',
'x-amz-crc32': '2745614147'
},
'RetryAttempts': 0
}
}
続いて、動作確認のために ECR に v1.0.2 のイメージを push してみます。
docker build -t tomcat .
docker tag tomcat:latest 123456789012.dkr.ecr.ap-northeast-1.amazonaws.com/tomcat:v1.0.2
docker push 123456789012.dkr.ecr.ap-northeast-1.amazonaws.com/tomcat:v1.0.2
DynamoDB から項目を確認すると、レコードが登録されていることが確認できます。
この場合、別の v1.0.1 と v1.0.2 とタグが異なりダイジェストが異なるため、DynamoDB にレコードが登録されます。
動作確認2
今回は、EventBridge2 から Lambda に直接渡すイベントはなく検証であるため Lambda を手動で実行してみます。
Slack に纏めて通知されたことが確認できました。
DynamoDB の各レコードで workflow_status が NEW ⇨ NOTIFIED に変更されていることも確認できました。
念の為 Lambda2 のログも確認し、Message sent to Slack successfully で Slack に通知されたこと、Updated workflow_status to NOTIFIED for record: で NEW ⇨ NOTIFIED に変更されるレコードが返ってきたら成功です。
Message sent to Slack successfully
Updated workflow_status to NOTIFIED for record:
{
'resources_id': {
'S': 'arn : aws : ecr : ap-northeast-1 : 123456789012 : repository/tomcat/sha256 : cd1cbd81d01ab8d373f57e4f061575f519c585153a72badc30e9141ee53eb528'
},
'title': {
'S': 'CVE-2021-46848 - libtasn1-6'
}
}
もう一度 Lambda2 を実行してみます。
動作未確認
Inspector は CVE を追加する度にスキャン(継続スキャン)が走りますが、この時にきちんと追加された CVE のみが通知されるか?ここは実際に継続スキャンが走っていないのでわからないため、もし確認できたら更新したいと思います。
その他検討
Inspector のレポート機能で S3 へエクスポートし、S3 へ格納されたらそのファイルを Slack へ送る。
このやり方でも良さそうである。
ただし、日時で行う場合、前日との差分を確認する必要がありそうなため、週次報告や月次報告なものに向いていると思う。
このあたりの動作確認は次回余裕があれば、書いていこうと思う。