カミナシ エンジニアブログ

株式会社カミナシのエンジニアが色々書くブログです

AWS Security Hub の通知を Amazon Bedrock を使ってアクショナブルにしてみた

どうもセキュリティエンジニアの西川です。これがきっと私にとっての今年最後のブログです。今年の AWS re:Invent は自身の登壇があったのでほとんど楽しめませんでした。悔しいのでこうしてブログを帰ってきてからいくつか書いています。カミナシのメンバーの中で私と CTO だけは自腹で AWS re:Invent へ参加していますが、来年の飛行機もすでにとっていたりします。来年ラスベガスでお会いしましょう。

AWS re:Invent の中でもたくさんの Generative AI のセッションがありました。私自身はほとんど参加していなかったのですが、Generative AI の GameDay に出たことをきっかけに Generative AI 意外と難しくないなと思い、Security Hub の通知をアクショナブルに変えてエンジニアの誰しもが対応できるようにしてみようと思ったのが本ブログを書くことに至った経緯です。

こんな人に読んでほしい

  • 専任のセキュリティエンジニアがいなくて Security Hub の通知だけでは何をしたらよいかわからないエンジニア
  • Security Hub の通知を飛ばしているけど、なかなか対応してもらえないセキュリティエンジニア
  • Generative AI 何それ美味しそう、使ってみたい!というエンジニア

構成

Security Hub で検知 → EventBridge → Lambda(Amazon Bedrock呼び出し)

というすごくシンプルな構成です。構成はシンプルなのですがハマりポイントもあるのでそれを踏まえてお伝えできればと思っています。

構築手順(Amazon Bedrock)

何はともあれ Bedrock を使うための準備が必要です。申請しましょう。

Bedrock のページへ遷移後、Model access から申請が可能です。

私は Claude 3.5 Sonnet を使いました。私の環境ではすでに Access granted になっていますが、申請する際は「Available to request」をクリックします。

クリックするとポップアップが出てくるので「Request model access」をクリックしましょう。そうするとモデルを選択する画面が出てくるので利用したいモデルを選択して Next をクリックしてください。

初回の申請の場合、会社名や利用用途を申請時に記載する必要があります。適宜記載して申請してください。私の場合申請後10分程度で利用ができるようになりました。これで Bedrock の設定は完了です。申請中でも以下の構築手順は進められますので進んでいきましょう。

※本ブログで Lambda のコードも紹介しますが、Claude 3.5 Sonnet が前提です。モデルによって若干インターフェースが変わることがありますので、違うモデルを使う場合は適宜修正が必要であることをご認識おきください。

構築手順(AWS Lambda)

何はともあれコードを書きます。例は Python です。region や Slack の Webhool URL など適宜変更してください

import json
import boto3
import requests

bedrock_client = boto3.client(service_name='bedrock-runtime', region_name="ap-northeast-1")

# Slack の Webhook URL
SLACK_WEBHOOK_URL = ""

def lambda_handler(event, context):
    try:
        print("START")
        # Security Hub Findings を取得
        findings = event.get('detail', {}).get('findings', [])
        if not findings:
            print("No findings received.")
            return

        # Findings の説明を Bedrock からもらう
        summary = summarize_findings_with_bedrock(findings)

        send_to_slack(summary)

        return {
            'statusCode': 200,
            'body': json.dumps('Notification sent successfully!')
        }

    except Exception as e:
        print(f"Error processing findings: {str(e)}")
        return {
            'statusCode': 500,
            'body': json.dumps(f"Error: {str(e)}")
        }

def summarize_findings_with_bedrock(findings):
    # Findings をまとめてテキストに変換
    findings_text = "\n".join([
        f"Id: {finding.get('Id')}\nDescription: {finding.get('Description')}"
        for finding in findings
    ])

    prompt = f"""
    {findings_text}

    上記 Security Hub findings の通知を受け取ったとき誰もが対応できるように、なぜ対応する必要があるかと対応方法について明確な指示と terraform コードの記載例を教えてください
    """ 

    body = json.dumps({
        "anthropic_version": "bedrock-2023-05-31",
        "max_tokens": 2048,
        "messages": [{"role": "user", "content": prompt}],
        "temperature": 0.7
    })

    response = bedrock_client.invoke_model(
        body=body,
        modelId='anthropic.claude-3-5-sonnet-20240620-v1:0',
        contentType="application/json",
        accept="application/json"
    )

    response_body = json.loads(response['body'].read())
    return findings_text + response_body['content'][0]['text']    

