Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Overview for SOC Teams

If you build detections, hunt, or respond to alerts in Splunk, this is the chapter that matters.

The Problem with SPL for Detection Logic

SPL is excellent at finding and aggregating events. It is poor at expressing judgement, and detection engineering is mostly judgement.

Consider a rule you have probably written: "alert when a host talks to a domain that looks algorithmically generated, but only if the volume is unusual for that host, and not if the domain is on our allow-list, and treat first-party CDNs differently".

In SPL that becomes a chain of eval expressions, three lookups, a subsearch you are slightly afraid of, and a comment explaining what the regular expression on line 14 is for. It works. Nobody wants to change it.

In Python it is thirty readable lines with a name for each concept.

What Changes

Logic lives where you can read it. Loops, dictionaries, functions, try/except. A detection becomes something a new starter can follow.

Enrichment happens inline. Threat intelligence, asset ownership, identity context: fetch it in the same search that found the event, rather than in a lookup that is refreshed by a different job you also have to maintain.

Iteration is instant. Change five characters, press enter. No app to package, no restart, no change request.

The prototype is the production artefact. The snippet you refined interactively is the same code the scheduled alert runs and the alert action executes. There is no rewrite step where the logic drifts.

The Four Places It Plugs in

WhereCommandRuns
DetectionsexecIn a scheduled search, shaping and scoring what it finds
Threat huntingexecInteractively, while you think
Adaptive responseexec in a saved searchFrom an Enterprise Security notable
Alert actionsexecalertAfter a scheduled search fires, over its results

And one that does not fit a table: Response plans, where the same code drives multi-step containment.

A Worked Example

Data exfiltration to a newly-registered domain. The detection has to combine volume, domain shape and history, which is three different kinds of judgement.

index=proxy earliest=-1h
| stats sum(bytes_out) as bytes_out count as requests
        dc(http_user_agent) as agents by host, user, dest_host
| exec inline="import math

def entropy(text):
    counts = {}
    for ch in text:
        counts[ch] = counts.get(ch, 0) + 1
    total = len(text)
    return -sum((c/total) * math.log2(c/total) for c in counts.values())

ALLOWED_SUFFIXES = ('.microsoft.com', '.github.com', '.splunk.com', '.gov.uk')

findings = []
for e in events:
    domain = e['dest_host']
    if domain.endswith(ALLOWED_SUFFIXES):
        continue

    labels = domain.split('.')
    score = 0
    reasons = []

    h = entropy(domain)
    if h > 3.6:
        score += 30
        reasons.append(f'high entropy ({h:.2f})')

    longest = max(len(l) for l in labels)
    if longest > 15:
        score += 25
        reasons.append(f'long label ({longest} chars)')

    megabytes = int(e['bytes_out']) / 1048576
    if megabytes > 50:
        score += 30
        reasons.append(f'{megabytes:.0f} MB outbound')

    if int(e['agents']) == 1 and int(e['requests']) > 50:
        score += 15
        reasons.append('single user agent, high volume')

    if score >= 50:
        e['risk_score'] = score
        e['reasons'] = reasons
        e['megabytes_out'] = round(megabytes, 1)
        e['entropy'] = round(h, 2)
        findings.append(e)

events = findings"
| sort - risk_score
| table host user dest_host risk_score megabytes_out entropy reasons

Entropy-based scoring in a real search

Every threshold has a name. Every contribution to the score is attached to the result as a reason, so the analyst who picks up the alert can see why it fired without opening the detection. That last property is worth more than the detection itself.

Getting the Data Right First

Do the heavy lifting in SPL and hand a small, shaped result to Python.

index=proxy
| stats sum(bytes_out) as bytes_out by host, dest_host   ← SPL: aggregate
| exec inline="..."                                       ← Python: judge

Aggregating 4 million events into 300 rows and then scoring them is fast. Scoring all 4 million works too, but there is rarely a reason to.

Where to Next