AWS関連

【AWS】CLI でGuardDuty を有効化する

はじめに

前回、「【AWS】GuardDuty による脅威検出のすすめ」 といった記事でGuardDuty の概要について触れましたが、今回は有効化の手順について整理していきます。

GuardDuty の有効化は「コンソール画面から数クリックで可能」 と聞くとめちゃくちゃ簡単そうに感じるのですが、各リージョンでの有効化が必要という点には注意が必要です。

実際そんなに負担ではないとは思いますが、、

個人的には画面でポチポチ作業を進めていくのが「非常にシンドい。。」 と感じたので、AWS CLI とシェルスクリプトを組み合わせることで実行してみよう、となりました。

また、検知した脅威を通知するためには別のサービスとの組み合わせにより構築を進める必要があり、関連サービスについても各リージョンでの構築が必要という点も地味に面倒な部分なので、併せて対応することにしました。

ちなみに、

巷では CloudFormation を利用した一撃有効化ツール(Lambdaで編集、SNS通知を含む) という超有益情報も目にした方も多いのではないでしょうか。(クラメソさんの下記リンク記事参照)

ただこの記事を見た当時は、私の CloudFormation に対する理解が乏しかったこともあり、

「何か知らんけど出来た」感で有効化してしまうのがイヤだったので、同様の構成を手動で?(シェルスクリプト+CLIで) つくってみよう!となった次第です。

シェルスクリプトを作るのもあまり慣れてなかったので、少し時間かかりましたが夜な夜なコツコツやりました。w

今でさえ(まだ何となくですが) CloudFormation についても理解が進んできたので、わざわざシェルスクリプトとか使って有効化する必要もないよな〜と感じる今日このごろですがw、、せっかく作ったので共有です。

コードを含む記事となりますので、ちょっとボリューミーな記事になると思いますが、お時間のあるときにでもご一読ください。

(p.s. CloudFormation の利用に抵抗が無い方は、CloudFormation による一撃有効化の方が圧倒的に楽なのでオススメです。w)

少しだけ補足…

  • Amazon Detective の有効化までは対応していません
  • AWS Security Hub の利用は考慮していません
  • 想定読者としては「GuardDuty を有効化して通知を受け取れるようにしたい」方、です
  • GuardDuty 未提供のリージョンについてはスクリプト内で処理をスキップしています
AWS CLI コマンド等を利用した操作方法について以下に記載しておりますが、操作を行う際はご自身で十分にコマンド等の確認を行った上で作業を行ってください。

もし下記スクリプトを参考にしていただける方がいらっしゃれば嬉しい限りですが、、処理実行に伴う問題について責任を取ることは出来かねますのでご了承いただければと思います。

すべて自己責任でお願いいたします。(テスト用アカウントで作業することを強くオススメいたします)

GuardDuty 活用の全体像

脅威検知から通知までの流れ

GuardDuty を有効化すれば脅威を検知することができるということはわかったけれど、その情報をどのように受け取るのか。

脅威検知から通知までの流れを整理してみました。

GuardDuty で検知した脅威はEventBridge のルールにてLambda関数に送られ整形され、SNSトピック経由で通知を受け取ることになります。

構築環境の詳細

次に、上で表現した「脅威検知から通知までの流れ」について深堀りしていきます。

ポイントとしてはリージョン単位での構築や、マルチアカウント構成における構築に関する部分なので以下をご一読いただければと思います。

マルチアカウント構成における考慮点

マルチアカウント構成の場合、”メンバーアカウント” の脅威検知情報は ”管理アカウント ”のGuardDuty に集約させることが可能です。

まずはOrganizations のマスターアカウント(管理アカウント)を、GuardDuty マスターアカウントとして設定します。

その上で、マスターアカウントから各メンバーアカウントのGuardDuty を有効化を行います。(ついでにメンバーアカウント追加時のGuardDuty 自動有効化の設定も行う)

設定作業としては負担は少ないですが、個別に各アカウント単体で(マスターアカウトを指定せず) GuardDuty を有効化して利用していこうとすると、脅威検知の結果が各アカウント内にバラバラに存在することとなりますので管理が煩雑になってしまいます。

各リージョンで構築が必要な関連サービス

次はリージョンごとに作成すべき各サービスについて。

以下のイメージ図からもわかるように、SNS を除くサービスについてもリージョンごとにリソースを配置する必要があります。

GuardDuty で検知した脅威情報の取り扱いについてはイメージ図内の矢印の通りで、各リージョン内で連携されいき、最終的にメインのリージョン(今回の場合は東京リージョン)のSNS トピックを利用して通知が飛ぶという仕組みになっています。

構築手順について、以下にまとめます。

GuardDuty の有効化

タスク一覧

  • 環境変数のセット
  • GuardDuty 有効化状況の確認(001.sh)
  • 各リージョンのGuardDuty 有効化(010.sh)
  • (以下、マルチアカウントの場合に実施)
  • GuardDuty マスターアカウント指定(020.sh)
  • メンバーアカウントのGuardDuty 自動有効化(030.sh)

環境変数を指定する

操作対象となるAWSアカウントのプロファイル情報やアカウントIDを環境変数にセットしていきます。

(前提事項:AWS CLI を利用するためのクリデンシャル設定等が完了している。ここでは “sayjoy_aws” としてプロファイル設定している。)

# プロファイルの指定
export MASTER_PROFILE='sayjoy_aws' \
&& echo ${MASTER_PROFILE}

# AWSアカウントIDを変数に格納
export MASTER_ACCOUNT_ID=$( \
aws sts get-caller-identity \
    --profile ${MASTER_PROFILE} \
    --query Account \
    --output text \
) && echo ${MASTER_ACCOUNT_ID}

# 変数確認
cat << END
  # MASTER_PROFILE="sayjoy_aws"
    MASTER_PROFILE="${MASTER_PROFILE}"
  # MASTER_ACCOUNT_ID="XXXXXXXXXXXX"
    MASTER_ACCOUNT_ID="${MASTER_ACCOUNT_ID}"
END

現状の確認

下記スクリプトを利用して、各リージョンにおけるGuardDuty の有効化状況を確認していきます。

"--finding-publishing-frequency" オプションを指定せずにGuardDuty を有効化すると、デフォルトでは"6h" が設定されるので"15min" に設定変更することが推奨されています。

更新を行う場合は、「おまけ」欄に記載している「GuardDuty の更新」 スクリプトをご参照ください。

(001.sh ↓)

#!/usr/bin/sh
# ${MASTER_PROFILE} アカウント内の「GuardDuty 有効化状態」を確認するシェルスクリプト

# GuardDuty の有効化状態チェック
StatusCheck_GuardDuty(){
    DETECTOR_ID=$( \
        aws guardduty list-detectors \
            --profile ${MASTER_PROFILE} \
            --region $1 \
            --query DetectorIds \
            --output text \
        )
    
    if [ "${#DETECTOR_ID}" -ne "0" ] ; then
        echo "GuardDuty が有効化されています。"
        echo " // DETECTOR_ID= ${DETECTOR_ID}"

        FINDING_PUB_FREQ=$(
            aws guardduty get-detector \
                --profile ${MASTER_PROFILE} \
                --region $1 \
                --detector-id ${DETECTOR_ID} \
                --query "FindingPublishingFrequency" \
            )
        echo " // FINDING_PUB_FREQ= ${FINDING_PUB_FREQ}"

    else
        echo "GuardDuty は有効化【されていません】。"
    fi
}

echo "■環境変数の確認"
echo "MASTER_PROFILE= ${MASTER_PROFILE}"
echo ""

