<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: splforge</title>
    <description>The latest articles on DEV Community by splforge (@splforge_dev).</description>
    <link>https://dev.to/splforge_dev</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4009747%2F7dabe053-59d0-4967-88b8-1e7a96418e78.png</url>
      <title>DEV Community: splforge</title>
      <link>https://dev.to/splforge_dev</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/splforge_dev"/>
    <language>en</language>
    <item>
      <title>I built a linter for Splunk SPL — then ran it against Splunk's own detection library</title>
      <dc:creator>splforge</dc:creator>
      <pubDate>Tue, 30 Jun 2026 14:32:13 +0000</pubDate>
      <link>https://dev.to/splforge_dev/i-built-a-linter-for-splunk-spl-then-ran-it-against-splunks-own-detection-library-88k</link>
      <guid>https://dev.to/splforge_dev/i-built-a-linter-for-splunk-spl-then-ran-it-against-splunks-own-detection-library-88k</guid>
      <description>&lt;p&gt;Every Splunk shop has them: the &lt;code&gt;index=*&lt;/code&gt; search that scans the world, the&lt;br&gt;
&lt;code&gt;join&lt;/code&gt; that silently drops half its rows, the &lt;code&gt;sort&lt;/code&gt; with no limit that pages&lt;br&gt;
gigabytes to disk. We catch them in code review — when we catch them at all.&lt;/p&gt;

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

&lt;p&gt;This post is less about the code and more about what happened when I pointed it&lt;br&gt;
at Splunk's own &lt;a href="https://github.com/splunk/security_content" rel="noopener noreferrer"&gt;&lt;code&gt;security_content&lt;/code&gt;&lt;/a&gt;&lt;br&gt;
repository — 2,114 production detection searches — and let it rip.&lt;/p&gt;
&lt;h2&gt;
  
  
  What splint checks
&lt;/h2&gt;

&lt;p&gt;splint ships with six rules today, split between performance and style:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Code&lt;/th&gt;
&lt;th&gt;Flags&lt;/th&gt;
&lt;th&gt;Why it matters&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;SPL001&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;index=*&lt;/code&gt; / &lt;code&gt;index=win*&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Wildcards in the index specifier force broad scans&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;SPL002&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;field=*value&lt;/code&gt; in the base search&lt;/td&gt;
&lt;td&gt;Leading wildcards defeat term/index lookups&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;SPL003&lt;/td&gt;
&lt;td&gt;&lt;code&gt;join&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Expensive, and silently truncates the subsearch&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;SPL004&lt;/td&gt;
&lt;td&gt;&lt;code&gt;transaction&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Memory-heavy and not distributable&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;SPL005&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;sort&lt;/code&gt; without a limit&lt;/td&gt;
&lt;td&gt;Sorts the entire result set&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;SPL101&lt;/td&gt;
&lt;td&gt;missing space around&lt;/td&gt;
&lt;td&gt;Readability&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;No runtime dependencies, runs on any Python 3.11+, and emits &lt;code&gt;text&lt;/code&gt;, &lt;code&gt;json&lt;/code&gt; or&lt;br&gt;
&lt;code&gt;sarif&lt;/code&gt; so it drops straight into CI:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;splint query.spl
&lt;span class="gp"&gt;query.spl:1:1: SPL001 `index=*` uses a wildcard;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;specify explicit indexes...
&lt;span class="go"&gt;query.spl:2:2: SPL003 `join` is costly and caps subsearch results (default 50k)...

Found 2 issues.
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Exit code &lt;code&gt;1&lt;/code&gt; when it finds something, &lt;code&gt;0&lt;/code&gt; when it's clean. That's the whole CI&lt;br&gt;
integration.&lt;/p&gt;
&lt;h2&gt;
  
  
  The honest part: testing on real SPL
&lt;/h2&gt;

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

