Splunk Search Head – SPL Tutorial

Search Processing Language · Schritt für Schritt erklärt

CSIRT Edition
🎓
So funktioniert SPL in 5 Minuten
Du liest eine Query immer von links nach rechts – jeder Schritt verarbeitet das Ergebnis des vorherigen.
1
Die Suchleiste
Oben im Splunk-UI gibst du deine Query ein. Was vor der ersten | (Pipe) steht, ist die Basis-Suche – hier bestimmst du, welche Daten durchsucht werden.
index=windows EventCode=4625 earliest=-7d
2
Basis-Suche: Index + Filter
Jede Query beginnt mit Index, Zeitraum und Feld-Filtern. Das ist die schnellste Methode, Daten einzugrenzen.
ElementBeispielErklärung
indexindex=windowsWelche Datenquelle?
sourcetypesourcetype=WinEventLog:SecurityWelcher Log-Typ?
earliest/latestearliest=-24hWelcher Zeitraum?
Feldfilterstatus=500Welche Werte?
3
Der Pipe-Operator |
Hinter der Basis-Suche kommen Pipes. Jede | leitet das Ergebnis an den nächsten Befehl weiter – wie in der Unix-Kommandozeile.
-- So denkst du eine Query: -- "Nimm alle Windows-Logons (4624)" | "zähle sie pro User" | "zeige nur User mit mehr als 10 Logons" | "sortiere absteigend"
4
Deine erste Query – von Rohdaten zur Erkenntnis
Das Ziel ist immer: Rohdaten → gefiltert → aggregiert → beantwortet. Hier der klassische Aufbau:
① Datenquelle
index=windows
|
② Filtern
search EventCode=4625
|
③ Aggregieren
stats count by src_ip
|
④ Entscheiden
where count > 10
index=windows EventCode=4625 | stats count by src_ip, Account_Name | where count > 10 | sort -count | table src_ip, Account_Name, count

Die Basis-Suche & das Pipe |

Merke: Alles vor der ersten Pipe ist die Basis-Suche. Danach kommt | + Befehl. Wie Unix-Pipes.
Nur Basis-Suche (Rohdaten)
index=firewall action=blocked
Basis-Suche + eine Pipe
index=firewall action=blocked | stats count by src_ip
Verkettete Pipes
index=firewall action=blocked | stats count by src_ip | sort -count | head 10
Tipp: Filtere so früh wie möglich in der Pipe-Kette → weniger Events zu verarbeiten = schneller.

Implizites AND zwischen Termen

Mehrere Begriffe ohne Operator werden mit AND verknüpft. Splunk fügt das AND implizit ein.
Diese Queries sind äquivalent
-- implizites AND -- index=windows EventCode=4625 User=admin
-- explizites AND -- index=windows EventCode=4625 AND User=admin
Logische Operatoren
OperatorBeispielBedeutung
ANDerror AND timeoutBeide müssen vorkommen
ORerror OR warningEines muss vorkommen
NOTNOT errorDarf nicht vorkommen
( )(A OR B) AND CGruppierung / Vorrang
Beispiel mit Gruppierung
index=auth (failed OR error) AND NOT src_ip=10.0.0.1

Index – Datenquelle bestimmen

Der Index ist der primäre Filter. Ohne index= durchsucht Splunk alle Indizes, auf die du Zugriff hast — teuer!
Syntax-Varianten
-- Einzelner Index -- index=windows -- Mehrere Indizes (OR-Logik) -- index=windows OR index=linux -- Wildcard -- index=prod_* -- Alle Indizes (vermeiden!) -- index=*
Typische CSIRT-Indizes
Windows
index=windows
Firewall / NSM
index=network
EDR / Endpoint
index=edr
Auth / AD
index=auth
Index + Sourcetype kombinieren
index=windows sourcetype=WinEventLog:Security index=network sourcetype=cisco:asa

Metadata – Indizes & Sourcetypes entdecken

