elasticsearch/docs/reference/eql/eql.asciidoc
Liam Thompson 33a71e3289
[DOCS] Refactor book-scoped variables in docs/reference/index.asciidoc (#107413)
* Remove `es-test-dir` book-scoped variable

* Remove `plugins-examples-dir` book-scoped variable

* Remove `:dependencies-dir:` and `:xes-repo-dir:` book-scoped variables

- In `index.asciidoc`, two variables (`:dependencies-dir:` and `:xes-repo-dir:`) were removed.
- In `sql/index.asciidoc`, the `:sql-tests:` path was updated to fuller path
- In `esql/index.asciidoc`, the `:esql-tests:` path was updated idem

* Replace `es-repo-dir` with `es-ref-dir`

* Move `:include-xpack: true` to few files that use it, remove from index.asciidoc
2024-04-17 14:37:07 +02:00

1503 lines
43 KiB
Text
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

[role="xpack"]
[[eql]]
= EQL search
++++
<titleabbrev>EQL</titleabbrev>
++++
Event Query Language (EQL) is a query language for event-based time series
data, such as logs, metrics, and traces.
[discrete]
[[eql-advantages]]
=== Advantages of EQL
* *EQL lets you express relationships between events.* +
Many query languages allow you to match single events. EQL lets you match a
sequence of events across different event categories and time spans.
* *EQL has a low learning curve.* +
<<eql-syntax,EQL syntax>> looks like other common query languages, such as SQL.
EQL lets you write and read queries intuitively, which makes for quick,
iterative searching.
* *EQL is designed for security use cases.* +
While you can use it for any event-based data, we created EQL for threat
hunting. EQL not only supports indicator of compromise (IOC) searches but can
describe activity that goes beyond IOCs.
[discrete]
[[eql-required-fields]]
=== Required fields
With the exception of sample queries, EQL searches require that the searched
data stream or index contains a _timestamp_ field. By default, EQL uses the
`@timestamp` field from the {ecs-ref}[Elastic Common Schema (ECS)].
EQL searches also require an _event category_ field, unless you use the
<<eql-syntax-match-any-event-category,`any` keyword>> to search for documents
without an event category field. By default, EQL uses the ECS `event.category`
field.
To use a different timestamp or event category field, see
<<specify-a-timestamp-or-event-category-field>>.
TIP: While no schema is required to use EQL, we recommend using the
{ecs-ref}[ECS]. EQL searches are designed to work with core ECS fields by
default.
[discrete]
[[run-an-eql-search]]
=== Run an EQL search
Use the <<eql-search-api,EQL search API>> to run a <<eql-basic-syntax,basic EQL
query>>.
////
[source,console]
----
DELETE /_data_stream/*
DELETE /_index_template/*
----
// TEARDOWN
////
[source,console]
----
GET /my-data-stream/_eql/search
{
"query": """
process where process.name == "regsvr32.exe"
"""
}
----
// TEST[setup:sec_logs]
By default, basic EQL queries return the 10 most recent matching events in the
`hits.events` property. These hits are sorted by timestamp, converted to
milliseconds since the {wikipedia}/Unix_time[Unix epoch], in ascending order.
[source,console-result]
----
{
"is_partial": false,
"is_running": false,
"took": 60,
"timed_out": false,
"hits": {
"total": {
"value": 2,
"relation": "eq"
},
"events": [
{
"_index": ".ds-my-data-stream-2099.12.07-000001",
"_id": "OQmfCaduce8zoHT93o4H",
"_source": {
"@timestamp": "2099-12-07T11:07:09.000Z",
"event": {
"category": "process",
"id": "aR3NWVOs",
"sequence": 4
},
"process": {
"pid": 2012,
"name": "regsvr32.exe",
"command_line": "regsvr32.exe /s /u /i:https://...RegSvr32.sct scrobj.dll",
"executable": "C:\\Windows\\System32\\regsvr32.exe"
}
}
},
{
"_index": ".ds-my-data-stream-2099.12.07-000001",
"_id": "xLkCaj4EujzdNSxfYLbO",
"_source": {
"@timestamp": "2099-12-07T11:07:10.000Z",
"event": {
"category": "process",
"id": "GTSmSqgz0U",
"sequence": 6,
"type": "termination"
},
"process": {
"pid": 2012,
"name": "regsvr32.exe",
"executable": "C:\\Windows\\System32\\regsvr32.exe"
}
}
}
]
}
}
----
// TESTRESPONSE[s/"took": 60/"took": $body.took/]
// TESTRESPONSE[s/"_index": ".ds-my-data-stream-2099.12.07-000001"/"_index": $body.hits.events.0._index/]
// TESTRESPONSE[s/"_id": "OQmfCaduce8zoHT93o4H"/"_id": $body.hits.events.0._id/]
// TESTRESPONSE[s/"_id": "xLkCaj4EujzdNSxfYLbO"/"_id": $body.hits.events.1._id/]
Use the `size` parameter to get a smaller or larger set of hits:
[source,console]
----
GET /my-data-stream/_eql/search
{
"query": """
process where process.name == "regsvr32.exe"
""",
"size": 50
}
----
// TEST[setup:sec_logs]
[discrete]
[[eql-search-sequence]]
=== Search for a sequence of events
Use EQL's <<eql-sequences,sequence syntax>> to search for a series of
ordered events. List the event items in ascending chronological order,
with the most recent event listed last:
[source,console]
----
GET /my-data-stream/_eql/search
{
"query": """
sequence
[ process where process.name == "regsvr32.exe" ]
[ file where stringContains(file.name, "scrobj.dll") ]
"""
}
----
// TEST[setup:sec_logs]
The response's `hits.sequences` property contains the 10 most recent matching
sequences.
[source,console-result]
----
{
...
"hits": {
"total": ...,
"sequences": [
{
"events": [
{
"_index": ".ds-my-data-stream-2099.12.07-000001",
"_id": "OQmfCaduce8zoHT93o4H",
"_source": {
"@timestamp": "2099-12-07T11:07:09.000Z",
"event": {
"category": "process",
"id": "aR3NWVOs",
"sequence": 4
},
"process": {
"pid": 2012,
"name": "regsvr32.exe",
"command_line": "regsvr32.exe /s /u /i:https://...RegSvr32.sct scrobj.dll",
"executable": "C:\\Windows\\System32\\regsvr32.exe"
}
}
},
{
"_index": ".ds-my-data-stream-2099.12.07-000001",
"_id": "yDwnGIJouOYGBzP0ZE9n",
"_source": {
"@timestamp": "2099-12-07T11:07:10.000Z",
"event": {
"category": "file",
"id": "tZ1NWVOs",
"sequence": 5
},
"process": {
"pid": 2012,
"name": "regsvr32.exe",
"executable": "C:\\Windows\\System32\\regsvr32.exe"
},
"file": {
"path": "C:\\Windows\\System32\\scrobj.dll",
"name": "scrobj.dll"
}
}
}
]
}
]
}
}
----
// TESTRESPONSE[s/ \.\.\.\n/"is_partial": false, "is_running": false, "took": $body.took, "timed_out": false,/]
// TESTRESPONSE[s/"total": \.\.\.,/"total": { "value": 1, "relation": "eq" },/]
// TESTRESPONSE[s/"_index": ".ds-my-data-stream-2099.12.07-000001"/"_index": $body.hits.sequences.0.events.0._index/]
// TESTRESPONSE[s/"_id": "OQmfCaduce8zoHT93o4H"/"_id": $body.hits.sequences.0.events.0._id/]
// TESTRESPONSE[s/"_id": "yDwnGIJouOYGBzP0ZE9n"/"_id": $body.hits.sequences.0.events.1._id/]
Use <<eql-with-maxspan-keywords,`with maxspan`>> to constrain matching sequences
to a timespan:
[source,console]
----
GET /my-data-stream/_eql/search
{
"query": """
sequence with maxspan=1h
[ process where process.name == "regsvr32.exe" ]
[ file where stringContains(file.name, "scrobj.dll") ]
"""
}
----
// TEST[setup:sec_logs]
Use `!` to match <<eql-missing-events,missing events>>: events in a sequence
that do not meet a condition within a given timespan:
[source,console]
----
GET /my-data-stream/_eql/search
{
"query": """
sequence with maxspan=1d
[ process where process.name == "cmd.exe" ]
![ process where stringContains(process.command_line, "ocx") ]
[ file where stringContains(file.name, "scrobj.dll") ]
"""
}
----
// TEST[setup:sec_logs]
Missing events are indicated in the response as `missing": true`:
[source,console-result]
----
{
...
"hits": {
"total": ...,
"sequences": [
{
"events": [
{
"_index": ".ds-my-data-stream-2023.07.04-000001",
"_id": "AnpTIYkBrVQ2QEgsWg94",
"_source": {
"@timestamp": "2099-12-07T11:06:07.000Z",
"event": {
"category": "process",
"id": "cMyt5SZ2",
"sequence": 3
},
"process": {
"pid": 2012,
"name": "cmd.exe",
"executable": "C:\\Windows\\System32\\cmd.exe"
}
}
},
{
"_index": "",
"_id": "",
"_source": {},
"missing": true
},
{
"_index": ".ds-my-data-stream-2023.07.04-000001",
"_id": "BHpTIYkBrVQ2QEgsWg94",
"_source": {
"@timestamp": "2099-12-07T11:07:10.000Z",
"event": {
"category": "file",
"id": "tZ1NWVOs",
"sequence": 5
},
"process": {
"pid": 2012,
"name": "regsvr32.exe",
"executable": "C:\\Windows\\System32\\regsvr32.exe"
},
"file": {
"path": "C:\\Windows\\System32\\scrobj.dll",
"name": "scrobj.dll"
}
}
}
]
}
]
}
}
----
// TESTRESPONSE[s/ \.\.\.\n/"is_partial": false, "is_running": false, "took": $body.took, "timed_out": false,/]
// TESTRESPONSE[s/"total": \.\.\.,/"total": { "value": 1, "relation": "eq" },/]
// TESTRESPONSE[s/"_index": ".ds-my-data-stream-2023.07.04-000001"/"_index": $body.hits.sequences.0.events.0._index/]
// TESTRESPONSE[s/"_id": "AnpTIYkBrVQ2QEgsWg94"/"_id": $body.hits.sequences.0.events.0._id/]
// TESTRESPONSE[s/"_id": "BHpTIYkBrVQ2QEgsWg94"/"_id": $body.hits.sequences.0.events.2._id/]
Use the <<eql-by-keyword,`by` keyword>> to match events that share the
same field values:
[source,console]
----
GET /my-data-stream/_eql/search
{
"query": """
sequence with maxspan=1h
[ process where process.name == "regsvr32.exe" ] by process.pid
[ file where stringContains(file.name, "scrobj.dll") ] by process.pid
"""
}
----
// TEST[setup:sec_logs]
If a field value should be shared across all events, use the `sequence by`
keyword. The following query is equivalent to the previous one.
[source,console]
----
GET /my-data-stream/_eql/search
{
"query": """
sequence by process.pid with maxspan=1h
[ process where process.name == "regsvr32.exe" ]
[ file where stringContains(file.name, "scrobj.dll") ]
"""
}
----
// TEST[setup:sec_logs]
The `hits.sequences.join_keys` property contains the shared field values.
[source,console-result]
----
{
...
"hits": ...,
"sequences": [
{
"join_keys": [
2012
],
"events": ...
}
]
}
}
----
// TESTRESPONSE[s/ \.\.\.\n/"is_partial": false, "is_running": false, "took": $body.took, "timed_out": false,/]
// TESTRESPONSE[s/"hits": \.\.\.,/"hits": { "total": { "value": 1, "relation": "eq" },/]
// TESTRESPONSE[s/"events": \.\.\./"events": $body.hits.sequences.0.events/]
Use the <<eql-until-keyword,`until` keyword>> to specify an expiration
event for sequences. Matching sequences must end before this event.
[source,console]
----
GET /my-data-stream/_eql/search
{
"query": """
sequence by process.pid with maxspan=1h
[ process where process.name == "regsvr32.exe" ]
[ file where stringContains(file.name, "scrobj.dll") ]
until [ process where event.type == "termination" ]
"""
}
----
// TEST[setup:sec_logs]
[discrete]
[[eql-search-sample]]
=== Sample chronologically unordered events
Use EQL's <<eql-samples,sample syntax>> to search for events that match one or
more join keys and a set of filters. Samples are similar to sequences, but do
not return events in chronological order. In fact, sample queries can run on
data without a timestamp. Sample queries can be useful to find correlations in
events that don't always occur in the same sequence, or that occur across long
time spans.
.Click to show the sample data used in the examples below
[%collapsible]
====
[source,console]
----
PUT /my-index-000001
{
"mappings": {
"properties": {
"ip": {
"type":"ip"
},
"version": {
"type": "version"
},
"missing_keyword": {
"type": "keyword"
},
"@timestamp": {
"type": "date"
},
"type_test": {
"type": "keyword"
},
"@timestamp_pretty": {
"type": "date",
"format": "dd-MM-yyyy"
},
"event_type": {
"type": "keyword"
},
"event": {
"properties": {
"category": {
"type": "alias",
"path": "event_type"
}
}
},
"host": {
"type": "keyword"
},
"os": {
"type": "keyword"
},
"bool": {
"type": "boolean"
},
"uptime" : {
"type" : "long"
},
"port" : {
"type" : "long"
}
}
}
}
PUT /my-index-000002
{
"mappings": {
"properties": {
"ip": {
"type":"ip"
},
"@timestamp": {
"type": "date"
},
"@timestamp_pretty": {
"type": "date",
"format": "yyyy-MM-dd"
},
"type_test": {
"type": "keyword"
},
"event_type": {
"type": "keyword"
},
"event": {
"properties": {
"category": {
"type": "alias",
"path": "event_type"
}
}
},
"host": {
"type": "keyword"
},
"op_sys": {
"type": "keyword"
},
"bool": {
"type": "boolean"
},
"uptime" : {
"type" : "long"
},
"port" : {
"type" : "long"
}
}
}
}
PUT /my-index-000003
{
"mappings": {
"properties": {
"host_ip": {
"type":"ip"
},
"@timestamp": {
"type": "date"
},
"date": {
"type": "date"
},
"event_type": {
"type": "keyword"
},
"event": {
"properties": {
"category": {
"type": "alias",
"path": "event_type"
}
}
},
"missing_keyword": {
"type": "keyword"
},
"host": {
"type": "keyword"
},
"os": {
"type": "keyword"
},
"bool": {
"type": "boolean"
},
"uptime" : {
"type" : "long"
},
"port" : {
"type" : "long"
}
}
}
}
POST /my-index-000001/_bulk?refresh
{"index":{"_id":1}}
{"@timestamp":"1234567891","@timestamp_pretty":"12-12-2022","missing_keyword":"test","type_test":"abc","ip":"10.0.0.1","event_type":"alert","host":"doom","uptime":0,"port":1234,"os":"win10","version":"1.0.0","id":11}
{"index":{"_id":2}}
{"@timestamp":"1234567892","@timestamp_pretty":"13-12-2022","event_type":"alert","type_test":"abc","host":"CS","uptime":5,"port":1,"os":"win10","version":"1.2.0","id":12}
{"index":{"_id":3}}
{"@timestamp":"1234567893","@timestamp_pretty":"12-12-2022","event_type":"alert","type_test":"abc","host":"farcry","uptime":1,"port":1234,"bool":false,"os":"win10","version":"2.0.0","id":13}
{"index":{"_id":4}}
{"@timestamp":"1234567894","@timestamp_pretty":"13-12-2022","event_type":"alert","type_test":"abc","host":"GTA","uptime":3,"port":12,"os":"slack","version":"10.0.0","id":14}
{"index":{"_id":5}}
{"@timestamp":"1234567895","@timestamp_pretty":"17-12-2022","event_type":"alert","host":"sniper 3d","uptime":6,"port":1234,"os":"fedora","version":"20.1.0","id":15}
{"index":{"_id":6}}
{"@timestamp":"1234568896","@timestamp_pretty":"17-12-2022","event_type":"alert","host":"doom","port":65123,"bool":true,"os":"redhat","version":"20.10.0","id":16}
{"index":{"_id":7}}
{"@timestamp":"1234567897","@timestamp_pretty":"17-12-2022","missing_keyword":"yyy","event_type":"failure","host":"doom","uptime":15,"port":1234,"bool":true,"os":"redhat","version":"20.2.0","id":17}
{"index":{"_id":8}}
{"@timestamp":"1234567898","@timestamp_pretty":"12-12-2022","missing_keyword":"test","event_type":"success","host":"doom","uptime":16,"port":512,"os":"win10","version":"1.2.3","id":18}
{"index":{"_id":9}}
{"@timestamp":"1234567899","@timestamp_pretty":"15-12-2022","missing_keyword":"test","event_type":"success","host":"GTA","port":12,"bool":true,"os":"win10","version":"1.2.3","id":19}
{"index":{"_id":10}}
{"@timestamp":"1234567893","missing_keyword":null,"ip":"10.0.0.5","event_type":"alert","host":"farcry","uptime":1,"port":1234,"bool":true,"os":"win10","version":"1.2.3","id":110}
POST /my-index-000002/_bulk?refresh
{"index":{"_id":1}}
{"@timestamp":"1234567991","type_test":"abc","ip":"10.0.0.1","event_type":"alert","host":"doom","uptime":0,"port":1234,"op_sys":"win10","id":21}
{"index":{"_id":2}}
{"@timestamp":"1234567992","type_test":"abc","event_type":"alert","host":"CS","uptime":5,"port":1,"op_sys":"win10","id":22}
{"index":{"_id":3}}
{"@timestamp":"1234567993","type_test":"abc","@timestamp_pretty":"2022-12-17","event_type":"alert","host":"farcry","uptime":1,"port":1234,"bool":false,"op_sys":"win10","id":23}
{"index":{"_id":4}}
{"@timestamp":"1234567994","event_type":"alert","host":"GTA","uptime":3,"port":12,"op_sys":"slack","id":24}
{"index":{"_id":5}}
{"@timestamp":"1234567995","event_type":"alert","host":"sniper 3d","uptime":6,"port":1234,"op_sys":"fedora","id":25}
{"index":{"_id":6}}
{"@timestamp":"1234568996","@timestamp_pretty":"2022-12-17","ip":"10.0.0.5","event_type":"alert","host":"doom","port":65123,"bool":true,"op_sys":"redhat","id":26}
{"index":{"_id":7}}
{"@timestamp":"1234567997","@timestamp_pretty":"2022-12-17","event_type":"failure","host":"doom","uptime":15,"port":1234,"bool":true,"op_sys":"redhat","id":27}
{"index":{"_id":8}}
{"@timestamp":"1234567998","ip":"10.0.0.1","event_type":"success","host":"doom","uptime":16,"port":512,"op_sys":"win10","id":28}
{"index":{"_id":9}}
{"@timestamp":"1234567999","ip":"10.0.0.1","event_type":"success","host":"GTA","port":12,"bool":false,"op_sys":"win10","id":29}
POST /my-index-000003/_bulk?refresh
{"index":{"_id":1}}
{"@timestamp":"1334567891","host_ip":"10.0.0.1","event_type":"alert","host":"doom","uptime":0,"port":12,"os":"win10","id":31}
{"index":{"_id":2}}
{"@timestamp":"1334567892","event_type":"alert","host":"CS","os":"win10","id":32}
{"index":{"_id":3}}
{"@timestamp":"1334567893","event_type":"alert","host":"farcry","bool":true,"os":"win10","id":33}
{"index":{"_id":4}}
{"@timestamp":"1334567894","event_type":"alert","host":"GTA","os":"slack","bool":true,"id":34}
{"index":{"_id":5}}
{"@timestamp":"1234567895","event_type":"alert","host":"sniper 3d","os":"fedora","id":35}
{"index":{"_id":6}}
{"@timestamp":"1234578896","host_ip":"10.0.0.1","event_type":"alert","host":"doom","bool":true,"os":"redhat","id":36}
{"index":{"_id":7}}
{"@timestamp":"1234567897","event_type":"failure","missing_keyword":"test","host":"doom","bool":true,"os":"redhat","id":37}
{"index":{"_id":8}}
{"@timestamp":"1234577898","event_type":"success","host":"doom","os":"win10","id":38,"date":"1671235200000"}
{"index":{"_id":9}}
{"@timestamp":"1234577899","host_ip":"10.0.0.5","event_type":"success","host":"GTA","bool":true,"os":"win10","id":39}
----
====
A sample query specifies at least one join key, using the <<eql-by-keyword,`by`
keyword>>, and up to five filters:
[source,console]
----
GET /my-index*/_eql/search
{
"query": """
sample by host
[any where uptime > 0]
[any where port > 100]
[any where bool == true]
"""
}
----
// TEST[continued]
By default, the responses `hits.sequences` property contains up to 10 samples.
Each sample has a set of `join_keys` and an array with one matching event for
each of the filters. Events are returned in the order of the filters they match:
[source,console-result]
----
{
...
"hits": {
"total": {
"value": 2,
"relation": "eq"
},
"sequences": [
{
"join_keys": [
"doom" <1>
],
"events": [
{ <2>
"_index": "my-index-000001",
"_id": "7",
"_source": {
"@timestamp": "1234567897",
"@timestamp_pretty": "17-12-2022",
"missing_keyword": "yyy",
"event_type": "failure",
"host": "doom",
"uptime": 15,
"port": 1234,
"bool": true,
"os": "redhat",
"version": "20.2.0",
"id": 17
}
},
{ <3>
"_index": "my-index-000001",
"_id": "1",
"_source": {
"@timestamp": "1234567891",
"@timestamp_pretty": "12-12-2022",
"missing_keyword": "test",
"type_test": "abc",
"ip": "10.0.0.1",
"event_type": "alert",
"host": "doom",
"uptime": 0,
"port": 1234,
"os": "win10",
"version": "1.0.0",
"id": 11
}
},
{ <4>
"_index": "my-index-000001",
"_id": "6",
"_source": {
"@timestamp": "1234568896",
"@timestamp_pretty": "17-12-2022",
"event_type": "alert",
"host": "doom",
"port": 65123,
"bool": true,
"os": "redhat",
"version": "20.10.0",
"id": 16
}
}
]
},
{
"join_keys": [
"farcry" <5>
],
"events": [
{
"_index": "my-index-000001",
"_id": "3",
"_source": {
"@timestamp": "1234567893",
"@timestamp_pretty": "12-12-2022",
"event_type": "alert",
"type_test": "abc",
"host": "farcry",
"uptime": 1,
"port": 1234,
"bool": false,
"os": "win10",
"version": "2.0.0",
"id": 13
}
},
{
"_index": "my-index-000001",
"_id": "10",
"_source": {
"@timestamp": "1234567893",
"missing_keyword": null,
"ip": "10.0.0.5",
"event_type": "alert",
"host": "farcry",
"uptime": 1,
"port": 1234,
"bool": true,
"os": "win10",
"version": "1.2.3",
"id": 110
}
},
{
"_index": "my-index-000003",
"_id": "3",
"_source": {
"@timestamp": "1334567893",
"event_type": "alert",
"host": "farcry",
"bool": true,
"os": "win10",
"id": 33
}
}
]
}
]
}
}
----
// TESTRESPONSE[skip:Response is illustrative only]
<1> The events in the first sample have a value of `doom` for `host`.
<2> This event matches the first filter.
<3> This event matches the second filter.
<4> This event matches the third filter.
<5> The events in the second sample have a value of `farcry` for `host`.
You can specify multiple join keys:
[source,console]
----
GET /my-index*/_eql/search
{
"query": """
sample by host
[any where uptime > 0] by os
[any where port > 100] by op_sys
[any where bool == true] by os
"""
}
----
// TEST[continued]
This query will return samples where each of the events shares the same value
for `os` or `op_sys`, as well as for `host`. For example:
[source,console-result]
----
{
...
"hits": {
"total": {
"value": 2,
"relation": "eq"
},
"sequences": [
{
"join_keys": [
"doom", <1>
"redhat"
],
"events": [
{
"_index": "my-index-000001",
"_id": "7",
"_source": {
"@timestamp": "1234567897",
"@timestamp_pretty": "17-12-2022",
"missing_keyword": "yyy",
"event_type": "failure",
"host": "doom",
"uptime": 15,
"port": 1234,
"bool": true,
"os": "redhat",
"version": "20.2.0",
"id": 17
}
},
{
"_index": "my-index-000002",
"_id": "6",
"_source": {
"@timestamp": "1234568996",
"@timestamp_pretty": "2022-12-17",
"ip": "10.0.0.5",
"event_type": "alert",
"host": "doom",
"port": 65123,
"bool": true,
"op_sys": "redhat",
"id": 26
}
},
{
"_index": "my-index-000001",
"_id": "6",
"_source": {
"@timestamp": "1234568896",
"@timestamp_pretty": "17-12-2022",
"event_type": "alert",
"host": "doom",
"port": 65123,
"bool": true,
"os": "redhat",
"version": "20.10.0",
"id": 16
}
}
]
},
{
"join_keys": [
"farcry",
"win10"
],
"events": [
{
"_index": "my-index-000001",
"_id": "3",
"_source": {
"@timestamp": "1234567893",
"@timestamp_pretty": "12-12-2022",
"event_type": "alert",
"type_test": "abc",
"host": "farcry",
"uptime": 1,
"port": 1234,
"bool": false,
"os": "win10",
"version": "2.0.0",
"id": 13
}
},
{
"_index": "my-index-000002",
"_id": "3",
"_source": {
"@timestamp": "1234567993",
"type_test": "abc",
"@timestamp_pretty": "2022-12-17",
"event_type": "alert",
"host": "farcry",
"uptime": 1,
"port": 1234,
"bool": false,
"op_sys": "win10",
"id": 23
}
},
{
"_index": "my-index-000001",
"_id": "10",
"_source": {
"@timestamp": "1234567893",
"missing_keyword": null,
"ip": "10.0.0.5",
"event_type": "alert",
"host": "farcry",
"uptime": 1,
"port": 1234,
"bool": true,
"os": "win10",
"version": "1.2.3",
"id": 110
}
}
]
}
]
}
}
----
// TESTRESPONSE[skip:Response is illustrative only]
<1> The events in this sample have a value of `doom` for `host` and a value of
`redhat` for `os` or `op_sys`.
By default, the response of a sample query contains up to 10 samples, with one
sample per unique set of join keys. Use the `size` parameter to get a smaller or
larger set of samples. To retrieve more than one sample per set of join keys,
use the `max_samples_per_key` parameter. Pipes are not supported for sample
queries.
[source,console]
----
GET /my-index*/_eql/search
{
"max_samples_per_key": 2, <1>
"size": 20, <2>
"query": """
sample
[any where uptime > 0] by host,os
[any where port > 100] by host,op_sys
[any where bool == true] by host,os
"""
}
----
// TEST[continued]
<1> Retrieve up to 2 samples per set of join keys.
<2> Retrieve up to 20 samples in total.
[discrete]
[[retrieve-selected-fields]]
=== Retrieve selected fields
By default, each hit in the search response includes the document `_source`,
which is the entire JSON object that was provided when indexing the document.
You can use the <<common-options-response-filtering,`filter_path`>> query
parameter to filter the API response. For example, the following search returns
only the timestamp and PID from the `_source` of each matching event.
[source,console]
----
GET /my-data-stream/_eql/search?filter_path=hits.events._source.@timestamp,hits.events._source.process.pid
{
"query": """
process where process.name == "regsvr32.exe"
"""
}
----
// TEST[setup:sec_logs]
The API returns the following response.
[source,console-result]
----
{
"hits": {
"events": [
{
"_source": {
"@timestamp": "2099-12-07T11:07:09.000Z",
"process": {
"pid": 2012
}
}
},
{
"_source": {
"@timestamp": "2099-12-07T11:07:10.000Z",
"process": {
"pid": 2012
}
}
}
]
}
}
----
You can also use the `fields` parameter to retrieve and format specific fields
in the response. This field is identical to the search API's
<<search-fields,`fields` parameter>>.
include::{es-ref-dir}/search/search-your-data/retrieve-selected-fields.asciidoc[tag=fields-param-desc]
The following search request uses the `fields` parameter to retrieve values for
the `event.type` field, all fields starting with `process.`, and the
`@timestamp` field. The request also uses the `filter_path` query parameter to
exclude the `_source` of each hit.
[source,console]
----
GET /my-data-stream/_eql/search?filter_path=-hits.events._source
{
"query": """
process where process.name == "regsvr32.exe"
""",
"fields": [
"event.type",
"process.*", <1>
{
"field": "@timestamp",
"format": "epoch_millis" <2>
}
]
}
----
// TEST[setup:sec_logs]
include::{es-ref-dir}/search/search-your-data/retrieve-selected-fields.asciidoc[tag=fields-param-callouts]
The response includes values as a flat list in the `fields` section for each
hit.
[source,console-result]
----
{
...
"hits": {
"total": ...,
"events": [
{
"_index": ".ds-my-data-stream-2099.12.07-000001",
"_id": "OQmfCaduce8zoHT93o4H",
"fields": {
"process.name": [
"regsvr32.exe"
],
"process.name.keyword": [
"regsvr32.exe"
],
"@timestamp": [
"4100324829000"
],
"process.command_line": [
"regsvr32.exe /s /u /i:https://...RegSvr32.sct scrobj.dll"
],
"process.command_line.keyword": [
"regsvr32.exe /s /u /i:https://...RegSvr32.sct scrobj.dll"
],
"process.executable.keyword": [
"C:\\Windows\\System32\\regsvr32.exe"
],
"process.pid": [
2012
],
"process.executable": [
"C:\\Windows\\System32\\regsvr32.exe"
]
}
},
....
]
}
}
----
// TESTRESPONSE[s/ \.\.\.\n/"is_partial": false, "is_running": false, "took": $body.took, "timed_out": false,/]
// TESTRESPONSE[s/"total": \.\.\.,/"total": { "value": 2, "relation": "eq" },/]
// TESTRESPONSE[s/"_index": ".ds-my-data-stream-2099.12.07-000001"/"_index": $body.hits.events.0._index/]
// TESTRESPONSE[s/"_id": "OQmfCaduce8zoHT93o4H"/"_id": $body.hits.events.0._id/]
// TESTRESPONSE[s/ \.\.\.\.\n/$body.hits.events.1/]
[discrete]
[[eql-use-runtime-fields]]
=== Use runtime fields
Use the `runtime_mappings` parameter to extract and create <<runtime,runtime
fields>> during a search. Use the `fields` parameter to include runtime fields
in the response.
The following search creates a `day_of_week` runtime field from the `@timestamp`
and returns it in the response.
[source,console]
----
GET /my-data-stream/_eql/search?filter_path=-hits.events._source
{
"runtime_mappings": {
"day_of_week": {
"type": "keyword",
"script": "emit(doc['@timestamp'].value.dayOfWeekEnum.toString())"
}
},
"query": """
process where process.name == "regsvr32.exe"
""",
"fields": [
"@timestamp",
"day_of_week"
]
}
----
// TEST[setup:sec_logs]
The API returns:
[source,console-result]
----
{
...
"hits": {
"total": ...,
"events": [
{
"_index": ".ds-my-data-stream-2099.12.07-000001",
"_id": "OQmfCaduce8zoHT93o4H",
"fields": {
"@timestamp": [
"2099-12-07T11:07:09.000Z"
],
"day_of_week": [
"MONDAY"
]
}
},
....
]
}
}
----
// TESTRESPONSE[s/ \.\.\.\n/"is_partial": false, "is_running": false, "took": $body.took, "timed_out": false,/]
// TESTRESPONSE[s/"total": \.\.\.,/"total": { "value": 2, "relation": "eq" },/]
// TESTRESPONSE[s/"_index": ".ds-my-data-stream-2099.12.07-000001"/"_index": $body.hits.events.0._index/]
// TESTRESPONSE[s/"_id": "OQmfCaduce8zoHT93o4H"/"_id": $body.hits.events.0._id/]
// TESTRESPONSE[s/ \.\.\.\.\n/$body.hits.events.1/]
[discrete]
[[specify-a-timestamp-or-event-category-field]]
=== Specify a timestamp or event category field
The EQL search API uses the `@timestamp` and `event.category` fields from the
{ecs-ref}[ECS] by default. To specify different fields, use the
`timestamp_field` and `event_category_field` parameters:
[source,console]
----
GET /my-data-stream/_eql/search
{
"timestamp_field": "file.accessed",
"event_category_field": "file.type",
"query": """
file where (file.size > 1 and file.type == "file")
"""
}
----
// TEST[setup:sec_logs]
The event category field must be mapped as a <<keyword,`keyword`>> family field
type. The timestamp field should be mapped as a <<date,`date`>> field type.
<<date_nanos,`date_nanos`>> timestamp fields are not supported. You cannot use a
<<nested,`nested`>> field or the sub-fields of a `nested` field as the timestamp
or event category field.
[discrete]
[[eql-search-specify-a-sort-tiebreaker]]
=== Specify a sort tiebreaker
By default, the EQL search API returns matching hits by timestamp. If two or
more events share the same timestamp, {es} uses a tiebreaker field value to sort
the events in ascending order. {es} orders events with no
tiebreaker value after events with a value.
If you don't specify a tiebreaker field or the events also share the same
tiebreaker value, {es} considers the events concurrent and may
not return them in a consistent sort order.
To specify a tiebreaker field, use the `tiebreaker_field` parameter. If you use
the {ecs-ref}[ECS], we recommend using `event.sequence` as the tiebreaker field.
[source,console]
----
GET /my-data-stream/_eql/search
{
"tiebreaker_field": "event.sequence",
"query": """
process where process.name == "cmd.exe" and stringContains(process.executable, "System32")
"""
}
----
// TEST[setup:sec_logs]
[discrete]
[[eql-search-filter-query-dsl]]
=== Filter using Query DSL
The `filter` parameter uses <<query-dsl,Query DSL>> to limit the documents on
which an EQL query runs.
[source,console]
----
GET /my-data-stream/_eql/search
{
"filter": {
"range": {
"@timestamp": {
"gte": "now-1d/d",
"lt": "now/d"
}
}
},
"query": """
file where (file.type == "file" and file.name == "cmd.exe")
"""
}
----
// TEST[setup:sec_logs]
[discrete]
[[eql-search-async]]
=== Run an async EQL search
By default, EQL search requests are synchronous and wait for complete results
before returning a response. However, complete results can take longer for
searches across large data sets or <<data-tiers,frozen>> data.
To avoid long waits, run an async EQL search. Set `wait_for_completion_timeout`
to a duration you'd like to wait for synchronous results.
[source,console]
----
GET /my-data-stream/_eql/search
{
"wait_for_completion_timeout": "2s",
"query": """
process where process.name == "cmd.exe"
"""
}
----
// TEST[setup:sec_logs]
If the request doesn't finish within the timeout period, the search becomes async
and returns a response that includes:
* A search ID
* An `is_partial` value of `true`, indicating the search results are
incomplete
* An `is_running` value of `true`, indicating the search is ongoing
The async search continues to run in the background without blocking other
requests.
[source,console-result]
----
{
"id": "FmNJRUZ1YWZCU3dHY1BIOUhaenVSRkEaaXFlZ3h4c1RTWFNocDdnY2FSaERnUTozNDE=",
"is_partial": true,
"is_running": true,
"took": 2000,
"timed_out": false,
"hits": ...
}
----
// TESTRESPONSE[s/FmNJRUZ1YWZCU3dHY1BIOUhaenVSRkEaaXFlZ3h4c1RTWFNocDdnY2FSaERnUTozNDE=/$body.id/]
// TESTRESPONSE[s/"is_partial": true/"is_partial": $body.is_partial/]
// TESTRESPONSE[s/"is_running": true/"is_running": $body.is_running/]
// TESTRESPONSE[s/"took": 2000/"took": $body.took/]
// TESTRESPONSE[s/"hits": \.\.\./"hits": $body.hits/]
To check the progress of an async search, use the <<get-async-eql-search-api,get
async EQL search API>> with the search ID. Specify how long you'd like for
complete results in the `wait_for_completion_timeout` parameter.
[source,console]
----
GET /_eql/search/FmNJRUZ1YWZCU3dHY1BIOUhaenVSRkEaaXFlZ3h4c1RTWFNocDdnY2FSaERnUTozNDE=?wait_for_completion_timeout=2s
----
// TEST[skip: no access to search ID]
If the response's `is_running` value is `false`, the async search has finished.
If the `is_partial` value is `false`, the returned search results are
complete.
[source,console-result]
----
{
"id": "FmNJRUZ1YWZCU3dHY1BIOUhaenVSRkEaaXFlZ3h4c1RTWFNocDdnY2FSaERnUTozNDE=",
"is_partial": false,
"is_running": false,
"took": 2000,
"timed_out": false,
"hits": ...
}
----
// TESTRESPONSE[s/FmNJRUZ1YWZCU3dHY1BIOUhaenVSRkEaaXFlZ3h4c1RTWFNocDdnY2FSaERnUTozNDE=/$body.id/]
// TESTRESPONSE[s/"took": 2000/"took": $body.took/]
// TESTRESPONSE[s/"hits": \.\.\./"hits": $body.hits/]
Another more lightweight way to check the progress of an async search is to use
the <<get-async-eql-status-api,get async EQL status API>> with the search ID.
[source,console]
----
GET /_eql/search/status/FmNJRUZ1YWZCU3dHY1BIOUhaenVSRkEaaXFlZ3h4c1RTWFNocDdnY2FSaERnUTozNDE=
----
// TEST[skip: no access to search ID]
[source,console-result]
----
{
"id": "FmNJRUZ1YWZCU3dHY1BIOUhaenVSRkEaaXFlZ3h4c1RTWFNocDdnY2FSaERnUTozNDE=",
"is_running": false,
"is_partial": false,
"expiration_time_in_millis": 1611690295000,
"completion_status": 200
}
----
// TESTRESPONSE[s/FmNJRUZ1YWZCU3dHY1BIOUhaenVSRkEaaXFlZ3h4c1RTWFNocDdnY2FSaERnUTozNDE=/$body.id/]
// TESTRESPONSE[s/"expiration_time_in_millis": 1611690295000/"expiration_time_in_millis": $body.expiration_time_in_millis/]
[discrete]
[[eql-search-store-async-eql-search]]
=== Change the search retention period
By default, the EQL search API stores async searches for five days. After this
period, any searches and their results are deleted. Use the `keep_alive`
parameter to change this retention period:
[source,console]
----
GET /my-data-stream/_eql/search
{
"keep_alive": "2d",
"wait_for_completion_timeout": "2s",
"query": """
process where process.name == "cmd.exe"
"""
}
----
// TEST[setup:sec_logs]
You can use the <<get-async-eql-search-api,get async EQL search API>>'s
`keep_alive` parameter to later change the retention period. The new retention
period starts after the get request runs.
[source,console]
----
GET /_eql/search/FmNJRUZ1YWZCU3dHY1BIOUhaenVSRkEaaXFlZ3h4c1RTWFNocDdnY2FSaERnUTozNDE=?keep_alive=5d
----
// TEST[skip: no access to search ID]
Use the <<delete-async-eql-search-api,delete async EQL search API>> to
manually delete an async EQL search before the `keep_alive` period ends. If the
search is still ongoing, {es} cancels the search request.
[source,console]
----
DELETE /_eql/search/FmNJRUZ1YWZCU3dHY1BIOUhaenVSRkEaaXFlZ3h4c1RTWFNocDdnY2FSaERnUTozNDE=
----
// TEST[skip: no access to search ID]
[discrete]
[[eql-search-store-sync-eql-search]]
=== Store synchronous EQL searches
By default, the EQL search API only stores async searches. To save a synchronous
search, set `keep_on_completion` to `true`:
[source,console]
----
GET /my-data-stream/_eql/search
{
"keep_on_completion": true,
"wait_for_completion_timeout": "2s",
"query": """
process where process.name == "cmd.exe"
"""
}
----
// TEST[setup:sec_logs]
The response includes a search ID. `is_partial` and `is_running` are `false`,
indicating the EQL search was synchronous and returned complete results.
[source,console-result]
----
{
"id": "FjlmbndxNmJjU0RPdExBTGg0elNOOEEaQk9xSjJBQzBRMldZa1VVQ2pPa01YUToxMDY=",
"is_partial": false,
"is_running": false,
"took": 52,
"timed_out": false,
"hits": ...
}
----
// TESTRESPONSE[s/FjlmbndxNmJjU0RPdExBTGg0elNOOEEaQk9xSjJBQzBRMldZa1VVQ2pPa01YUToxMDY=/$body.id/]
// TESTRESPONSE[s/"took": 52/"took": $body.took/]
// TESTRESPONSE[s/"hits": \.\.\./"hits": $body.hits/]
Use the <<get-async-eql-search-api,get async EQL search API>> to get the
same results later:
[source,console]
----
GET /_eql/search/FjlmbndxNmJjU0RPdExBTGg0elNOOEEaQk9xSjJBQzBRMldZa1VVQ2pPa01YUToxMDY=
----
// TEST[skip: no access to search ID]
Saved synchronous searches are still subject to the `keep_alive` parameter's
retention period. When this period ends, the search and its results are deleted.
You can also check only the status of the saved synchronous search without
results by using <<get-async-eql-status-api,get async EQL status API>>.
You can also manually delete saved synchronous searches using the
<<delete-async-eql-search-api,delete async EQL search API>>.
[discrete]
[[run-eql-search-across-clusters]]
=== Run an EQL search across clusters
experimental::[]
The EQL search API supports <<modules-cross-cluster-search,cross-cluster search>>.
However, the local and <<remote-clusters,remote clusters>>
must use the same {es} version
if they have versions prior to 7.17.7 (included) or prior to 8.5.1 (included).
The following <<cluster-update-settings,cluster update settings>> request
adds two remote clusters: `cluster_one` and `cluster_two`.
[source,console]
----
PUT /_cluster/settings
{
"persistent": {
"cluster": {
"remote": {
"cluster_one": {
"seeds": [
"127.0.0.1:9300"
]
},
"cluster_two": {
"seeds": [
"127.0.0.1:9301"
]
}
}
}
}
}
----
// TEST[setup:host]
// TEST[s/127.0.0.1:930\d+/\${transport_host}/]
To target a data stream or index on a remote cluster, use the
`<cluster>:<target>` syntax.
[source,console]
----
GET /cluster_one:my-data-stream,cluster_two:my-data-stream/_eql/search
{
"query": """
process where process.name == "regsvr32.exe"
"""
}
----
// TEST[continued]
// TEST[setup:sec_logs]
// TEST[teardown:data_stream_cleanup]
[discrete]
[[eql-circuit-breaker]]
=== EQL circuit breaker settings
The relevant circuit breaker settings can be found in the <<circuit-breakers-page-eql, Circuit Breakers page>>.
include::syntax.asciidoc[]
include::functions.asciidoc[]
include::pipes.asciidoc[]
include::detect-threats-with-eql.asciidoc[]