DEV Community

splforge
splforge

Posted on

I built a linter for Splunk SPL — then ran it against Splunk's own detection library

Every Splunk shop has them: the index=* search that scans the world, the
join that silently drops half its rows, the sort with no limit that pages
gigabytes to disk. We catch them in code review — when we catch them at all.

Python has ruff. JavaScript has eslint. Terraform has tflint. SPL, the
language at the heart of every Splunk dashboard, alert and correlation search,
had nothing. So I built splint: a fast, zero-dependency linter for SPL.

This post is less about the code and more about what happened when I pointed it
at Splunk's own security_content
repository — 2,114 production detection searches — and let it rip.

What splint checks

splint ships with six rules today, split between performance and style:

Code Flags Why it matters
SPL001 index=* / index=win* Wildcards in the index specifier force broad scans
SPL002 field=*value in the base search Leading wildcards defeat term/index lookups
SPL003 join Expensive, and silently truncates the subsearch
SPL004 transaction Memory-heavy and not distributable
SPL005 sort without a limit Sorts the entire result set
SPL101 missing space around Readability

No runtime dependencies, runs on any Python 3.11+, and emits text, json or
sarif so it drops straight into CI:

$ splint query.spl
query.spl:1:1: SPL001 `index=*` uses a wildcard; specify explicit indexes...
query.spl:2:2: SPL003 `join` is costly and caps subsearch results (default 50k)...

Found 2 issues.
Enter fullscreen mode Exit fullscreen mode

Exit code 1 when it finds something, 0 when it's clean. That's the whole CI
integration.

The honest part: testing on real SPL

Toy examples prove nothing. A linter lives or dies on its false-positive rate —
the fastest way to get a tool uninstalled is to make it cry wolf. So before
writing a single line of marketing, I cloned security_content, extracted the
search: field from every detection into a .spl file, and ran splint across
all 2,114 of them.

The first run was humbling.

SPL002 (leading-wildcard): 1766
SPL101 (pipe-spacing):     1136
SPL003 (join):                53
SPL004 (transaction):          9
SPL005 (unbounded-sort):      23
Enter fullscreen mode Exit fullscreen mode

Two numbers jumped out — and both were telling me something was wrong with the
linter, not the detections.

SPL101: 1,136 "errors" that weren't

Splunk searches very often begin with a pipe:

| tstats `summariesonly` count from datamodel=Endpoint.Processes by ...
Enter fullscreen mode Exit fullscreen mode

splint was dutifully flagging every one of those leading pipes as "missing
space before |". But there's nothing before the pipe — it's the start of the
search. That's not a style violation, it's idiomatic SPL.

One fix later — a pipe with only whitespace before it is a leading pipe; only
the spacing after it is meaningful
— and SPL101 dropped from 1,136 to 69.
The 69 that remained were genuine: )| stats, count|table, real missing
spaces mid-pipeline.

SPL002: correct, but crying wolf

The 1,766 leading-wildcard hits were a subtler problem. They weren't wrong
Image="*\\powershell.exe" really does have a leading wildcard. But in
detection engineering, matching a process by its executable name requires
that wildcard; you rarely know the full path. 85% of the hits were exactly this
shape, and they were largely unavoidable.

A rule that fires 1,766 times, mostly on things the author can't change, is a
rule people will disable wholesale — taking the genuinely useful hits with it.

So I made two changes:

  1. Severity → info. Leading wildcards are worth knowing about, not worth failing a build over.
  2. Scoped to the base search. A leading wildcard hurts most during index retrieval. Inside a tstats against an accelerated data model, or in a downstream where/eval, the cost is marginal — so splint no longer flags those.

What was left

After the tuning, the warnings across 2,114 real detections settled into a
tight, trustworthy set:

  • 53 uses of join
  • 9 uses of transaction
  • 23 unbounded sorts

That's ~85 genuine, actionable performance findings in Splunk's own flagship
content — every one worth a second look — instead of 2,900 mostly-noise alerts.
That ratio is the difference between a tool you keep and a tool you mute.

Using it

pip install splint-spl       # Linux, macOS, Windows; Python 3.11+
Enter fullscreen mode Exit fullscreen mode

Pre-commit hook:

repos:
  - repo: https://github.com/splforge/splint
    rev: v0.1.0
    hooks:
      - id: splint
Enter fullscreen mode Exit fullscreen mode

Inline suppression when you really do mean that wildcard, using an SPL comment:

index=* | stats count   ```noqa: SPL001```
Enter fullscreen mode Exit fullscreen mode

Configuration via .spl-lint.toml or a [tool.splint] table in
pyproject.toml, with select / ignore lists.

What's next

The CLI is the foundation. The next step is a Splunk add-on — a | splint
custom search command and a deployment-wide audit dashboard that scans every
saved search in your environment and reports the lint issues back into Splunk
itself. That's where this becomes useful to admins, not just CI pipelines.

If you write SPL for a living, I'd love your eyes on the rules — especially
which checks you'd want next. The repo is on GitHub, the package is on PyPI, and
the issue tracker is open.

(Unrelated to the older splint Python linter on PyPI — this one installs as splint-spl.)

Lint your searches before they lint your weekend.

Top comments (0)