# リージョン一覧の取得
REGIONS=$( \
    aws ec2 describe-regions \
        --all-regions \
        --query "Regions[].RegionName" \
        --output text \
        --profile ${MASTER_PROFILE} \
    )

echo "(■ステータス確認 start)"

for region in ${REGIONS[@]}; do
    echo "------------------------"
    echo "-- ${region}"
    if [ "${region}" = "af-south-1" ] || # ケープタウン
       [ "${region}" = "eu-south-1" ] || # ミラノ
       [ "${region}" = "me-south-1" ] || # バーレーン
       [ "${region}" = "ap-east-1" ] ; then # 香港
        echo "// Skip (GuardDuty 未提供)"
    else
        # 当該リージョンで、GuardDuty が有効化されているか確認する
        StatusCheck_GuardDuty ${region}
    fi
done

echo ""
echo "■ステータス確認 end"

一部リージョンで既に有効化されていたとしても何ら問題はないのですが(後半の有効化スクリプトでエラー[無視]されるだけなので問題ない)、一応。

各リージョンにて、GuardDuty を有効化する

スクリプト内で全リージョンをループさせ、GuardDuty の有効化を行います。(GuardDuty 未提供のリージョンはスキップ)

(010.sh ↓)

#!/usr/bin/sh
# ${MASTER_PROFILE} アカウント内の「GuardDuty を有効化」するシェルスクリプト

# GuardDutyの有効化
CreateDetector(){
    aws guardduty create-detector \
        --finding-publishing-frequency FIFTEEN_MINUTES \
        --region $1 \
        --profile ${MASTER_PROFILE} \
        --enable
    
    # 有効化の確認
    DETECTOR_ID=$( \
        aws guardduty list-detectors \
            --region $1 \
            --query DetectorIds \
            --output text \
            --profile ${MASTER_PROFILE} \
        )

    if [ "${#DETECTOR_ID}" -ne "0" ] ; then
        echo "GuardDuty が有効化されました。(DETECTOR_ID= ${DETECTOR_ID})"
    else
        echo "GuardDuty 有効化に【失敗しました】。"
    fi
}

echo "■環境変数の確認"
echo "MASTER_PROFILE= ${MASTER_PROFILE}"
echo ""

# リージョン一覧の取得
REGIONS=$( \
    aws ec2 describe-regions \
        --all-regions \
        --query "Regions[].RegionName" \
        --output text \
        --profile ${MASTER_PROFILE} \
    )

echo "■GuardDuty 有効化 start"

for region in ${REGIONS[@]}; do
    echo "------------------------"
    echo "-- ${region}"
    if [ "${region}" = "af-south-1" ] || # ケープタウン
       [ "${region}" = "eu-south-1" ] || # ミラノ
       [ "${region}" = "me-south-1" ] || # バーレーン
       [ "${region}" = "ap-east-1" ] ; then # 香港
        echo "// Skip (GuardDuty 未提供)"
    else
        # 当該リージョンで、GuardDuty を有効化する(探知機を作成する)
        CreateDetector ${region}
    fi
    echo ""
done

echo ""
echo "■GuardDuty 有効化 end"
  • create-detector コマンド利用時には --finding-publishing-frequency オプションにて更新結果がエクスポートされる頻度を15分に指定する

(マルチアカウントの場合)

GuardDuty マスターアカウントを指定する

Organizations を利用したマルチアカウント構成の場合には、、Organizations のマスターアカウントを GuardDuty のマスターアカウントとして設定します。

(020.sh ↓)

#!/usr/bin/sh
# 【マルチアカウント構成 + Organizations 利用してるの場合のみ使用】
# Organizations のマスターアカウント ${MASTER_PROFILE} を、GuardDuty のマスターアカウントに指定するシェルスクリプト

# GuardDutyマスターアカウントに、Organizations のマスターアカウントを指定する
EnableAdminAccount(){
    echo "$1 リージョンで、GuardDuty マスターアカウントを設定します。"
    aws guardduty enable-organization-admin-account \
        --admin-account-id ${MASTER_ACCOUNT_ID} \
        --region $1 \
        --profile ${MASTER_PROFILE}
}

echo "■環境変数の確認"
echo "MASTER_PROFILE= ${MASTER_PROFILE}"
echo "MASTER_ACCOUNT_ID= ${MASTER_ACCOUNT_ID}"
echo ""

# リージョン一覧の取得
REGIONS=$( \
    aws ec2 describe-regions \
        --all-regions \
        --query "Regions[].RegionName" \
        --output text \
        --profile ${MASTER_PROFILE} \
    )

echo "■GuardDuty Admin Account 設定 start"

for region in ${REGIONS[@]}; do
    echo "------------------------"
    echo "-- ${region}"
    if [ "${region}" = "af-south-1" ] || # ケープタウン
       [ "${region}" = "eu-south-1" ] || # ミラノ
       [ "${region}" = "me-south-1" ] || # バーレーン
       [ "${region}" = "ap-east-1" ] ; then # 香港
        echo "// Skip (GuardDuty 未提供)"
    else
        EnableAdminAccount ${region}
    fi
done

echo ""
echo "■GuardDuty Admin Account 設定 end"

メンバーアカウントのGuardDuty を有効化する & 自動有効化を設定する

メンバーアカウントのGuardDuty 有効化を行い、今後のメンバーアカウント追加時のGuardDuty 自動有効化設定を行います。

(030.sh ↓)

#!/usr/bin/sh
# 【マルチアカウント構成 + Organizations 利用してるの場合のみ使用】
# メンバーアカウントのGuardDuty 有効化および、アカウント追加時のGuardDuty 自動有効化設定を行うスクリプト

# メンバーアカウント追加時のGuardDuty 有効化自動設定
AutoEnableConfig(){
    echo "$1 リージョンで、メンバーアカウント追加時のGuardDuty 自動有効化をオンにします。"
    echo "(DETECTOR_ID= $2)"
    aws guardduty update-organization-configuration \
        --detector-id $2 \
        --auto-enable \
        --region $1 \
        --profile ${MASTER_PROFILE}
}

# メンバーアカウントの追加(GuardDuty の有効化)
CreateMembers(){
    echo "$1 リージョンにて、メンバーアカウントを追加します。"
    aws guardduty create-members \
        --detector-id $2  \
        --account-details AccountId=$3,Email=$4 \
        --region $1 \
        --profile ${MASTER_PROFILE}
}

