SPL Cheat Sheet¶
In one sentence
SPL is a pipeline: the first part finds events (index, sourcetype, keywords), every | after that transforms what came before — filter, compute, group, count, sort, present. Learn ~25 commands and you can do 95% of incident-response work.
Anatomy of a search¶
index=botsv3 sourcetype=WinEventLog EventCode=4625 earliest=-24h latest=now
| eval user=lower(Account_Name)
| stats count by user, src_ip
| where count > 10
| sort - count
| head 20
Read it top to bottom: 1) get failed-logon events (4625) from the last 24 hours in the botsv3 index; 2) make a lowercase user field; 3) count events per user + source IP; 4) keep only pairs with more than 10; 5) biggest first; 6) top 20. Every line receives the rows the previous line produced.
Rules of thumb: always name the index (index=* is slow and often forbidden in production); put the most selective terms first (EventCode=4625 before free text); filter early with the search bar rather than late with where; use stats instead of table on large result sets.
Search-time filtering (before the first |)¶
| Syntax | Meaning |
|---|---|
index=botsv3 |
Which index |
sourcetype=WinEventLog · source="WinEventLog:Security" · host=FYODOR-L |
Metadata fields |
EventCode=4688 |
Field = value (case-insensitive value match for strings) |
EventCode IN (4624, 4625, 4648) |
Any of a list |
user=admin* |
Wildcard (at end is fast; leading *admin is slow) |
NOT user=SYSTEM · user!=SYSTEM |
Exclude (NOT field=x also keeps events without the field; field!=x drops them) |
"cmd.exe /c" |
Phrase (quotes) |
mimikatz OR procdump · A AND B (AND is implicit) |
Booleans |
earliest=-7d@d latest=@d |
Time modifiers — @d snaps to midnight; -7d@d = midnight seven days ago |
earliest="09/15/2026:00:00:00" |
Absolute time |
src_ip=10.0.0.0/24 |
CIDR works on IP fields |
TERM(10.0.0.5) |
Match a raw token exactly, fast, avoids segmentation surprises |
CASE(Mimikatz) |
Case-sensitive keyword |
Commands — the ones you use daily¶
Each row: what it does, then a copy-ready example.
Filter & shape rows¶
| Command | Does | Example |
|---|---|---|
search |
Filter again mid-pipeline (same syntax as the search bar) | … \| search count > 5 user!=svc* |
where |
Filter with an expression (functions, comparisons between fields) | \| where bytes_out > 10*bytes_in AND like(dest, "10.%") |
dedup |
Keep first event per unique value(s) | \| dedup user, host · \| dedup 3 host sortby -_time |
head / tail |
First / last N rows | \| head 50 |
sort |
Order rows (- = descending; 0 = no 10k limit) |
\| sort 0 - count, +user |
reverse |
Flip order | |
fields |
Keep (or - remove) columns — speeds things up |
\| fields _time, host, user, CommandLine · \| fields - _raw |
table |
Show columns as a table (presentation only) | \| table _time host user CommandLine |
rename |
Rename fields (wildcards ok) | \| rename Account_Name as user, "Source Network Address" as src_ip |
regex |
Filter rows by regex on a field | \| regex CommandLine="(?i)-enc\|-e\s+[A-Za-z0-9+/]{20,}" |
Compute new fields¶
| Command | Does | Example |
|---|---|---|
eval |
Create/modify a field with functions | \| eval MB=round(bytes/1024/1024,2), user=lower(user) |
eval + if / case |
Conditional values | \| eval type=case(Logon_Type==10,"RDP", Logon_Type==3,"Network", true(),"Other") |
eval + coalesce |
First non-null of several fields | \| eval user=coalesce(Account_Name, user, User) |
eval + string funcs |
len, upper, lower, substr, replace, split, mvindex, trim, urldecode |
\| eval domain=mvindex(split(query,"."),-2) |
eval + time funcs |
strftime, strptime, relative_time, now() |
\| eval day=strftime(_time,"%Y-%m-%d") |
eval + match / like / cidrmatch |
Pattern tests (return true/false) | \| eval internal=if(cidrmatch("10.0.0.0/8",dest_ip),"yes","no") |
rex |
Extract fields with regex named groups | \| rex field=CommandLine "-enc\s+(?<b64>[A-Za-z0-9+/=]+)" — see rex & fields |
rex mode=sed |
Find/replace in a field | \| rex field=user mode=sed "s/^CORP\\\\//" |
spath |
Pull fields out of JSON/XML | \| spath input=_raw path=Event.EventData.Data{@Name} |
lookup |
Enrich from a CSV/KV lookup | \| lookup asset_inventory ip as dest_ip OUTPUT owner, criticality |
iplocation |
GeoIP | \| iplocation src_ip |
fillnull |
Replace nulls | \| fillnull value="-" user, src_ip |
makemv / mvexpand |
Split a string into multivalue / one row per value | \| makemv delim="," answers \| mvexpand answers |
convert |
Convert formats (epoch → readable etc.) | \| convert ctime(first) ctime(last) |
Group, count, summarise (the heart of SPL)¶
| Command | Does | Example |
|---|---|---|
stats |
Aggregate over all rows, optionally by group |
\| stats count, dc(dest) as hosts, values(CommandLine) as cmds by user |
stats functions |
count, dc (distinct count), sum, avg, min, max, stdev, median, perc95, values (unique list), list (all, in order), first, last, earliest, latest, range |
\| stats earliest(_time) as first, latest(_time) as last, count by host |
eventstats |
Same as stats but keeps every row and appends the result |
\| eventstats avg(count) as avg_count by user |
streamstats |
Running/rolling calculations in row order | \| streamstats current=f last(_time) as prev by src, dest \| eval gap=_time-prev |
timechart |
Aggregate into time buckets (for charts) | \| timechart span=1h count by EventCode |
bin (a.k.a. bucket) |
Round _time (or numbers) into buckets, then stats |
\| bin _time span=10m \| stats count by _time, src_ip |
chart |
2-D pivot table | \| chart count over user by Logon_Type |
top / rare |
Most / least common values with count and percent | \| top limit=20 Image · \| rare limit=20 parent_process |
tstats |
Fast stats on indexed fields / data models / accelerated data | \| tstats count where index=botsv3 sourcetype=WinEventLog by _time span=1h, host |
transaction |
Group events into sessions by a field + time gap (slow; prefer stats) |
\| transaction Logon_ID maxspan=8h |
addtotals / addcoltotals |
Row/column totals |
Combine searches¶
| Command | Does | Example |
|---|---|---|
append |
Glue results of a subsearch below | \| append [search index=zeek sourcetype=zeek:conn …] |
join |
SQL-style join on a field (limits: 50k rows, slow) | \| join type=left uid [search index=zeek sourcetype=zeek:http \| fields uid, host, uri] |
[ subsearch ] in the search bar |
Use one search's result as the filter for another (10k-row limit) | index=botsv3 sourcetype=WinEventLog [search index=botsv3 EventCode=1102 \| fields host] |
stats … by with OR |
The fast alternative to join: search both sourcetypes at once and stats values() by the shared key |
(sourcetype=zeek:conn OR sourcetype=zeek:http) \| stats values(host) as http_host, sum(orig_bytes) as up by uid |
inputlookup / outputlookup |
Read / write a lookup table | \| inputlookup known_admins.csv · \| outputlookup baseline_ua.csv |
map |
Run a search per result row (slow, niche) |
Presentation & housekeeping¶
| Command | Does |
|---|---|
fieldsummary |
Per-field stats (count, distinct, top values) — first look at unknown data |
metadata type=sourcetypes index=botsv3 |
List sourcetypes with first/last time and counts (instant) |
\| tstats count where index=botsv3 by sourcetype |
Same, faster and more flexible |
eventcount summarize=false index=* |
Event count per index (allowed use of * — metadata only) |
rest /services/data/indexes |
List indexes |
typeof, isnull, isnotnull |
Type/null checks inside eval/where |
format |
Turn subsearch rows into an (a OR b OR c) expression |
collect |
Write results into a summary index |
Patterns you will reuse¶
Baseline vs. today (find new things)
index=botsv3 sourcetype=WinEventLog EventCode=4688 earliest=-30d@d latest=@d
| stats count by New_Process_Name
| eval period="baseline"
| append [ search index=botsv3 sourcetype=WinEventLog EventCode=4688 earliest=@d
| stats count by New_Process_Name | eval period="today" ]
| stats values(period) as seen by New_Process_Name
| where mvcount(seen)=1 AND seen="today"
The first search lists every process name seen in the 30 days before today, labelled baseline; append adds today's list labelled today; stats values(period) merges them into one row per process with the set of labels; a process whose only label is today never appeared in the baseline.
Rarity (long tail)
index=botsv3 sourcetype="XmlWinEventLog:Microsoft-Windows-Sysmon/Operational" EventID=1
| stats dc(host) as hosts, count by Image
| where hosts <= 2
| sort count
dc(host) counts how many distinct machines ran each binary; a program on one or two hosts out of the fleet is worth a look, and the rarest are at the top after sort count (ascending).
Time-bucketed spike
index=botsv3 sourcetype=WinEventLog EventCode=4625
| bin _time span=5m
| stats count, dc(Account_Name) as users by _time, Source_Network_Address
| where count > 20
bin rounds each event's time to the 5-minute bucket it falls in; stats then counts failures and distinct accounts per bucket per source; > 20 failures in 5 minutes from one IP is a brute force (many users = spraying).
First/last seen
index=botsv3 sourcetype=WinEventLog EventCode=4624 Logon_Type=10
| stats earliest(_time) as first, latest(_time) as last, count by Account_Name, Source_Network_Address, host
| convert ctime(first) ctime(last)
| sort first
earliest/latest return the first and last timestamp for each group; convert ctime() makes them readable; sorting by first shows when each RDP source/account pair appeared for the first time — new pairs during the incident window are the interesting rows.
Speed & etiquette¶
- Name the index and sourcetype; narrow the time range; put keywords in the search bar not in
where. fieldsearly,tablelate.statsbeatstransaction;stats … by keybeatsjoin.tstatsfor counting on indexed fields (host,source,sourcetype,_time, and any field in an accelerated data model) — orders of magnitude faster.- Avoid leading wildcards (
*admin) andindex=*. - Save useful searches as Reports; put the pattern with a placeholder in this handbook.