Skip to content
Sign In

Learn · Telemetry

Searching with KQL

Stop scrolling for evidence and start asking for it: one precise question in the query bar, answered across every host at once.

About 10 min

In this unit
How to find the events that answer your question in Elastic: time ranges, filters, exclusions, wildcards and pivots.
Not yet read in this browserSign in to see where you have applied this.

Why it matters

Most answers in an investigation are not printed on the alert. An account, a host, a destination, when it started: you find them by asking the SIEM a precise question and reading what comes back. Scrolling an unfiltered list of events finds them by luck.

Search is also how you test an idea cheaply. "Did this account sign in anywhere else?" is one query and a few seconds, not an hour of reading.

What analysts see

In Elastic Security, most searching happens in Discover, the screen you open first:

  • A query bar that takes KQL (Kibana Query Language), with a time picker beside it.
  • A results table of matching events, each one expandable to every field it contains.
  • A field list down the side. Click a field to see its top values in the current results, without writing a query.
  • Filter pills above the results: conditions pinned in place, separate from the query bar, that stay put while you change the query.

Discover is where you search. When findings start adding up, Elastic Security's Timeline is where you build the investigation: add events to it from Discover, keep notes beside them, and query within it using KQL or ES|QL. Your First Investigation walks through it.

Core concept

A KQL query is a set of conditions written as field: value. Combine them with and, or and not, group them with parentheses, and the results are every event in the time range that meets them.

Two details decide most results. The time range is part of every query even though it is not in the query bar: a correct query over the wrong window returns nothing, and Discover opens on a recent window by default. And fields such as account names, hosts and addresses compare the whole value, so quote it exactly; a * wildcard works only outside quotes.

Beyond field: value: field: * asks whether a field is present at all, ranges such as source.bytes >= 1000000 compare numbers, and a leading wildcard such as url.domain: *.sync-cdn.example matches every name under a domain.

Annotated examples

An exact match: a single user

Every event for the account d.pryor over the last day:

KQL:
user.name: "d.pryor" (note 1)
results:
214 events (note 2)
host.name (top value):
"ws-310.corp.example" (note 3)
  1. Note 1: Quoted, whole value: only this account. An unquoted wildcard (d.pryor*) would also match any account that starts the same way.
  2. Note 2: Read the count before any event. 214 in a day is an account at work; 2, or 20,000, would each say something different.
  3. Note 3: The field list shows the top values without another query: this account works almost entirely from one workstation.

Count first, then read. The number and the top values show what normal looks like before you look for what is not.

Subtracting the usual

The same account, minus its own workstation:

KQL:
user.name: "d.pryor" and not host.name: "ws-310.corp.example" (note 1)
results:
3 events (note 2)
source.ip:
"198.51.100.23" (note 3)
  1. Note 1: not removes what you already understand, so what is left is the question.
  2. Note 2: Three events out of 214 is few enough to read one by one.
  3. Note 3: An address from outside the organization, using an account that normally stays on one workstation: worth a closer look.

Subtracting the normal is often faster than describing the abnormal.

Where a program ran from

Programs rarely run from the shared Public folder. Every process that did:

KQL:
process.executable: C\:\\Users\\Public\\* (note 1)
results:
2 events
process.name:
"updater_x64.exe" (note 2)
process.parent.name:
"explorer.exe" (note 3)
user.name:
"d.pryor"
  1. Note 1: Unquoted so the * works, which means the : and each \ in the path are escaped with a backslash.
  2. Note 2: A name that sounds routine. The folder, not the name, is what made it stand out.
  3. Note 3: Started from the desktop shell: someone opened it, rather than a service starting it.

Search on where and how something ran, not only on what it is called: a name is easy to choose, a path and a parent less so.

A wildcard across a domain

Every web request to any name under one domain:

KQL:
url.domain: *.sync-cdn.example (note 1)
results:
1,862 events (note 2)
host.name (top value):
"ws-310.corp.example" (note 3)
  1. Note 1: The leading * matches any subdomain. It works, but it is slower over a lot of data, so set a shorter time range first.
  2. Note 2: Nearly two thousand requests to one domain in a day is a pattern, not browsing.
  3. Note 3: Almost all of them from one host: the same workstation as before.