Was ist metadata? Ein spezieller SPL-Befehl, der nicht Events durchsucht, sondern die Metadaten der Indizes abfragt – z.B. welche Sourcetypes, Hosts oder Sources existieren, wann zuletzt Daten kamen, und wie viele Events vorhanden sind. Extrem schnell, da keine Event-Daten gelesen werden.
Alle verfügbaren Indizes anzeigen
-- Methode 1: über tstats (empfohlen, schnell) -- | tstats count as totalCount, max(_time) as recentTime where index=* by index | rename index as title | table title, totalCount, recentTime -- Methode 2: über eventcount -- | eventcount summarize=false index=* | dedup index | table index
Alle Sourcetypes eines Index anzeigen
-- Methode 1: über metadata (schnell, kein Event-Scan) -- | metadata type=sourcetypes index=botsv1 -- Methode 2: über stats (langsamer, liest Events) -- index=botsv1 | stats count by sourcetype
Alle Indizes mit ihren Sourcetypes (Gesamtübersicht)
-- tstats liefert index UND sourcetype zurück -- | tstats count as totalCount, max(_time) as recentTime where index=* by index, sourcetype | eval last_seen=strftime(recentTime, "%Y-%m-%d %H:%M:%S") | table index, sourcetype, totalCount, last_seen | sort index
Metadata-Typen
type=ZeigtTypischer Use Case
sourcetypesAlle SourcetypesWelche Log-Formate sind vorhanden?
sourcesAlle Source-PfadeWoher kommen die Daten?
hostsAlle HostsWelche Systeme liefern Logs?
Tipp: metadata ist ideal, um sich in einer unbekannten Splunk-Umgebung schnell zurechtzufinden – z.B. bei BOTSv1/v2/v3 Datasets.

Zeitraum & Time Handling

-- Relativ (empfohlen) -- index=windows earliest=-24h latest=now index=windows earliest=-7d@d latest=@d -- Absolut (Splunk-Format MM/DD/YYYY:HH:MM:SS) -- index=windows earliest="06/01/2025:00:00:00" latest="06/11/2025:23:59:59"
KürzelBedeutung
-1hLetzte Stunde
-24hLetzte 24 Stunden
-7d@d7 Tage, auf Tagesbeginn gerundet
@w0Anfang dieser Woche (Sonntag)
@monAnfang dieses Monats
Zeitkonvertierung in der Pipeline
-- Unix-Timestamp → lesbares Datum -- | eval human = strftime(_time, "%Y-%m-%d %H:%M:%S %Z") -- Datum-String → Unix-Timestamp -- | eval ts = strptime(date, "%d/%b/%Y:%H:%M:%S") -- Zeitdifferenz berechnen -- | eval minuten = round((end-start)/60, 2) -- Zeitgruppierung für Trends -- | bin _time span=5m | stats count by _time

Wichtige SPL-Befehle

BefehlBeschreibungBeispiel
statsAggregation: count, sum, avg, dc, valuesstats count by host
tableFelder als Tabelle ausgebentable _time, host, status
fieldsFelder ein-/ausschließenfields + host, status
whereFiltert nach Ausdruck auf Feldernwhere len(user) > 3
searchVolltext-Filter in der Pipelinesearch status=500
evalNeue Felder berechnen / transformiereneval gb=bytes/1GB
rexRegex-Extraktion auf Felderrex "user=(?<u>\w+)"
dedupDuplikate entfernendedup src_ip
sortSortieren (- = absteigend)sort -count, +host
head/tailErste / letzte N Ergebnissehead limit=20
renameFelder umbenennenrename data.user as user
top/rareHäufigste / seltenste Wertetop limit=10 useragent
timechartZeitbasierte Aggregation für Chartstimechart count by status
chartStatistik-Tabelle mit Splitchart avg(cpu) over host
lookupExterne Tabelle joinen (IOC)lookup ioc.csv ip OUTPUT cat
transactionEvents zu Sessions gruppierentransaction session maxspan=30m
eventstatsStatistiken als Felder hinzufügeneventstats avg(rt) by host
streamstatsLaufende Summen / Kumulativstreamstats sum(bytes) as total
binWerte in Buckets gruppierenbin _time span=5m
tstatsHigh-Perf. auf indizierten Felderntstats count where index=web
appendpipeSummenzeile ans Ergebnis anhängenappendpipe [stats sum(count)]
fillnullNull-Werte ersetzenfillnull value="N/A"
mvexpandMultivalue in Einzel-Eventsmvexpand recipients
inputlookupCSV als Datenquelle ladeninputlookup employees.csv
outputlookupErgebnisse in CSV schreibenoutputlookup results.csv

Praxis-Queries (CSIRT)

