LoginSignup
0
1

Inspector から Security Hub に統合した脆弱性通知について考える

Posted at

やりたいこと

inspector.png

以下のページで紹介されている改善後の構成で、

  1. 脆弱性が検出されると EventBridge から Lambda1 を起動し、DynamoDB に脆弱性情報を格納する。この時 DynamoDB の パーティションキーとソートキーで重複をチェックし、既にレコードが存在する場合はスキップし、レコードが存在しない場合は DynamoDB に格納する

  2. 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 テーブルを作成します。

create-dynamodb-table.json
{
    "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. の実装です。

Lambda1.py
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. の実装です。

Lambda2.py
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 を指定します。

EventBridge1.json
{
  "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 を指定します。

EventBridge2.cron
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 から検出結果を確認します。

スクリーンショット 2024-05-24 9.12.03.png

DynamoDB から項目を確認すると、レコードが登録されていることが確認できます。

スクリーンショット 2024-05-24 1.37.07.png

念の為 Lambda1 のログも確認し、Item added successfully: でレスポンスが 200 で返却されていたら成功です。

Lambda1_response.json
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

Security Hub から検出結果を確認します。
スクリーンショット 2024-05-24 9.26.53.png

DynamoDB から項目を確認すると、レコードが登録されていることが確認できます。
この場合、別の v1.0.1 と v1.0.2 とタグが異なりダイジェストが異なるため、DynamoDB にレコードが登録されます。
スクリーンショット 2024-05-24 9.28.22.png

動作確認2

今回は、EventBridge2 から Lambda に直接渡すイベントはなく検証であるため Lambda を手動で実行してみます。

Slack に纏めて通知されたことが確認できました。

スクリーンショット 2024-05-24 10.28.24.png

DynamoDB の各レコードで workflow_status が NEW ⇨ NOTIFIED に変更されていることも確認できました。

スクリーンショット 2024-05-24 10.31.06.png

念の為 Lambda2 のログも確認し、Message sent to Slack successfully で Slack に通知されたこと、Updated workflow_status to NOTIFIED for record: で NEW ⇨ NOTIFIED に変更されるレコードが返ってきたら成功です。

Lambda2_response.json

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 を実行してみます。

Slack に次の様に通知されていれば成功です。
スクリーンショット 2024-05-24 10.52.08.png

動作未確認

Inspector は CVE を追加する度にスキャン(継続スキャン)が走りますが、この時にきちんと追加された CVE のみが通知されるか?ここは実際に継続スキャンが走っていないのでわからないため、もし確認できたら更新したいと思います。

その他検討

Inspector のレポート機能で S3 へエクスポートし、S3 へ格納されたらそのファイルを Slack へ送る。

このやり方でも良さそうである。

ただし、日時で行う場合、前日との差分を確認する必要がありそうなため、週次報告や月次報告なものに向いていると思う。

このあたりの動作確認は次回余裕があれば、書いていこうと思う。

0
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
1