EQL 事件查询与序列检测

讲解 Elasticsearch EQL 事件查询:事件序列语法与 until 边界、sequence 与 sample 模式、by 分组与 maxspan 取舍、与 Query DSL 的能力差异、安全检测场景建模,以及序列匹配的性能开销与常见报错。

用 Query DSL 找「某个进程创建了子进程」是简单的 term 匹配,但要回答「同一个主机上先出现登录失败、随后出现登录成功、中间没有出现锁定」这类问题,DSL 就非常笨拙:你得写多轮查询,再用应用层代码把时间戳排好、手工做关联。EQL(Event Query Language)正是为这类事件序列问题设计的:它把时间维度写进语法,用 sequence 与 until 直接表达「A 之后跟着 B,除非中间出现 C」。本文讲清 EQL 的语法、边界控制、安全建模方式与性能代价。

1. 为什么需要 EQL

1.1 从文档检索到事件序列

Elasticsearch 的默认检索模型是「文档匹配」:给定查询,返回满足条件的文档集合,文档之间彼此独立、没有顺序关系。日志与安全场景的数据本质上是一条条带时间戳的事件,很多检测逻辑天然带顺序:「暴力破解成功」不是单条事件,而是「N 次失败 + 1 次成功」这个模式。

1.2 用 Query DSL 硬做的代价

用 DSL 表达序列,标准做法是查询一个时间窗口内的所有相关事件,按 host 分组、按时间排序,再在应用层做状态机匹配。这会带来三个问题:一是需要把大量原始事件拉到客户端,网络与内存成本高;二是关联逻辑散落在应用代码里,难以复用与审计;三是窗口切分(一条序列跨越两个查询窗口)容易出错,产生漏报。

1.3 EQL 的定位

EQL 把「序列」提升为一等公民。请求发给 _eql/search 接口,引擎在分片内部完成候选事件的时间排序与状态匹配,只把命中的序列返回。它不替代 Query DSL:单事件的过滤、统计、聚合仍用 DSL 与聚合框架;EQL 专注于「多个事件按时间组成的模式」。

1.4 与 DSL 的能力对照

能力Query DSLEQL
单事件过滤完整支持支持
跨事件顺序不支持,需应用层实现原生支持
分组关联聚合分组sequence by
时间边界靠 range 手工圈定maxspan、until
聚合统计完整支持不支持
排序打分完整支持按时间返回
更新删除支持只读查询

结论很直接:需要统计与排序时用 DSL,需要「谁在谁之后」时用 EQL。

2. EQL 语法基础

2.1 数据前提:event category

EQL 要求事件文档带有 event.category 字段,取值是固定枚举(process、network、authentication、file、registry、database 等)。查询必须以 category 开头声明数据类别,引擎据此决定加载哪个字段映射与序列缓冲策略。

process where process.name == "powershell.exe"

这条查询的含义是:在所有 process 类别的事件里,匹配进程名为 powershell.exe 的文档。字段访问用点号路径,比较用 ==(等于)、!=(不等于)、:(大小写不敏感的包含)等运算符。

2.2 运算符与字面量

EQL 支持完整的比较运算符与逻辑组合:

process where process.name in ("cmd.exe", "powershell.exe") and
  process.command_line : "*Invoke-Expression*" and
  process.pid >= 1000 and
  not process.name == "svchost.exe"

in 做集合匹配,: 做通配与大小写不敏感匹配,and/or/not 组合条件。字面量支持字符串、整数、浮点与布尔。注意 EQL 不做分词检索,它匹配的是字段原始值,因此字段必须是 keyword 类型或已开启 doc_values,否则会报映射错误。

2.3 基本事件匹配与缺失字段

字段不存在时,比较结果视为 null,null 参与任何比较都不会命中。若希望「字段不存在也算命中」,要显式写成:

process where process.parent.name == null or
  process.parent.name != "explorer.exe"

这条规则常用于捕获「父进程信息缺失的可疑进程」,因为很多注入类攻击会伪造或抹掉父进程字段。

2.4 sequence 与方括号

序列用方括号包裹每个阶段,阶段之间按时间顺序隐含连接:

sequence
  [ authentication where event.outcome == "failure" ]
  [ authentication where event.outcome == "success" ]

方括号的每一段是一个独立的事件匹配器,引擎按时间戳顺序寻找「第一段命中后,第二段才命中」的组合。默认情况下,同一序列内的事件必须是不同文档,同一个文档不能同时充当两个阶段。

3. 序列的边界控制

3.1 until 的语义

until 是 EQL 最有价值的设计:它声明「序列在遇到某类事件时立即作废」。

sequence by user.name
  [ authentication where event.outcome == "failure" ]
  [ authentication where event.outcome == "success" ]
until [ authentication where event.action == "account_locked" ]

没有 until 时,一个用户在锁定前后产生的失败与成功会被错误地拼成一条「爆破成功」序列。加上 until 后,账户锁定事件会打断序列,避免误报。这是把业务语义写进查询、而不是写进后处理代码的典型做法。

