AWS CDKで構築したSIEM on Amazon ESで、FSx for Windows File Serverのファイルアクセス監査ログを可視化してみた

ログはイケてるダッシュボードでまとめて確認したいですよね? それ、SIEM on Amazon ESでできます。
2021.06.21

目grep ああ目grep 目grep

こんにちは、のんピ です。

皆さんは目grep目diffは得意ですか? 私は得意です。

目grepや目diffとは、コマンドに頼らず、人力でログや設定ファイルから特定の文字列やパターン、差分を探しだす特殊スキルです。

私は一時期、ファイヤーウォールやWAF、プロキシなどの各種ログを見る機会が多かったので、目grepや目diff力がかなり鍛えられました。

しかし、目grepや目diffはかなりの集中力が必要とされます。加えて、視力を犠牲にしたにも関わらず、以下のようなデメリットがあります。

  • 真心を込めて一つ一つログを見ていくので、ログ全体の傾向を掴みにくい
  • 個人のスキルに大きく依存するので、情報の抜け漏れがある可能性がある
  • 一朝一夕で目grepや目diffスキルを身につけること難しく、結果として負荷が特定の人に偏りがち
  • 確認するべきログの量に比例して疲労が溜まる

そのログ分析の辛みから解放されるために、今回はログの可視化やセキュリティ分析をする「SIEM on Amazon Elasticsearch Service」を使ってログ分析してみようと思います。

分析対象のログは、私注目のFSx for Windows File Serverのファイルアクセス監査ログにします。

FSx for Windows File Serverのファイルアクセス監査ログについては、以下ブログをご参照ください。

SIEM on Amazon ES とは

SIEM on Amazon ES とは、AWSがOSSで公開している SIEM ソリューションです。

このソリューションを利用することで、CloudTrailやVPC Flow Logs、GuardDuty、OSなどの各種ログの可視化やセキュリティ分析を行う事が可能となります。

詳しくは、以下ブログをご参照ください。

SIEM on Amazon ESの特徴は以下の通りです。

  • マネージメントサービスとサーバーレスのみで構成
  • AWSサービス専用の正規化、ダッシュボード
  • CloudFormation/CDKによるデプロイ、約20分で完了
  • クラウドサービスをご利用した分だけの従量制料金
  • マルチアカウント、マルチリージョン対応

抜粋: Startup.fm: SIEM on Amazon Elasticsearch Serviceでログ調査&分析を楽にしよう

FSx for Windows File Serverのファイルアクセス監査ログをSIEM on Amazon ESに取り込むには整形する必要がある

SIEM on Amazon ESに取り込むことができるログは以下のように形式が決まっています。

AWS 以外のログをログ用 S3 バケットにエクスポートすることで SIEM on Amazon ES に取り込むことができます。ファイルフォーマットはテキスト形式、JSON 形式、CSV 形式に対応しています。テキスト形式は1行ログを取り込むことができますが、複数行ログには対応していません。S3 へのエクスポートは Logstash や Fluentd のプラグインを使う方法があります。

SIEM on Amazon ES の設定変更 - AWS サービス以外のログの取り込み

一方で、FSx for Windows File Serverのファイルアクセス監査ログの形式は、前回のブログで紹介した通り、以下のようなXML形式です。

<Event xmlns='http://schemas.microsoft.com/win/2004/08/events/event'>
	<System>
		<Provider Name='Microsoft-Windows-Security-Auditing' Guid='{54849625-5478-4994-A5BA-3E3B0328C30D}'/>
		<EventID>4659</EventID>
		<Version>0</Version>
		<Level>0</Level>
		<Task>12800</Task>
		<Opcode>0</Opcode>
		<Keywords>0x8020000000000000</Keywords>
		<TimeCreated SystemTime='2021-06-09T01:58:00.304083200Z'/>
		<EventRecordID>287180</EventRecordID>
		<Correlation/>
		<Execution ProcessID='4' ThreadID='1720'/>
		<Channel>Security</Channel>
		<Computer>amznfsxa67gtfky.corp.non-97.net</Computer>
		<Security/>
	</System>
	<EventData>
		<Data Name='SubjectUserSid'>S-1-5-21-2080479861-2474753211-478539028-1113</Data>
		<Data Name='SubjectUserName'>Admin</Data>
		<Data Name='SubjectDomainName'>corp</Data>
		<Data Name='SubjectLogonId'>0xa6bf97</Data>
		<Data Name='ObjectServer'>Security</Data>
		<Data Name='ObjectType'>File</Data>
		<Data Name='ObjectName'>\Device\HarddiskVolume13\share\test-folder\test-file.txt</Data>
		<Data Name='HandleId'>0x0</Data>
		<Data Name='TransactionId'>{00000000-0000-0000-0000-000000000000}</Data>
		<Data Name='AccessList'>%%1537 %%4423
		</Data>
		<Data Name='AccessMask'>0x10080</Data>
		<Data Name='PrivilegeList'>-</Data>
		<Data Name='ProcessId'>0x4</Data>
	</EventData>
</Event>

XML形式はSIEM on Amazon ES に取り込むことができるファイルフォーマットに含まれていないので、S3バケットに保存する前に、テキスト形式、JSON形式、CSV形式など別のファイルフォーマットに変換してあげる必要があります。

今回は、元々のXMLが複雑なデータ構造なので、配列とオブジェクトを使えるJSON形式に変換します

検証環境

検証を行う環境は以下の通りです。

FSx for Windows File Serverのファイルアクセス監査ログだけダッシュボードに表示させても少し寂しいので、CloudTrailとVPC Flow Logsも分析対象となるように構成します。

分析対象のログは単一のS3バケットに集約して、es-loaderと呼ばれるLambda関数でETL処理してから、Elasticsearch Serviceに保存します。

SIEM on Amazon ESのデプロイ

AWS CDKの構成

SIEM on Amazon ESのデプロイ方法は、CloudFormationを利用するパターンと、AWS CDKを利用するパターンと2つあります。

こちらのブログでは、CloudFormationでデプロイをしていたので、今回はAWS CDKを使ってデプロイをしていこうと思います。

SIEM on Amazon ESをデプロイして作られるリソースは、以下の図の赤い破線で囲った箇所になります。

なお、本来はSQSキューやSNSトピック、KMSキーなども作成されますが、図をシンプルにするために省略しています。

デプロイ準備

まず、デプロイする前の準備として、GeoLite2に登録してアクセスキーを取得します。

GeoLite2はフリーでGeoIPのデータベースを提供しているサービスです。 

今回の用途としては、CloudTrailや、VPC Flow Logsの送信元IPアドレスがどこの国のIPアドレスなのかを判断するために使います。

GeoLite2の登録及び、アクセスキー取得の詳細な手順については、以下ブログをご参照ください。

デプロイ

それでは、SIEM on Amazon ESをAWS CDKでデプロイします。

デプロイする際は、こちらの公式ドキュメントに従いデプロイします。

1. ソースコードのクローン

ソースコードはGitHub上で管理されているので、git cloneでソースコードをクローンします。

> git clone https://github.com/aws-samples/siem-on-amazon-elasticsearch.git

Python 3.8やgit、jqなどがインストールされていない環境の場合は、こちらの公式ドキュメントに従ってインストールします。

2. 環境変数の設定

AWS CDKを実行するにあたって必要な環境変数を確認、設定します。

# 環境変数の確認
> echo $CDK_DEFAULT_ACCOUNT
> echo $AWS_DEFAULT_REGION

# 環境変数に意図した値が設定されていなかった場合は、環境変数の設定
> export CDK_DEFAULT_ACCOUNT=<AWS_ACCOUNT>
> export AWS_DEFAULT_REGION=<AWS_REGION>

3. AWS Lambda デプロイパッケージの作成

S3バケットに収集されたログをSIEM on Amazon ESに取り込む際に、Lambda関数が使用されます。

