It converted, it never fired: three Sigma-to-Elastic bugs found by testing on a real engine
A Sigma rule can convert to an Elastic query without a single error, deploy without a warning, and never fire. I found three ways that happens while building a detection-as-code pipeline, reproduced each one on Elasticsearch 9.5.5, and reported them to the SigmaHQ project with fixes for two.
None of them showed up in conversion. All of them showed up the first time the converted query ran against real events.
The rule that caught them
The pipeline keeps Sigma rules as the single source of truth and converts them for Splunk, Microsoft Sentinel and Defender, and Elastic. I set one rule on day one: no converted query is trusted until it runs on a real engine in CI and returns exactly the events the Sigma logic says it should.
That means every rule carries test events, and every converted query is run against them. A clean conversion is not a pass. The query has to find the events that should match and skip the ones that should not.
The Elastic results did not line up. The cause was not in my rules. It was in the open-source converter that turns Sigma into Elastic queries.
Bug 1: a regex that has to match the whole value
Sigma's |re modifier works like Python's re.search: the pattern can match anywhere in the field. A rule looking for ping in a command line matches cmd /c ping 127.0.0.1.
Elasticsearch evaluates regular expressions with Lucene, in Lucene queries, EQL and ES|QL's RLIKE alike. A Lucene regex has to match the entire value. The converter passed Sigma's pattern through unchanged:
from * metadata _id, _index, _version | where CommandLine rlike "ping"
That query only matches a command line that is literally ping. Against cmd /c ping 127.0.0.1 it returns nothing. There is no error and no warning. The rule just never alerts.
You can see the difference on any Elasticsearch with ES|QL, no data needed:
ROW CommandLine = "cmd /c ping 127.0.0.1" | WHERE CommandLine RLIKE "ping"
ROW CommandLine = "cmd /c ping 127.0.0.1" | WHERE CommandLine RLIKE ".*(ping).*"
The first returns no rows. The second, wrapped the way the fix wraps it, returns one.
I was not the first to notice. Another contributor had already reported it for the Lucene backend and tested the Windows rules in SigmaHQ: 73 of the 74 rules that use |re had at least one regex condition that did not behave as written. I added ES|QL results from 9.5.5 to that issue, confirming the same problem in RLIKE.
Why wrapping it is not enough
The obvious fix is to wrap every pattern as .*(...).*. That handles the common case, but a Sigma regex is a Python regex, and Lucene's dialect differs in more than anchoring:
^and$are not anchors in Lucene. A pattern that starts with^should drop the leading.*, not keep a literal caret.- Lucene has no non-capturing groups, so
(?:...)has to become(...). - Lucene has no inline flags, so
(?i)has to become a character class for each letter:abecomes[aA]. ",#,@,&,<,>and~are operators in Lucene and plain characters in Python, so they have to be escaped.- Some Python constructs have no exact Lucene equivalent at all: lookarounds, back-references and
\b.
For that last group, I chose to make the converter refuse the rule with a clear error instead of producing a query that quietly matches something else. A loud failure is a ticket. A silent one is a blind spot.
Measuring the fix
To check the translation, I took every |re condition in the SigmaHQ rule set, 221 conditions across 124 rules, and ran them against 51,199 test values on a real Elasticsearch, with Python's own re.search as the reference for what each one should match.
| Backend | Conditions matching Python before | After the fix |
|---|---|---|
| Lucene | 25 | 211 |
| ES|QL | 25 | 211 |
| EQL | 24 | 207 |
Of the 221 conditions, 215 now translate and 6 are refused because they use constructs Lucene cannot express. On EQL, query errors dropped from 56 to 4. The remaining differences are documented in the change: four patterns Elasticsearch still rejects as too complex (bug 3), and edge cases around line breaks, ASCII-only character classes and a few Unicode case pairs.
Bug 2: backslashes break LIKE
The ES|QL backend turns Sigma's contains, and any value with wildcards, into a LIKE pattern. In a LIKE pattern, the backslash is the escape character. A literal backslash has to be doubled in the pattern, and doubled again for the string literal around it.
The converter only did the second step. A rule matching a Windows path produced:
from * metadata _id, _index, _version | where Image like "*\\Users\\Public\\*"
Elasticsearch reads \U and \P as escape sequences and rejects the query. What it needed was:
from * metadata _id, _index, _version | where Image like "*\\\\Users\\\\Public\\\\*"
Literal * and ? in Sigma values, written \* and \?, had the same problem: they need escaping inside the pattern so they are not treated as wildcards.
This one is at least loud: the query fails instead of silently missing. But Windows paths are everywhere in Sigma rules. A comment on an older issue estimated about 645 SigmaHQ rules are affected. Elastic had already closed a related Elasticsearch issue as expected behavior, so the fix belongs in the converter.
I reported it and submitted a fix that gives LIKE its own escaping and leaves ==, starts_with() and ends_with() unchanged. The maintainer had started a different fix for the same bug in early 2025 that was never merged. My pull request points to it, notes that it produces identical queries under the same tests, and offers to go with either approach.
Bug 3: the fix that is too complex to run
Wrapping a pattern in .*(...).* is correct, but it is not free. Lucene compiles a regex into an automaton, and some patterns grow too large once wrapped. One SigmaHQ rule looks for:
ping\s+-n.{6,64}echo EEEE\s?>\s?
Wrapped, Elasticsearch refuses it with "Pattern was too complex to determinize." Testing the bounded repeat on 9.5.5, .{6,16} and .{6,24} worked, and .{6,32} and above were rejected.
There is no exact rewrite that avoids this, so the translation does not try. The query fails loudly when it runs, and the pull request notes the four SigmaHQ patterns that hit this limit.
Where things stand
Both fixes are open pull requests, waiting on SigmaHQ's maintainers. They are submitted, not merged:
- #241: translate Sigma regexes for Lucene's whole-value matching, across the Lucene, EQL and ES|QL backends
- #238: escape backslashes and literal wildcards in ES|QL
LIKEpatterns
Until they merge, if you convert Sigma rules to Elastic with the current release, test your |re rules and any ES|QL rule with a backslash in a LIKE against real events.
Building it with an AI assistant
I built the pipeline and both fixes with an AI coding assistant, and said so in both pull requests. What made that workable was the same rule that found the bugs: the AI writes the code, and the real engine and the reference engine decide whether it is right. I reproduced each bug and the before-and-after results myself before anything went upstream.
What I took from it
- "It converted" does not mean "it works." A converter can only check that the output is valid syntax. It cannot know whether the query means what the rule meant.
- Test detections against a real engine with known events. A rule that has never matched a test event is a guess.
- Prefer loud failures. A query Elasticsearch rejects gets fixed. A query that silently matches nothing can sit in production for years.
- Translating between query languages is hard. None of this is a criticism of the Sigma maintainers. Sigma's whole value is that one rule can target many engines, and that only holds if each engine's quirks are handled somewhere. Reporting them with a reproduction and a fix is how that happens.