def send_to_slack(message):
    slack_message = {
        "text": f"Security Hub Findings Summary:\n{message}"
    }
    
    response = requests.post(
        SLACK_WEBHOOK_URL,
        headers={"Content-Type": "application/json"},
        data=json.dumps(slack_message)
    )

    if response.status_code != 200:
        raise ValueError(f"Failed to send message to Slack: {response.status_code}, {response.text}")

ここでは terraform コードをサンプルとして出すことを要求していますが、自組織で使うものに変更したり IaC 化できていなければ、その分のプロンプトは削除してください。

anthropic_version を body に含めて送信していますが、これは anthropic のモデルを使う時だけ使用するものです。anthropic でもモデルによって必要なかったりするかもしれませんが調査していません。また、messages の構造や prompt として渡す内容などもモデルによって異なるのでハマりポイントとなっています。もし別のモデルを使用して動作しないときはこの辺を確認すると良いでしょう。

設定と Layer の追加

私はタイムアウトの設定を1分程度にしました。10秒とかだと返って来ない可能性があります。少なくとも Lambda のデフォルトの3秒では絶対返ってこないので伸ばす必要があります。

Bedrock を Lambda から呼び出すためのインラインポリシーを追加します。下記に表示されている Role name をクリックしてロールの設定をしていきましょう。

Add permissions のところから Create inline policy をクリックします。

JSON を選択し、下記の内容を記載していきます。bedrock のアクションは InvokeModel だけあれば OK です。

一応テキストでも書いておきます。

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Action": "bedrock:InvokeModel",
            "Resource": "*",
            "Effect": "Allow"
        }
    ]
}

次に Lambda に Layer を追加します。

requests と boto3 を使っているので雑に作っていきます。下記手順で Python ライブラリを入れ込んだ zip ファイルを作成します。boto3 は現状最新バージョンが必要なので必ずこの手順を踏みます。

mkdir libraries
pip3 install requests -t libraries/
pip3 install boto3 -t libraries/
pip3 install botocore -t libraries/
zip -r layer.zip libraries/

上記が完了したら Layers から Create layer していきます。

適当に名前を入れて、先ほど作成した zip ファイルをアップロードするだけです。

Layer が出来上がったら Layer の詳細ページで arn をコピーしておきましょう。

先ほど作成した Lambda function に戻り Add a layer をクリックします。

Specify an ARN を選択し、先ほどコピーした arn を貼り付けて Add ボタンをクリックすれば完了です。

構築手順(Amazon EventBridge)

最後に EventBridge のページに遷移し、Buses の 中のRules をクリックし、Create rule でルールを作成していきます。

適当にルールの名前を入れて、任意の Event bus を選択します。それから「Rule with an event pattern」を選択し、Next をクリックします。

基本的にはデフォルトの選択のままで Event pattern だけ選択していきます。AWS service は Security Hub を選択し、 Event type は Security Hub Findings - Imported を選択します。それから、通知の対象を設定しますが MEDIUM 以下も含めてしまうと量が多くなってしまいがちなので、HIGH と CRITICAL に限定します。また、ワークフローステータスも新しく作られたもののみを対象としたいので NEW だけを選択します。

次のページへ進んで先ほど作成した Lambda を指定しましょう。

あとは他に何かタグをつけるなど必要がなければ Create rule してしまって大丈夫です。 以上で、準備は完了です。

Security Hub で検知させる

検証するのに一番簡単な方法はオールオープンな Security Group を作成することです。検証には非常に便利です!

適当に名前や説明を記載し、 VPC を選択し、Inbound rules に下記の設定を入れます。Create security group ボタンをクリックし、あとは検知されるのを待つだけです。