そのLambda関数が使用するライブラリをローカルにダウンロードをして、Lambda関数のデプロイ用のパッケージを作成します。

まず、デプロイパッケージを作成するスクリプトを確認します。

> cd siem-on-amazon-elasticsearch/deployment/cdk-solution-helper/
> ls
step1-build-lambda-pkg.sh   step2-setup-cdk-env.sh

step1-build-lambda-pkg.shと、step2-setup-cdk-env.shがあることが確認できます。

公式ドキュメントを確認すると、step1-build-lambda-pkg.shを実行する様です。スクリプトを実行する前に、一旦スクリプトの中身を確認してみます。

./siem-on-amazon-elasticsearch/deployment/cdk-solution-helper/step1-build-lambda-pkg.sh

#!/bin/bash
# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
# SPDX-License-Identifier: MIT-0

source_template_dir="${PWD}/.."
source_dir="$source_template_dir/../source"

if !(type pip3 > /dev/null 2>&1); then
    echo "No pip3. Install python3."
    echo "exist!"
    exit
fi

echo "------------------------------------------------------------------------------"
echo "[Packing] pip and Source Folder"
echo "------------------------------------------------------------------------------"
python3 -m pip install pip==20.3.3 --user
function pip_zip_for_lambda () {
    if [ -e $1.zip ]; then
      echo "rm $1.zip"
      rm $1.zip
    fi
    cd $1
    mv README.md README.md.org
    echo "# delete old pip version"
    for dir in `ls -v -r -d *.dist-info 2>/dev/null`; do
        echo "rm -r" $(echo "${dir}" | sed -e 's/-.*.dist-info/*/')
        rm -r $(echo "${dir}" | sed -e 's/-.*.dist-info/*/')
    done
    if [ -e requirements.txt ]; then
        python3 -m pip install -t . -r requirements.txt -U
    fi
    find . -name __pycache__ | xargs rm -fr
    rm -f .DS_Store
    #rm -fr *dist-info
    echo "# delete python libraries which are already installed in lambda environment"
    echo "rm -fr boto* aiohttp* future* urllib3* dateutil* python_dateutil* s3transfer* six*"
    rm -fr boto* aiohttp* future* urllib3* dateutil* python_dateutil* s3transfer* six*
    if [ -d requests_aws4auth ]; then
        mv LICENSE README.md HISTORY.md requests_aws4auth-*-info/
    fi
    mv -f README.md.org README.md
    echo "cp -f $source_template_dir/../LICENSE $source_template_dir/../CODE_OF_CONDUCT.md $source_template_dir/../CONTRIBUTING.md ${source_dir}/lambda/$1/"
    cp -f $source_template_dir/../LICENSE $source_template_dir/../CODE_OF_CONDUCT.md $source_template_dir/../CONTRIBUTING.md ${source_dir}/lambda/$1/
    echo "zip -r -9 ../$1.zip *"
    zip -r -9 ../$1.zip * > /dev/null
    echo "rm ${source_dir}/lambda/$1/LICENSE ${source_dir}/lambda/$1/CODE_OF_CONDUCT.md ${source_dir}/lambda/$1/CONTRIBUTING.md"
    rm ${source_dir}/lambda/$1/LICENSE ${source_dir}/lambda/$1/CODE_OF_CONDUCT.md ${source_dir}/lambda/$1/CONTRIBUTING.md
    cd ..
}

cd ${source_dir}/lambda

echo 'rm -f deploy_es/dashboard.ndjson.zip'
rm -f deploy_es/dashboard.ndjson.zip
echo 'zip deploy_es/dashboard.ndjson.zip -jD ../saved_objects/dashboard.ndjson'
zip deploy_es/dashboard.ndjson.zip -jD ../saved_objects/dashboard.ndjson

echo "# start packing es_loader"
pip_zip_for_lambda "es_loader"
echo "# start packing deploy_es"
pip_zip_for_lambda "deploy_es"
echo "# start packing geoip_downloader"
pip_zip_for_lambda "geoip_downloader"

17行目にpython3 -m pip install pip==20.3.3 --userがありますね。私の環境はpipをインストール済みなので、コメントアウトしてから実行します。

実行時のログは以下の通りです。./source/lambda/配下に各Lambda関数用のデプロイパッケージ(zipファイル)が作成されていることが確認できます。

# デプロイパッケージの作成前確認
> cd ../../source/lambda/
> ls
deploy_es            es_loader            geoip_downloader

# スクリプトのバックアップを作成
> cd ../../deployment/cdk-solution-helper/
> cp -a step1-build-lambda-pkg.sh step1-build-lambda-pkg.sh.(date +"%Y%m%d")

# スクリプトの中身を書き換え
> vi step1-build-lambda-pkg.sh

# 変更前と変更後の差分情報の確認
> diff -u step1-build-lambda-pkg.sh.20210615 step1-build-lambda-pkg.sh  
--- step1-build-lambda-pkg.sh.20210615	2021-06-15 13:13:34.000000000 +0900
+++ step1-build-lambda-pkg.sh	2021-06-15 13:15:52.000000000 +0900
@@ -14,7 +14,7 @@
 echo "------------------------------------------------------------------------------"
 echo "[Packing] pip and Source Folder"
 echo "------------------------------------------------------------------------------"
-python3 -m pip install pip==20.3.3 --user
+# python3 -m pip install pip==20.3.3 --user
 function pip_zip_for_lambda () {
     if [ -e $1.zip ]; then
       echo "rm $1.zip"
       
# スクリプトを実行して、デプロイパッケージを作成
> ./step1-build-lambda-pkg.sh

# デプロイパッケージの作成確認
> cd ../../source/lambda/
> ls
deploy_es            es_loader            geoip_downloader
deploy_es.zip        es_loader.zip        geoip_downloader.zip

4. AWS CDKのセットアップ

次にAWS CDKのセットアップをします。

公式ドキュメントを確認すると、./step2-setup-cdk-env.shを実行する様です。一旦スクリプトの中身を確認してみます。

./siem-on-amazon-elasticsearch/deployment/step2-setup-cdk-env.sh

#!/bin/bash
# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
# SPDX-License-Identifier: MIT-0

source_template_dir="$PWD/../"
source_dir="$source_template_dir/../source"

is_ami2=$(cat /etc/system-release 2> /dev/null | grep -oi Karoo)
if [ -z $is_ami2 ]; then
    echo "Not AMI2."
    read -p "Do you realy continue? (y/N): " yn
    case "$yn" in [yY]*) ;; *) echo "abort." ; exit ;; esac
fi

if !(type pip3 > /dev/null 2>&1); then
    echo "No pip3. Install python3."
    echo "exist!"
    exit
fi

echo "python3 -m pip install boto3 --user"
python3 -m pip install boto3 --user