A wildcard turns many different-looking values into one question.

Pin the host, then change the question

With the host pinned as a filter, the query bar asks only about that host:

filter:
host.name: "ws-310.corp.example" (note 1)
KQL:
source.bytes >= 1000000 (note 2)
results:
1 event
destination.ip:
"203.0.113.140" (note 3)
source.bytes:
48211904
  1. Note 1: Added with "Filter for value" on any result ("Filter out value" excludes instead). It stays while the query changes, so every question after it is about this host.
  2. Note 2: A range: connections where this host sent at least a million bytes.
  3. Note 3: One large transfer to one outside address, from the host the last three queries kept pointing at.

Pin what you are sure of, and keep the query bar for what you are still asking.

Two accounts, one exclusion

KQL:
user.name: ("d.pryor" or "a.halloran") and not host.name: "ws-310.corp.example"

Parentheses group the two accounts, so the exclusion applies to both. Without them, and is read before or: user.name: "d.pryor" or user.name: "a.halloran" and not host.name: "ws-310.corp.example" returns every event for the first account and only the off-host events for the second. When a count is larger than you expected, check where the parentheses are.

How to investigate

  1. 1.

    Set the time range first.

    Start wider than you think the activity lasted, confirm there are events at all, then shorten it.

  2. 2.

    Ask one question and read the count.

    One condition at a time. The count and the top values in the field list tell you whether to add a condition or widen next.

    user.name: "d.pryor"
  3. 3.

    Subtract what you understand.

    Use not to remove the expected, then read what is left.

    user.name: "d.pryor" and not host.name: "ws-310.corp.example"
  4. 4.

    Pivot on a value you found.

    Take a value from a result, such as a host, an address or a file path, and make it the next query, or pin it as a filter.

    process.executable: C\:\\Users\\Public\\*
  5. 5.

    Keep the queries that answered something.

    Save them, or copy them into your notes with their time range: a query without its window cannot be run again to the same answer.

Common mistakes

  • Mistake: Deciding the query is wrong because it returned nothing.

    InsteadCheck the time range and the field name first. A field that does not exist in these events matches nothing, silently.

  • Mistake: Putting a wildcard inside quotes.

    InsteadInside quotes, * is a literal asterisk. Leave wildcards unquoted and escape special characters (:, \, spaces) with a backslash.

  • Mistake: Searching for part of an account, host or address.

    InsteadThese fields compare whole values. Use a wildcard for "starts with", or the field list to see the exact spelling.

  • Mistake: Retyping the same condition into every query.

    InsteadPin it as a filter once, then change only the question.

  • Mistake: Reading thousands of results one by one.

    InsteadAdd conditions until the results are few enough to read, or read the top values instead.

Field references

FieldWhat it tells youSeen in
@timestampWhen the event happened; what the time range filters on.Every event
user.nameThe account the activity ran as. Compared as a whole value.Windows Security, Sysmon, auditd
host.nameThe machine that recorded the event.Every host-based event
source.ip, destination.ipWhere a connection or sign-in came from, and where it went.Network, Windows Security
source.bytesBytes the source sent; searchable as a range.Network
process.name, process.executableThe program, and its full path on disk.Sysmon, auditd
process.parent.nameThe process that started it.Sysmon
url.domainThe domain part of a requested URL.Proxy, Web logs

Check yourself

  1. 1.
    Your query is correct, but it returns no events. What do you check first?

  2. 2.
    You want events for the account a.halloran and no other. Which query?

  3. 3.
    Which query returns events for both accounts, excluding their usual host?

  4. 4.
    Which query matches every subdomain of files-cdn.example?

  5. 5.
    You've found one event worth a closer look on ws-120.corp.example. What's the most useful next step?

Apply it in a Case

Pick a case, and before you open a single event, write down three questions its briefing raises. Answer each with one query, and note the count it returned.

Browse every case on the Range →

References