マネージドサービス部 佐竹です。
表題の通り、調査で AWS WAF のログの分析が必要となりましたが、必要なクエリがネットに落ちておらず独自に SQL を記載したため、そちらをご紹介します。
はじめに
AWS WAF のログ分析には Amazon Athena を利用する場面が多いでしょう。具体的には以下の AWS ドキュメントを参考にテーブルを作成し、SQL クエリを実行することが多い状況です*1。
パーティション射影を使用して Athena で AWS WAF S3 ログ用テーブルを作成する
パーティショニングなしで AWS WAF ログのテーブルを作成する
今回はこのドキュメントのうち、後者の「パーティショニングなし」のパターンを利用しての調査を行った場合の記録になります。
AWS WAF のログ調査で困っていたこと
AWS WAF を COUNT モードのみで運用している状態では、リクエストは BLOCK されずに全てのルールを通過し、ログに ALLOW
且つ COUNT
として出力されます。
これは AWS WAF の COUNT モードが終了アクション(terminating action)ではないためで、場合によっては複数のルールにカウントされる可能性があります。
この状態を絵にすると以下のようになります。
終了アクションがある場合の構成

全てカウントとした場合の構成

今回は図の後者である「AWS WAF を COUNT モードのみで運用している状態」の話です。

この状態の時 AWS WAF のログを見ると、全てが「Default_Action」で「ALLOW」されているよう見えます。

上記画面キャプチャーは以下のブログでもご紹介しております CloudFront セキュリティダッシュボードのものですが、画像の通り全て「Allowed」となっていることがこちらでも分かります。
そして「COUNT」は Athena で SQL クエリを記載して調査する時に、「action」に記載がされません。ここが「困った」ポイントです。以下は先ほどと同じログの再掲です。

全てが「Default_Action」で「ALLOW」されている状態のログです。
「action」には終了アクションが入る想定であり、先に記載した通り COUNT は終了アクションではないためこの「action」が検索に利用できないこととなります。
ではどうすれば COUNT のログを調査できるでしょうか?
どうすれば COUNT のログが分析できるか?
ということで、 COUNT モードのログを調査することが可能な Amazon Athena で実績のある、実用性の高い SQL クエリを以下で紹介していきます。
まずは1つ目です。
① 既存のテーブル定義で利用できる調査用 SQL
SELECT from_unixtime(timestamp / 1000, 'Asia/Tokyo') AS JST_timestamp, httprequest.clientip, httprequest.country, httprequest.uri, r.rulegroupid, n.ruleid, n.action, n.rulematchdetails FROM "default"."waf_logs" CROSS JOIN UNNEST(rulegrouplist) AS t(r) CROSS JOIN UNNEST(r.nonterminatingmatchingrules) AS t2(n) WHERE n.action = 'COUNT' LIMIT 10;
上記 SQL を実行頂ければ以下の画面キャプチャー通りの結果が返ります。