echo "Install Node.js"
curl -s -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.38.0/install.sh | bash
. ~/.nvm/nvm.sh
nvm install --lts node
nvm alias default lts/*
node -e "console.log('Running Node.js ' + process.version)"
nvm use lts/*
echo "Install CDK"
npm install -g aws-cdk

cd ${source_dir}/cdk
python3 -m venv .env
source .env/bin/activate
python3 -m pip install -r requirements.txt

BACK=$RANDOM
if [ -e cdk.json ]; then
    mv cdk.json cdk.json.$BACK
fi

cp cdk.json.public.sample cdk.json
cdk synth aes-siem -o ${source_template_dir}/cdk-solution-helper/cdk.out 1>/dev/null

if [ -e cdk.json.$BACK ]; then
    mv -f cdk.json.$BACK cdk.json
fi

./step2-setup-cdk-env.shを実行すると、以下のソフトウェアがインストールされます。なお、AWS CDKやBoto3などはグローバルでインストールされるようです。

  • Node Version Manager (nvm)
  • Node.js
  • AWS SDK for Python (Boto3)
  • AWS CDK

私はグローバルへのインストールはなるべく避けたい派なので、必要なソフトウェアを手動でインストールしていきます。

# AWS CDKのインストール
> cd siem-on-amazon-elasticsearch/source/cdk
> npm install aws-cdk

# venvで仮想環境を作成
> python3 -m venv .env

# 仮想環境をアクティベート
# 私の環境ではfishを使用しているので、source .env/bin/activate.fish を実行
# 以降のSIEM on Amazon ES関係の操作は全て仮想環境上で行う
> source .env/bin/activate.fish

# 仮想環境に必要なパッケージをインストール
> python3 -m pip install -r requirements.txt
> python3 -m pip install boto3

5. SIEM on Amazon ESをパブリックアクセス (Amazon VPC 外)にデプロイ

以下コマンドで、SIEM on Amazon ESをデプロイします。

# SIEM on Amazon ES をパブリックアクセス環境にデプロイする場合のcdk.jsonを作成
> cp -a cdk.json.public.sample cdk.json

# AllowedSourceIpAddresses: Amazon VPC 外に SIEM on Amazon ES をデプロイした時に、アクセスを許可するIPアドレス。複数アドレスはスペース区切り
# GeoLite2LicenseKey: Maxmindのライセンスキー。IP アドレスに国情報を付与
> npx cdk deploy \                                                                                                                       
      --parameters AllowedSourceIpAddresses="10.0.0.0/8 172.16.0.0/12 192.168.0.0/16 <自端末のIPアドレス>" \
      --parameters GeoLite2LicenseKey=<事前に取得したGeoLite2のライセンスキー>

(中略)

 ✅  aes-siem

Outputs:
aes-siem.KibanaAdmin = aesadmin
aes-siem.KibanaPassword = 4@MqyiXy
aes-siem.KibanaUrl = https://search-aes-siem-xxxxxxxxxxxx.us-east-1.es.amazonaws.com/_plugin/kibana/
aes-siem.RoleDeploy = arn:aws:iam::<AWSアカウントID>:role/aes-siem-deploy-role-for-lambda

Stack ARN:
arn:aws:cloudformation:us-east-1:<AWSアカウントID>:stack/aes-siem/89e941d0-ce46-11eb-ab44-0abfd97085ad

20〜30分ほどでデプロイは完了し、KibanaのURLやID、パスワードが発行されます。

デプロイが完了したら、Elasticsearch Serviceのコンソールを確認してみます。以下のように、ドメインが作成されていることが確認できます。

FSx for Windows File Serverのファイルアクセス監査ログの取り込み設定

2021年6月20日時点で、SIEM on Amazon ES は以下のログを取り込むことができます。

AWS Service Log
セキュリティ、ID、およびコンプライアンス AWS Security Hub Security Hub findings
GuardDuty findings
Amazon Macie findings
Amazon Inspector findings
AWS IAM Access Analyzer findings
セキュリティ、ID、およびコンプライアンス AWS WAF AWS WAF Web ACL traffic information
AWS WAF Classic Web ACL traffic information
セキュリティ、ID、およびコンプライアンス Amazon GuardDuty GuardDuty findings
セキュリティ、ID、およびコンプライアンス AWS Network Firewall Flow logs
Alert logs
管理とガバナンス AWS CloudTrail CloudTrail Log Event
ネットワーキングとコンテンツ配信 Amazon CloudFront Standard access log
Real-time log
ネットワーキングとコンテンツ配信 Amazon Route 53 Resolver VPC DNS query log
ネットワーキングとコンテンツ配信 Amazon Virtual Private Cloud (Amazon VPC) VPC Flow Logs (Version5)
ネットワーキングとコンテンツ配信 Elastic Load Balancing Application Load Balancer access logs
Network Load Balancer access logs
Classic Load Balancer access logs
ストレージ Amazon Simple Storage Service (Amazon S3) access log
データベース Amazon Relational Database Service (Amazon RDS)
(Experimental Support)
Amazon Aurora(MySQL)
Amazon Aurora(PostgreSQL)
Amazon RDS for MariaDB
Amazon RDS for MySQL
Amazon RDS for PostgreSQL
分析 Amazon Managed Streaming for Apache Kafka (Amazon MSK) Broker log
コンピューティング Linux OS
via CloudWatch Logs
/var/log/messages
/var/log/secure
コンテナ Amazon Elastic Container Service (Amazon ECS)
via FireLens
Framework only

抜粋: SIEM on Amazon Elasticsearch Service - 対応ログ

現時点では、FSx for Windows File Serverのファイルアクセス監査ログは対応していないようですね。そのため、ログの取り込みのカスタマイズを行う必要があります。

ログの取り込みのカスタマイズは、以下公式ドキュメントによると、user.iniを作成してLambdaレイヤーでes-loaderというLambda関数に追加すれば良いようです。

SIEM on Amazon ES へのログの取り込みをカスタマイズできます。S3 バケットにエクスポートされたログを、Lambda 関数の es-loader が正規化して SIEM on Amazon ES にロードしています。デプロイされた Lambda 関数名は aes-siem-es-loader となります。この Lambda 関数 es-loader は、S3 バケットから「すべてのオブジェクト作成イベント」のイベント通知を受け取って、起動します。S3 バケットに保存されたファイル名やファイルパスから、ログの種類を特定して、ログ種類毎に定義された方法でフィールドを抽出し、Elastic Common Schema へマッピングをして、最後に SIEM on Amazon ES へインデックス名を指定してロードします。

このプロセスは設定ファイル (aws.ini) に定義された初期値に基づいています。任意の値に変えることもできます。例えば、あるログの S3 バケットへのエクスポートは初期値とは違うファイルパスにしたり、インデックス名の変更をしたり、インデックスのローテーション間隔を変更する場合等です。変更は、aws.ini を参考に user.ini を作成して項目と値を定義します。user.ini に設定した値は aws.ini よりも優先度が高く設定されており、初期値の値を内部で上書きします。

user.ini の保存は、Lambda レイヤーによる追加(推奨)か、AWS マネジメントコンソールから直接編集をしてください。SIEM on Amazon ES をアップデートすると、Lambda 関数が新しい関数に入れ替わります。Lambda レイヤーであれば独立しているので user.ini は維持されますが、AWS マネジメントコンソールから直接編集した user.ini は削除されるので、再度 user.ini を作成する必要があります。

SIEM on Amazon ES の設定変更 - ログ取り込み方法のカスタマイズ

FSx for Windows File Serverのファイルアクセス監査ログを取り込むにあたって作成したuser.iniは以下の通りです。

# Import audit logs for FSx for Windows File Server
[fsx-windows-audit-log]

# Elaticseatch Serviceにアップロードした際のインデックス名
index_name = log-aws-fsx-windows-audit

# S3バケットから取り込み対象のオブジェクトを判断するための文字列
# FSx for Windows File Serverのファイルアクセス監査ログは、"AWSLogs/<アカウントID>/fsx-windows-audit-log/<リージョン名>/"配下に出力するため、"fsx-windows-audit-log"
s3_key = fsx-windows-audit-log

# s3_keyにマッチしても無視したいオブジェクトを判断するための文字列
# Kinesis Data FirehoseがFSx for Windows File Serverのファイルアクセス監査ログを正常にJSON形式にフォーマットできない場合は、"AWSLogs/<アカウントID>/fsx-windows-audit-log/processing-failed/<リージョン名>/"配下に出力するため、"fsx-windows-audit-log/processing-failed"
s3_key_ignored = fsx-windows-audit-log/processing-failed

# ログフォーマット。text, json, csv, multiline を指定可能
# 今回はJSON形式なので、json
file_format = json

# 正規化して使う ECS(Elastic Common Schema )フィールドをスペース区切りで列挙
# 次に正規化として使うECSフィールドを key、オリジナルフィールドを value に入力
ecs = event.system.eventid
    event.system.version
    event.system.level
    event.system.task
    event.system.opcode
    event.system.keywords
    event.system.timecreated.systemtime
    event.system.eventrecordid
    event.system.correlation
    event.system.channel
    event.system.computer
    event.system.security
event.system.eventid = Event.System.0.EventID
event.system.version = Event.System.0.Version
event.system.level = Event.System.0.Level
event.system.task = Event.System.0.Task
event.system.opcode = Event.System.0.Opcode
event.system.keywords = Event.System.0.Keywords
event.system.timecreated.systemtime = Event.System.0.TimeCreated.0.$$.SystemTime
event.system.eventrecordid = Event.System.0.EventRecordID
event.system.correlation = Event.System.0.Correlation
event.system.channel = Event.System.0.Channel
event.system.computer = Event.System.0.Computer
event.system.security = Event.System.0.Security


# @timestamp に代入する生ログのオリジナルフィールド名
timestamp_key = Event.System.0.TimeCreated.0.$$.SystemTime

# timestamp と指定されたフィールドのタイムフォーマットを指定
# 代入できるのは、epoch, syslog, iso8601
# 該当がなければ Python の Datetime フォーマットで設定
timestamp_format = %Y-%m-%dT%H:%M:%S.%f%z

# nano秒が含まれていれば、切り捨ててmicro秒に変換
# Windowsのイベントログはnano秒まで含まれているため、切り捨ててmicro秒に変換する必要がある
timestamp_nano = True

# 定数を入力したい ECS フィールドをスペース区切りで列挙
# 次に ECS フィールドを key、定数を value に入力
# Windowsの監査ログであることを絞るために設定
static_ecs = event.kind event.category event.module
event.kind = event
event.category = winodws
event.module = audit

# scriptで処理する ECS フィールドをスペース区切りで列挙
# ドキュメトで自動生成するためと、scriptのモジュールを実行させるために入力
# 実際のロジックはモジュール内で処理
# Event.EventData配下のフィールドはログによって異なるので、スクリプトで関連付ける
script_ecs = event.eventdata

ここで、user.iniの71行目のscript_ecsについて補足します。

FSx for Windows File Serverのファイルアクセス監査ログをXML形式から、JSON形式に変換すると以下のようになります。

{
  "Event": {
    "$": {
      "xmlns": "http://schemas.microsoft.com/win/2004/08/events/event"
    },
    "System": [{
      "Provider": [{
        "$": {
          "Name": "Microsoft-Windows-Security-Auditing",
          "Guid": "{54849625-5478-4994-A5BA-3E3B0328C30D}"
        }
      }],
      "EventID": ["4663"],
      "Version": ["1"],
      "Level": ["0"],
      "Task": ["12800"],
      "Opcode": ["0"],
      "Keywords": ["0x8020000000000000"],
      "TimeCreated": [{
        "$": {
          "SystemTime": "2021-06-16T10:00:15.351449700Z"
        }
      }],
      "EventRecordID": ["290972"],
      "Correlation": [""],
      "Execution": [{
        "$": {
          "ProcessID": "4",
          "ThreadID": "348"
        }
      }],
      "Channel": ["Security"],
      "Computer": ["amznfsx8ye6jaez.corp.non-97.net"],
      "Security": [""]
    }],
    "EventData": [{
      "Data": [{
        "_": "S-1-5-21-2210980199-3792033766-1836880630-1113",
        "$": {
          "Name": "SubjectUserSid"
        }
      }, {
        "_": "Admin",
        "$": {
          "Name": "SubjectUserName"
        }
      }, {
        "_": "corp",
        "$": {
          "Name": "SubjectDomainName"
        }
      }, {
        "_": "0x82b651",
        "$": {
          "Name": "SubjectLogonId"
        }
      }, {
        "_": "Security",
        "$": {
          "Name": "ObjectServer"
        }
      }, {
        "_": "File",
        "$": {
          "Name": "ObjectType"
        }
      }, {
        "_": "\\Device\\HarddiskVolume13\\share\\share-folder\\test - コピー - コピー",
        "$": {
          "Name": "ObjectName"
        }
      }, {
        "_": "0x132c",
        "$": {
          "Name": "HandleId"
        }
      }, {
        "_": "%%4423",
        "$": {
          "Name": "AccessList"
        }
      }, {
        "_": "0x80",
        "$": {
          "Name": "AccessMask"
        }
      }, {
        "_": "0x4",
        "$": {
          "Name": "ProcessId"
        }
      }, {
        "$": {
          "Name": "ProcessName"
        }
      }, {
        "_": "S:AI",
        "$": {
          "Name": "ResourceAttributes"
        }
      }]
    }]
  }
}
{
  "Event": {
    "$": {
      "xmlns": "http://schemas.microsoft.com/win/2004/08/events/event"
    },
    "System": [{
      "Provider": [{
        "$": {
          "Name": "Microsoft-Windows-Security-Auditing",
          "Guid": "{54849625-5478-4994-A5BA-3E3B0328C30D}"
        }
      }],
      "EventID": ["4670"],
      "Version": ["0"],
      "Level": ["0"],
      "Task": ["13570"],
      "Opcode": ["0"],
      "Keywords": ["0x8020000000000000"],
      "TimeCreated": [{
        "$": {
          "SystemTime": "2021-06-16T10:00:38.976150700Z"
        }
      }],
      "EventRecordID": ["291000"],
      "Correlation": [""],
      "Execution": [{
        "$": {
          "ProcessID": "4",
          "ThreadID": "344"
        }
      }],
      "Channel": ["Security"],
      "Computer": ["amznfsx8ye6jaez.corp.non-97.net"],
      "Security": [""]
    }],
    "EventData": [{
      "Data": [{
        "_": "S-1-5-21-2210980199-3792033766-1836880630-1113",
        "$": {
          "Name": "SubjectUserSid"
        }
      }, {
        "_": "Admin",
        "$": {
          "Name": "SubjectUserName"
        }
      }, {
        "_": "corp",
        "$": {
          "Name": "SubjectDomainName"
        }
      }, {
        "_": "0x82b651",
        "$": {
          "Name": "SubjectLogonId"
        }
      }, {
        "_": "Security",
        "$": {
          "Name": "ObjectServer"
        }
      }, {
        "_": "File",
        "$": {
          "Name": "ObjectType"
        }
      }, {
        "_": "\\Device\\HarddiskVolume13\\share\\share-folder\\test - コピー - コピー (2)\\test\\test-text - コピー (3) - コピー - ショートカット.lnk",
        "$": {
          "Name": "ObjectName"
        }
      }, {
        "_": "0x8c8",
        "$": {
          "Name": "HandleId"
        }
      }, {
        "_": "D:AI(A;ID;0x1301bf;;;AU)(A;ID;FA;;;SY)(A;ID;FA;;;S-1-5-21-2210980199-3792033766-1836880630-1119)",
        "$": {
          "Name": "OldSd"
        }
      }, {
        "_": "D:AI(A;;FA;;;S-1-5-21-2210980199-3792033766-1836880630-1113)(A;;FA;;;SY)(A;;0x1200a9;;;S-1-5-5-0-598823)",
        "$": {
          "Name": "NewSd"
        }
      }, {
        "_": "0x4",
        "$": {
          "Name": "ProcessId"
        }
      }, {
        "$": {
          "Name": "ProcessName"
        }
      }]
    }]
  }
}

104行目までの最初のイベントには、104行目以降のイベントにはある"Name": "OldSd"や、"Name": "NewSd"といったオブジェクトがないことが確認できると思います。
このようにファイルアクセス監査ログはイベントによって、Event.EventData配下に存在するキーは異なります。

このようなイベントによって動的に変化するレコードについては、script_ecsを使用します。

script_ecsに指定したフィールドは、Pythonで記述されたスクリプトによって処理をさせた値を関連付けることができます。例えば、|や、スペースで区切られているメッセージから、情報を抽出するなどです。

今回作成したスクリプトは以下の通りです。フィールドがevent.eventdata.<Event.EventData配下に存在するキー名>の形式になるように処理をします。

./siem/sf_fsx_windows_audit_log.py

def transform(logdata):

    if not 'eventdata' in logdata['event']:
        logdata['event']['eventdata'] = {}

    eventData = logdata['Event']['EventData'][0]['Data']

    for data in eventData:
        index = data['$']['Name'].lower()
        value = repr(data[next(iter(data))])

        logdata['event']['eventdata'][index] = value

    return logdata

logdataという辞書には、S3バケットから読み取ったログが辞書型で保存されています。

例えば、logdata['Event']['EventData'][0]['Data']は、JSONでいうEvent.EventData.0.Dataを指しています。

スクリプト内ではlogdata['event']['eventdata']という辞書の配下に、Event.EventData.0.Data.$.Nameというキーとそのキーに関連付く値を辞書型で保存していきます。

なお、以下公式ドキュメントに記載がある通り、スクリプトの名前には命名規約があります。

ファイル名は sf_ログの種類.py としてください。この例では、sf_apache.py となります。「ログの種類」に - (ダッシュ)が含まれている場合には、_ (アンダーバー)に置換してください。例) ログの種類: cloudfront-realtime => ファイル名: sf_cloudfront_realtime.py

このファイルを es-loader の siem ディレクトリに保存するか、Lambda レイヤーの siem ディレクトリに保存してください。

SIEM on Amazon ES の設定変更 - AWS サービス以外のログの取り込み

今回、user.iniで定義したログの種類は、fsx-windows-audit-logです。そのため、スクリプトの名前は、sf_fsx_windows_audit_log.pyになります。

Lambdaレイヤーで、作成したuser.iniや、sf_fsx_windows_audit_log.pyは以下のようなディレクトリ構成にしてzipで固めます。

configure-es-loader
├── siem
│   └── sf_fsx_windows_audit_log.py
└── user.ini

なお、今回はディレクトリの名前をconfigure-es-loaderにしましたが、こちらは命名規約はないので任意の名前で問題ありません。

zipで固める際は以下の様なコマンドを実行します。

> zip -r configure-es-loader.zip user.ini siem/
  adding: user.ini (deflated 69%)
  adding: siem/ (stored 0%)
  adding: siem/sf_fsx_windows_audit_log.py (deflated 59%)

それではLambdaレイヤーを作成します。

Lambdaのコンソールからレイヤーを選択して、レイヤーの作成をクリックします。

レイヤーの設定をしていきます。

レイヤーの名前とスクリプトのランタイムの入力をし、先ほど作成したzipファイルをアップロードして、作成をクリックします。

レイヤーが作成完了すると以下のようなメッセージが表示されます。

作成したLambdaレイヤーをLambda関数に関連付けます。

es-loaderのLambda関数を選択して、レイヤーの追加をクリックします。

カスタムレイヤーを選択して、作成したLambdaレイヤーの情報を入力し、追加をクリックします。

Lambdaレイヤーが正しく関連付いていることを確認します。

FSx for Windows File Serverのデプロイ

AWS CDKの構成

FSx for Windows File Serverも例に漏れず、AWS CDKでデプロイします。こちらのAWS CDKでデプロイされるリソースは、以下の図の赤い破線で囲った箇所になります。

なお、検証時点のAWS CDK(ver. 1.108.1)では、FSx for Windows File Serverのファイルアクセス監査ログを設定することはできなかったので、デプロイ後手動で設定をします。

デプロイ

AWS CDKの実行環境のディレクトリの構成は以下の通りです。

> tree
.
├── .gitignore
├── .npmignore
├── README.md
├── bin
│   └── fsx-windows-file-server.ts
├── cdk.json
├── jest.config.js
├── lib
│   └── fsx-windows-file-server-stack.ts
├── package-lock.json
├── package.json
├── src
│   └── lambda
│       └── functions
│           ├── package-lock.json
│           ├── package.json
│           └── xml-convert-json.ts
├── test
│   └── fsx-windows-file-server.test.ts
└── tsconfig.json

6 directories, 18 files

メインで動かすのは./lib/fsx-windows-file-server-stack.tsです。ここで全てのリソースを作成しています。

./lib/fsx-windows-file-server-stack.tsの大まかな処理の流れは以下の通りです。

  • SIEM on Amazon ESで作成したログ収集用S3バケットのARN、名前の取得と、KMSキーのARNの取得
  • CloudTrailの設定
  • XML形式からJSON形式に変換するLambda関数の宣言
  • IAMロールの作成
    • Kinesis Data Firehose用
    • VPC Flow Logs用
    • SSM用
  • Kinesis Data Firehose配信ストリームの作成
  • VPCの作成
  • VPC Flow Logsの有効化
  • Managed Microsoft ADのAdminユーザー用のパスワードの作成
  • Managed Microsoft ADの作成
  • FSx for Windows File Serverの作成
  • Managed Microsoft ADのDNSSサーバー情報を元に、DHCPオプションセットの作成、VPCへの割り当て
  • EC2インスタンスの作成

実際のコードは以下の通りです。

./lib/fsx-windows-file-server-stack.ts

import * as cdk from "@aws-cdk/core";
import * as cloudtrail from "@aws-cdk/aws-cloudtrail";
import * as lambda from "@aws-cdk/aws-lambda";
import * as nodejs from "@aws-cdk/aws-lambda-nodejs";
import * as iam from "@aws-cdk/aws-iam";
import * as kinesisfirehose from "@aws-cdk/aws-kinesisfirehose";
import * as ec2 from "@aws-cdk/aws-ec2";
import * as secretsmanager from "@aws-cdk/aws-secretsmanager";
import * as directoryservice from "@aws-cdk/aws-directoryservice";
import * as fsx from "@aws-cdk/aws-fsx";

export class FsxWindowsFileServerStack extends cdk.Stack {
  constructor(scope: cdk.Construct, id: string, props?: cdk.StackProps) {
    super(scope, id, props);

    // Declare AWS account ID, region and stackName.
    const { accountId, region, stackName } = new cdk.ScopedAws(this);

    // ARN and Name of the S3 bucket where the logs are aggregated
    const logBucketArn = this.node.tryGetContext("logBucketArn");
    const logBucketName = this.node.tryGetContext("logBucketName");

    // KMS key Arn
    const kmsKeyArn = this.node.tryGetContext("kmsKeyArn");

    // Enabled CloudTrail
    new cloudtrail.CfnTrail(this, "CloudTrail", {
      isLogging: true,
      s3BucketName: logBucketName,
      includeGlobalServiceEvents: true,
      isMultiRegionTrail: true,
      kmsKeyId: kmsKeyArn,
    });

    // Lambda Function for notifying Slack of the execution results of Step Functions.
    const fsxWindowsFileServerAuditLogConvertJsonFunction =
      new nodejs.NodejsFunction(
        this,
        "FsxWindowsFileServerAuditLogConvertJsonFunction",
        {
          entry: "src/lambda/functions/xml-convert-json.ts",
          runtime: lambda.Runtime.NODEJS_14_X,
          bundling: {
            minify: true,
          },
          timeout: cdk.Duration.seconds(180),
        }
      );

    // Create VPC Flow Logs IAM role
    const flowLogsIamRole = new iam.Role(this, "FlowLogsIamRole", {
      assumedBy: new iam.ServicePrincipal("vpc-flow-logs.amazonaws.com"),
    });

    // Create Delivery Stream IAM role
    const fsxWindowsFileServerAuditLogDeliveryStreamIamRole = new iam.Role(
      this,
      "FsxWindowsFileServerAuditLogDeliveryStreamIamRole",
      {
        assumedBy: new iam.ServicePrincipal("firehose.amazonaws.com"),
      }
    );

    // Create SSM IAM role
    const ssmIamRole = new iam.Role(this, "SsmIamRole", {
      assumedBy: new iam.ServicePrincipal("ec2.amazonaws.com"),
      managedPolicies: [
        iam.ManagedPolicy.fromAwsManagedPolicyName(
          "AmazonSSMManagedInstanceCore"
        ),
      ],
    });

    // Create VPC Flow Logs IAM Policy
    const flowLogsIamPolicy = new iam.Policy(this, "FlowLogsIamPolicy", {
      statements: [
        new iam.PolicyStatement({
          effect: iam.Effect.ALLOW,
          actions: ["iam:PassRole"],
          resources: [flowLogsIamRole.roleArn],
        }),
        new iam.PolicyStatement({
          effect: iam.Effect.ALLOW,
          actions: [
            "logs:CreateLogStream",
            "logs:PutLogEvents",
            "logs:DescribeLogStreams",
          ],
          resources: [logBucketArn],
        }),
      ],
    });

    // Atach VPC Flow Logs IAM Policy
    flowLogsIamRole.attachInlinePolicy(flowLogsIamPolicy);

    // Create Delivery Stream IAM Policy
    const fsxWindowsFileServerAuditLogDeliveryStreamIamPolicy = new iam.Policy(
      this,
      "FsxWindowsFileServerAuditLogDeliveryStreamIamPolicy",
      {
        statements: [
          new iam.PolicyStatement({
            effect: iam.Effect.ALLOW,
            actions: ["iam:PassRole"],
            resources: [
              fsxWindowsFileServerAuditLogDeliveryStreamIamRole.roleArn,
            ],
          }),
          new iam.PolicyStatement({
            effect: iam.Effect.ALLOW,
            actions: [
              "glue:GetTable",
              "glue:GetTableVersion",
              "glue:GetTableVersions",
            ],
            resources: [
              `arn:aws:glue:${region}:${accountId}:catalog`,
              `arn:aws:glue:${region}:${accountId}:database/%FIREHOSE_POLICY_TEMPLATE_PLACEHOLDER%`,
              `arn:aws:glue:${region}:${accountId}:table/%FIREHOSE_POLICY_TEMPLATE_PLACEHOLDER%/%FIREHOSE_POLICY_TEMPLATE_PLACEHOLDER%`,
            ],
          }),
          new iam.PolicyStatement({
            effect: iam.Effect.ALLOW,
            actions: [
              "s3:AbortMultipartUpload",
              "s3:GetBucketLocation",
              "s3:GetObject",
              "s3:ListBucket",
              "s3:ListBucketMultipartUploads",
              "s3:PutObject",
            ],
            resources: [logBucketArn, `${logBucketArn}/*`],
          }),
          new iam.PolicyStatement({
            effect: iam.Effect.ALLOW,
            actions: [
              "lambda:InvokeFunction",
              "lambda:GetFunctionConfiguration",
            ],
            resources: [
              fsxWindowsFileServerAuditLogConvertJsonFunction.functionArn,
            ],
          }),
          new iam.PolicyStatement({
            effect: iam.Effect.ALLOW,
            actions: ["kms:GenerateDataKey", "kms:Decrypt"],
            resources: [kmsKeyArn],
            conditions: {
              StringEquals: {
                "kms:ViaService": `s3.${region}.amazonaws.com`,
              },
              StringLike: {
                "kms:EncryptionContext:aws:s3:arn": [`${logBucketArn}/*`],
              },
            },
          }),
          new iam.PolicyStatement({
            effect: iam.Effect.ALLOW,
            actions: ["logs:PutLogEvents"],
            resources: [
              `arn:aws:logs:${region}:${accountId}:log-group:/aws/kinesisfirehose/*:log-stream:*`,
            ],
          }),
          new iam.PolicyStatement({
            effect: iam.Effect.ALLOW,
            actions: [
              "kinesis:DescribeStream",
              "kinesis:GetShardIterator",
              "kinesis:GetRecords",
              "kinesis:ListShards",
            ],
            resources: [
              `arn:aws:kinesis:${region}:${accountId}:stream/%FIREHOSE_POLICY_TEMPLATE_PLACEHOLDER%`,
            ],
          }),
          new iam.PolicyStatement({
            effect: iam.Effect.ALLOW,
            actions: ["kms:Decrypt"],
            resources: [
              `arn:aws:kms:${region}:${accountId}:key/%FIREHOSE_POLICY_TEMPLATE_PLACEHOLDER%`,
            ],
            conditions: {
              StringEquals: {
                "kms:ViaService": `kinesis.${region}.amazonaws.com`,
              },
              StringLike: {
                "kms:EncryptionContext:aws:kinesis:arn": [
                  `arn:aws:kinesis:${region}:${accountId}:stream/%FIREHOSE_POLICY_TEMPLATE_PLACEHOLDER%`,
                ],
              },
            },
          }),
        ],
      }
    );

    // Atach VPC Flow Logs IAM Policy
    fsxWindowsFileServerAuditLogDeliveryStreamIamRole.attachInlinePolicy(
      fsxWindowsFileServerAuditLogDeliveryStreamIamPolicy
    );

    // Create Delivery Stream for FSx for Windows File Server audit-log to S3 Bucket
    const fsxWindowsFileServerAuditLogDeliveryStream =
      new kinesisfirehose.CfnDeliveryStream(
        this,
        "FsxWindowsFileServerAuditLogDeliveryStream",
        {
          deliveryStreamName: `aws-fsx-windows-audit-log-${stackName}`,
          deliveryStreamType: "DirectPut",
          extendedS3DestinationConfiguration: {
            bucketArn: logBucketArn,
            roleArn: fsxWindowsFileServerAuditLogDeliveryStreamIamRole.roleArn,
            compressionFormat: "GZIP",
            prefix: `AWSLogs/${accountId}/fsx-windows-audit-log/${region}/`,
            processingConfiguration: {
              enabled: true,
              processors: [
                {
                  type: "Lambda",
                  parameters: [
                    {
                      parameterName: "LambdaArn",
                      parameterValue:
                        fsxWindowsFileServerAuditLogConvertJsonFunction.functionArn,
                    },
                  ],
                },
              ],
            },
          },
        }
      );

    // Create VPC
    const vpc = new ec2.Vpc(this, "Vpc", {
      cidr: "10.0.0.0/16",
      enableDnsHostnames: true,
      enableDnsSupport: true,
      natGateways: 2,
      maxAzs: 2,
      subnetConfiguration: [
        { name: "Public", subnetType: ec2.SubnetType.PUBLIC, cidrMask: 24 },
        { name: "Private", subnetType: ec2.SubnetType.PRIVATE, cidrMask: 24 },
      ],
    });

    // Setting VPC Flow Logs
    new ec2.CfnFlowLog(this, "FlowLogToLogs", {
      resourceId: vpc.vpcId,
      resourceType: "VPC",
      trafficType: "ALL",
      deliverLogsPermissionArn: flowLogsIamRole.roleArn,
      logDestination: logBucketArn,
      logDestinationType: "s3",
      logFormat:
        "${version} ${account-id} ${interface-id} ${srcaddr} ${dstaddr} ${srcport} ${dstport} ${protocol} ${packets} ${bytes} ${start} ${end} ${action} ${log-status} ${vpc-id} ${subnet-id} ${instance-id} ${tcp-flags} ${type} ${pkt-srcaddr} ${pkt-dstaddr} ${region} ${az-id} ${sublocation-type} ${sublocation-id} ${pkt-src-aws-service} ${pkt-dst-aws-service} ${flow-direction} ${traffic-path}",
      maxAggregationInterval: 60,
    });

    // Create Security Group
    // Security Group for EC2 Instances
    const ec2InstanceSg = new ec2.SecurityGroup(this, "Ec2InstanceSg", {
      allowAllOutbound: true,
      vpc: vpc,
    });

    // Security Group for FSx for Windows File Server
    const fsxWindowsFileServerSg = new ec2.SecurityGroup(
      this,
      "FsxWindowsFileServerSg",
      {
        allowAllOutbound: true,
        vpc: vpc,
      }
    );
    fsxWindowsFileServerSg.addIngressRule(
      ec2InstanceSg,
      ec2.Port.tcp(445),
      "Allow SMB"
    );
    fsxWindowsFileServerSg.addIngressRule(
      ec2InstanceSg,
      ec2.Port.tcp(5985),
      "Allow WinRM-HTTP"
    );

    // Create MSAD secrets
    const msadSecret = new secretsmanager.Secret(this, "MsadSecret", {
      secretName: `${stackName}/msadSecret`,
      generateSecretString: {
        generateStringKey: "password",
        secretStringTemplate: '{"username": "Admin"}',
        passwordLength: 32,
      },
    });

    // Create MSAD
    const msad = new directoryservice.CfnMicrosoftAD(this, "Msad", {
      name: "corp.non-97.net",
      password: msadSecret.secretValueFromJson("password").toString(),
      vpcSettings: {
        vpcId: vpc.vpcId,
        subnetIds: vpc.selectSubnets({
          subnetGroupName: "Private",
        }).subnetIds,
      },
      edition: "Standard",
    });

    // Create FSx for Windows File Server
    const fsxWindowsFileServer = new fsx.CfnFileSystem(
      this,
      "FsxWindowsFileServer",
      {
        fileSystemType: "WINDOWS",
        subnetIds: vpc.selectSubnets({
          subnetGroupName: "Private",
        }).subnetIds,
        securityGroupIds: [fsxWindowsFileServerSg.securityGroupId],
        storageCapacity: 32,
        storageType: "SSD",
        windowsConfiguration: {
          throughputCapacity: 32,
          activeDirectoryId: msad.ref,
          automaticBackupRetentionDays: 1,
          copyTagsToBackups: true,
          dailyAutomaticBackupStartTime: "10:00",
          deploymentType: "MULTI_AZ_1",
          preferredSubnetId: vpc
            .selectSubnets({
              subnetGroupName: "Private",
              availabilityZones: [vpc.availabilityZones[0]],
            })
            .subnetIds.toString(),
          weeklyMaintenanceStartTime: "7:13:30",
        },
      }
    );

    // Create DHCP Option Set
    const dhcpOption = new ec2.CfnDHCPOptions(this, "DhcpOption", {
      domainName: msad.name,
      domainNameServers: msad.attrDnsIpAddresses,
    });

    // Association of DHCP Option Set to VPC
    new ec2.CfnVPCDHCPOptionsAssociation(this, "DhcpOptionAssociation", {
      dhcpOptionsId: dhcpOption.ref,
      vpcId: vpc.vpcId,
    });

    // Create EC2 Instance
    const ec2Instance = new ec2.Instance(this, `Ec2Instance$`, {
      machineImage: ec2.MachineImage.latestWindows(
        ec2.WindowsVersion.WINDOWS_SERVER_2019_JAPANESE_FULL_BASE
      ),
      instanceType: new ec2.InstanceType("t3.micro"),
      vpc: vpc,
      keyName: this.node.tryGetContext("key-pair"),
      role: ssmIamRole,
      vpcSubnets: vpc.selectSubnets({
        subnetGroupName: "Private",
      }),
      securityGroup: ec2InstanceSg,
    });
  }
}

./lib/fsx-windows-file-server-stack.tsの20-21行目、24行目のSIEM on Amazon ESのAWS CDKで作成されたリソースの情報は、以下のようにcdk.jsonで設定します。

./cdk.json

{
  "app": "npx ts-node --prefer-ts-exts bin/fsx-windows-file-server.ts",
  "context": {
    "key-pair": "<キーペアの名前>",
    "logBucketArn": "<SIEM on Amazon ESのスタックで作成したログ収集用S3バケットのARN>",
    "logBucketName": "<SIEM on Amazon ESのスタックで作成したログ収集用S3バケットの名前>",
    "kmsKeyArn": "<SIEM on Amazon ESのスタックで作成したKMSキーのARN>",
    "@aws-cdk/aws-apigateway:usagePlanKeyOrderInsensitiveId": true,
    "@aws-cdk/core:enableStackNameDuplicates": "true",
    "aws-cdk:enableDiffNoFail": "true",
    "@aws-cdk/core:stackRelativeExports": "true",
    "@aws-cdk/aws-ecr-assets:dockerIgnoreSupport": true,
    "@aws-cdk/aws-secretsmanager:parseOwnedSecretName": true,
    "@aws-cdk/aws-kms:defaultKeyPolicies": true,
    "@aws-cdk/aws-s3:grantWriteWithoutAcl": true,
    "@aws-cdk/aws-ecs-patterns:removeDefaultDesiredCount": true,
    "@aws-cdk/aws-rds:lowercaseDbIdentifier": true,
    "@aws-cdk/aws-efs:defaultEncryptionAtRest": true,
    "@aws-cdk/aws-lambda:recognizeVersionProps": true
  }
}

Kinesis Data Firehoseに送られたログファイルをXML形式からJSON形式に変換するLambda関数は以下の通りです。

改行や水平タブなど不要な情報を削除したあと、xml2jsというパッケージを使用してXML形式から、JSON形式に変換しています。

./src/lambda/functions/xml-convert-json.ts

import { Context, Callback } from "aws-lambda";
import { parseString } from "xml2js";

interface Record {
  recordId: string;
  approximateArrivalTimestamp?: string;
  result?: string;
  data: string;
}

interface OutputRecords {
  records: Record[];
}

// Function to convert xml to json.
const xmlToJson = async (record: Record): Promise<Record> => {
  const payload: string = Buffer.from(record.data, "base64")
    .toString("utf-8")
    .replace(/\r?\n?\t/g, "");

  console.log("Decoded payload:", payload);

  return new Promise((resolve, reject) => {
    parseString(payload, (error, result) => {
      if (error) {
        /* Failed event, notify the error and leave the record intact */
        console.log(error.message);
        console.log("Error message:", error.message);
        console.log(payload);

        reject({
          recordId: record.recordId,
          result: "ProcessingFailed",
          data: record.data,
        });
      } else {
        console.log(JSON.stringify(result));

        resolve({
          recordId: record.recordId,
          result: "Ok",
          data: Buffer.from(JSON.stringify(result) + "\n", "utf8").toString(
            "base64"
          ),
        });
      }
    });
  });
};