と、ここで最後の注意なのですが、AWS Config の設定で検知頻度が Daily になっている場合、すぐに検知されないことがありますのでその場合はひたすら待ってください。一時的に変更する以外のワークアラウンドは多分ないです。。都度検知になっている場合は体感5分以内で検知されました。

実際に通知された内容

Security Hub Findings Summary:

Id: arn:aws:securityhub:ap-northeast-1:637423304033:security-control/EC2.13/finding/f7c00134-a1c1-47eb-90f2-28a2be6d2f3a

Description: This control checks whether an Amazon EC2 security group allows ingress from ‘0.0.0.0/0’ or ‘::/0’ to port 22. The control fails if the security group allows ingress from ‘0.0.0.0/0’ or ‘::/0’ to port 22.

この通知は重要なセキュリティ問題を指摘しています。以下に、なぜ対応が必要か、どのように対応すべきか、そしてTerraformでの対応例を説明します。

  1. なぜ対応する必要があるか:   

  2. ポート22(SSH)が全てのIPアドレス(0.0.0.0/0 または ::/0)からアクセス可能な状態は、潜在的なセキュリティリスクとなります。

  3. 不正アクセスやブルートフォース攻撃のリスクが高まります。
  4. ベストプラクティスでは、SSHアクセスを必要な特定のIPアドレスのみに制限することが推奨されています。

  5. 対応方法:   

  6. 既存のセキュリティグループのルールを確認する。

  7. ポート22への0.0.0.0/0または::/0からのアクセスを許可しているルールを特定する。
  8. 該当するルールを削除し、代わりに必要な特定のIPアドレスまたはIPレンジからのみアクセスを許可するルールを追加する。

  9. Terraformでの対応例:

hcl
resource "aws_security_group" "example" {
  name        = "example-sg"
  description = "Example security group"
  vpc_id      = aws_vpc.main.id

  # SSHアクセスを特定のIPアドレスに制限
  ingress {
    from_port   = 22
    to_port     = 22
    protocol    = "tcp"
    cidr_blocks = ["YOUR_SPECIFIC_IP_ADDRESS/32"]  # 例: "203.0.113.0/24"
  }

  # その他の必要なルール...

  egress {
    from_port   = 0
    to_port     = 0
    protocol    = "-1"
    cidr_blocks = ["0.0.0.0/0"]
  }

  tags = {
    Name = "example-sg"
  }
}

このTerraformコードでは、SSHアクセス(ポート22)を特定のIPアドレスまたはIPレンジのみに制限しています。YOUR_SPECIFIC_IP_ADDRESS/32の部分を、実際に許可したいIPアドレスまたはIPレンジに置き換えてください。

注意点:

  • 可能な限り狭いIPレンジを使用してください。
  • 複数のIPアドレスやレンジが必要な場合は、複数のingressブロックを追加するか、cidr_blocksリストに複数のエントリを追加してください。
  • 変更を適用する前に、terraform planを実行して変更内容を確認してください。
  • 既存のインフラストラクチャに変更を加える場合は、影響を慎重に評価してください。これらの対応により、EC2インスタンスへのSSHアクセスをより安全に管理できます。

終わりに

いかがでしたでしょうか、こうして通知の内容を見てみるとなぜ検知が行われて、何をしなければいけないかがパッとわかるようになりました。次回からは設定に気をつけるようになりそうです。なかなか Security Hub findings の通知内容そのままでは分かりづらいですが、このように Generative AI を通すことによって非常に分かりやすくなります。

また、もしインフラストラクチャをコード管理しているのであれば、そのソース自体を Generative AI に渡して、具体的にどこを変えたら良いかを直接聞いてしまうのもアリかと思います。

この辺の工夫は無限にありそうですよね。最後まで読んでくださりありがとうございました。

カミナシでは AWS re:Invent が大好き、もしくは参加してみたいソフトウェアエンジニアを募集しています。

https://herp.careers/v1/kaminashi/requisition-groups/ae8150ab-e044-41d8-b034-ac2987cc29db