Brute-Force Logins erkennen
index=windows sourcetype=WinEventLog:Security EventCode=4625 | stats count by src_ip, Account_Name | where count > 10 | sort -count
PowerShell Execution mit Encoded Command
index=edr process_name="powershell.exe" CommandLine=*-EncodedCommand* | table _time, host, user, CommandLine | sort -_time
Lateral Movement via Remote Services (4648)
index=windows EventCode=4648 earliest=-1h | eval pair=src_user." → ".dest_host | stats count values(pair) as targets by src_host | where count > 3
IOC-Lookup gegen Threat Intel Liste
index=network sourcetype=firewall | lookup malicious_ips.csv dest_ip OUTPUT threat_category | where isnotnull(threat_category) | table _time, src_ip, dest_ip, threat_category
Neues Gerät im Netz (first seen)
index=network earliest=-30d | stats min(_time) as first_seen by src_mac, src_ip | where first_seen > relative_time(now(), "-24h") | convert ctime(first_seen)

Wildcards & Feldsuche

-- Wildcard am Ende (effizient) -- process_name=powershell* -- Wildcard in der Mitte (teurer) -- CommandLine=*mimikatz* -- Feld existiert / ist befüllt -- src_ip=* -- Feld nicht vorhanden -- NOT error_code=*
Performance: Wildcards am Anfang (*xyz) erzwingen einen Full-Scan. Wenn möglich vermeiden oder mit Index-Filterung kombinieren.

eval – Felder berechnen

-- String-Konkatenation -- | eval asset=host.":".src_ip -- Bedingte Logik -- | eval severity=if(count>100, "HIGH", "LOW") -- case()-Ausdruck (sauberer als verschachteltes if) -- | eval risk=case( score>80, "Critical", score>50, "Medium", 1==1, "Low" ) -- Coalesce – ersten nicht-null Wert zurückgeben -- | eval user=coalesce(username, email, "anonymous") -- Unix-Zeit zu lesbarem Datum -- | eval human_time=strftime(_time, "%Y-%m-%d %H:%M:%S") -- Datum-String zu Unix-Timestamp -- | eval epoch=strptime(date_field, "%d/%b/%Y:%H:%M:%S") -- Math: Bytes in GB -- | eval gb=bytes/1024/1024/1024 -- Standardabweichung für Ausreisser -- | eventstats avg(rt) as avg_rt, stdev(rt) as stdev_rt | where rt > (avg_rt + 2*stdev_rt)

String-Operationen

Konkatenation
| eval full_name = first_name . " " . last_name
Substring
| eval short = substr(word, 1, 3)
Regex Replacement (rex mode=sed)
| rex mode=sed field=msg "s/\s+/ /g" | rex mode=sed field=email "s/@/ [at] /"
replace – einfache Ersetzung
| replace "localhost" with "127.0.0.1" in host
Whitespace ignorieren beim Match
-- Erst extrahieren, dann vergleichen -- | rex field=MessageStatus "(?<status>\w+)" | eval ok=if(status="delivered", "yes", "no")
Multivalue-Felder
-- In einzelne Events aufsplitten -- | mvexpand recipients -- Bestimmten Index aus Multivalue -- | eval first = mvindex(items, 0) -- Mit Trennzeichen joinen -- | eval userlist = mvjoin(users, ", ") -- Multivalue-Felder filtern -- | eval errors = mvfilter(match(msgs, "ERROR"))

Filterung: where vs search

where – Ausdrücke & Funktionen
-- Feldnamen OHNE Anführungszeichen, KEINE Wildcards -- | where isnotnull(user) AND len(user) > 3 | where like(url, "%/api/%")
search – Keyword-Matching mit Wildcards
-- Strings werden als Literale behandelt, Wildcards erlaubt -- | search user=admin* status!=404
NOT vs != (wichtiger Unterschied!)
-- != schliesst NULL/leere Felder NICHT aus -- -- NOT schliesst auch leere Felder aus -- status!=500 -- 500er werden gefiltert, NULL bleibt drin NOT status=500 -- auch Events OHNE status-Feld werden gefiltert
Merke: != filtert nur explizite Werte. Mit NOT werden auch Events ohne das Feld ausgeschlossen. Für "alles außer X mit Feld" nimm !=; für "garantiert keine X" nimm NOT.

Analyse: Perzentile & Ausreisser

Perzentile
index=web | stats avg(rt) as avg_rt, median(rt) as p50_rt, perc95(rt) as p95_rt, perc99(rt) as p99_rt by endpoint
Ausreisser mit Standardabweichung
index=web | eventstats avg(rt) as avg_rt, stdev(rt) as stdev_rt | where rt > (avg_rt + 2*stdev_rt)
Statistische Verteilung
| stats count, dc(src_ip) as unique_visitors, values(useragent) as ua_list by endpoint