&lt;p&gt;The first run was humbling.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;SPL002 (leading-wildcard): 1766
SPL101 (pipe-spacing):     1136
SPL003 (join):                53
SPL004 (transaction):          9
SPL005 (unbounded-sort):      23
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two numbers jumped out — and both were telling me something was wrong with the&lt;br&gt;
&lt;strong&gt;linter&lt;/strong&gt;, not the detections.&lt;/p&gt;
&lt;h3&gt;
  
  
  SPL101: 1,136 "errors" that weren't
&lt;/h3&gt;

&lt;p&gt;Splunk searches very often &lt;em&gt;begin&lt;/em&gt; with a pipe:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;| tstats `summariesonly` count from datamodel=Endpoint.Processes by ...
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



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

&lt;p&gt;One fix later — &lt;em&gt;a pipe with only whitespace before it is a leading pipe; only&lt;br&gt;
the spacing after it is meaningful&lt;/em&gt; — and SPL101 dropped from &lt;strong&gt;1,136 to 69&lt;/strong&gt;.&lt;br&gt;
The 69 that remained were genuine: &lt;code&gt;)| stats&lt;/code&gt;, &lt;code&gt;count|table&lt;/code&gt;, real missing&lt;br&gt;
spaces mid-pipeline.&lt;/p&gt;
&lt;h3&gt;
  
  
  SPL002: correct, but crying wolf
&lt;/h3&gt;

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

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

&lt;p&gt;So I made two changes:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Severity → &lt;code&gt;info&lt;/code&gt;.&lt;/strong&gt; Leading wildcards are worth knowing about, not worth
failing a build over.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Scoped to the base search.&lt;/strong&gt; A leading wildcard hurts most during index
retrieval. Inside a &lt;code&gt;tstats&lt;/code&gt; against an accelerated data model, or in a
downstream &lt;code&gt;where&lt;/code&gt;/&lt;code&gt;eval&lt;/code&gt;, the cost is marginal — so splint no longer flags
those.&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;
  
  
  What was left
&lt;/h3&gt;

&lt;p&gt;After the tuning, the warnings across 2,114 real detections settled into a&lt;br&gt;
tight, trustworthy set:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;53&lt;/strong&gt; uses of &lt;code&gt;join&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;9&lt;/strong&gt; uses of &lt;code&gt;transaction&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;23&lt;/strong&gt; unbounded &lt;code&gt;sort&lt;/code&gt;s&lt;/li&gt;
&lt;/ul&gt;

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


&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;pip install splint-spl       #&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;Linux, macOS, Windows&lt;span class="p"&gt;;&lt;/span&gt; Python 3.11+
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Pre-commit hook:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;repos&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;repo&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;https://github.com/splforge/splint&lt;/span&gt;
    &lt;span class="na"&gt;rev&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;v0.1.0&lt;/span&gt;
    &lt;span class="na"&gt;hooks&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;splint&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Inline suppression when you really do mean that wildcard, using an SPL comment:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;index=* | stats count   ```noqa: SPL001```
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Configuration via &lt;code&gt;.spl-lint.toml&lt;/code&gt; or a &lt;code&gt;[tool.splint]&lt;/code&gt; table in&lt;br&gt;
&lt;code&gt;pyproject.toml&lt;/code&gt;, with &lt;code&gt;select&lt;/code&gt; / &lt;code&gt;ignore&lt;/code&gt; lists.&lt;/p&gt;

&lt;h2&gt;
  
  
  What's next
&lt;/h2&gt;

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

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

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;GitHub:&lt;/strong&gt; &lt;a href="https://github.com/splforge/splint" rel="noopener noreferrer"&gt;https://github.com/splforge/splint&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;PyPI:&lt;/strong&gt; &lt;a href="https://pypi.org/project/splint-spl/" rel="noopener noreferrer"&gt;https://pypi.org/project/splint-spl/&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;em&gt;(Unrelated to the older &lt;code&gt;splint&lt;/code&gt; Python linter on PyPI — this one installs as &lt;code&gt;splint-spl&lt;/code&gt;.)&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Lint your searches before they lint your weekend.&lt;/p&gt;

</description>
      <category>splunk</category>
      <category>security</category>
      <category>python</category>
      <category>devops</category>
    </item>
  </channel>
</rss>