// main
exports.handler = async (
  event: any,
  context: Context,
  callback: Callback
): Promise<OutputRecords> => {
  console.log(event);

  /* Process the list of records and transform them */
  const output: Record[] = await Promise.all(
    event.records.map(async (record: Record) => {
      return await xmlToJson(record);
    })
  );

  console.log(`Processing completed.`);
  console.log("output");
  console.log(output);

  return { records: output };
};

あとは、cdk deployを実行することで、各種リソースがデプロイされます。

ファイルアクセス監査ログの設定

FSx for Windows File Serverの設定

先述した通り、現時点ではAWS CDKでFSx for Windows File Serverのファイルアクセス監査ログを設定することはできなかったので、手動で設定をします。

FSxのコンソールを開き、作成されたFSx for Windows File Serverをクリックします。その後、管理タブから、管理をクリックします。

続いて、各ログ記録を有効にし、送信先に作成した配信ストリームを指定して保存をクリックします。

設定が完了すると、各種ログ記録が有効になり、監査イベントログの送信先も作成した配信ストリームになっていることが確認できます。

監査エントリの追加

FSx for Windows File Serverの設定が完了したので、続いて共有フォルダーに対して監査エントリーを追加します。

最初に、Managed Microsoft ADのAdminユーザーのパスワードの確認をします。