Lookup-Tabellen

CSV-Lookup zur Anreicherung
index=network | lookup threat_intel.csv ip as src_ip OUTPUT threat_level, category
Lookup als Datenquelle nutzen
| inputlookup employees.csv | search department="IT"
Ergebnisse in Lookup schreiben
index=web status=500 | stats count by src_ip | where count > 100 | outputlookup suspicious_ips.csv
Use Case: IOC-Liste als CSV anlegen und per lookup gegen Firewall-Logs joinen → sofortige Trefferanzeige.

Transaktionen

Session Grouping (teuer)
index=web | transaction session_id maxspan=30m maxpause=5m | where duration > 60 | table session_id, duration, eventcount
Alternative mit stats (schneller)
| stats min(_time) as start, max(_time) as end, count as events by session_id | eval duration = end - start | where duration > 60
Performance: transaction ist ressourcenintensiv. Nutze stats + min(_time) / max(_time) wenn möglich.

Mehrere Queries kombinieren

Subsearch
-- Ergebnisse einer Suche als Input für die Hauptsuche -- index=web user=[search index=vpn | dedup user | fields user] | stats count by user
Join
index=main | join type=left user [ search index=hr | stats count by user ]
Append
index=web status=500 | stats count as errors by host | append [ search index=web status=200 | stats count as success by host ]
Tipp: Subsearch-Ergebnisse werden auf 10.000 Events begrenzt (einstellbar mit format). Subsearch immer in eckige Klammern [ ] setzen!

Performance-Tipps

-- ① Früh filtern: Index + Sourcetype zuerst -- index=web sourcetype="access_log" status=500 statt: index=* | search sourcetype="access_log" -- ② Keine Wildcards am Anfang -- *error ✗ (Full-Scan) error* ✓ (Index-gefiltert) -- ③ fields-Kommando nutzen -- | fields host, status, response_time | stats avg(response_time) by host -- ④ tstats für indizierte Felder (schneller) -- | tstats count where index=web by host, status -- ⑤ Möglichst kurzen Zeitraum wählen -- earliest=-7d statt earliest=-30d -- ⑥ stats statt transaction verwenden -- | stats min(_time) as start by session -- ✓ schnell | transaction session -- ✗ langsam

Reguläre Ausdrücke (rex)

Felder extrahieren
-- Einfache Extraktion -- | rex field=_raw "user=(?<username>\w+)" -- Mehrere Felder auf einmal -- | rex field=url "\/api\/(?<version>v\d+)\/(?<endpoint>\w+)"
Mehrere Treffer pro Event (max_match)
| rex field=_raw max_match=0 "error_code=(?<codes>\d+)"
Häufige Regex-Patterns
# IP-Adresse: (?<ip>\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}) # E-Mail: (?<email>[\w.+-]+@[\w-]+\.\w+) # UUID: (?<uuid>[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}) # URL-Pfad: (?<path>\/[\w\/.-]+) # Domain aus URL: (?<domain>(?:https?:\/\/)?([\w.-]+)\/)

Tips & Tricks

Debugging
fieldsummary
|
Alerting
error_rate > 5%
|
Macro
`get_errors(index)`
Schnelle Feldübersicht
| fieldsummary -- Alle Felder + Stats | top limit=20 useragent -- Häufigste Werte | highlight error, warning, critical -- Keywords hervorheben
Alerting – Error Rate überwachen
index=web | bin _time span=5m | stats count(eval(status>=500)) as errors, count as total by _time | eval error_rate = round(errors/total*100, 2) | where error_rate > 5
Nützliche SPL-Idiome
-- Null-Werte ersetzen -- | fillnull value="N/A" username, email -- Multivalue deduplizieren & joinen -- | eval users = mvjoin(mvdedup(user_list), ", ") -- Bedingte Aggregation -- | stats count(eval(status="success")) as successes, count(eval(status="failure")) as failures -- dedup mit Sortierung (jüngsten Treffer behalten) -- | sort -_time | dedup src_ip -- rename für verschachtelte Felder vor eval -- | rename signals.ip_address as ip_addr | eval ip_addr = if(isnull(ip_addr), "unknown", ip_addr)

JSON & strukturierte Daten (spath)