# メンバーアカウントの取得 + ループしてGuardDuty のメンバーアカウントとして追加
GetMemberAccounts(){
    AccountLists=$( \
        aws organizations list-accounts \
            --query Accounts[].[Id,Email] \
            --profile ${MASTER_PROFILE} \
            --output text \
        )

    AccountID=""
    Email=""
    
    for account in ${AccountLists[@]}; do
        if [ ${#AccountID} -eq 0 ] ; then
            AccountID=${account}
        else
            Email=${account}
            echo "(${AccountID} / ${Email})"
            if [ ${AccountID} -eq ${MASTER_ACCOUNT_ID} ] ; then
                echo "// Skip (master account)"
            else
                CreateMembers $1 $2 ${AccountID} ${Email}
            fi
            AccountID=""
            Email=""
        fi
    done
}

# (該当リージョンのマスターアカウントにおける)探知機IDの取得
GetDetectorID(){
    # GuardDuty の有効化チェック
    DETECTOR_ID=$( \
        aws guardduty list-detectors \
            --region $1 \
            --query DetectorIds \
            --output text \
            --profile ${MASTER_PROFILE} \
        )
    
    if [ "${#DETECTOR_ID}" -ne "0" ] ; then
        # 呼び出し元でリターンするためのecho
        echo ${DETECTOR_ID}
    else
        echo "マスターアカウントのGuardDuty が有効化されていません。"
    fi
}

echo "■環境変数の確認"
echo "MASTER_PROFILE= ${MASTER_PROFILE}"
echo "MASTER_ACCOUNT_ID= ${MASTER_ACCOUNT_ID}"
echo ""

# リージョン一覧の取得
REGIONS=$( \
    aws ec2 describe-regions \
        --all-regions \
        --query "Regions[].RegionName" \
        --output text \
        --profile ${MASTER_PROFILE} \
    )

echo "■GuardDuty Create Members start"

for region in ${REGIONS[@]}; do
    echo ""
    echo "------------------------"
    echo "-- ${region}"
    if [ "${region}" = "af-south-1" ] || # ケープタウン
       [ "${region}" = "eu-south-1" ] || # ミラノ
       [ "${region}" = "me-south-1" ] || # バーレーン
       [ "${region}" = "ap-east-1" ] ; then # 香港
        echo "// Skip (GuardDuty 未提供)"
    else
        # 当該リージョンで、マスターアカウントの探知機IDを取得する
        Master_DetectorID=$(GetDetectorID ${region})
        # 当該リージョンで、Organizations のメンバーアカウントを取得する + GuardDuty メンバーアカウントとして追加する
        GetMemberAccounts ${region} ${Master_DetectorID}
        # 当該リージョンで、Organization に追加されたメンバーアカウントのGuardDuty 自動有効化をオンにする
        AutoEnableConfig ${region} ${Master_DetectorID}
    fi
done

echo ""
echo "■GuardDuty Create Members end"

SNS トピックの作成

タスク一覧

  • 環境変数のセット
  • SNSトピックの作成
  • SNSトピックの購読

SNSトピックを作成する

前処理

作業に必要となる変数をセットします。

# デフォルトリージョンの設定
export AWS_DEFAULT_REGION='ap-northeast-1' \
&& echo ${AWS_DEFAULT_REGION}

# 作業対象アカウントのプロファイル設定
export MASTER_PROFILE='sayjoy_aws' \
&& echo ${MASTER_PROFILE}

# SNSトピック名の指定
SNS_TOPIC_NAME="GuardDutyNotification" \
&& echo ${SNS_TOPIC_NAME}

# 変数確認
cat << END
  # AWS_DEFAULT_REGION="ap-northeast-1"
    AWS_DEFAULT_REGION="${AWS_DEFAULT_REGION}"
  # MASTER_PROFILE="XXXXXXXXXXXXXX"
    MASTER_PROFILE="${MASTER_PROFILE}"
  # SNS_TOPIC_NAME="GuardDutyNotification"
    SNS_TOPIC_NAME="${SNS_TOPIC_NAME}"
END

更新処理

SNSトピックを作成します。

# SNSトピックの作成
aws sns create-topic \
  --name ${SNS_TOPIC_NAME} \
  --region ${AWS_DEFAULT_REGION} \
  --profile ${MASTER_PROFILE} 

結果確認

SNSトピックが作成されていることを確認します。

# SNSトピックの存在確認
aws sns list-topics \
  --query "Topics[?contains(TopicArn, \`${SNS_TOPIC_NAME}\`)].TopicArn" \
  --region ${AWS_DEFAULT_REGION} \
  --profile ${MASTER_PROFILE}

SNSトピックの購読設定を行う

前処理

作業に必要となる変数をセットします。

# SNSトピックのARNを取得
SNS_TOPIC_ARN=$( \
  aws sns list-topics \
    --query "Topics[?contains(TopicArn, \`${SNS_TOPIC_NAME}\`)].TopicArn" \
    --region ${AWS_DEFAULT_REGION} \
    --profile ${MASTER_PROFILE} \
    --output text \
) \
&& echo ${SNS_TOPIC_ARN}

# SNSトピック購読のプロトコルを指定
SNS_SUBSCRIPTION_PROTOCOL="email" \
&& echo ${SNS_SUBSCRIPTION_PROTOCOL}

# SNSトピックを購読するメールアドレスを指定
SNS_SUBSCRIPTION_ENDPOINT="hoge@gmail.com" \
&& echo ${SNS_SUBSCRIPTION_ENDPOINT}

# 変数確認
cat << END
  # AWS_DEFAULT_REGION="ap-northeast-1"
    AWS_DEFAULT_REGION="${AWS_DEFAULT_REGION}"
  # MASTER_PROFILE="XXXXXXXXXXXXXX"
    MASTER_PROFILE="${MASTER_PROFILE}"
  # SNS_TOPIC_ARN="arn:aws:sns:ap-northeast-1:999999999999:GuardDutyNotification"
    SNS_TOPIC_ARN="${SNS_TOPIC_ARN}"
  # SNS_SUBSCRIPTION_PROTOCOL="email"
    SNS_SUBSCRIPTION_PROTOCOL="${SNS_SUBSCRIPTION_PROTOCOL}"
  # SNS_SUBSCRIPTION_ENDPOINT="XXXXXXXX@gmail.com"
    SNS_SUBSCRIPTION_ENDPOINT="${SNS_SUBSCRIPTION_ENDPOINT}"
END

更新処理

SNSトピックに対して、購読設定を追加します。これにより指定したメールアドレスに承認メールが届くので、メール内の『Confirm subscription』 リンクをクリックすることで承認を行います。

# 購読の設定
aws sns subscribe \
  --topic-arn ${SNS_TOPIC_ARN} \
  --protocol ${SNS_SUBSCRIPTION_PROTOCOL} \
  --region ${AWS_DEFAULT_REGION} \
  --profile ${MASTER_PROFILE} \
  --notification-endpoint ${SNS_SUBSCRIPTION_ENDPOINT}

結果確認

承認リンクをクリックした上で、購読設定が完了していることを確認します。

# subscription_arn の取得
SUBSCRIPTION_ARN=$( \
  aws sns list-subscriptions-by-topic \
    --query "Subscriptions[].SubscriptionArn" \
    --topic-arn ${SNS_TOPIC_ARN} \
    --profile ${MASTER_PROFILE}  \
    --output text
) \
&& echo ${SUBSCRIPTION_ARN}

# 購読設定の確認
aws sns get-subscription-attributes \
  --subscription-arn ${SUBSCRIPTION_ARN} \
  --profile ${MASTER_PROFILE}

get-subscription-attributes コマンドの実行結果で、「PendingConfirmation」 の値が "false" となっていれば承認済みの状態(購読設定が完了している)となります。

※承認待ちの状態の場合、list-subscriptions-by-topic コマンドの実行結果で出力される "SubscriptionArn" にはARN 情報ではなく『PendingConfirmation』 の文言が表示されます。

Lambda 関数の作成

Lambda 関数を作成するにあたり、関数に権限付与するためのIAM まわりのリソース作成も必要となります。

タスク一覧

  • 環境変数のセット
  • IAM ポリシーの作成
  • IAM ロールの作成
  • Lambda 関数の作成
  • Lambda 関数のプロビジョニング(051.sh)
  • Lambda 関数への権限付与

IAMポリシーを作成する

「SNS のpublish 権限」 と、「CloudWatch Logs への書き込み権限」 を含むIAM ポリシーを作成していきます。

前処理

処理に必要な変数セットや、ファイル作成等を行います。

# デフォルトリージョンの設定
export AWS_DEFAULT_REGION='ap-northeast-1' \
&& echo ${AWS_DEFAULT_REGION}

# 作業対象アカウントのプロファイル設定
export MASTER_PROFILE='sayjoy_aws' \
&& echo ${MASTER_PROFILE}

# AWSアカウントIDを取得
export MASTER_ACCOUNT_ID=$( \
aws sts get-caller-identity \
    --profile ${MASTER_PROFILE} \
    --query Account \
    --output text \
) && echo ${MASTER_ACCOUNT_ID}

# (複数回作業する予定がある場合は、作業用ディレクトリを都度作成して作業を行った方がよいです)
# →以降のコマンドでディレクトリを作成する部分があるので、複数回の処理実行でバッティングが発生してしまいます。
# 例: mkdir 20211112_01 && cd $_


# IAMポリシー名を指定
IAM_POLICY_NAME="GuardDutyLambdaPolicy" \
&& echo ${IAM_POLICY_NAME}

# IAMポリシードキュメントのファイル名を指定
IAM_POLICY_DOC_NAME="${IAM_POLICY_NAME}.json" \
&& echo ${IAM_POLICY_DOC_NAME}

# 現在のディレクトリ情報を取得
DIR_CURRENT_DIRECTORY=$(pwd) \
&& echo ${DIR_CURRENT_DIRECTORY}

# IAMポリシードキュメント格納用のディレクトリ名を指定
DIR_IAM_POLICY_DOC="IAM_POLICY" \
&& echo ${DIR_IAM_POLICY_DOC}

# IAMポリシードキュメント格納用のディレクトリを作成
mkdir -p ${DIR_IAM_POLICY_DOC}

# IAMポリシードキュメントのファイルパスを指定
FILE_IAM_POLICY_DOC="${DIR_CURRENT_DIRECTORY}/${DIR_IAM_POLICY_DOC}/${IAM_POLICY_DOC_NAME}" \
&& echo ${FILE_IAM_POLICY_DOC}

# SNSトピック名の指定
SNS_TOPIC_NAME="GuardDutyNotification" \
&& echo ${SNS_TOPIC_NAME}

# SNS トピックARNの取得
SNS_TOPIC_ARN=$( \
    aws sns list-topics \
        --query "Topics[?contains(TopicArn, \`${SNS_TOPIC_NAME}\`)].TopicArn" \
        --profile ${MASTER_PROFILE} \
        --region ${AWS_DEFAULT_REGION} \
        --output text \
    ) \
&& echo ${SNS_TOPIC_ARN}

# IAMポリシードキュメントの追記
# (ロググループへの更新権限と、SNSpublish権限を含める)
# (ロググループは、リージョンを指定しない(各リージョンでログ作成を許可出来る想定))
cat << EOF > ${FILE_IAM_POLICY_DOC}
{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Allow",
            "Action": "logs:CreateLogGroup",
            "Resource": "arn:aws:logs:*:${MASTER_ACCOUNT_ID}:*"
        },
        {
            "Effect": "Allow",
            "Action": [
                "logs:CreateLogStream",
                "logs:PutLogEvents"
            ],
            "Resource": [
                "arn:aws:logs:*:${MASTER_ACCOUNT_ID}:log-group:/aws/lambda/*"
            ]
        },
        {
            "Effect": "Allow",
            "Action": "sns:Publish",
            "Resource": "${SNS_TOPIC_ARN}"
        }
    ]
}
EOF
cat ${FILE_IAM_POLICY_DOC}