パスワードはSecrets Managerで作成したので、Secrets Managerのコンソールを確認します。
対象のシークレットを確認すると、以下のようにパスワードが生成されています。

それでは、SSMセッションマネージャーでポートフォワーディングして、EC2インスタンスにRDP接続をします。

> aws ssm start-session --target  <インスタンスID> --document-name AWS-StartPortForwardingSession --parameters portNumber=3389,localPortNumber=13389 --region <インスタンスをデプロイしたリージョン>

なお、この時点ではEC2インスタンスはドメインに参加していないので、ローカルのAdministratorユーザーでログインします。

ログイン後は、PowerShellを起動し、以下コマンドでドメインに参加します。

> Add-Computer -DomainName corp.non-97.net -Credential Admin -PassThru -Verbose -Restart

上述したコマンドを実行すると、自動でOSが再起動します。OSの再起動完了後、再度RDP接続をします。
この際は、Secrets Managerで確認したパスワードを使って、ドメインのAdminユーザーでログインします。

ドメインのAdminユーザーでログインした後は、以下コマンドで共有フォルダーをZドライブに割り当てます。

> net use Z: \\amznfsxafmmqvqp.corp.non-97.net\share

Zドライブに割り当てた共有フォルダー上で、PowerShellを起動します。
以下コマンドで、共有フォルダ配下にshare-folderというフォルダーを作成して、そのフォルダーに監査エントリーを追加します。

