📊

AWS WAFのログをCSVで取得する (CloudWatch Logs)

2023/03/25に公開1

はじめに

AWS WAFv2の利用時にCloudWatch Logsにログを送信している場合には、CloudWatch Logs Insightsを利用してクエリを実行し、ログを取得し結果を分析することができます。
本記事では、ログ全体を取得して必要な項目を抽出し、CSVに変換するbashスクリプトの例を紹介しています。

前提知識

ログ取得の推奨方法

ログを見る方法について、基本的にはAWS公式のブログに書かれていますので、まずはこの方法がお薦めです。

https://aws.amazon.com/jp/blogs/news/aws-waf-log-analysis-considerations/

本記事の方法は、 「ログ全体(@message全体)を取得した上で必要な情報を抜き出したい」 といったモチベーションによるもので、後述しますが誤検知の原因調査などに役立てられるのではないかと思っています。

CloudWatch Logs Insights

WAFのログはいくつか出力先を選択でき、最近ではCloudWatch Logsへの出力が可能となっています。そしてCloudWatch Logsのログは、CloudWatch Logs Insightsによる独自の文法でクエリを発行し、データを抽することが可能です。 AWS CLIでクエリを実行しようとした場合には、以下の通り2段階の手順を踏む必要があります。

  1. start-query
  2. get-query-results

文字通りですが、1.でクエリを発行し、2.でJSON形式などでローカルに出力します。

https://docs.aws.amazon.com/ja_jp/AmazonCloudWatch/latest/logs/CWL_QuerySyntax.html

WAFのActionについて

AWS WAFにはルールにマッチした際に取るアクションについて、ALLOWアクション、BLOCKアクション以外に、COUNTアクションと呼ばれる動作モードがあります。(厳密には他にもあります)
これは、実際にはBLOCKせずしかし記録したいといった用途に使えるモードで、例えばWAF導入前の個別ルールの検討などに利用できます。本記事で紹介するスクリプトでは、BLOCKアクションまたはCOUNTアクションを想定しています。実はそれぞれログの構造が異なっているため、本来ログ取得時には配慮が必要となります。

ログ取得

bashスクリプト例

実際のスクリプトの例です。

このスクリプトではJSON形式で取得したログを 最終的にCSVとして出力すること を目的としています。CSVとした後は、ExcelなりGoogle SpreadSheetなりで読み込んでグラフや表として可視化してもらえればという意図です。

get-waf-log.sh
#!/bin/bash
set -euo pipefail

### Set Log Group Name
declare -r LOG_GROUP="aws-waf-logs-example"
###

declare DAYS

while getopts a:d: OPT
do
    case $OPT in
        a)
            readonly ACTION=${OPTARG};;
        d)
            readonly DAYS=${OPTARG};;
        *)
            echo "Error: オプションを正しく指定してください。"
            exit 1;;
    esac
done

case ${ACTION} in
    "block" )
        readonly QUERY_STRING='fields @timestamp, @message | filter @message like "BLOCK" | sort @timestamp desc'
        readonly RULE_ID=".terminatingRuleId"
        ;;
    "count" )
        readonly QUERY_STRING='fields @timestamp, @message | filter @message like /"action":"COUNT"/ | sort @timestamp desc'
        readonly RULE_ID=".nonTerminatingMatchingRules[].ruleId"
        ;;
    "*" )
        echo "Error: アクションの指定が正しくありません。"
        exit 1
        ;;
esac

declare END_TIME && END_TIME=$(date +%s)
declare START_TIME && START_TIME=$((END_TIME - 3600 * 24 * DAYS))

QUERY_ID=$(aws logs start-query \
    --limit 10000 \
    --log-group-name "${LOG_GROUP}" \
    --start-time "${START_TIME}" \
    --end-time "${END_TIME}" \
    --query-string "${QUERY_STRING}" \
    | jq -r .queryId)

echo "Query ID: ${QUERY_ID}"

status="Running"

while [[ $status = "Running" ]]; do
    sleep 3
    aws logs get-query-results --query-id "${QUERY_ID}" > waf-log-all.json
    status=$(cat waf-log-all.json | jq -r .status)
    echo "$status"
done

cat waf-log-all.json \
    | jq -r --exit-status '.results[][] | {(.field): .value} | select(."@message" !=null)."@message"' \
    | jq -s . \
    > waf-log.json

