AWS WAFのログをDuckDBで分析する

AWS WAFのログをDuckDBで分析する

Athenaで料金を気にしながらクエリを実行していた方へ。
Clock Icon2025.03.06

こんにちは、なおにしです。

DuckDBを使用してAWS WAFのログを確認する機会がありましたのでご紹介します。

はじめに

S3バケットに保存されたログをAthenaを使用して分析する際、料金体系やPartition Projectionを意識していたけれども想定外の料金が発生してしまったというケースは弊社記事でも紹介されています。

https://dev.classmethod.jp/articles/cost-matters-when-querying-large-s3-objects-with-amazon-athena/

https://dev.classmethod.jp/articles/partition-projection-shikujiri-s3-cost-increase/

Athenaを使用することでAWS上でログを分析できるというメリットはありますが、探索的にログを調査したいようなケースにおいて想定外の料金の心配なんてしたくないと思います。

そこで、OSSのDuckDBを使用したAWS WAFのログ分析を試してみます。DuckDBとは?については以下をご参照ください。

https://duckdb.org/why_duckdb

併せてWikipediaからの機械翻訳も引用しておきます(執筆時点では日本語ページがありませんでした)。

DuckDBは、オープンソース の列指向 リレーショナルデータベース管理システム(RDBMS)です。組み込み構成の大規模データベースに対する複雑なクエリで高いパフォーマンスを発揮するように設計されています。数百の列と数十億の行を持つテーブルを組み合わせるなどです。他の組み込みデータベース( SQLiteなど)とは異なり、DuckDBはトランザクション(OLTP )アプリケーションに焦点を当てておらず、オンライン分析処理(OLAP)ワークロードに特化しています。

やってみた

事前準備

インストール方法については公式ドキュメントに則ります。brewによるインストールも案内がありましたので今回はそちらを利用します。

brew install duckdb

ログインしてみます。なお、一般的な対話モードと同様に「Ctrl+D」でセッションを終了できます。

$ duckdb

v1.2.0 5f5512b827
Enter ".help" for usage hints.
Connected to a transient in-memory database.
Use ".open FILENAME" to reopen on a persistent database.
D 
D SHOW DATABASES;
┌───────────────┐
│ database_name │
│    varchar    │
├───────────────┤
│ memory        │
└───────────────┘
D 

何となく打ってみた「SHOW DATABASES;」でも結果が表示されました。改めてドキュメントと確認するとSHOWはDESCRIBEのエイリアスのようですが、こちらのドキュメントによると「SHOW DATABASES;」に関しては「PRAGMA show_databases;」のエイリアスのようです。この辺りはプロダクトにおける定義なのでしっかりとドキュメントを確認する必要がありそうです。
なお、ログイン時のプロンプトにも記載のとおり、特に何も指定せずにDuckDBを実行した場合は一時的なインメモリデータベースに接続されている状態です。

ローカルにダウンロードしたログを開いてみる

まずはS3からダウンロードしたログファイルの内容を読み込んでみます。AWS WAFのログはS3バケットに配信する場合JSON形式のログがgz圧縮されて格納されるので、簡略のために以下のとおりファイルを作成します。

$ gzcat 123456789012_waflogs_cloudfront_naonishi-test-waf-webacl_20250228T0000Z_d1e34deb.log.gz | head -1 > input.json

続いて、DuckDBで操作します。なお、duckdbコマンドは上記で出力したファイルが存在するディレクトリと同じ階層で実行しています。以下のようにコマンドを実行した階層をカレントディレクトリとして認識するためです。glob関数についてはドキュメントをご参照ください。

$ duckdb                                                                                                               
v1.2.0 5f5512b827
Enter ".help" for usage hints.
Connected to a transient in-memory database.
Use ".open FILENAME" to reopen on a persistent database.
D SELECT * FROM glob('*');
┌─────────────────────────────────────────────────────────────────────────────────────────┐
│                                          file                                           │
│                                         varchar                                         │
├─────────────────────────────────────────────────────────────────────────────────────────┤
│ 123456789012_waflogs_cloudfront_naonishi-test-waf-webacl_20250228T0000Z_d1e34deb.log.gz │
│ input.json                                                                              │
└─────────────────────────────────────────────────────────────────────────────────────────┘
D 

それではJSONファイルの読み込みをドキュメントに記載のとおり確認します。

D SELECT *
  FROM read_json_auto('input.json');