# share-folderの作成
> New-Item share-folder -ItemType Directory
> cd share-folder

# share-folderの監査エントリーの確認
> $path = "Z:\share-folder"
> $ACL = Get-Acl -Path $path
> $ACL | Format-List

# 監査エントリーの追加及び、監査エントリーが正常に追加されているかを確認
> $AuditUser = "Everyone"
> $AuditRules = "FullControl"
> $InheritType = "ContainerInherit,ObjectInherit"
> $AuditType = "Success,Failure"
> $AccessRule = New-Object System.Security.AccessControl.FileSystemAuditRule($AuditUser,$AuditRules,$InheritType,"None",$AuditType)
> $ACL.SetAuditRule($AccessRule)
> Set-Acl -Path $path -AclObject $ACL
> Get-Acl -Path $path -Audit | Format-List

コマンドを実行すると、以下のようにAuditEveryone Success, Failure FullControlという情報があることから、正常に監査エントリ設定されたことが確認できます。

以上でログの設定は完了です。

この後は、イベントを発生させるため、Z:\share-folder配下で、適当にフォルダーやファイルを作成・削除、データの追記などの操作を行いました。

ログの可視化

Kibanaの設定

いよいよログの可視化です。

ログの可視化をするにあたって、ログを可視化するために必要なダッシュボードやインデックスパターンを設定します。

