Lesson 2 of 5 · 30 min read

SPL Essentials — search, stats, eval, rex, lookup

SPL (Splunk Processing Language) is the query language for Splunk SIEM. It’s the most widely deployed SIEM query language in enterprise environments. The pipe model makes it readable but has some quirks — especially around performance.


The Core SPL Commands

search (the implicit first command)

index=windows EventCode=4625

Splunk’s search is implicit — the first pipe stage is always a search. Always start with:

  1. An index or source filter (index=windows)
  2. One or more field=value pairs that narrow the result set
index=windows EventCode=4688 NewProcessName="*powershell.exe"

This searches Windows events for 4688 (process creation) where NewProcessName ends in powershell.exe.

Performance rule: The more specific your initial search, the less data the subsequent commands process.


stats — aggregate everything

index=windows EventCode=4625
| stats count as FailCount, dc(WorkstationName) as UniqueHosts, values(LogonType) as LogonTypes
  by TargetUserName
| where FailCount > 10
| sort -FailCount

Result: a table with one row per username showing their total failed logon count, how many distinct workstations they failed on, and which logon types were used.

Common stats functions:

FunctionWhat it does
countTotal events
dc(field)Distinct count of field values
values(field)List of unique field values
list(field)List of all field values (may repeat)
avg(field)Numeric average
sum(field)Numeric sum
earliest(field)Earliest value
latest(field)Latest value
first(field)First occurrence in time

eval — compute and transform

index=windows EventCode=4688
| eval IsElevated=if(IntegrityLevel="High" OR IntegrityLevel="System", "YES", "NO")
| eval ProcessBin=lower(mvindex(split(NewProcessName, "\\"), -1))
| where IsElevated="YES" AND ProcessBin IN ("powershell.exe", "cmd.exe", "wscript.exe")
| stats count by host, TargetUserName, ProcessBin

eval in detail:


rex — field extraction from text

index=linux sourcetype=syslog
| rex field=_raw "sshd\[\d+\]: Accepted (?P<auth_method>\w+) for (?P<username>\S+) from (?P<src_ip>[\d\.]+)"
| stats count by src_ip, username, auth_method
| sort -count

rex uses named capture groups ((?P<fieldname>pattern)) to extract structured fields from raw text. Run this on only the filtered subset — rex is slow when applied to millions of events.


lookup — enrich with reference data

index=network sourcetype=firewall action=allowed
| stats sum(bytes_out) as TotalBytesOut by dest_ip
| lookup ip_reputation.csv ip AS dest_ip OUTPUT threat_score, category
| where threat_score > 70
| sort -TotalBytesOut
| table dest_ip, TotalBytesOut, threat_score, category

This query:

  1. Gets all allowed firewall traffic
  2. Sums outbound bytes per destination IP
  3. Enriches with threat intel (threat_score, category)
  4. Filters for high-threat destinations
  5. Shows IPs with the most data sent to high-threat destinations

This is data exfiltration detection via lookup enrichment.


Detection Patterns in SPL

Password spray detection

index=windows EventCode=4625 earliest=-1h
| stats count as Attempts, dc(TargetUserName) as UniqueAccounts
  by IpAddress
| eval SprayScore=UniqueAccounts/Attempts
| where UniqueAccounts > 10 AND SprayScore > 0.5
| sort -UniqueAccounts

High UniqueAccounts and high SprayScore (each attempt hits a different account) = spray pattern. Low UniqueAccounts and high Attempts on one account = brute force.

Process tree with pipe

index=windows EventCode=4688 earliest=-24h
| eval ParentProcess=lower(mvindex(split(ParentProcessName,"\\"),-1))
| eval ChildProcess=lower(mvindex(split(NewProcessName,"\\"),-1))
| where ParentProcess IN ("word.exe","excel.exe","outlook.exe","powerpnt.exe")
  AND ChildProcess IN ("cmd.exe","powershell.exe","wscript.exe","mshta.exe","cscript.exe")
| table _time, host, TargetUserName, ParentProcess, ChildProcess, CommandLine
| sort -_time

Office applications spawning shells = malicious macro execution or exploit.

Splunk Search Commands— Splunk Docs