spath extrahiert Felder aus JSON/XML. Splunk parst Top-Level-JSON oft automatisch – für verschachtelte Pfade brauchst du spath explizit.
Verschachteltes Feld extrahieren
index=api sourcetype=json | spath output=user path=event.actor.username | spath output=action path=event.action | table _time, user, action
JSON aus einem Feld (statt _raw) parsen
| spath input=payload -- danach sind alle Keys als Felder verfügbar --
Array-Elemente ansprechen & aufsplitten
| spath path=event.tags{} output=tags | mvexpand tags -- Einzelnes Array-Element (0-basiert) -- | spath path=event.ips{0} output=first_ip
Tipp: {} steht für „alle Array-Elemente", {0} für ein bestimmtes. Mit | eval x=json_extract(_raw, "a.b") geht Ähnliches direkt in eval.

IP- & Netzwerk-Analyse

CIDR-Match – gehört IP zu Subnetz?
index=network | where cidrmatch("10.0.0.0/8", src_ip) -- interne vs. externe IPs klassifizieren -- | eval zone=if(cidrmatch("10.0.0.0/8", src_ip), "intern", "extern")
GeoIP – Herkunft anreichern
index=network action=blocked | iplocation src_ip | stats count by Country, City | sort -count
Geo-Karte rendern
| iplocation src_ip | geostats count
Verdächtige Länder / seltene Geo-Logins
index=auth action=success | iplocation src_ip | stats dc(Country) as laender values(Country) as liste by user | where laender > 1
Impossible Travel: Logins desselben Users aus mehreren Ländern in kurzer Zeit sind ein starkes Kompromittierungs-Signal.

Encoding & Decoding (CSIRT)

Angreifer verschleiern gerne per Base64 / URL-Encoding. Beim Decodieren wird der wahre Befehl sichtbar.
URL-Decoding (nativ)
| eval clear = urldecode(request_uri)
PowerShell -EncodedCommand isolieren
index=edr CommandLine=*-enc* | rex field=CommandLine "-[eE]nc(?:odedCommand)?\s+(?<b64>[A-Za-z0-9+/=]+)" | table _time, host, user, b64
Base64 decodieren (URL Toolbox App)
-- benötigt Add-on "URL Toolbox" (SplunkBase) -- | eval decoded = base64decode(b64) -- PowerShell nutzt UTF-16LE → NUL-Bytes entfernen -- | rex mode=sed field=decoded "s/\x00//g"
Hinweis: base64decode() ist keine Core-Funktion – sie kommt aus der App URL Toolbox. Ohne App per Lookup/Script decodieren.

Windows Security Event Codes (Referenz)

EventCodeBedeutungRelevanz für Detection
4624Erfolgreiche AnmeldungLogon-Type prüfen (3=Netzwerk, 10=RDP)
4625Fehlgeschlagene AnmeldungBrute-Force / Password-Spraying
4634/4647LogoffSession-Dauer, Transaktionen
4648Logon mit expliziten CredentialsLateral Movement, RunAs
4672Sonderrechte bei AnmeldungAdmin-/Privileged-Logon
4688Prozess erstelltProcess-Tree, verdächtige CommandLine
4689Prozess beendetLebensdauer kurzlebiger Prozesse
4720Benutzerkonto erstelltPersistenz / Rogue-Accounts
4726Benutzerkonto gelöschtAnti-Forensik
4728/4732Zu (Global/Local) Gruppe hinzugefügtPrivilege Escalation
4740Konto gesperrt (Lockout)Brute-Force-Nebeneffekt
4768/4769Kerberos TGT / Service-TicketKerberoasting, Golden/Silver Ticket
1102Audit-Log gelöschtAnti-Forensik – immer alarmieren
7045Neuer Dienst installiertPersistenz (System-Log)
4104PowerShell Script Block LoggingObfuskierte Skripte (Op.-Log)
Tipp: 4688 & 4104 müssen per GPO aktiviert werden – sonst fehlen dir die wertvollsten Detection-Quellen.

Data Models & tstats (CIM)

Beschleunigte Data Models (CIM) erlauben blitzschnelle Suchen über normalisierte Felder – ideal für Dashboards & Correlation Searches.
tstats auf beschleunigtem Data Model
| tstats summariesonly=true count from datamodel=Authentication where Authentication.action=failure by Authentication.user, Authentication.src
Data Model als Datenquelle
| datamodel Network_Traffic All_Traffic search | stats sum(All_Traffic.bytes) as bytes by All_Traffic.dest
Verfügbare Data Models auflisten
| datamodel
summariesonly=true nutzt nur beschleunigte Daten (sehr schnell), verpasst aber noch nicht beschleunigte Events. Für Vollständigkeit: false.