設定はSIEM on Amazon ESが提供しているファイルをインポートして行います。インポートするファイルはこちらからダウンロードします。
ダウンロードしたファイルは解凍しておきます。

続いて、Kibanaにログインします。ログイン先のURLやID、パスワードは、AWS CDK実行時のログに出力されているので、そちらをご確認ください。

サンプルデータではなく、自分で収集したログを使いたいので、Explore on my ownをクリックします。

ユーザー専用のダッシュボード等を用意してカスタマイズしたいので、Privateを選択して、Confirmクリックします。

左メニューからStack Managementをクリックします。

Saved ObjectImportをクリックします。

解凍したフォルダ内のdashboard.ndjsonをアップロードし、Importをクリックします。

インポートが完了したことを確認します。

ダッシュボードやインデックスパターンがインポートされたことが確認できます。

CloudTrailの可視化

それでは、CloudTrailのログを可視化してみます。

Dashboardから、CloudTrail Summaryをクリックします。

すると、以下のようにCloudTrailのダッシュボードが表示され、接続元IPアドレスや、実行されたイベントなど様々な情報を確認することができます。イケてる感じで良いですね。

VPC Flow Logsの可視化

続いて、VPC Flow Logsのログを可視化してみます。

Dashboardから、VPCFlowLogs Summaryをクリックします。