3.2 by 字段关联

sequence by 指定分组键,同一键值的事件才会被串成序列。分组键等价于 DSL 里按字段分组,但由引擎在分片内完成:

sequence by host.name, user.name with maxspan=10m
  [ process where process.name == "whoami.exe" ]
  [ network where destination.port == 445 ]

多个分组字段用逗号分隔,语义是「同时按这些字段取值分组」。分组键应选基数适中、语义稳定的字段:host.name、user.name、source.ip 都合适;process.pid 这类高基数且不稳定的字段会让每条序列都孤立,失去关联意义。

3.3 maxspan 的取舍

with maxspan=10m 限定整条序列的总时间跨度。跨度越大,引擎需要保留的候选事件越多,内存占用越高;跨度越小,跨窗口的慢速攻击越容易漏掉。实践中按检测语义设定:横向移动检测常用 5~15 分钟,慢速爆破可能需要小时级,此时更适合改用聚合统计而非序列匹配。

sequence by source.ip with maxspan=30s
  [ authentication where event.outcome == "failure" ] with runs=5
  [ authentication where event.outcome == "success" ]

3.4 runs 与连续事件

对「连续 N 次失败」这类模式,可以用 runs 语法指定重复次数:

sequence by source.ip with maxspan=1m
  [ authentication where event.outcome == "failure" ] with runs=5

runs=5 表示该阶段需要连续命中 5 次才算成立,比写五段方括号更简洁。runs 只能用于非 until 阶段,且与 by 组合时按分组键独立计数。

4. 高级匹配能力

4.1 missing events 与 not

阶段前加 ! 表示「该阶段必须不发生」,用于表达「A 之后没有 B」:

sequence by host.name
  [ process where process.parent.name == "winword.exe" ]
  ! [ process where process.name == "outlook.exe" ]

这在检测「宏文档启动了子进程但没有拉起正常办公程序」时非常直观。注意 ! 阶段同样受 maxspan 约束:若在窗口内始终没有该事件,序列才成立;一旦出现,序列作废。

4.2 sample 采样

sample 用于在同一时间窗口内随机采样事件,适合做数据探查而非精确检测:

sample by host.name
  [ process where true ]
  [ network where true ]

它保证每个分组最多返回一条组合,主要用于验证字段是否齐备、序列是否可构造,不要用于生产告警。用 sample 配合 size 可以在几秒内判断一批新接入的日志是否满足 EQL 的数据要求。

4.3 内置函数

EQL 提供了一批字符串与集合函数,常用的是:

函数作用示例
wildcard通配匹配wildcard(process.name, "p*.exe")
stringContains子串包含(大小写敏感)stringContains(user.name, "admin")
startsWith前缀匹配startsWith(file.path, "/etc/")
endsWith后缀匹配endsWith(file.name, ".dll")
length字符串长度length(process.command_line) > 200
concat拼接concat(host.name, "-", user.name)
between数值区间between(process.pid, 1000, 2000)
cidrMatchIP 网段匹配cidrMatch(source.ip, "10.0.0.0/8")
number转数值number(process.args[0]) > 100

函数只能作用于字段,不能嵌套调用过深;引擎会拒绝无法下推到 Lucene 查询的复杂表达式,报 verification_exception。把能下推的条件(如 host.name == "web-01")放在 filter 参数里预筛,能显著减少引擎需要评估的文档数。

4.4 数组字段与索引访问

ECS 里 process.args、file.path 等字段常是数组。EQL 对数组字段做「任一元素匹配」的语义:

process where process.args : "*--encrypt*" and process.args : "*.pdf"

两条件可以命中数组的不同元素。若要求同一元素同时满足,需要用函数组合或改写数据模型,EQL 不提供「同一下标」约束。

5. 安全场景建模

5.1 检测规则的写法

一条可用的检测规则应当包含三部分:序列模式、分组键、时间边界。下面是一个「可疑下载并执行」的完整规则:

sequence by host.name, user.name with maxspan=5m
  [ network where network.protocol == "http" and
      process.name in ("curl", "wget", "certutil.exe") ]
  [ process where event.type == "start" and
      process.parent.name in ("curl", "wget", "certutil.exe") and
      process.name : ("*.sh", "*.exe", "*.ps1") ]

规则用 _eql/search 执行:

curl -X POST "localhost:9200/logs-*/_eql/search?pretty" \
  -H "Content-Type: application/json" -d'
{
  "query": "sequence by host.name with maxspan=5m [ network where true ] [ process where true ]",
  "size": 100,
  "timestamp_field": "@timestamp",
  "event_category_field": "event.category"
}'

返回结构里每条命中包含 sequences 数组,每个序列下是多个阶段的 events,附带 join_keys 说明分组键取值,可直接映射到告警面板。

5.2 与检测规则的集成