REST API & Introspection

Der | rest-Befehl fragt die Splunk-Management-API ab – z.B. für Indizes (auch leere!), User oder laufende Suchen.
Alle Indizes inkl. Größe & Zeitspanne
| rest /services/data/indexes | table title, totalEventCount, currentDBSizeMB, maxTime, minTime | sort -currentDBSizeMB
Aktuell laufende Suchen (Jobs)
| rest /services/search/jobs | search isDone=0 | table author, label, runDuration, sid
Aktivierte Correlation Searches / Alerts
| rest /services/saved/searches | search is_scheduled=1 disabled=0 | table title, cron_schedule, search
Use Case: | rest ist perfekt fürs Onboarding auf einem fremden Search Head – zeigt Lizenz, Indizes und Konfiguration ohne CLI-Zugriff.

Testing & Utility-Befehle

Testdaten ohne Index (makeresults)
| makeresults count=5 | streamstats count as row | eval status = if(row%2==0, "ok", "fail")
foreach – über mehrere Felder iterieren
| foreach *_bytes [ eval <<FIELD>>_mb = '<<FIELD>>'/1024/1024 ]
Summenzeile / Spaltensummen
| stats count by host, status | addtotals row=true col=true labelfield=host
Feld-Diagnose einer unbekannten Quelle
index=neu | fieldsummary | table field, count, distinct_count, values
Ergebnisspalten transponieren
| stats count by status | transpose

Detection Engineering: Baselines & Anomalien

Rolling Baseline mit streamstats (Zeitfenster)
index=proxy | bin _time span=1h | stats sum(bytes_out) as bytes by _time, src_ip | streamstats window=24 avg(bytes) as avg, stdev(bytes) as sd by src_ip | eval zscore = round((bytes-avg)/sd, 2) | where zscore > 3 -- 3 Sigma = Ausreisser
Rare / erstmalig gesehene Kombinationen (first seen)
index=edr earliest=-30d | stats earliest(_time) as first, latest(_time) as last, count by host, process_name | where first > relative_time(now(), "-24h") -- erst heute aufgetaucht | convert ctime(first) ctime(last)
Beaconing erkennen (gleichmäßige Intervalle)
index=proxy | sort 0 src_ip, _time | streamstats current=f last(_time) as prev by src_ip, dest | eval delta = _time - prev | stats count, avg(delta) as avg_delta, stdev(delta) as sd_delta by src_ip, dest | where count > 20 AND sd_delta < 5 -- regelmäßig = verdächtig
Textcluster – ähnliche Log-Zeilen gruppieren
index=app error | cluster field=_raw t=0.8 showcount=true | table cluster_count, _raw | sort -cluster_count
Merke: eventstats/streamstats berechnen Statistiken ohne die Events zu kollabieren – dadurch kannst du jedes Event gegen seine Baseline vergleichen (Z-Score, Abweichung).

Search Macros – Wiederverwendung

Macros kapseln wiederkehrende SPL-Bausteine. Aufruf mit Backticks `name`. Definiert unter Settings → Advanced Search → Macros.
Macro ohne Argumente aufrufen
index=windows | `windows_logons` -- expandiert z.B. zu: EventCode=4624 OR EventCode=4625 --
Macro mit Argumenten
-- Definition: failed_logins(1) => src=$src$ EventCode=4625 -- index=windows | `failed_logins(10.0.0.5)`
Macro-Definition ansehen (Introspection)
| rest /servicesNS/-/-/admin/macros | table title, definition, args
Tipp: Macro-Namen mit Argumenten brauchen die Anzahl im Namen: failed_logins(1). Ideal für standardisierte Detection-Logik im ganzen Team.

Alerts & Scheduled Searches