すると、以下のようにVPC Flow Logsのダッシュボードが表示され、どの時間帯にどのような通信が多かったのかなど様々な情報を確認することができます。

FSx for Windows File Serverのファイルアクセス監査ログの可視化

Discoverの表示

最後に、FSx for Windows File Serverのファイルアクセス監査ログを可視化します。

まず、そもそもElasticsearch Serviceにログが取り込まれているかを確認をします。

Discoverをクリックすると、取り込んだログの一覧を出力することができます。

53,540件もありますね。この中から、FSx for Windows File Serverのファイルアクセス監査ログのみ抽出するには、user.iniで定義したevent.categoryを使用します。

FSx for Windows File Serverのファイルアクセス監査ログのevent.categorywindowsと定義しました。この条件で抽出をすると、以下のように、FSx for Windows File Serverのファイルアクセス監査ログのみ抽出することができました。

1つ1つのログは畳まれているので、展開してみます。展開すると以下のように、各フィールドと値が紐づいていることが確認できます。

このままだと、生のログを出しているだけです。より、条件を絞って情報を表示してみます。

以下の例では、イベントIDやアクセスリスト、ユーザー名、操作対象のオブジェクト名のみを抽出してみました。このように表示されると、いつ誰が、何を、どのような操作をしたのか確認しやすいですね。

インデックスパターンの作成

FSx for Windows File Serverのファイルアクセス監査ログもダッシュボードで良い感じに表示させるためには、どのフィールドが、どのようなType(型)なのかを定義する必要があります。そのための作業として、FSx for Windows File Serverのファイルアクセス監査ログのインデックスパターンを定義します。

左メニューから、Stack Managementを選択し、Index PatternsCreate index patternをクリックします。

インデックスパターンの名前を設定します。
FSx for Windows File Serverのファイルアクセス監査ログのインデックスパターン名(log-aws-fsx-windows-audit-*)を入力し、Next Stepをクリックします。

次に、作成するインデックスパターンのtimeフィールドを設定します。
今回は、user.initimestamp_keyを使って、@timestampにイベントの発生時間を渡しているので、@timestampを選択します。選択後、Create index patternで、インデックスパターンを作成します。

すると、以下のようなインデックスパターンが作成されました。フィールド名やTypeは、今まで取り込んだログから自動で判断してくれます。

ダッシュボードの作成

最後にダッシュボードの作成です。

Dashboardから、Create dashboardをクリックします。

空のダッシュボードが作成されます。Create newをクリックして、ダッシュボードにオブジェクトを追加します。

今回は、イベントIDの割合を表示させてみようと思います。割合を表示させるために、Pieを選択します。

続いて、先ほど作成したインデックスパターンlog-aws-fsx-windows-audit-*を選択します。

その後は、対象のフィールドや表示させる数などを設定します。最後に、Updateをクリックすることで、以下のようなイベントID毎の割合を表示するオブジェクトが作成されます。作成後は忘れずに、Saveもクリックしましょう。

ダッシュボードにも先ほど作成したオブジェクトが追加されていることが確認できます。

他にもダッシュボードに色々なオブジェクトを追加してみました。

ダッシュボードを作成すると、イベント発生時刻やイベントID、ユーザー名を絞って、簡単に全体を把握することができます。

ログはダッシュボードで可視化するに限る

FSx for Windows File Serverのファイルアクセス監査ログやCloudTrail、VPC Flow LogsをSIEM on Amazon ESで可視化してみました。

ダッシュボードでログを確認できるようになると、どのような事象が発生しているのかが分かりやすくなることはもちろん、なんだかテンションも上がりますよね。

SIEM on Amazon ESを使えば、非常に簡単にログの分析基盤を作成することができるので、

  • 今までログは取っているけど、イマイチ活用できていない
  • 一つの画面で様々な情報を見比べながら調査をしたい
  • セキュリティインシデント発生時の調査をもっと簡単にしたい

といった要望・課題に対してうってつけのソリューションだと思います。

このブログが誰かの助けになれば幸いです。

以上、東京オフィスの のんピ(@non____97)でした!