本クエリでも action
を表示できていますが、これは UNNEST(r.nonterminatingmatchingrules) AS t2(n)
で取得しているまた別の action
の結果です。
この点については以下の AWS 公式ブログにある「1: “nonTerminatingMatchingRules”内に“action”:“COUNT” として記録」の周辺箇所も合わせてご覧ください。
さて、先の SQL でも十分にワークはするのですが、私は1点どうしても追加したい情報がありました。それは httprequest 内に格納されている host 情報です。
ただし、この情報を取得する場合は AWS ドキュメントにある DDL ではうまく動作しないことがわかりました。そのため、DDL から見直した調査 SQL クエリを以下に記載します。
修正版のテーブル定義 (DDL)
CREATE EXTERNAL TABLE `waf_logs`( `timestamp` bigint, `formatversion` int, `webaclid` string, `terminatingruleid` string, `terminatingruletype` string, `action` string, `terminatingrulematchdetails` array < struct < conditiontype: string, sensitivitylevel: string, location: string, matcheddata: array < string > > >, `httpsourcename` string, `httpsourceid` string, `rulegrouplist` array < struct < rulegroupid: string, terminatingrule: struct < ruleid: string, action: string, rulematchdetails: array < struct < conditiontype: string, sensitivitylevel: string, location: string, matcheddata: array < string > > > >, nonterminatingmatchingrules: array < struct < ruleid: string, action: string, overriddenaction: string, rulematchdetails: array < struct < conditiontype: string, sensitivitylevel: string, location: string, matcheddata: array < string > > >, challengeresponse: struct < responsecode: string, solvetimestamp: string >, captcharesponse: struct < responsecode: string, solvetimestamp: string > > >, excludedrules: string > >, `ratebasedrulelist` array < struct < ratebasedruleid: string, limitkey: string, maxrateallowed: int > >, `nonterminatingmatchingrules` array < struct < ruleid: string, action: string, rulematchdetails: array < struct < conditiontype: string, sensitivitylevel: string, location: string, matcheddata: array < string > > >, challengeresponse: struct < responsecode: string, solvetimestamp: string >, captcharesponse: struct < responsecode: string, solvetimestamp: string > > >, `requestheadersinserted` array < struct < name: string, value: string > >, `responsecodesent` string, `httprequest` struct < clientip: string, country: string, headers: array < struct < name: string, value: string > >, uri: string, args: string, httpversion: string, httpmethod: string, requestid: string, fragment: string, scheme: string, host: string >, `labels` array < struct < name: string > >, `captcharesponse` struct < responsecode: string, solvetimestamp: string, failureReason: string >, `challengeresponse` struct < responsecode: string, solvetimestamp: string, failureReason: string >, `ja3Fingerprint` string, `oversizefields` string, `requestbodysize` int, `requestbodysizeinspectedbywaf` int, `ja4Fingerprint` string ) ROW FORMAT SERDE 'org.openx.data.jsonserde.JsonSerDe' STORED AS INPUTFORMAT 'org.apache.hadoop.mapred.TextInputFormat' OUTPUTFORMAT 'org.apache.hadoop.hive.ql.io.HiveIgnoreKeyTextOutputFormat' LOCATION 's3://amzn-s3-demo-bucket/prefix/'
LOCATION 's3://amzn-s3-demo-bucket/prefix/'
は適宜修正ください。

なお、上図は AWS ドキュメントに記載のある DDL との部分的な diff を示しているのですが「host: string」を追加することで「host」を httprequest.host
として使いやすく処理しています。
② 修正したテーブル定義で利用できる調査用 SQL
上記の修正版 DDL で実行できる SQL が以下です。Select するカラムの順序は並び替えたため、先ほどと異なっております。
SELECT from_unixtime(timestamp / 1000, 'Asia/Tokyo') AS JST_timestamp, httprequest.clientip, httprequest.country, httprequest.uri, httprequest.host, r.rulegroupid, n.ruleid, n.action, n.rulematchdetails FROM "default"."waf_logs" CROSS JOIN UNNEST(rulegrouplist) AS t(r) CROSS JOIN UNNEST(r.nonterminatingmatchingrules) AS t2(n) WHERE n.action = 'COUNT' LIMIT 10;
この SQL を実行頂ければ以下の画面キャプチャー通りになります。

こうすることで httprequest
の host も含めて COUNT モードの AWS WAF ログが分析できるようになり、調査に有用でしょう。
まとめ
本ブログでは AWS WAF の COUNT モードのログを調査のためにクエリする際、httprequest
の host も合わせて取得したいという要望に応えるために DDL と SQL クエリを作成しましたので、本文にてご紹介しました。
以下がまとめです。
- 通常の DDL を利用したシンプルな SQL では
action
に COUNT が含まれないため調査が難しい UNNEST(r.nonterminatingmatchingrules)
として COUNT モードのログを調査することが可能になる (SQL ①)- ただし httprequest の host を取得したい場合は DDL から改変すると良い
- 改変版の DDL であれば
httprequest.host
として host の情報も調査に活用しやすくなる (SQL ②)
このブログが AWS WAF の調査を行っているどなたかに、ほんの少しでも役に立てば幸いです。
では、またお会いしましょう。
*1:S3 Select を使うことも過去ありましたが、本サービスは新規の AWS アカウントでは利用できないため割愛します
佐竹 陽一 (Yoichi Satake) エンジニアブログの記事一覧はコチラ
マネージドサービス部所属。AWS資格全冠。2010年1月からAWSを業務利用してきています。主な表彰歴 2021-2022 AWS Ambassadors/2020-2025 Japan AWS Top Engineers/2020-2025 All Certifications Engineers。AWSのコスト削減やマルチアカウント管理と運用を得意としています。