┌───────────────┬───────────────┬──────────────────────┬───────────────────┬─────────────────────┬─────────┬───┬──────────────────┬──────────────────────┬──────────────────────┬──────────────────────┬──────────────────────┐
│   timestamp   │ formatVersion │       webaclId       │ terminatingRuleId │ terminatingRuleType │ action  │ … │ responseCodeSent │     httpRequest      │        labels        │    ja3Fingerprint    │    ja4Fingerprint    │
│     int64     │     int64     │       varcharvarcharvarcharvarchar │   │       json       │ struct(clientip va…  │ struct("name" varc…  │         uuid         │       varchar        │
├───────────────┼───────────────┼──────────────────────┼───────────────────┼─────────────────────┼─────────┼───┼──────────────────┼──────────────────────┼──────────────────────┼──────────────────────┼──────────────────────┤
│ 17407008047521       │ arn:aws:wafv2:us-e…  │ Default_Action    │ REGULAR             │ ALLOW   │ … │ NULL             │ {'clientIp': xx.xx…  │ [{'name': awswaf:n…  │ 89fcd7af-c2d5-abd5…  │ t13df61200_0189716…  │
├───────────────┴───────────────┴──────────────────────┴───────────────────┴─────────────────────┴─────────┴───┴──────────────────┴──────────────────────┴──────────────────────┴──────────────────────┴──────────────────────┤
│ 1 rows                                                                                                                                                                                                18 columns (11 shown) │
└─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘
D 

自動的にカラムを認識してくれています。続いてテーブルを作成します。

D SHOW tables;
┌─────────┐
│  name   │
│ varchar │
├─────────┤
│ 0 rows  │
└─────────┘
D CREATE TABLE new_tbl AS
      SELECT *
      FROM read_json_auto('input.json');
D 
D SHOW tables;
┌─────────┐
│  name   │
│ varchar │
├─────────┤
│ new_tbl │
└─────────┘
D 
D DESCRIBE new_tbl;
┌──────────────────────┬─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┬─────────┬─────────┬─────────┬─────────┐
│     column_name      │                                                                                 column_type                                                                                 │  nullkeydefault │  extra  │
│       varcharvarcharvarcharvarcharvarcharvarchar │
├──────────────────────┼─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┼─────────┼─────────┼─────────┼─────────┤
│ timestampBIGINT                                                                                                                                                                      │ YES     │ NULLNULLNULL    │
│ formatVersion        │ BIGINT                                                                                                                                                                      │ YES     │ NULLNULLNULL    │
│ webaclId             │ VARCHAR                                                                                                                                                                     │ YES     │ NULLNULLNULL    │
│ terminatingRuleId    │ VARCHAR                                                                                                                                                                     │ YES     │ NULLNULLNULL    │
│ terminatingRuleType  │ VARCHAR                                                                                                                                                                     │ YES     │ NULLNULLNULL    │
│ actionVARCHAR                                                                                                                                                                     │ YES     │ NULLNULLNULL    │
│ terminatingRuleMat…  │ JSON[]                                                                                                                                                                      │ YES     │ NULLNULLNULL    │
│ httpSourceName       │ VARCHAR                                                                                                                                                                     │ YES     │ NULLNULLNULL    │
│ httpSourceId         │ VARCHAR                                                                                                                                                                     │ YES     │ NULLNULLNULL    │
│ ruleGroupList        │ STRUCT(ruleGroupId VARCHAR, terminatingRule JSON, nonTerminatingMatchingRules STRUCT(ruleId VARCHAR, "action" VARCHAR, overriddenAction VARCHAR, ruleMatchDetails JSON[])…  │ YES     │ NULLNULLNULL    │
│ rateBasedRuleList    │ STRUCT(rateBasedRuleId VARCHAR, rateBasedRuleName VARCHAR, limitKey VARCHAR, maxRateAllowed BIGINT, evaluationWindowSec BIGINT, limitValue VARCHAR)[]                       │ YES     │ NULLNULLNULL    │
│ nonTerminatingMatc…  │ JSON[]                                                                                                                                                                      │ YES     │ NULLNULLNULL    │
│ requestHeadersInse…  │ JSON                                                                                                                                                                        │ YES     │ NULLNULLNULL    │
│ responseCodeSent     │ JSON                                                                                                                                                                        │ YES     │ NULLNULLNULL    │
│ httpRequest          │ STRUCT(clientIp VARCHAR, country VARCHAR, headers STRUCT("name" VARCHAR, "value" VARCHAR)[], uri VARCHAR, args VARCHAR, httpVersion VARCHAR, httpMethod VARCHAR, requestI…  │ YES     │ NULLNULLNULL    │
│ labels               │ STRUCT("name" VARCHAR)[]                                                                                                                                                    │ YES     │ NULLNULLNULL    │
│ ja3Fingerprint       │ UUID                                                                                                                                                                        │ YES     │ NULLNULLNULL    │
│ ja4Fingerprint       │ VARCHAR                                                                                                                                                                     │ YES     │ NULLNULLNULL    │
├──────────────────────┴─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┴─────────┴─────────┴─────────┴─────────┤
│ 18 rows                                                                                                                                                                                                                          6 columns │
└────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘
D 
D SELECT * FROM new_tbl;
┌───────────────┬───────────────┬──────────────────────┬───────────────────┬─────────────────────┬─────────┬───┬──────────────────┬──────────────────────┬──────────────────────┬──────────────────────┬──────────────────────┐
│   timestamp   │ formatVersion │       webaclId       │ terminatingRuleId │ terminatingRuleType │ action  │ … │ responseCodeSent │     httpRequest      │        labels        │    ja3Fingerprint    │    ja4Fingerprint    │
│     int64     │     int64     │       varcharvarcharvarcharvarchar │   │       json       │ struct(clientip va…  │ struct("name" varc…  │         uuid         │       varchar        │
├───────────────┼───────────────┼──────────────────────┼───────────────────┼─────────────────────┼─────────┼───┼──────────────────┼──────────────────────┼──────────────────────┼──────────────────────┼──────────────────────┤
│ 17407008047521       │ arn:aws:wafv2:us-e…  │ Default_Action    │ REGULAR             │ ALLOW   │ … │ NULL             │ {'clientIp': xx.xx…  │ [{'name': awswaf:n…  │ 89fcd7af-c2d5-abd5…  │ t13df61200_0189716…  │
├───────────────┴───────────────┴──────────────────────┴───────────────────┴─────────────────────┴─────────┴───┴──────────────────┴──────────────────────┴──────────────────────┴──────────────────────┴──────────────────────┤
│ 1 rows                                                                                                                                                                                                18 columns (11 shown) │
└─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘
D 

JSONデータに基づいたテーブルを簡単に作成できました。ドキュメント内の残りの例については割愛して、gzファイルからの取り込みを試してみます。

件数確認
$ gzcat 123456789012_waflogs_cloudfront_naonishi-test-waf-webacl_20250228T0000Z_d1e34deb.log.gz | wc -l
    1419
$ gzcat 123456789012_waflogs_cloudfront_naonishi-test-waf-webacl_20250228T0000Z_fbc3f0fc.log.gz | wc -l
    1096
D SELECT * FROM glob('*');
┌─────────────────────────────────────────────────────────────────────────────────────────┐
│                                          file                                           │
│                                         varchar                                         │
├─────────────────────────────────────────────────────────────────────────────────────────┤
│ 123456789012_waflogs_cloudfront_naonishi-test-waf-webacl_20250228T0000Z_d1e34deb.log.gz │
│ 123456789012_waflogs_cloudfront_naonishi-test-waf-webacl_20250228T0000Z_fbc3f0fc.log.gz │
└─────────────────────────────────────────────────────────────────────────────────────────┘
D 
D CREATE TABLE new_tbl AS
      SELECT *
      FROM read_json_auto('123456789012_waflogs_cloudfront_naonishi-test-waf-webacl_*.log.gz', union_by_name=true);
D 
D DESCRIBE new_tbl;
┌──────────────────────┬─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┬─────────┬─────────┬─────────┬─────────┐
│     column_name      │                                                                                 column_type                                                                                 │  nullkeydefault │  extra  │
│       varcharvarcharvarcharvarcharvarcharvarchar │
├──────────────────────┼─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┼─────────┼─────────┼─────────┼─────────┤
│ timestampBIGINT                                                                                                                                                                      │ YES     │ NULLNULLNULL    │
│ formatVersion        │ BIGINT                                                                                                                                                                      │ YES     │ NULLNULLNULL    │
│ webaclId             │ VARCHAR                                                                                                                                                                     │ YES     │ NULLNULLNULL    │
│ terminatingRuleId    │ VARCHAR                                                                                                                                                                     │ YES     │ NULLNULLNULL    │
│ terminatingRuleType  │ VARCHAR                                                                                                                                                                     │ YES     │ NULLNULLNULL    │
│ actionVARCHAR                                                                                                                                                                     │ YES     │ NULLNULLNULL    │
│ terminatingRuleMat…  │ JSON[]                                                                                                                                                                      │ YES     │ NULLNULLNULL    │
│ httpSourceName       │ VARCHAR                                                                                                                                                                     │ YES     │ NULLNULLNULL    │
│ httpSourceId         │ VARCHAR                                                                                                                                                                     │ YES     │ NULLNULLNULL    │
│ ruleGroupList        │ STRUCT(ruleGroupId VARCHAR, terminatingRule STRUCT(ruleId VARCHAR, "action" VARCHAR, ruleMatchDetails JSON), nonTerminatingMatchingRules STRUCT(ruleId VARCHAR, "action" …  │ YES     │ NULLNULLNULL    │
│ rateBasedRuleList    │ STRUCT(rateBasedRuleId VARCHAR, rateBasedRuleName VARCHAR, limitKey VARCHAR, maxRateAllowed BIGINT, evaluationWindowSec BIGINT, limitValue VARCHAR)[]                       │ YES     │ NULLNULLNULL    │
│ nonTerminatingMatc…  │ STRUCT(ruleId VARCHAR, "action" VARCHAR, ruleMatchDetails JSON[])[]                                                                                                         │ YES     │ NULLNULLNULL    │
│ requestHeadersInse…  │ JSON                                                                                                                                                                        │ YES     │ NULLNULLNULL    │
│ responseCodeSent     │ JSON                                                                                                                                                                        │ YES     │ NULLNULLNULL    │
│ httpRequest          │ STRUCT(clientIp VARCHAR, country VARCHAR, headers STRUCT("name" VARCHAR, "value" VARCHAR)[], uri VARCHAR, args VARCHAR, httpVersion VARCHAR, httpMethod VARCHAR, requestI…  │ YES     │ NULLNULLNULL    │
│ labels               │ STRUCT("name" VARCHAR)[]                                                                                                                                                    │ YES     │ NULLNULLNULL    │
│ ja3Fingerprint       │ UUID                                                                                                                                                                        │ YES     │ NULLNULLNULL    │
│ ja4Fingerprint       │ VARCHAR                                                                                                                                                                     │ YES     │ NULLNULLNULL    │
│ requestBodySize      │ BIGINT                                                                                                                                                                      │ YES     │ NULLNULLNULL    │
│ requestBodySizeIns…  │ BIGINT                                                                                                                                                                      │ YES     │ NULLNULLNULL    │
├──────────────────────┴─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┴─────────┴─────────┴─────────┴─────────┤
│ 20 rows                                                                                                                                                                                                                          6 columns │
└────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘
D 
D SELECT COUNT(*) FROM new_tbl;
┌──────────────┐
│ count_star() │
│    int64     │
├──────────────┤
│     2515     │
└──────────────┘
D
D SELECT * FROM new_tbl LIMIT 1;
┌───────────────┬───────────────┬──────────────────────┬───────────────────┬─────────────────────┬─────────┬───┬──────────────────────┬──────────────────────┬──────────────────────┬─────────────────┬──────────────────────┐
│   timestamp   │ formatVersion │       webaclId       │ terminatingRuleId │ terminatingRuleType │ action  │ … │        labels        │    ja3Fingerprint    │    ja4Fingerprint    │ requestBodySize │ requestBodySizeIns…  │
│     int64     │     int64     │       varcharvarcharvarcharvarchar │   │ struct("name" varc…  │         uuid         │       varchar        │      int64      │        int64         │
├───────────────┼───────────────┼──────────────────────┼───────────────────┼─────────────────────┼─────────┼───┼──────────────────────┼──────────────────────┼──────────────────────┼─────────────────┼──────────────────────┤
│ 17407008047521       │ arn:aws:wafv2:us-e…  │ Default_Action    │ REGULAR             │ ALLOW   │ … │ [{'name': awswaf:n…  │ 89fdddaf-c2d5-abd5…  │ t13d3612f0_0189716…  │      NULLNULL         │
├───────────────┴───────────────┴──────────────────────┴───────────────────┴─────────────────────┴─────────┴───┴──────────────────────┴──────────────────────┴──────────────────────┴─────────────────┴──────────────────────┤
│ 1 rows                                                                                                                                                                                               20 columns (11 shown) │
└────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘
D 

問題なく取り込めました。なお、ドキュメントを確認するとcompressionパラメータがデフォルトでautoのため拡張子から判断しているようです。

また、複数ファイルを同時に読み込む場合は、スキーマについてドキュメントに以下の記載があるためご注意ください。今回はWAFログだけを対象としていますが、requestBodySizeフィールドなどは必ずしもログに含まれているとは限らないようなので、union_by_nameパラメータをtrue(デフォルト:false)に設定しました。

By default, DuckDB reads the schema of the first file provided, and then unifies columns in subsequent files by column position. This works correctly as long as all files have the same schema. If the schema of the files differs, you might want to use the union_by_name option to allow DuckDB to construct the schema by reading all of the names instead.

S3から直接取得したログを開いてみる

前述までの方法によって、調査対象のログを一度S3からローカルにダウンロードしてしまえば安心してクエリを実行できる状態になっているかと思いますが、ログファイルを個別にダウンロードせずにDuckDBから直接S3に保存されているログを参照することも可能です。

まず、S3と連携するための拡張機能「httpfs」をインストールしてロードします。

D INSTALL httpfs;
D LOAD httpfs;

インストールでは何も表示されませんでしたが、インストール前にロードしようとすると以下のようにエラーが出力されました。

D LOAD httpfs;
IO Error:
Extension "/Users/nishimura.naoki/.duckdb/extensions/v1.2.0/osx_arm64/httpfs.duckdb_extension" not found.
Extension "httpfs" is an existing extension.

Install it first using "INSTALL httpfs".

続いて、S3にアクセスするためのクレデンシャルを設定します。ドキュメントに記載のとおり、AWS CLI等を使用するために既に設定済みのプロファイルを使用するには以下のクエリを実行します。クレデンシャルのシークレット情報を確認する方法についてはこちらに記載があります。

D SELECT * FROM duckdb_secrets();
┌─────────┬─────────┬──────────┬────────────┬─────────┬───────────┬───────────────┐
│  name   │  type   │ provider │ persistent │ storage │   scope   │ secret_string │
│ varcharvarcharvarcharbooleanvarcharvarchar[]varchar    │
├─────────┴─────────┴──────────┴────────────┴─────────┴───────────┴───────────────┤
│                                     0 rows                                      │
└─────────────────────────────────────────────────────────────────────────────────┘
D 
D CREATE SECRET secret2 (
      TYPE s3,
      PROVIDER credential_chain
  );
┌─────────┐
│ Success │
│ boolean │
├─────────┤
│ true    │
└─────────┘
D 
D SELECT * FROM duckdb_secrets();
┌─────────┬─────────┬──────────────────┬────────────┬─────────┬──────────────────────┬───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐
│  name   │  type   │     provider     │ persistent │ storage │        scope         │                                                                     secret_string                                                                     │
│ varcharvarcharvarcharbooleanvarcharvarchar[]varchar                                                                        │
├─────────┼─────────┼──────────────────┼────────────┼─────────┼──────────────────────┼───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┤
│ secret2 │ s3      │ credential_chain │ false      │ memory  │ [s3://, s3n://, s3…  │ name=secret2;type=s3;provider=credential_chain;serializable=true;scope=s3://,s3n://,s3a://;endpoint=s3.amazonaws.com;key_id=(アクセスキー);re…  │
└─────────┴─────────┴──────────────────┴────────────┴─────────┴──────────────────────┴───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘
D 

それではS3に格納されているログファイルを直接読み込んでみます。また、こちらに記載のとおり以下のように複数ファイルを指定することも可能です。

D SELECT COUNT(*)
  FROM read_json_auto('s3://aws-waf-logs-naonishi-test-s3bucket/AWSLogs/123456789012/WAFLogs/cloudfront/naonishi-test-waf-webacl/2025/02/28/00/00/123456789012_waflogs_cloudfront_naonishi-test-waf-webacl_20250228T0000Z_d1e34deb.log.gz');
┌──────────────┐
│ count_star() │
│    int64     │
├──────────────┤
│     1419     │
└──────────────┘
D 
D SELECT COUNT(*)
        FROM read_json_auto('s3://aws-waf-logs-naonishi-test-s3bucket/AWSLogs/123456789012/WAFLogs/cloudfront/naonishi-test-waf-webacl/2025/02/*/*/*/123456789012_waflogs_cloudfront_naonishi-test-waf-webacl_*.log.gz', union_by_name=true);
100% ▕████████████████████████████████████████████████████████████▏ 
┌──────────────┐
│ count_star() │
│    int64     │
├──────────────┤
│     2515     │
└──────────────┘

問題なく読み込めたようです。ですが、このままではDuckDBを実行する都度S3からログファイルを取得してメモリに展開する必要があるので、ローカルのファイルに保存してクエリを実行できるようにします。

DuckDBを起動する際にファイル名を指定します。なお、拡張子についてはこちらに言及があります。

$ duckdb naonishi-test.duckdb
v1.2.0 5f5512b827
Enter ".help" for usage hints.
D SHOW DATABASES;
┌───────────────┐
│ database_name │
│    varchar    │
├───────────────┤
│ naonishi-test │
└───────────────┘
D (一旦セッションを終了)
$ ls -lh naonishi-test.duckdb 
-rw-r--r--  1 nishimura.naoki  staff    12K  3  5 17:25 naonishi-test.duckdb
$ duckdb naonishi-test.duckdb

DBファイルが作成されました。こちらにS3に格納されているログを展開します。

D LOAD httpfs;
D 
D CREATE SECRET secret2 (
        TYPE s3,
        PROVIDER credential_chain
    );
┌─────────┐
│ Success │
│ boolean │
├─────────┤
│ true    │
└─────────┘
D 
D CREATE TABLE new_tbl AS
        SELECT *
        FROM read_json_auto('s3://aws-waf-logs-naonishi-test-s3bucket/AWSLogs/123456789012/WAFLogs/cloudfront/naonishi-test-waf-webacl/2025/02/*/*/*/123456789012_waflogs_cloudfront_naonishi-test-waf-webacl_*.log.gz', union_by_name=true);
100% ▕████████████████████████████████████████████████████████████▏ 
D DESCRIBE new_tbl;
┌──────────────────────┬─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┬─────────┬─────────┬─────────┬─────────┐
│     column_name      │                                                                                 column_type                                                                                 │  nullkeydefault │  extra  │
│       varcharvarcharvarcharvarcharvarcharvarchar │
├──────────────────────┼─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┼─────────┼─────────┼─────────┼─────────┤
│ timestampBIGINT                                                                                                                                                                      │ YES     │ NULLNULLNULL    │
│ formatVersion        │ BIGINT                                                                                                                                                                      │ YES     │ NULLNULLNULL    │
│ webaclId             │ VARCHAR                                                                                                                                                                     │ YES     │ NULLNULLNULL    │
│ terminatingRuleId    │ VARCHAR                                                                                                                                                                     │ YES     │ NULLNULLNULL    │
│ terminatingRuleType  │ VARCHAR                                                                                                                                                                     │ YES     │ NULLNULLNULL    │
│ actionVARCHAR                                                                                                                                                                     │ YES     │ NULLNULLNULL    │
│ terminatingRuleMat…  │ JSON[]                                                                                                                                                                      │ YES     │ NULLNULLNULL    │
│ httpSourceName       │ VARCHAR                                                                                                                                                                     │ YES     │ NULLNULLNULL    │
│ httpSourceId         │ VARCHAR                                                                                                                                                                     │ YES     │ NULLNULLNULL    │
│ ruleGroupList        │ STRUCT(ruleGroupId VARCHAR, terminatingRule STRUCT(ruleId VARCHAR, "action" VARCHAR, ruleMatchDetails JSON), nonTerminatingMatchingRules STRUCT(ruleId VARCHAR, "action" …  │ YES     │ NULLNULLNULL    │
│ rateBasedRuleList    │ STRUCT(rateBasedRuleId VARCHAR, rateBasedRuleName VARCHAR, limitKey VARCHAR, maxRateAllowed BIGINT, evaluationWindowSec BIGINT, limitValue VARCHAR)[]                       │ YES     │ NULLNULLNULL    │
│ nonTerminatingMatc…  │ STRUCT(ruleId VARCHAR, "action" VARCHAR, ruleMatchDetails JSON[])[]                                                                                                         │ YES     │ NULLNULLNULL    │
│ requestHeadersInse…  │ JSON                                                                                                                                                                        │ YES     │ NULLNULLNULL    │
│ responseCodeSent     │ JSON                                                                                                                                                                        │ YES     │ NULLNULLNULL    │
│ httpRequest          │ STRUCT(clientIp VARCHAR, country VARCHAR, headers STRUCT("name" VARCHAR, "value" VARCHAR)[], uri VARCHAR, args VARCHAR, httpVersion VARCHAR, httpMethod VARCHAR, requestI…  │ YES     │ NULLNULLNULL    │
│ labels               │ STRUCT("name" VARCHAR)[]                                                                                                                                                    │ YES     │ NULLNULLNULL    │
│ ja3Fingerprint       │ UUID                                                                                                                                                                        │ YES     │ NULLNULLNULL    │
│ ja4Fingerprint       │ VARCHAR                                                                                                                                                                     │ YES     │ NULLNULLNULL    │
│ requestBodySize      │ BIGINT                                                                                                                                                                      │ YES     │ NULLNULLNULL    │
│ requestBodySizeIns…  │ BIGINT                                                                                                                                                                      │ YES     │ NULLNULLNULL    │
├──────────────────────┴─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┴─────────┴─────────┴─────────┴─────────┤
│ 20 rows                                                                                                                                                                                                                          6 columns │
└────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘
D 
D SELECT COUNT(*) FROM new_tbl;
┌──────────────┐
│ count_star() │
│    int64     │
├──────────────┤
│     2515     │
└──────────────┘
D (セッション終了)

この時点で改めてファイルサイズを確認すると以下のとおりです。そのまま再度同ファイルを指定してDackDBを起動します。

$ ls -lh naonishi-test.duckdb
-rw-r--r--  1 nishimura.naoki  staff   780K  3  5 17:52 naonishi-test.duckdb
$
$ duckdb naonishi-test.duckdb

中身を確認してみます。

D SELECT * FROM duckdb_secrets();
┌─────────┬─────────┬──────────┬────────────┬─────────┬───────────┬───────────────┐
│  name   │  type   │ provider │ persistent │ storage │   scope   │ secret_string │
│ varcharvarcharvarcharbooleanvarcharvarchar[]varchar    │
├─────────┴─────────┴──────────┴────────────┴─────────┴───────────┴───────────────┤
│                                     0 rows                                      │
└─────────────────────────────────────────────────────────────────────────────────┘
D SELECT COUNT(*) FROM new_tbl;
┌──────────────┐
│ count_star() │
│    int64     │
├──────────────┤
│     2515     │
└──────────────┘
D 

シークレット情報は保存されず、もちろんS3上のデータを再取得する必要もなく、テーブルは作成されていてとても良い感じです。

調査用クエリを実行してみる

Athenaを使用した前回の記事の内容をDuckDBに置き換えてみます。ログの出力時刻に対してPartition Projectionを適用していることから、2段階で検索対象の時刻を絞る必要があった前回の内容に対して、今回はtimestampフィールド(UNIXミリ秒)のみ条件に指定すれば良いので、特にその辺りの修正が必要でした。

以下、ミリ秒単位でログを絞り込んだ結果です。時刻関連の内容はこちらをご参照ください。

SELECT
    make_timestamp(timestamp*1000) AT TIME ZONE 'UTC' AS japan_time,
    timestamp,
    terminatingRuleId
FROM
    new_tbl
WHERE
    timestamp BETWEEN epoch_ms('2025-02-28 09:00:04.752+09'::TIMESTAMPTZ) AND
                      epoch_ms('2025-02-28 09:00:04.752+09'::TIMESTAMPTZ)
ORDER BY
    timestamp DESC;

実行結果は以下のとおりでした。

┌────────────────────────────┬───────────────┬───────────────────┐
│         japan_time         │   timestamp   │ terminatingRuleId │
│  timestamp with time zone  │     int64     │      varchar      │
├────────────────────────────┼───────────────┼───────────────────┤
│ 2025-02-28 09:00:04.752+091740700804752 │ Default_Action    │
└────────────────────────────┴───────────────┴───────────────────┘
D 

こちらの記事にその他のサンプルクエリもいくつか記載されていますので、上記の時刻指定と組み合わせるのも良いかと思います。

それでは前回の記事に記載の2パターンをDuckDBのクエリに置き換えてみます。

【パターン1】WAF ログ直下の "nonTerminatingMatchingRules" フィールドにある "action": "COUNT"を抽出

時刻指定のためだけにWITH句を使用する必要がなくなりましたので、だいぶスッキリしました。

SELECT
    make_timestamp(timestamp*1000) AT TIME ZONE 'UTC' AS japan_time,
    nonTermRule,
    httprequest
FROM
    new_tbl,
    unnest(nonTerminatingMatchingRules) AS t(nonTermRule)
WHERE
    timestamp BETWEEN epoch_ms('2025-02-28 09:00:04.752+09'::TIMESTAMPTZ) AND
                      epoch_ms('2025-02-28 09:00:08.752+09'::TIMESTAMPTZ)
    AND nonTermRule.action = 'COUNT'
    AND nonTermRule.ruleid = 'AWSManagedRulesAnonymousIpList'
ORDER BY
    timestamp DESC;

実行結果は以下のとおりでした。

┌──────────────────────┬──────────────────────┬──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐
│      japan_time      │     nonTermRule      │                                                                                         httpRequest                                                                                          │
│ timestamp with tim…  │ struct(ruleid varc…  │ struct(clientip varchar, country varchar, headers struct("name" varchar, "value" varchar)[], uri varchar, args varchar, httpversion varchar, httpmethod varchar, requestid varchar, fragme…  │
├──────────────────────┼──────────────────────┼──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┤
│ 2025-02-28 09:00:0…  │ {'ruleId': AWSMana…  │ {'clientIp': xxx.xxx.xxx.xxx, 'country': FR, 'headers': [{'name': host, 'value': example.com}, {'name': user-agent, 'value': Mozilla/5.0 (compatible; AhrefsBot/7.0; +http://a…  │2025-02-28 09:00:0…  │ {'ruleId': AWSMana…  │ {'clientIp': yyy.yyy.yyy.yyy, 'country': NL, 'headers': [{'name': host, 'value': example.com}, {'name': cache-control, 'value': max-age=0}, {'name': sec-ch-ua, 'value': "Chro…  │
└──────────────────────┴──────────────────────┴──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘
D 

【パターン2】"ruleGroupList" フィールド内の "nonTerminatingMatchingRules" フィールドにある "action": "COUNT" を抽出

こちらはフィールドの構造が入り組んでいるので、WITH句を使用した方が良さそうでした。それでも時刻指定を1箇所ですれば良いだけなので、前回よりはスッキリしています。

WITH
      waf_logs AS (
          SELECT
              *
          FROM
              new_tbl,
              unnest(rulegrouplist) AS t(rulegroup)
          WHERE
              len(rulegroup.nonterminatingmatchingrules) > 0
      )
  SELECT
      make_timestamp(timestamp*1000) AT TIME ZONE 'UTC' AS japan_time,
      nonterminatingmatchingrule,
      httprequest
  FROM
      waf_logs,
      unnest(rulegroup.nonterminatingmatchingrules) AS t(nonterminatingmatchingrule)
  WHERE
      timestamp BETWEEN epoch_ms('2025-02-28 09:00:04.752+09'::TIMESTAMPTZ) AND
                        epoch_ms('2025-02-28 09:00:08.752+09'::TIMESTAMPTZ)
      AND nonterminatingmatchingrule.action = 'COUNT'
      AND nonterminatingmatchingrule.ruleid = 'TGT_VolumetricIpTokenAbsent'
  ORDER BY
      timestamp DESC;

実行結果は以下のとおりでした。

┌──────────────────────┬──────────────────────┬──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐
│      japan_time      │ nonterminatingmatc…  │                                                                                         httpRequest                                                                                          │
│ timestamp with tim…  │ struct(ruleid varc…  │ struct(clientip varchar, country varchar, headers struct("name" varchar, "value" varchar)[], uri varchar, args varchar, httpversion varchar, httpmethod varchar, requestid varchar, fragme…  │
├──────────────────────┼──────────────────────┼──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┤
│ 2025-02-28 09:00:0…  │ {'ruleId': TGT_Vol…  │ {'clientIp': xxx.xxx.xxx.xxx, 'country': KR, 'headers': [{'name': host, 'value': example.com}, {'name': cache-control, 'value': max-age=0}, {'name': sec-ch-ua, 'value': "Not(A…  │
│ 2025-02-28 09:00:0…  │ {'ruleId': TGT_Vol…  │ {'clientIp': yyy.yyy.yyy.yyy, 'country': NL, 'headers': [{'name': host, 'value': example.com}, {'name': cache-control, 'value': max-age=0}, {'name': sec-ch-ua, 'value': "Chro…  │
│ 2025-02-28 09:00:0…  │ {'ruleId': TGT_Vol…  │ {'clientIp': zzz.zzz.zzz.zzz, 'country': LV, 'headers': [{'name': Accept, 'value': text/html, application/xhtml+xml; q=0.9}, {'name': User-Agent, 'value': Mozilla/5.0 AppleWebKit/537.36 (KH…  │
└──────────────────────┴──────────────────────┴──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘
D

まとめ

DuckDBを使用することでAthenaと同じようにSQLでログを調査できることが確認できました。取り掛かりやすくするために細かめに手順と引用ドキュメントを記載しましたので、AWS WAFログだけではなくその他のログ分析にも本記事を活用いただければ幸いです。
また、文章では伝わりにくいのですが、クエリに対する応答や操作性なども非常に快適でしたので是非お試しください。

VPCフローログの分析に関しては以下もご参照ください。

https://dev.classmethod.jp/articles/query-vpc-flow-logs-by-duckdb/

本記事がどなたかのお役に立てれば幸いです。

Share this article

facebook logohatena logotwitter logo

© Classmethod, Inc. All rights reserved.