Eine gespeicherte Suche + Trigger-Bedingung + Aktion (E-Mail, Webhook, Notable) = Alert. Zeitplan per Cron.
Trigger-Query: mehr als N Treffer
index=windows EventCode=4625 | stats count by src_ip | where count > 20 -- Alert-Trigger: "Number of Results > 0" --
Throttling gegen Alert-Flut
EinstellungZweck
Trigger: per-resultPro Treffer feuern
Throttle by src_ipGleiche IP nicht doppelt melden
Suppress 1hRuhezeit nach Alarm
Cron-Zeitplan (Beispiele)
*/5 * * * * -- alle 5 Minuten 0 * * * * -- jede volle Stunde 0 6 * * 1 -- montags 06:00 Uhr
Best Practice: Zeitfenster (earliest) etwas größer als das Cron-Intervall wählen (Überlappung), damit keine Events zwischen zwei Läufen durchrutschen.

join & Subsearch vermeiden – bessere Muster

join und Subsearches sind auf 50.000 Zeilen (join) bzw. 10.000 Events / 60 s (subsearch) begrenzt und oft langsam. Meist gibt es ein schnelleres, vollständiges stats-Muster.
❌ join (langsam, limitiert)
index=web | join user [ search index=hr | fields user, dept ]
✓ stats-Merge (schnell, kein Limit)
index=web OR index=hr | stats values(dept) as dept, count(eval(index=="web")) as hits by user | where hits > 0
✓ Lookup statt Subsearch-Filter
-- statt: index=web user=[search index=vpn | fields user] -- index=vpn | stats count by user | outputlookup vpn_users.csv index=web | lookup vpn_users.csv user | where isnotnull(count)
✓ format – Subsearch bewusst als Filter
-- wenn Subsearch nötig: Ergebnis als (a OR b OR ...) -- index=vpn | top limit=100 user | fields user | format
Faustregel: Sobald du join tippst, frag dich: „Geht das mit stats … by <key> über beide Indizes?" – meistens ja, und ohne Zeilenlimit.

MITRE ATT&CK – Detection-Mapping

Beispiel-Queries je Taktik. Feldnamen an dein Datenmodell anpassen – die Muster gelten generisch (auch für BOTSv1/v2/v3).
TechnikDetection-IdeeSPL-Kern
T1110 Brute ForceViele 4625 pro QuelleEventCode=4625 | stats count by src_ip | where count>20
T1059.001 PowerShellEncoded/obfuskiert4104 OR CommandLine=*-enc* | search *FromBase64String*
T1071 C2 / BeaconingGleichmäßige Intervallestreamstats … | stats stdev(delta) | where sd<5
T1078 Valid AccountsLogin aus neuem Landiplocation src_ip | stats dc(Country) by user | where >1
T1053 Scheduled Taskschtasks / 4698EventCode=4698 OR process_name=schtasks.exe
T1003 Cred Dumpinglsass-Zugriff / mimikatzCommandLine=*mimikatz* OR TargetImage=*lsass.exe*
T1070.001 Log ClearingAudit-Log gelöschtEventCode=1102
T1105 Ingress Transfercertutil/bitsadmin Downloadprocess_name IN (certutil.exe,bitsadmin.exe) CommandLine=*http*
Tipp: Wenn die App Splunk Security Essentials installiert ist, findest du für viele Techniken fertige Beispiel-Suchen inkl. ATT&CK-Navigator-Export.

Boss of the SOC (BOTS) – Starter-Queries

Die BOTSv1/v2/v3-Datasets sind Übungsdaten für SOC-Analysten. Diese Queries helfen beim ersten Erkunden – Index heißt je nach Setup botsv1, botsv2 oder botsv3.
1 · Überblick: was steckt drin?
| tstats count where index=botsv1 by sourcetype | sort -count
2 · Web-Angriffe finden (Stream/Suricata)
index=botsv1 sourcetype=stream:http http_method=POST | search form_data=*passwd* OR uri=*sql* | table _time, src_ip, dest_ip, uri, http_user_agent
3 · Verdächtige User-Agents (Scanner-Fingerprint)
index=botsv1 sourcetype=stream:http | stats count by http_user_agent | search http_user_agent=*acunetix* OR http_user_agent=*nikto* OR http_user_agent=*sqlmap*
4 · Sysmon: Prozess-Ketten (Parent → Child)
index=botsv1 sourcetype="XmlWinEventLog:Microsoft-Windows-Sysmon/Operational" EventCode=1 | table _time, Computer, ParentImage, Image, CommandLine | sort -_time
Workflow: Erst mit Query 1 die Sourcetypes entdecken, dann gezielt pro Sourcetype tiefer graben. Genau dafür ist | tstats … by sourcetype der schnellste Einstieg.
Keine Treffer für „" – andere Suche versuchen.