cat waf-log.json \
    | jq -r ".[] | {
        timestamp: .timestamp,
        rule: ${RULE_ID},
        http_uri: .httpRequest.uri,
        http_ip: .httpRequest.clientIp,
        country: .httpRequest.country
    } | [.timestamp, .rule, .http_uri, .http_ip, .country] | @csv" \
    > waf-log.csv

使い方

前提条件として特に必要なものはありません。(あえて言うとAWS CLIとjqコマンドは必要です)

まず、スクリプトの冒頭でCloudWatch Logsのロググループを予め設定しておきます。WAFのロググループはプリフィックスは、 aws-waf-logs- と決まっており、WAF(Web ACL)構築時に設定します。

そして以下のように実行します。引数にアクションと日数を忘れずにご指定下さい。

./get-waf-log.sh -a count -d 2

解説

このスクリプトで取得できる項目は、

となります。他にも取得できる項目は多く何を選択しても良いですが、個人的にURIは割と見どころがあるんじゃないかと思うので、是非集計してみてください。攻撃手法について明るくないため一般的な例であるかわからないのですが、 ルールによっては/.env.aws などが含まれているかもしれません。

途中、中間ファイルを出力しcatするという冗長なやり方でどうかと思いますが、ルールにマッチした該当箇所( ruleMatchDetails )を参照する際や、デバッグのために便利だったりもします。このあたりは好みなので、よしなに修正下さい。

また蛇足ですが、日付の指定(START_TIME, END_TIME)を直接変更して日付指定しても良いです。
Macの場合はdateコマンドを使えばunixtimeに変換できます。

date -j -f "%Y-%m-%d %H:%M:%S" "2023-03-01 00:00:00" "+%s"

注意点

1. Logs Insightsのステータス

上記の手順によるログ取得において、特に注意したいポイントがあります。
お気づきの方もいるかもしれませんが get-query-results の前にsleepがあります。
実は、 start-query の実行が完了していないと、すべてのログが取得できません。しかし Running の状態でも、コマンド自体は成功し、一部のログが取得できてしまうという点が誤認しやすいポイントです。(私は最初知らなくてハマりました...)
必ずログ末尾に記載されるStatusが Complete となっていることを確認するようにして下さい。
そもそもあまり大規模なデータを扱うことは想定していないスクリプトですが、クエリの条件やリクエスト数によってはかなり時間がかることも予想されますので、まずは期間を1日と短く設定して試してみると良さそうです。

2. COUNTのログ数

COUNTアクションの場合について、少し補足します。
BLOCKアクションでは TerminatingRule として出力されていましたが、対してCOUNTアクションではnonTerminatingMatchingRulesとなります。
Rule ではなく、 Rules なのです。COUNTでは該当のリクエストが複数のルールにマッチする可能性があり、ここではそれらが1つのrowとして出力されるようにしています。そのため、実際にリクエストがあった数よりもCSVの桁数は大きくなるはずです。
一見それだけの違いなら、何もメリットが無いように思えますが、COUNTアクションであればルールを相対的に評価・比較できる という見方もできると思います。一方で、BLOCKアクションの場合には 優先度の設定に従い唯ひとつのルールにより評価されてしまう ため、純粋にルールごとの効果を測定をしようとしても難しいのではないでしょうか。

3. 検証

多くの場合で上記のスクリプトは使えると思いますが、検証は必ず行って下さい。特にマネージドルールによっては COUNT ではなく EXCLUDED_AS_COUNT として出力される場合もありますので、この点もご留意下さい。2022年11月にアップデートがあり、個別に COUNT を選択できるようになっているため、その場合には COUNT への変更がおすすめです。
そして、それとは関係なくCloudWatch Logsのログと、手元のCSVを突合して齟齬がないかは必ずご確認ください。

おわりに

簡単な分析であれば公式ブログなどのクエリ例を参考にすれば良いですし、AWSマネージメントコンソールのWAFページでも単純な Top ○○ であれば表示することが可能です。
しかし、誤検知を防ぐためのルールカスタマイズなど実際的な目的があって使用する場合には不十分なこともあるため、本記事を参考にしていただければと思います。

今回のスクリプトについては、雑なものでしたが、改めてpython(Boto3)で書きツール化しましたので、それについては近日またブログを書きたいと思います。
どなたかに届けば幸いです。

Discussion