この記事は約5分で読めます。
マネージドサービス部 佐竹です。
表題の通り、調査で 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
SELECTfrom_unixtime(timestamp / 1000, 'Asia/Tokyo') AS JST_timestamp,httprequest.clientip,httprequest.country,httprequest.uri,r.rulegroupid,n.ruleid,n.action,n.rulematchdetailsFROM"default"."waf_logs"CROSS JOIN UNNEST(rulegrouplist) AS t(r)CROSS JOIN UNNEST(r.nonterminatingmatchingrules) AS t2(n)WHEREn.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 するカラムの順序は並び替えたため、先ほどと異なっております。
SELECTfrom_unixtime(timestamp / 1000, 'Asia/Tokyo') AS JST_timestamp,httprequest.clientip,httprequest.country,httprequest.uri,httprequest.host,r.rulegroupid,n.ruleid,n.action,n.rulematchdetailsFROM"default"."waf_logs"CROSS JOIN UNNEST(rulegrouplist) AS t(r)CROSS JOIN UNNEST(r.nonterminatingmatchingrules) AS t2(n)WHEREn.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/2023-2024 Japan AWS Top Engineers/2020-2024 All Certifications Engineers。AWSのコスト削減、最適化を得意としています。