# IAMポリシードキュメントのフォーマットが崩れていないか確認(何も表示されなければOK)
jsonlint -q ${FILE_IAM_POLICY_DOC}

# 変数確認
cat << END
  # MASTER_PROFILE="sayjoy_aws"
    MASTER_PROFILE="${MASTER_PROFILE}"
  # IAM_POLICY_NAME="GuardDutyLambdaPolicy"
    IAM_POLICY_NAME="${IAM_POLICY_NAME}"
  # FILE_IAM_POLICY_DOC="${DIR_CURRENT_DIRECTORY}/${DIR_IAM_POLICY_DOC}/${IAM_POLICY_NAME}.json"
    FILE_IAM_POLICY_DOC="${FILE_IAM_POLICY_DOC}"
END

更新処理

IAM ポリシーを作成します。

# ポリシー作成
aws iam create-policy \
  --profile ${MASTER_PROFILE} \
  --policy-name ${IAM_POLICY_NAME} \
  --policy-document file://${FILE_IAM_POLICY_DOC}

結果確認

IAM ポリシーが作成されていることを確認します。

# ポリシー名で検索する
aws iam list-policies \
  --profile ${MASTER_PROFILE} \
  --scope Local \
  --max-items 20 \
  --query "Policies[?PolicyName == \`${IAM_POLICY_NAME}\`].[PolicyName]" \
  --output text

# (おまけ)
# キーワード"GuardDutyLambdaPolicy" で検索する
aws iam list-policies \
  --profile ${MASTER_PROFILE} \
  --scope Local \
  --max-items 20 \
  --query "Policies[?contains(PolicyName,\`GuardDutyLambdaPolicy\`)].[PolicyName]" \
  --output text

IAMロールを作成する

IAM ロールの作成を行い、上で作成したIAM ポリシーをアタッチしていきます。

前処理

処理に必要な変数セットや、ファイル作成等を行います。

# IAMロール名を指定
IAM_ROLE_NAME="GuardDutyLambdaRole" \
&& echo ${IAM_ROLE_NAME}

# IAMロールドキュメントのファイル名を指定
IAM_ROLE_DOC_NAME="${IAM_ROLE_NAME}.json" \
&& echo ${IAM_ROLE_DOC_NAME}

# 現在のディレクトリ情報を取得
DIR_CURRENT_DIRECTORY=$(pwd) \
&& echo ${DIR_CURRENT_DIRECTORY}

# IAMロールキュメント格納用のディレクトリ名を指定
DIR_IAM_ROLE_DOC="IAM_ROLE" \
&& echo ${DIR_IAM_ROLE_DOC}

# IAMロールドキュメント格納用のディレクトリを作成
mkdir -p ${DIR_IAM_ROLE_DOC}

# IAMロールドキュメントのファイルパスを指定
FILE_IAM_ROLE_DOC="${DIR_CURRENT_DIRECTORY}/${DIR_IAM_ROLE_DOC}/${IAM_ROLE_DOC_NAME}" \
&& echo ${FILE_IAM_ROLE_DOC}

# IAMロールドキュメントの追記
cat << EOF > ${FILE_IAM_ROLE_DOC}
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Principal": {
        "Service": "lambda.amazonaws.com"
      },
      "Action": "sts:AssumeRole"
    }
  ]
}
EOF
cat ${FILE_IAM_ROLE_DOC}

# IAMポリシードキュメントのフォーマットが崩れていないか確認(何も表示されなければOK)
jsonlint -q ${FILE_IAM_ROLE_DOC}

# ポリシーのARNを取得して、変数にセット
IAM_POLICY_ARN=$( \
    aws iam list-policies \
    --profile ${MASTER_PROFILE} \
    --scope Local \
    --max-items 20 \
    --query "Policies[?PolicyName == \`${IAM_POLICY_NAME}\`].Arn" \
    --output text \
    ) \
&& echo ${IAM_POLICY_ARN}

