Skip to content

Commit 1e5d1e5

Browse files
authored
Merge pull request #66 from SigmaHQ/copilot/fix-deferred-conditions-regex
Fix pipeline conditions (index/source) placement with OR-ed regex
2 parents 9a1157c + 4f3ca56 commit 1e5d1e5

2 files changed

Lines changed: 99 additions & 0 deletions

File tree

sigma/backends/splunk/splunk.py

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,17 @@ def get_field_match(cls, field):
101101
def get_field_condition(cls, field):
102102
return cls.construct_field_variable(field, "Condition")
103103

104+
@classmethod
105+
def get_all_condition_fields(cls):
106+
"""Return the set of all condition field names created by deferred OR regex expressions."""
107+
result = set()
108+
for field, count in cls.field_counts.items():
109+
cleaned = cls.clean_field(field)
110+
for i in range(1, count + 1):
111+
suffix = "" if i == 1 else str(i)
112+
result.add(f"{cleaned}Condition{suffix}")
113+
return result
114+
104115
@classmethod
105116
def reset(cls):
106117
cls.field_counts = {}
@@ -188,6 +199,11 @@ class SplunkBackend(TextQueryBackend):
188199
deferred_separator: ClassVar[str] = "\n| "
189200
deferred_only_query: ClassVar[str] = "*"
190201

202+
# Pattern matching a leading field=value term in a query string.
203+
_field_eq_val_re: ClassVar[Pattern] = re.compile(
204+
r'([\w.]+)(?:="[^"]*"|=[^\s")]+)\s*'
205+
)
206+
191207
# Correlations
192208
correlation_methods: ClassVar[Dict[str, str]] = {
193209
"stats": "Correlation using stats command (more efficient, static time window)",
@@ -330,6 +346,12 @@ def finish_query(
330346
remaining_deferred.append(deferred_expression)
331347

332348
if deferred_regex_or_expressions:
349+
# Collect all condition field names created by deferred OR
350+
# regex expressions before resetting, so finalize methods
351+
# can identify which query parts don't depend on them.
352+
state.processing_state["deferred_or_condition_fields"] = (
353+
SplunkDeferredORRegularExpression.get_all_condition_fields()
354+
)
333355
SplunkDeferredORRegularExpression.reset()
334356
state.deferred[:] = remaining_deferred
335357
query = (
@@ -348,6 +370,40 @@ def finalize_query_default(
348370
index: int,
349371
state: ConversionState,
350372
) -> str:
373+
# When OR-ed regex expressions are deferred, extract leading field=value
374+
# conditions that don't depend on any deferred eval field and place them
375+
# before the deferred rex/eval pipeline commands. This ensures conditions
376+
# like index/source are at the beginning of the query for efficient
377+
# initial data retrieval.
378+
deferred_condition_fields = state.processing_state.get(
379+
"deferred_or_condition_fields"
380+
)
381+
search_marker = "\n| search "
382+
if deferred_condition_fields and search_marker in query:
383+
marker_idx = query.index(search_marker)
384+
deferred_part = query[:marker_idx]
385+
search_query = query[marker_idx + len(search_marker) :]
386+
387+
prefix_parts = []
388+
pos = 0
389+
while pos < len(search_query):
390+
m = self._field_eq_val_re.match(search_query, pos)
391+
if m and m.group(1) not in deferred_condition_fields:
392+
prefix_parts.append(m.group().strip())
393+
pos = m.end()
394+
else:
395+
break
396+
397+
if prefix_parts:
398+
prefix = " ".join(prefix_parts)
399+
remaining_query = search_query[pos:]
400+
query = (
401+
prefix
402+
+ deferred_part
403+
+ search_marker
404+
+ remaining_query
405+
)
406+
351407
if isinstance(rule, SigmaRule) and rule.fields:
352408
return query + " | table " + ",".join(rule.fields)
353409
return query

tests/test_backend_splunk.py

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -263,6 +263,49 @@ def test_splunk_regex_query_explicit_or_with_nested_fields():
263263
]
264264

265265

266+
def test_splunk_regex_query_explicit_or_with_add_condition():
267+
"""Test that conditions added by processing pipelines (index, source) are
268+
placed before deferred rex/eval pipeline commands instead of inside
269+
the trailing '| search' clause."""
270+
271+
pipeline = ProcessingPipeline.from_yaml(
272+
"""
273+
name: Test
274+
priority: 100
275+
transformations:
276+
- id: prefix_source_and_index
277+
type: add_condition
278+
conditions:
279+
index: test
280+
source: test
281+
"""
282+
)
283+
splunk_backend = SplunkBackend(pipeline)
284+
285+
collection = SigmaCollection.from_yaml(
286+
"""
287+
title: Test
288+
status: test
289+
logsource:
290+
category: test_category
291+
product: test_product
292+
detection:
293+
selection:
294+
EventID: 4688
295+
CommandLine|re:
296+
- "suspicious_command"
297+
selection2:
298+
Image|re:
299+
- "suspicious_command"
300+
condition: selection or selection2
301+
"""
302+
)
303+
304+
assert splunk_backend.convert(collection) == [
305+
'index="test" source="test"\n| rex field=CommandLine "(?<CommandLineMatch>suspicious_command)"\n| eval CommandLineCondition=if(isnotnull(CommandLineMatch), "true", "false")\n| rex field=Image "(?<ImageMatch>suspicious_command)"\n| eval ImageCondition=if(isnotnull(ImageMatch), "true", "false")\n| search (EventID=4688 CommandLineCondition="true") OR ImageCondition="true"'
306+
]
307+
308+
266309
def test_splunk_regex_group_name_is_capped_for_long_fields():
267310
SplunkDeferredORRegularExpression.reset()
268311
field = "msg_normalized_header_subject"

0 commit comments

Comments
 (0)