eql
EQL(Event Query Language)徹底解説
1. はじめに
1.1 本記事の目的
本記事では、Elasticsearchのイベントベース時系列データに特化したクエリ言語EQL(Event Query Language)について包括的に解説します。EQLは単一のイベントのマッチングだけでなく、複数のイベント間の時系列的な関係性やパターンを検出できる点が最大の特徴です。元々セキュリティの脅威ハンティングのために開発されましたが、ログ分析、インシデント調査、コンプライアンス監査など、あらゆるイベント駆動型データの分析に応用できます。
本記事を読み終えることで、以下の知識が得られます。
- EQLのアーキテクチャと設計思想
- 基本クエリ、シーケンスクエリ、サンプルクエリの構文と使い方
- 演算子、関数、パイプの全リファレンス
- EQL Search APIの使い方(同期・非同期)
- セキュリティユースケースでの実践的な活用パターン
- パフォーマンスとベストプラクティス
1.2 EQLの位置づけ
| 観点 | EQL | Query DSL | ES|QL |
|---|---|---|---|
| 対象データ | イベントベース時系列データ | あらゆるデータ | あらゆるデータ |
| 最大の強み | イベント間の時系列パターン検出 | 柔軟性と全機能アクセス | パイプライン型のアドホック分析 |
| シーケンス検出 | ネイティブサポート | 非対応 | 非対応 |
| 学習コスト | 低い(SQL風の構文) | 中〜高い | 低い |
| 主な用途 | 脅威ハンティング、インシデント調査 | アプリケーション組み込み | データ探索、ログ分析 |
1.3 動作環境
- Elasticsearch: 7.12以降(8.x / 9.x推奨)
- 必須フィールド: タイムスタンプフィールド(デフォルト:
@timestamp)、イベントカテゴリフィールド(デフォルト:event.category) - 推奨スキーマ: Elastic Common Schema (ECS)
2. EQLのアーキテクチャ
2.1 設計思想
EQLは以下の3つの原則に基づいて設計されています。
- イベント間の関係性の表現: 多くのクエリ言語が個々のイベントの検索に限定されるのに対し、EQLは異なるカテゴリ・異なるタイムスパンにまたがるイベント間の関係を表現できる
- 低い学習コスト: SQLに似た構文で直感的にクエリを構築でき、反復的な検索が迅速に行える
- セキュリティ特化の設計: IOC(侵害の痕跡)検索とTTP(戦術・技術・手順)ベースの脅威検出の両方をサポート
2.2 必須フィールド
EQLクエリを実行するためには、対象インデックスに以下の2つのフィールドが必要です。
| フィールド | デフォルト値 | 型 | 説明 |
|---|---|---|---|
| タイムスタンプ | @timestamp | date | イベントの発生時刻(date_nanos は非対応) |
| イベントカテゴリ | event.category | keyword | イベントの種別分類 |
カスタマイズも可能です。
GET /my-logs/_eql/search
{
"timestamp_field": "my_timestamp",
"event_category_field": "my_category",
"query": """
my_event_type where host.name == "server-01"
"""
}
2.3 Elastic Common Schema (ECS)
EQLはECSの採用を強く推奨しています。ECSに準拠することで以下のメリットがあります。
- EQLクエリの互換性と再利用性が最大化される
- Elastic Securityのビルトインルールとの互換性
event.categoryのバリエーション(process,file,network,authenticationなど)が標準化される- コミュニティ間でのクエリの共有が容易になる
2.4 実行フロー
EQLクエリ → パース → 最適化 → シャード分散実行 → イベント収集 → シーケンス/サンプル結合 → レスポンス
- パース: EQLクエリ文字列がAST(抽象構文木)に変換される
- Query DSLフィルタの適用:
filterパラメータのQuery DSL条件で事前にドキュメントを絞り込み - シャード分散実行: 各シャードでイベントの検索が並列実行される
- シーケンス/サンプル結合: シーケンスクエリの場合、収集されたイベントが時系列的に結合される
- レスポンス構築: 結果がクライアントに返却される
3. 基本クエリ
3.1 構文
EQLの基本クエリは、イベントカテゴリと条件を where で結合します。
<イベントカテゴリ> where <条件>
process where process.name == "powershell.exe"
このクエリは、event.category が process で、process.name が powershell.exe であるイベントにマッチします。デフォルトでは直近10件が時系列順で返されます。
3.2 any キーワード
any を使用すると、イベントカテゴリに関係なく全イベントを対象にできます。
any where host.name == "server-01" and message : "*error*"
3.3 比較演算子
| 演算子 | 説明 | 例 |
|---|---|---|
== | 等しい(大文字小文字区別) | status == "active" |
!= | 等しくない | status != "inactive" |
< | より小さい | port < 1024 |
<= | 以下 | severity <= 3 |
> | より大きい | bytes > 10000 |
>= | 以上 | count >= 5 |
: | 大文字小文字無視の等値比較 | user.name : "admin" |
3.4 : 演算子(大文字小文字無視 + ワイルドカード)
: 演算子は特に重要で、以下の機能を提供します。
// 大文字小文字を無視した比較
process where process.name : "PowerShell.exe"
// ワイルドカード検索
file where file.path : "C:\\Users\\*\\Downloads\\*.exe"
// リストマッチ
process where process.name : ("cmd.exe", "powershell.exe", "wscript.exe")
| パターン | 意味 |
|---|---|
* | 0文字以上の任意の文字列 |
? | 任意の1文字 |
3.5 パターンマッチング
// like(ワイルドカードベース)
process where process.command_line like "* -enc *"
// like~(大文字小文字無視)
process where process.command_line like~ "* -ENC *"
// regex(正規表現ベース)
process where process.name regex """[a-z]{3,5}\.exe"""
// regex~(大文字小文字無視)
process where process.name regex~ """power[a-z]+\.exe"""
3.6 論理演算子
// AND
process where process.name == "cmd.exe" and process.args : "whoami"
// OR
process where process.name : ("cmd.exe", "powershell.exe") or process.parent.name == "explorer.exe"
// NOT
process where process.name == "svchost.exe" and not process.parent.name == "services.exe"
3.7 IN演算子
process where process.name in ("cmd.exe", "powershell.exe", "bash", "sh")
process where process.name not in ("svchost.exe", "csrss.exe", "lsass.exe")
// 大文字小文字無視
process where process.name in~ ("CMD.exe", "PowerShell.EXE")
3.8 算術演算子
network where source.bytes + destination.bytes > 1000000
process where process.args_count % 2 == 0
4. シーケンスクエリ
シーケンスクエリはEQLの最も強力かつ差別化された機能です。順序付けられた複数のイベントが時系列に沿って発生するパターンを検出します。
4.1 基本構文
sequence
[process where process.name == "cmd.exe"]
[file where file.extension == "bat"]
[network where destination.port == 443]
この例では、以下の3つのイベントがこの順番で発生した場合にマッチします。
cmd.exeプロセスの実行.batファイルへのアクセス- ポート443への通信
4.2 maxspan(時間制約)
sequence with maxspan=5m
[authentication where event.outcome == "failure"]
[authentication where event.outcome == "failure"]
[authentication where event.outcome == "failure"]
[authentication where event.outcome == "success"]
maxspan はシーケンス全体(最初のイベントから最後のイベントまで)の時間窓を指定します。この例は5分以内に3回の認証失敗と1回の認証成功が連続するパターンを検出します。
サポートされる時間単位: s(秒)、m(分)、h(時間)、d(日)
4.3 by キーワード(イベント相関)
// 各イベントごとに個別のby条件
sequence
[process where process.name == "cmd.exe"] by process.pid
[file where file.extension == "ps1"] by process.pid
[network where destination.port == 443] by process.pid
by キーワードは指定フィールドの値が共通するイベント同士を相関させます。
// sequence by で全イベントに共通条件を適用
sequence by user.name with maxspan=10m
[authentication where event.outcome == "failure"]
[authentication where event.outcome == "failure"]
[authentication where event.outcome == "success"]
[process where process.name == "whoami"]
sequence by は by 条件をシーケンスの全イベントに一括で適用します。
4.4 欠落イベント(! 演算子)
sequence with maxspan=1h
[process where event.action == "start"] by process.pid
![process where event.action == "end"] by process.pid
! を使用すると、特定のイベントが発生しなかったことを条件にできます。この例は、プロセスが開始されたが1時間以内に終了しなかったケースを検出します。maxspan が必須です。
4.5 until キーワード(終了条件)
sequence by process.pid with maxspan=1h
[process where event.action == "start"]
[network where destination.port != 80]
until [process where event.action == "end"]
until は、シーケンスの追跡を打ち切るイベントを定義します。終了イベントが発生すると、その時点でそのプロセスIDに対するシーケンスの追跡は中止されます。
4.6 with runs(繰り返し)
sequence with maxspan=10m
[authentication where event.outcome == "failure"] with runs=5
[authentication where event.outcome == "success"]
with runs を使用すると、同じ条件のイベントが指定回数繰り返されるパターンを検出できます(1〜100回)。この例は5回の認証失敗後に認証成功が発生するパターンです。
4.7 実践的なシーケンスパターン
ブルートフォース攻撃の検出
sequence by source.ip with maxspan=5m
[authentication where event.outcome == "failure"] with runs=10
[authentication where event.outcome == "success"]
ラテラルムーブメントの検出
sequence by user.name with maxspan=30m
[authentication where event.action == "logged-in" and source.ip != "10.0.0.0/8"]
[process where process.name : ("psexec.exe", "wmic.exe", "winrm.exe")]
[network where destination.port in (445, 135, 5985)]
ランサムウェアの振る舞い検出
sequence by process.entity_id with maxspan=5m
[process where process.name : ("*.exe", "*.scr")]
[file where event.action == "creation" and file.extension : ("encrypted", "locked", "crypto")]
[file where event.action == "deletion" and file.name : "*readme*"]
権限昇格の検出
sequence by host.name with maxspan=15m
[process where process.name == "whoami"]
[process where process.name : ("net.exe", "net1.exe") and process.args : "localgroup"]
[process where process.name : ("runas.exe", "sudo")]
5. サンプルクエリ
5.1 概要
サンプルクエリは、タイムスタンプの順序に依存せずにイベントを相関させます。シーケンスクエリとは異なり、イベントの発生順序は問わず、結合キーの値が共通するイベントグループを見つけます。
5.2 構文
sample by host.name
[file where file.extension == "exe"]
[network where destination.port == 22]
[process where process.name == "bash"]
5.3 制約
- 最大5つのフィルタを指定可能
maxspan、runs、untilは使用不可- パイプはサポートされない
- 結果は時系列順にソートされない
max_samples_per_keyとsizeで結果数を制御
5.4 ユースケース
// 同一ホストでの疑わしい活動パターン(順序不問)
sample by host.name
[authentication where event.outcome == "failure"]
[process where process.name : ("mimikatz*", "procdump*")]
[network where destination.port not in (80, 443)]
6. 関数リファレンス
6.1 文字列関数
| 関数 | 説明 | 例 |
|---|---|---|
concat(s1, s2, ...) | 文字列結合 | concat(user.domain, "\\", user.name) |
substring(s, start, end) | 部分文字列 | substring(file.name, 0, 5) |
length(s) | 文字列の長さ | length(process.command_line) > 200 |
startsWith(s, prefix) | プレフィックス判定 | startsWith(file.path, "C:\\Windows") |
endsWith(s, suffix) | サフィックス判定 | endsWith(file.name, ".exe") |
stringContains(s, sub) | 部分文字列の存在判定 | stringContains(process.args, "-enc") |
indexOf(s, sub, start) | 部分文字列の位置 | indexOf(url.path, "/api/") >= 0 |
between(s, left, right) | 区切り文字間の抽出 | between(email, "", "@") |
string(val) | 文字列への変換 | string(process.pid) |
大文字小文字を無視するバリアント: startsWith~, endsWith~, stringContains~
6.2 数学関数
| 関数 | 説明 | 例 |
|---|---|---|
add(a, b) | 加算 | add(source.bytes, destination.bytes) |
subtract(a, b) | 減算 | subtract(event.end, event.start) |
multiply(a, b) | 乗算 | multiply(price, quantity) |
divide(a, b) | 除算 | divide(total_bytes, 1024) |
modulo(a, b) | 剰余 | modulo(process.pid, 2) == 0 |
number(s, base) | 数値への変換 | number("0xFF", 16) |
6.3 ネットワーク関数
| 関数 | 説明 | 例 |
|---|---|---|
cidrMatch(ip, cidr1, ...) | CIDR範囲内の判定 | cidrMatch(source.ip, "10.0.0.0/8", "172.16.0.0/12") |
6.4 関数の使用例
// 長いコマンドラインでBase64エンコーディングを使用するプロセス
process where length(process.command_line) > 500
and stringContains(process.command_line, "-enc")
// 内部ネットワーク以外からの接続
network where not cidrMatch(source.ip, "10.0.0.0/8", "172.16.0.0/12", "192.168.0.0/16")
and destination.port : (22, 3389, 5985)
// 拡張子が偽装されたファイル
file where endsWith(file.name, ".pdf.exe")
or (endsWith(file.name, ".exe") and stringContains(file.name, ".doc"))
7. パイプ
パイプはEQLクエリの結果を後処理するための機能です。
7.1 head パイプ
最も古いイベントから指定件数を返します。
process where process.name == "powershell.exe"
| head 5
7.2 tail パイプ
最も新しいイベントから指定件数を返します。
process where process.name == "svchost.exe"
| tail 10
注意: パイプはサンプルクエリではサポートされていません。
8. EQL Search API
8.1 基本的なリクエスト
GET /security-events-*/_eql/search
{
"query": """
process where process.name == "regsvr32.exe"
and process.args : "scrobj.dll"
""",
"size": 20
}
8.2 主要パラメータ
| パラメータ | 型 | デフォルト | 説明 |
|---|---|---|---|
query | string | (必須) | EQLクエリ文字列 |
size | integer | 10 | 返却するマッチングイベント/シーケンスの最大数 |
timestamp_field | string | @timestamp | タイムスタンプフィールド名 |
event_category_field | string | event.category | イベントカテゴリフィールド名 |
tiebreaker_field | string | なし | タイムスタンプ同値時のソートフィールド |
filter | object | なし | 事前フィルタリング用のQuery DSL |
fields | array | なし | 返却する特定フィールドの指定 |
runtime_mappings | object | なし | ランタイムフィールドの定義 |
keep_alive | string | 5d | 非同期検索結果の保持期間 |
keep_on_completion | boolean | false | 同期検索結果の保持 |
wait_for_completion_timeout | string | なし | 非同期検索のタイムアウト |
8.3 Query DSLフィルタの組み合わせ
GET /logs-*/_eql/search
{
"query": """
process where process.name == "cmd.exe"
""",
"filter": {
"bool": {
"filter": [
{ "range": { "@timestamp": { "gte": "now-24h" } } },
{ "term": { "host.os.family": "windows" } }
]
}
}
}
8.4 ランタイムフィールド
GET /logs-*/_eql/search
{
"runtime_mappings": {
"day_of_week": {
"type": "keyword",
"script": "emit(doc['@timestamp'].value.dayOfWeekEnum.getDisplayName(TextStyle.FULL, Locale.ENGLISH))"
}
},
"query": """
process where process.name == "powershell.exe" and day_of_week : ("Saturday", "Sunday")
""",
"fields": ["day_of_week", "process.name", "@timestamp"]
}
8.5 非同期検索
// 1. 非同期検索の開始
GET /logs-*/_eql/search
{
"query": """
sequence by process.pid with maxspan=10m
[process where process.name == "cmd.exe"]
[file where file.extension == "ps1"]
[network where destination.port == 443]
""",
"wait_for_completion_timeout": "2s"
}
// レスポンス(タイムアウト時)
// {
// "id": "FmNJRUZ1YWZCU3dHY1BIOUhaenVSRkEaaXFlZ3h4c1RTWFNocDdnY2FSS...",
// "is_partial": true,
// "is_running": true,
// ...
// }
// 2. 結果の取得
GET /_eql/search/FmNJRUZ1YWZCU3dHY1BIOUhaenVSRkEaaXFlZ3h4c1RTWFNocDdnY2FSS...
// 3. ステータスの確認
GET /_eql/search/status/FmNJRUZ1YWZCU3dHY1BIOUhaenVSRkEaaXFlZ3h4c1RTWFNocDdnY2FSS...
// 4. 検索の削除
DELETE /_eql/search/FmNJRUZ1YWZCU3dHY1BIOUhaenVSRkEaaXFlZ3h4c1RTWFNocDdnY2FSS...
8.6 クロスクラスタ検索
GET /cluster_one:security-events-*,cluster_two:security-events-*/_eql/search
{
"query": """
sequence by user.name with maxspan=15m
[authentication where event.outcome == "failure"] with runs=5
[authentication where event.outcome == "success"]
"""
}
9. 実践的なユースケース
9.1 MITRE ATT&CKフレームワークに基づく検出ルール
T1059.001 - PowerShellの悪用
process where process.name == "powershell.exe"
and (
process.args : "*-EncodedCommand*"
or process.args : "*-enc*"
or process.args : "*-WindowStyle Hidden*"
or process.args : "*-ExecutionPolicy Bypass*"
or length(process.command_line) > 500
)
T1003 - 資格情報のダンプ
sequence by host.name with maxspan=5m
[process where process.name : ("mimikatz*", "procdump*", "taskmgr.exe")]
[file where file.name : ("*.dmp", "lsass*")]
T1021.002 - SMBを使用したラテラルムーブメント
sequence by source.ip with maxspan=30m
[authentication where event.action == "logged-in" and event.outcome == "success"]
[network where destination.port == 445]
[process where process.name : ("psexec*", "wmic*")]
9.2 異常検出パターン
営業時間外の管理者活動
process where user.name : ("admin*", "root")
and not cidrMatch(source.ip, "10.0.0.0/8")
異常な親子プロセス関係
process where process.parent.name == "explorer.exe"
and process.name : ("cmd.exe", "powershell.exe", "wscript.exe", "cscript.exe", "mshta.exe")
and process.args : ("*http*", "*ftp*", "*\\\\*")
9.3 インシデントレスポンス
ファイルの改ざん追跡
sequence by file.path with maxspan=1h
[file where event.action == "modification"]
[file where event.action == "modification"]
[file where event.action == "modification"]
永続化メカニズムの検出
sequence by host.name with maxspan=1h
[process where event.action == "start"]
[registry where registry.path : "*\\CurrentVersion\\Run*"]
10. パフォーマンスとベストプラクティス
10.1 パフォーマンス最適化
| テクニック | 説明 |
|---|---|
filter パラメータ | Query DSLフィルタで事前にデータを絞り込む |
maxspan の適切な設定 | シーケンスの時間窓を可能な限り短くする |
by キーワードの活用 | 結合キーを指定してマッチング候補を絞り込む |
tiebreaker_field | タイムスタンプ同値時の一貫したソート |
| 非同期検索 | 大規模データセットには非同期検索を使用 |
| ECSの採用 | 標準的なフィールドマッピングでインデックス効率を最大化 |
10.2 制限事項
| 制限 | 説明 |
|---|---|
date_nanos 非対応 | タイムスタンプフィールドは date 型のみ |
text フィールド非対応 | EQLはtext型フィールドを直接検索できない |
nested フィールドの制限 | ネストされたフィールドはタイムスタンプ/カテゴリに使用不可 |
| フィールド間比較不可 | 2つのフィールドの値を直接比較できない |
| サンプルクエリでのパイプ不可 | サンプルクエリではパイプがサポートされない |
| シングルクォート非対応 | 文字列はダブルクォートのみ |
10.3 クエリ設計のベストプラクティス
- ECSに準拠する: フィールド名をECSに合わせることで、クエリの互換性と再利用性を最大化
- シーケンスにはmaxspanを必ず設定: パフォーマンスとメモリ使用量の制御
- byキーワードで結合: 誤検出を減らし、パフォーマンスを向上させる
- filterで事前絞り込み: Query DSLフィルタで時間範囲やホストを事前に限定
- sizeの適切な設定: 必要以上の結果を返さない
- 大規模検索は非同期で: タイムアウトを防ぐ
11. まとめ
11.1 EQLの強み
| 強み | 説明 |
|---|---|
| シーケンス検出 | 複数イベント間の時系列パターンをネイティブに検出 |
| セキュリティ特化 | MITRE ATT&CKフレームワークとの親和性 |
| 低学習コスト | SQL風の直感的な構文 |
| サンプル相関 | 時系列に依存しないイベントの相関 |
| クロスクラスタ | 複数クラスタを横断した検索 |
| 非同期検索 | 大規模データセットへの対応 |
11.2 適用場面ガイド
| シナリオ | 推奨言語 |
|---|---|
| 攻撃チェーンの検出(複数ステップ) | EQL |
| IOC(侵害の痕跡)の検索 | EQL または Query DSL |
| ログのアドホック分析 | ES|QL |
| アプリケーション組み込みの検索 | Query DSL |
| ダッシュボードのフィルタリング | KQL |