在 Kibana 的 Detection Engine 里,EQL 规则与 DSL 规则共享调度与告警通道。把 EQL 用于「多步攻击链」,把 DSL 用于「单事件 IOC 匹配」,两者互补。规则要配上 false_positives 说明与 interval 调度周期,避免序列窗口与调度周期错配导致漏报——若 maxspan 是 10 分钟,调度周期必须显著小于它。

5.3 测试与回放

上线前用历史数据回放验证:把规则跑在 -30d 的时间范围上,统计命中数量与样本,人工判定真阳性比例。EQL 查询支持 filter 参数先做粗筛,把回放范围缩小到特定主机或用户,避免全量扫描拖慢集群。

curl -X POST "localhost:9200/logs-*/_eql/search?pretty" \
  -H "Content-Type: application/json" -d'
{
  "query": "sequence by host.name with maxspan=5m [ network where true ] [ process where true ]",
  "filter": { "term": { "host.name": "web-01" } },
  "size": 50
}'

5.4 常见误报与调优

安全规则的第一版几乎总是误报偏高。常见来源有三类:一是分组键选得太宽,把不同主机的事件混在一起;二是缺少 until,把正常业务操作拼成攻击链;三是 maxspan 过大,把间隔数小时的不相关事件串起来。调优顺序建议是:先收窄分组键,再补 until,最后缩短 maxspan。

6. 性能与限制

6.1 索引与字段要求

EQL 对字段类型敏感:序列阶段涉及的字段若是 text 类型,匹配会退化成无法下推的脚本执行,性能急剧下降。安全数据务必用 ECS 映射,@timestamp、event.category、host.name、process.name 都应是 keyword 或 date。索引的 index.query.default_field 对 EQL 无效,它只按显式字段路径匹配。

6.2 序列的内存开销

引擎需要为每个分组键保留候选事件直到 maxspan 过期。分组基数越高、maxspan 越长,堆内存压力越大。经验值:单次 EQL 查询的候选事件数控制在百万级以内;超过时应缩小时间范围或增加 filter 预筛。查询是分片级并发的,协调节点还要把各分片的序列结果做跨分片归并,因此序列跨分片时开销会进一步上升。

6.3 跨分片与时间排序

EQL 要求每个分组的事件在时间上可排序。若同一分组键的事件散落在多个分片,引擎需要拉取各分片的局部序列再归并,正确性由 @timestamp 保证,但延迟会增加。写日志时用 _routing 或数据流按时间分片,能减少跨分片归并。

6.4 常见报错

报错原因处理
unknown field字段不在映射中补 mapping 或改用存在字段
verification_exception表达式无法下推拆成更简单的条件
too_many_nested_clauses阶段过多或嵌套过深拆分规则
超时时间范围过大加 filter、缩小 maxspan
结果为空event.category 缺失补 ECS 字段
search_phase_execution_exception字段类型不匹配检查 keyword 映射

排查时先用 sample 查询确认字段有值,再用最简 sequence 逐步加条件,定位是哪一段匹配不上。配合慢查询日志可以看到 EQL 被翻译后的 Lucene 查询,判断是否发生了下推失败。

7. 与其他查询方式的分工

EQL 不是万能工具,它和 DSL、ES|QL 各有分工:

需求推荐方式
单事件精确过滤Query DSL
统计、分组、指标聚合框架
表格化分析查询ES
多事件顺序检测EQL
向量语义检索kNN 查询

把 EQL 当成「检测专用语言」而非通用查询入口,是最省心的用法。它适合写进检测规则、由调度器周期执行;不适合塞进业务 API 做交互式查询,因为它的响应结构(序列数组)对前端并不友好。

8. 总结

环节要点
适用场景多事件按时间组成的模式,如爆破成功、攻击链
数据前提必须有 event.category 与 ECS 字段映射
核心语法sequence 方括号分阶段,until 打断序列
分组与边界by 指定关联键,maxspan 控制时间跨度
高级能力! 表达不发生,runs 表达重复,内置函数做字符串判断
性能红线字段须 keyword、控制分组基数与 maxspan
排错顺序sample 验证字段 → 最简序列 → 逐条加条件
定位检测语言,不替代 DSL 与聚合

EQL 把序列检测从应用层代码搬进了查询引擎,代价是对数据模型的严格要求与内存开销。用得好,它能用一条可审计的规则替代上百行关联代码;用得不好,它会因为 maxspan 与分组基数失控把集群拖垮。落地时先统一 ECS 映射,再谈规则编写。日志管道的搭建可阅读《日志分析与 ELK 栈》,字段与映射设计可阅读《数据建模与 Mapping 设计》。

延伸阅读

继续阅读

探索更多技术文章

浏览归档,发现更多关于系统设计、工具链和工程实践的内容。

全部文章 返回首页

「elasticsearch」更多文章

  1. 组合模板与索引生命周期
  2. 批量写入调优与背压
  3. 相关性调优与离线评测