AWS WAF の COUNT モードのログをクエリする時に httprequest の host を取得したい

記事タイトルとURLをコピーする

マネージドサービス部 佐竹です。
表題の通り、調査で AWS WAF のログの分析が必要となりましたが、必要なクエリがネットに落ちておらず独自に SQL を記載したため、そちらをご紹介します。

はじめに

AWS WAF のログ分析には Amazon Athena を利用する場面が多いでしょう。具体的には以下の AWS ドキュメントを参考にテーブルを作成し、SQL クエリを実行することが多い状況です*1

パーティション射影を使用して Athena で AWS WAF S3 ログ用テーブルを作成する

docs.aws.amazon.com

パーティショニングなしで AWS WAF ログのテーブルを作成する

docs.aws.amazon.com

今回はこのドキュメントのうち、後者の「パーティショニングなし」のパターンを利用しての調査を行った場合の記録になります。

AWS WAF のログ調査で困っていたこと

AWS WAF を COUNT モードのみで運用している状態では、リクエストは BLOCK されずに全てのルールを通過し、ログに ALLOW 且つ COUNT として出力されます。

これは AWS WAF の COUNT モードが終了アクション(terminating action)ではないためで、場合によっては複数のルールにカウントされる可能性があります。

この状態を絵にすると以下のようになります。

終了アクションがある場合の構成

終了アクションがある場合は都度終了される

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

全てカウントモードの場合は、デフォルトアクションで許可される

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

Default_Action

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

CloudFront セキュリティダッシュボード

上記画面キャプチャーは以下のブログでもご紹介しております CloudFront セキュリティダッシュボードのものですが、画像の通り全て「Allowed」となっていることがこちらでも分かります。

blog.serverworks.co.jp

そして「COUNT」は Athena で SQL クエリを記載して調査する時に、「action」に記載がされません。ここが「困った」ポイントです。以下は先ほどと同じログの再掲です。

Default_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” として記録」の周辺箇所も合わせてご覧ください。

aws.amazon.com

さて、先の 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/' は適宜修正ください。

diff

なお、上図は 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のコスト削減やマルチアカウント管理と運用を得意としています。