# 変数確認
cat << END
  # MASTER_PROFILE="XXXXXXXXXXXXXX"
    MASTER_PROFILE="${MASTER_PROFILE}"
  # IAM_ROLE_NAME="GuardDutyLambdaRole"
    IAM_ROLE_NAME="${IAM_ROLE_NAME}"
  # FILE_IAM_ROLE_DOC="${DIR_CURRENT_DIRECTORY}/${DIR_IAM_ROLE_DOC}/${IAM_ROLE_NAME}.json"
    FILE_IAM_ROLE_DOC="${FILE_IAM_ROLE_DOC}"
  # IAM_POLICY_ARN="arn:aws:iam::999999999999:policy/GuardDutyLambdaPolicy"
    IAM_POLICY_ARN="${IAM_POLICY_ARN}"
END

更新処理

IAM ロールを作成し、IAM ポリシーをアタッチします。

# ロールの作成
aws iam create-role \
    --profile ${MASTER_PROFILE} \
    --role-name ${IAM_ROLE_NAME} \
    --assume-role-policy-document file://${FILE_IAM_ROLE_DOC}

# ロールにポリシーをアタッチ
aws iam attach-role-policy \
    --profile ${MASTER_PROFILE} \
    --role-name ${IAM_ROLE_NAME} \
    --policy-arn ${IAM_POLICY_ARN}

結果確認

IAM ロールが存在することを確認します。

# ロール名で検索する("None" の行も表示されてしまう)
aws iam list-roles \
    --profile ${MASTER_PROFILE} \
    --query "Roles[?RoleName == \`${IAM_ROLE_NAME}\`].[RoleName]" \
    --output text

# (おまけ)
# キーワード"GuardDutyLambdaRole" で検索する("None" の行も表示されてしまう)
aws iam list-roles \
    --profile ${MASTER_PROFILE} \
    --query "Roles[?contains(RoleName,\`GuardDutyLambdaRole\`)].[RoleName]" \
    --output text

Lambda 関数ファイルを作成する

Lambda 関数を作成して、zip ファイルに圧縮します。

前処理

処理に必要な変数セットや、ファイル作成等を行います。

# Lambda関数名を指定
# (プロビジョニング時にはリージョン名を末尾に付与して利用する)
LAMBDA_FUNC_NAME="GuardDutyFunction" \
&& echo ${LAMBDA_FUNC_NAME}

# IAMロールのARNを取得して、変数にセット
IAM_ROLE_ARN=$( \
    aws iam get-role \
        --profile ${MASTER_PROFILE} \
        --role-name ${IAM_ROLE_NAME} \
        --query "Role.Arn" \
        --output text \
    ) \
&& echo ${IAM_ROLE_ARN}

# ハンドラ名のセット
LAMBDA_FUNC_HUNDLER_NAME="lambda_function.lambda_handler" \
&& echo ${LAMBDA_FUNC_HUNDLER_NAME}

# 関数ファイル名の指定
LAMBDA_FUNC_PY_NAME="lambda_function.py" \
&& echo ${LAMBDA_FUNC_PY_NAME}

# 現在のディレクトリ情報を取得
DIR_CURRENT_DIRECTORY=$(pwd) \
&& echo ${DIR_CURRENT_DIRECTORY}

# Lambda関連ファイル格納用のディレクトリ名を指定
DIR_LAMBDA_DOC="LAMBDA" \
&& echo ${DIR_LAMBDA_DOC}

# Lambda関連ファイル格納用のディレクトリを作成
mkdir -p ${DIR_LAMBDA_DOC}

# Lambda関連ファイル格納用のディレクトリパスを指定
DIR_LAMBDA="${DIR_CURRENT_DIRECTORY}/${DIR_LAMBDA_DOC}" \
&& echo ${DIR_LAMBDA}

# Lambdaの関数ドキュメントのファイルパスを指定
FILE_LAMBDA_DOC="${DIR_LAMBDA}/${LAMBDA_FUNC_PY_NAME}" \
&& echo ${FILE_LAMBDA_DOC}

# zipファイル名の指定
LAMBDA_FUNC_ZIP_NAME="${LAMBDA_FUNC_NAME}.zip" \
&& echo ${LAMBDA_FUNC_ZIP_NAME}

# zipファイルのファイルパスを指定
FILE_LAMBDA_FUNC_ZIP="${DIR_LAMBDA}/${LAMBDA_FUNC_ZIP_NAME}" \
&& echo ${FILE_LAMBDA_FUNC_ZIP}

# SNSトピック名の指定
SNS_TOPIC_NAME="GuardDutyNotification" \
&& echo ${SNS_TOPIC_NAME}

# SNS トピックARNの取得
SNS_TOPIC_ARN=$( \
    aws sns list-topics \
        --query "Topics[?contains(TopicArn, \`${SNS_TOPIC_NAME}\`)].TopicArn" \
        --profile ${MASTER_PROFILE} \
        --output text \
    ) \
&& echo ${SNS_TOPIC_ARN}

# Lambda関数にセットする環境変数情報の指定
STRING_LAMBDA_FUNCTION_ENVIRONMENT="Variables={TOPIC_ARN=${SNS_TOPIC_ARN}, TOPIC_REGION=${AWS_DEFAULT_REGION}}" \
&& echo ${STRING_LAMBDA_FUNCTION_ENVIRONMENT}

関数の追記

作成した関数ファイルに処理を追記します。

  • format_message にて通知メッセージの整形行う
  • get_severity_level にて重大度のラベル付け、および通知の要否を指定する
  • 通知内容のサンプルは後述
# Lambdaの関数ファイルに追記する
cat << EOF > ${FILE_LAMBDA_DOC}
import json
import os
import urllib.request
import boto3

subject = ""
message = ""
notify = True

def format_message(data):
    severity_level = get_severity_level(data['detail']['severity'])
    global subject
    subject = '[%s] GuardDuty Notification. (%s)' % (severity_level['label'], data['detail']['type'])
    print("// subject:")
    print(subject)

    msg = '%s\n\n' % (data['detail']['description'])
    msg += '- Severity : %s [%s]\n' % (data['detail']['severity'], severity_level['label'])
    msg += '- FindingType : %s\n' % (data['detail']['type'])
    msg += '- AccountID : %s\n' % (data['detail']['accountId'])
    msg += '- Region : %s\n' % (data['detail']['region'])
    msg += '- Count : %s\n\n' % (data['detail']['service']['count'])
    msg += 'For more details open the GuardDuty console at https://'
    msg += '%s' % (data['detail']['region'])
    msg += '.console.aws.amazon.com/guardduty/home?region=%s' % (data['detail']['region'])
    msg += '#/findings?search=id%3D'
    msg += '%s\n' % (data['detail']['id'])
    print("// msg:")
    print(msg)
    
    global notify
    notify = severity_level['notification']
    
    return msg

def get_severity_level(severity):
    # ref: http://docs.aws.amazon.com/guardduty/latest/ug/guardduty_findings.html#guardduty_findings-severity
    if severity == 0.0:
        level = {'label': 'Information', 'notification': True}
    elif 0.1 <= severity <= 3.9:
        level = {'label': 'Low', 'notification': True}
    elif 4.0 <= severity <= 6.9:
        level = {'label': 'Medium', 'notification': True}
    elif 7.0 <= severity <= 8.9:
        level = {'label': 'High', 'notification': True}
    elif 9.0 <= severity <= 10.0:
        level = {'label': 'Critical', 'notification': True}
    else:
        level = {'label': 'Unknow', 'notification': False}
    return level

def sns_publish(ARN, MSG, SUB):
    topic_region = os.environ['TOPIC_REGION']
    client = boto3.client('sns', region_name=topic_region)
    response = client.publish(
        TopicArn = ARN,
        Message = MSG,
        Subject = SUB
    )
    
    print("// json.dumps(response):")
    print(json.dumps(response))
    return response

def lambda_handler(event, context):
    print("// start function //")
    topic_arn = os.environ['TOPIC_ARN']
    # print("// json.dumps(event):")
    # print(json.dumps(event))
    
    message = format_message(event)
    # print(json.dumps(message))
    
    if notify == True:
        sns_publish(topic_arn, message, subject)
    else:
        print("// skip notify ///////////////////////////////")

    print("// end function //")
EOF
cat ${FILE_LAMBDA_DOC}

(通知内容のサンプルは以下の通り)

■件名
[Low] GuardDuty Notification. (Recon:EC2/PortProbeUnprotectedPort)

■本文
EC2 instance has an unprotected port which is being probed by a known malicious host.

- Severity : 2 [Low]
- FindingType : Recon:EC2/PortProbeUnprotectedPort
- AccountID : 999999999999
- Region : ap-northeast-1
- Count : 1

For more details open the GuardDuty console at https://ap-northeast-1.console.aws.amazon.com/guardduty/home?region=ap-northeast-1#/findings?search=id%XXXXXXXXXXXXXXXXXX

関数ファイルのzip 化

作成したLambda 関数ファイルを圧縮します。

# 関数ファイルのzip 化
cd ${DIR_LAMBDA_DOC}
zip ${FILE_LAMBDA_FUNC_ZIP} ./*

# zipファイルの存在確認
ls -l

Lambda 関数をプロビジョニングする

Lambda 関数のプロビジョニング用シェルスクリプトを実行する前に、以下の変数確認を実施します。

※コマンド操作で作成した「LAMBDAディレクトリ」の場所は、シェルスクリプト実行まで移動しないこと(指定したファイルパスを変数として利用するため)

# 変数確認
cat << END
  # IAM_ROLE_ARN="arn:aws:iam::XXXXXXXXXXXX:role/GuardDutyLambdaRole"
    IAM_ROLE_ARN="${IAM_ROLE_ARN}"
  # LAMBDA_FUNC_HUNDLER_NAME="lambda_function.lambda_handler"
    LAMBDA_FUNC_HUNDLER_NAME="${LAMBDA_FUNC_HUNDLER_NAME}"
  # FILE_LAMBDA_FUNC_ZIP="${DIR_LAMBDA}/${LAMBDA_FUNC_ZIP_NAME}"
    FILE_LAMBDA_FUNC_ZIP="${FILE_LAMBDA_FUNC_ZIP}"
  # SNS_TOPIC_ARN="arn:aws:sns:ap-northeast-1:999999999999:GuardDutyNotification"
    SNS_TOPIC_ARN="${SNS_TOPIC_ARN}"
  # AWS_DEFAULT_REGION="ap-northeast-1"
    AWS_DEFAULT_REGION="${AWS_DEFAULT_REGION}"
  # STRING_LAMBDA_FUNCTION_ENVIRONMENT="Variables={TOPIC_ARN=${SNS_TOPIC_ARN}, TOPIC_REGION=${AWS_DEFAULT_REGION}}"
    STRING_LAMBDA_FUNCTION_ENVIRONMENT="${STRING_LAMBDA_FUNCTION_ENVIRONMENT}"
  # LAMBDA_FUNC_NAME="GuardDutyFunction"
    LAMBDA_FUNC_NAME="${LAMBDA_FUNC_NAME}"
END

 
Lambda関数のプロビジョニングを行います。また、併せてEventBridge からの関数呼び出しを許可します。

(051.sh ↓)

#!/usr/bin/sh
# 各リージョンでLambda関数を作成していくシェルスクリプト

# Lambda関数の作成
CreateFunction(){
    aws lambda create-function \
        --profile ${MASTER_PROFILE} \
        --region $1 \
        --function-name $2 \
        --handler ${LAMBDA_FUNC_HUNDLER_NAME} \
        --zip-file fileb://${FILE_LAMBDA_FUNC_ZIP} \
        --environment "${STRING_LAMBDA_FUNCTION_ENVIRONMENT}" \
        --role ${IAM_ROLE_ARN} \
        --runtime python3.8 \
        --timeout 15

    # 関数作成結果の確認
    FUNCTION_NAME=$( \
        aws lambda list-functions \
            --profile ${MASTER_PROFILE} \
            --region $1 \
            --query "Functions[?FunctionName == \`$2\`].FunctionName" \
            --output text \
        )

    if [ "${#FUNCTION_NAME}" -ne "0" ] ; then
        echo "Lambda関数が作成されました。($1 / ${FUNCTION_NAME})"
    else
        echo "Lambda関数の作成に【失敗しました】。"
    fi
}

# EventBridge にLambda関数の呼び出しを許可する
AddPermission(){
    aws lambda add-permission \
    --profile ${MASTER_PROFILE} \
    --region $1 \
    --function-name $2 \
    --statement-id EventBridge \
    --action 'lambda:InvokeFunction' \
    --principal events.amazonaws.com


    # 関数作成結果の確認
    POLICY=$( \
        aws lambda get-policy \
            --profile ${MASTER_PROFILE} \
            --region $1 \
            --query Policy \
            --function-name $2 \
        )

    # NULL が返された時どのような判定になるか要確認
    if [ "${#POLICY}" -ne "0" ] ; then
        echo "Lambda関数の呼び出しが許可されました。($1 / ${POLICY})"
    else
        echo "Lambda関数の呼び出し許可に【失敗しました】。"
    fi
}

echo "■環境変数の確認"
echo "MASTER_PROFILE                     = ${MASTER_PROFILE}"
echo "LAMBDA_FUNC_NAME                   = ${LAMBDA_FUNC_NAME}"
echo "LAMBDA_FUNC_HUNDLER_NAME           = ${LAMBDA_FUNC_HUNDLER_NAME}"
echo "FILE_LAMBDA_FUNC_ZIP               = ${FILE_LAMBDA_FUNC_ZIP}"
echo "IAM_ROLE_ARN                       = ${IAM_ROLE_ARN}"
echo "STRING_LAMBDA_FUNCTION_ENVIRONMENT = ${STRING_LAMBDA_FUNCTION_ENVIRONMENT}"
echo ""

# リージョン一覧の取得
REGIONS=$( \
    aws ec2 describe-regions \
        --all-regions \
        --query "Regions[].RegionName" \
        --output text \
        --profile ${MASTER_PROFILE} \
    )

echo "(■Lambda関数の作成 start)"
LAMBDA_FUNC_NAME_REGION=""

for region in ${REGIONS[@]}; do
    echo "------------------------"
    echo "-- ${region}"
    if [ "${region}" = "af-south-1" ] || # ケープタウン
       [ "${region}" = "eu-south-1" ] || # ミラノ
       [ "${region}" = "me-south-1" ] || # バーレーン
       [ "${region}" = "ap-east-1" ] ; then # 香港
        echo "// Skip (GuardDuty 未提供)"
    else
        # ファンクション名を指定する
        LAMBDA_FUNC_NAME_REGION="${LAMBDA_FUNC_NAME}_${region}"
        # Lambda関数を作成する
        CreateFunction ${region} ${LAMBDA_FUNC_NAME_REGION}
        echo "- - - - -"
        # Lambda関数の呼び出しをEventBridge に許可する
        AddPermission ${region} ${LAMBDA_FUNC_NAME_REGION}
    fi
done

echo ""
echo "■Lambda関数の作成 end"

スクリプトの実行結果に出力される「Lambda関数が作成されました。」、「Lambda関数の呼び出しが許可されました。」等のメッセージにて、処理の完了を確認します。

EventBridge ルールの作成

タスク一覧

  • 環境変数のセット
  • ルールを作成する(061.sh)

環境変数を指定する

作業に必要となる変数をセットします。

# デフォルトリージョンの設定
export AWS_DEFAULT_REGION='ap-northeast-1' \
&& echo ${AWS_DEFAULT_REGION}

# 作業対象アカウントのプロファイル設定
export MASTER_PROFILE='sayjoy_aws' \
&& echo ${MASTER_PROFILE}

# CloudWatch Event ルール名を指定
EVENT_RULE_NAME="GuardDutyEventRule" \
&& echo ${EVENT_RULE_NAME}

# Lambda関数名を指定(ここでは末尾のリージョン情報を含めない)
LAMBDA_FUNC_NAME="GuardDutyFunction" \
&& echo ${LAMBDA_FUNC_NAME}

# 変数確認
cat << END
  # AWS_DEFAULT_REGION="ap-northeast-1"
    AWS_DEFAULT_REGION="${AWS_DEFAULT_REGION}"
  # MASTER_PROFILE="XXXXXXXXXXXXXXX"
    MASTER_PROFILE="${MASTER_PROFILE}"
  # LAMBDA_FUNC_NAME="GuardDutyFunction"
    LAMBDA_FUNC_NAME="${LAMBDA_FUNC_NAME}"
  # EVENT_RULE_NAME="GuardDutyEventRule"
    EVENT_RULE_NAME="${EVENT_RULE_NAME}"
END

ルールを作成する

EventBridge ルールを作成していきます。

(061.sh ↓)

#!/usr/bin/sh
# 各リージョンでEventBridge を作成していくシェルスクリプト

# EventBridge ルールの作成
PutRule(){
    echo "-------"
    echo "ルールの作成"
    # ルールの作成
    aws events put-rule \
        --profile ${MASTER_PROFILE} \
        --region $1 \
        --description 'Notify the GuardDuty Finding.' \
        --event-pattern '{"source":["aws.guardduty"],"detail-type":["GuardDuty Finding"]}' \
        --name $2

    echo "-------"
    # ルール作成結果の確認
    echo "ルール作成結果の確認"
    RULE_NAME=$( \
        aws events describe-rule \
            --profile ${MASTER_PROFILE} \
            --region $1 \
            --name $2 \
            --query Name \
            --output text \
        )

    if [ "${#RULE_NAME}" -ne "0" ] ; then
        echo "// EventBridge ルールが作成されました。($1 / ${RULE_NAME})"
    else
        echo "// EventBridge ルールの作成に【失敗】しました。"
    fi
}

# EventBridge ルールのターゲットにLambda関数を指定する
PutTarget(){
    echo "-------"
    # Lambda関数のARN取得
    echo "Lambda関数のARN取得"
    LAMBDA_FUNC_ARN=$( \
        aws lambda get-function \
            --profile ${MASTER_PROFILE} \
            --region $1 \
            --function-name $3 \
            --query 'Configuration.FunctionArn' \
            --output text \
        )

    if [ "${#LAMBDA_FUNC_ARN}" -ne "0" ] ; then
        echo "// Lambda のARN 取得に成功しました。($1 / ${LAMBDA_FUNC_ARN})"
        # ターゲット情報を変数にセットする
        STRING_EVENT_RULE_TARGETS="Id=1,Arn=${LAMBDA_FUNC_ARN}"
    else
        echo "// Lambda のARN 取得に【失敗】しました。"
    fi

    echo "-------"
    # ルールのターゲットを設定
    echo "ルールのターゲットを設定"
    aws events put-targets \
        --profile ${MASTER_PROFILE} \
        --region $1 \
        --targets "${STRING_EVENT_RULE_TARGETS}" \
        --rule $2

    echo "-------"
    # ターゲット設定結果の確認
    echo "ターゲット設定結果の確認"
    TARGETS=$( \
        aws events list-targets-by-rule \
            --profile ${MASTER_PROFILE} \
            --region $1 \
            --query "Targets[].Arn"  \
            --rule  $2 \
            --output text \
        )

    if [ "${#TARGETS}" -ne "0" ] && [ "${TARGETS}" != "null" ] ; then
        echo "// EventBridge ルールへのターゲット設定が完了しました。($1 / ${TARGETS})"
    else
        echo "// EventBridge ルールへのターゲット設定に【失敗】しました。"
    fi
}

echo "■環境変数の確認"
echo "MASTER_PROFILE             = ${MASTER_PROFILE}"
echo "LAMBDA_FUNC_NAME           = ${LAMBDA_FUNC_NAME}"
echo "EVENT_RULE_NAME            = ${EVENT_RULE_NAME}"
echo ""

# リージョン一覧の取得
REGIONS=$( \
    aws ec2 describe-regions \
        --all-regions \
        --query "Regions[].RegionName" \
        --output text \
        --profile ${MASTER_PROFILE} \
    )

echo "(■EventBridge ルールの作成 start)"

for region in ${REGIONS[@]}; do
    echo "------------------------"
    echo "-- ${region}"
    if [ "${region}" = "af-south-1" ] || # ケープタウン
       [ "${region}" = "eu-south-1" ] || # ミラノ
       [ "${region}" = "me-south-1" ] || # バーレーン
       [ "${region}" = "ap-east-1" ] ; then # 香港
        echo "// Skip (GuardDuty 未提供)"
    else
        # ルール名/ファンクション名を指定する
        EVENT_RULE_NAME_REGION="${EVENT_RULE_NAME}_${region}"
        LAMBDA_FUNC_NAME_REGION="${LAMBDA_FUNC_NAME}_${region}"
        # EventBridge (ルール)を作成する
        PutRule ${region} ${EVENT_RULE_NAME_REGION}
        echo "- - - -"
        # EventBridge ルールのターゲットにLambda関数を指定する
        PutTarget ${region} ${EVENT_RULE_NAME_REGION} ${LAMBDA_FUNC_NAME_REGION}
    fi
done

echo ""
echo "■EventBridge ルールの作成 end"

まとめ

シェルスクリプト素人なので改善の余地はあるかと思いますが、上記手順の通り作業を進めればGuardDuty の有効化を行った上で、所定のメールアドレスに対する通知まで行う環境構築が出来たと思います。

正直なところ、冒頭に補足として記載した「Amazon Detective」 や「AWS Security Hub」 まで導入して一段落といった感覚はあるのですが、、今回はここまでです。

気になる点などありましたら、気軽にTwitter等でお声がけいただけると嬉しいです。

以上です。最後まで読んでいただきありがとうございました。

(おまけ)

余談にはなりますが、作業時に使用したリソースの「更新」用・「削除」用のスクリプトも以下に記載します。

GuardDuty の更新

検知した脅威情報について、”CloudWatcdh Logs 等に更新する周期”を更新するスクリプトです。

(011.sh ↓)

#!/usr/bin/sh
# CloudWatchイベントなどに結果がエクスポートされる頻度を更新するためのスクリプト
# "create-detector" コマンドでGuardDuty を有効化する際、"--finding-publishing-frequency" オプションを
# 指定せずにGuardDuty を有効化した場合、デフォルトで"6h" が設定されるので"15min" に設定変更する

# GuardDutyの更新
CreateDetector(){
    # 探知機IDの取得
    DETECTOR_ID=$( \
        aws guardduty list-detectors \
            --profile ${MASTER_PROFILE} \
            --region $1 \
            --query DetectorIds \
            --output text \
        )

    if [ "${#DETECTOR_ID}" -ne "0" ] ; then
        # 探知機の更新
        aws guardduty update-detector \
            --profile ${MASTER_PROFILE} \
            --region $1 \
            --detector-id ${DETECTOR_ID} \
            --finding-publishing-frequency FIFTEEN_MINUTES
        
        # FindingPublishingFrequencyの確認
        FINDING_PUB_FREQ=$( \
            aws guardduty get-detector \
                --profile ${MASTER_PROFILE} \
                --region $1 \
                --detector-id ${DETECTOR_ID} \
                --query FindingPublishingFrequency \
                --output text \
            )

        if [ "${#FINDING_PUB_FREQ}" -ne "0" ] ; then
            echo "GuardDuty の更新が完了しました。"
            echo " // DETECTOR_ID= ${DETECTOR_ID}"
            echo " // FINDING_PUB_FREQ= ${FINDING_PUB_FREQ}"
        else
            echo "GuardDuty の更新に【失敗しました】。"
        fi
    else
        echo "GuardDuty は有効化【されていません】。"
    fi
}

echo "■環境変数の確認"
echo "MASTER_PROFILE= ${MASTER_PROFILE}"
echo ""

# リージョン一覧の取得
REGIONS=$( \
    aws ec2 describe-regions \
        --all-regions \
        --query "Regions[].RegionName" \
        --output text \
        --profile ${MASTER_PROFILE} \
    )

echo "■GuardDuty 更新 start"

for region in ${REGIONS[@]}; do
    echo "------------------------"
    echo "-- ${region}"
    if [ "${region}" = "af-south-1" ] || # ケープタウン
       [ "${region}" = "eu-south-1" ] || # ミラノ
       [ "${region}" = "me-south-1" ] || # バーレーン
       [ "${region}" = "ap-east-1" ] ; then # 香港
        echo "// Skip (GuardDuty 未提供)"
    else
        # 当該リージョンで、GuardDuty を有効化する(探知機を作成する)
        CreateDetector ${region}
    fi
    echo ""
done

echo ""
echo "■GuardDuty 更新 end"

Lambda 関数の削除

(059.sh ↓)

#!/usr/bin/sh
# 各リージョンでLambda関数を”削除”していくシェルスクリプト

# Lambda関数の削除
DeleteFunction(){
    aws lambda delete-function \
        --profile ${MASTER_PROFILE} \
        --region $1 \
        --function-name $2

    # 関数削除結果の確認
    FUNCTION_NAME=$( \
        aws lambda list-functions \
            --profile ${MASTER_PROFILE} \
            --region $1 \
            --query "Functions[?FunctionName == \`$2\`].FunctionName" \
            --output text \
        )

    if [ "${#FUNCTION_NAME}" -ne "0" ] ; then
        echo "Lambda関数が存在します。($1 / ${FUNCTION_NAME})"
    else
        echo "Lambda関数が存在しません(削除完了)"
    fi
}


echo "■環境変数の確認"
echo "MASTER_PROFILE                     = ${MASTER_PROFILE}"
echo "LAMBDA_FUNC_NAME                   = ${LAMBDA_FUNC_NAME}"
echo ""

# リージョン一覧の取得
REGIONS=$( \
    aws ec2 describe-regions \
        --all-regions \
        --query "Regions[].RegionName" \
        --output text \
        --profile ${MASTER_PROFILE} \
    )

echo "(■Lambda関数の削除 start)"
LAMBDA_FUNC_NAME_REGION=""

for region in ${REGIONS[@]}; do
    echo "------------------------"
    echo "-- ${region}"
    if [ "${region}" = "af-south-1" ] || # ケープタウン
       [ "${region}" = "eu-south-1" ] || # ミラノ
       [ "${region}" = "me-south-1" ] || # バーレーン
       [ "${region}" = "ap-east-1" ] ; then # 香港
        echo "// Skip"
    else
        # ファンクション名を指定する
        LAMBDA_FUNC_NAME_REGION="${LAMBDA_FUNC_NAME}_${region}"
        # Lambda関数を削除する
        DeleteFunction ${region} ${LAMBDA_FUNC_NAME_REGION}
    fi
done

echo ""
echo "■Lambda関数の削除 end"

EventBridge ルールの削除

  • ルールの削除前にターゲットの削除を行う必要がある
  • ここでは全てのリージョンにて削除処理をトライしている

(069.sh ↓)

#!/usr/bin/sh
# 各リージョンでEventBridge を削除していくシェルスクリプト

# EventBridge ルールの削除(事前にターゲットを削除する必要あり)
DeleteRule(){

    echo "-------"
    # ルールの設定状況確認
    echo "ルールの設定状況確認"
    RULE_NAME=$( \
        aws events describe-rule \
            --profile ${MASTER_PROFILE} \
            --region $1 \
            --query Name \
            --name $2 \
            --output text \
        )

    if [ "${#RULE_NAME}" -ne "0" ] ; then
        echo "// EventBridge ルールが存在します ($1 / ${RULE_NAME})"

        echo "-------"
        # ルールIDの取得
        echo "ルールIDの取得"
        RULE_ID=$( \
            aws events list-targets-by-rule \
                --profile ${MASTER_PROFILE} \
                --region $1 \
                --rule $2 \
                --query Targets[].Id \
                --output text \
            )

        echo "-------"
        # ターゲットの削除
        echo "ターゲットの削除"
        aws events remove-targets \
            --profile ${MASTER_PROFILE} \
            --region $1 \
            --rule $2 \
            --ids $RULE_ID


        echo "-------"
        # ターゲット削除結果の確認
        echo "ターゲット削除結果の確認"
        aws events list-targets-by-rule \
            --profile ${MASTER_PROFILE} \
            --region $1 \
            --rule $2 \
            --query Targets[]

        echo "-------"
        # ルールの削除
        echo "ルールの削除"
        aws events delete-rule \
            --profile ${MASTER_PROFILE} \
            --region $1 \
            --name $2

        echo "-------"
        # ルール削除結果の確認
        echo "ルール削除結果の確認"
        RULE_NAME=$( \
            aws events describe-rule \
                --profile ${MASTER_PROFILE} \
                --region $1 \
                --query Name \
                --name $2 \
                --output text \
            )

        if [ "${#RULE_NAME}" -ne "0" ] ; then
            echo "// EventBridge ルールが存在します(削除失敗)($1 / ${RULE_NAME})"
        else
            echo "// EventBridge ルールは存在しません(削除完了)"
        fi
    else
        echo "// EventBridge ルールが存在しないのでSkip"
    fi
}

echo "■環境変数の確認"
echo "MASTER_PROFILE             = ${MASTER_PROFILE}"
echo "LAMBDA_FUNC_NAME           = ${LAMBDA_FUNC_NAME}"
echo "EVENT_RULE_NAME            = ${EVENT_RULE_NAME}"
echo ""

# リージョン一覧の取得
REGIONS=$( \
    aws ec2 describe-regions \
        --all-regions \
        --query "Regions[].RegionName" \
        --output text \
        --profile ${MASTER_PROFILE} \
    )

echo "(■EventBridge ルールの削除 start)"

for region in ${REGIONS[@]}; do
    echo ""
    echo "------------------------"
    echo "-- ${region}"
    # ルール名/ファンクション名を指定する
    EVENT_RULE_NAME_REGION="${EVENT_RULE_NAME}_${region}"
    # EventBridge (ルール)を削除する
    DeleteRule ${region} ${EVENT_RULE_NAME_REGION}
done

echo ""
echo "■EventBridge ルールの削除 end"