Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
64 changes: 58 additions & 6 deletions sigma/backends/splunk/splunk.py
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,17 @@ def get_field_match(cls, field):
def get_field_condition(cls, field):
return cls.construct_field_variable(field, "Condition")

@classmethod
def get_all_condition_fields(cls):
"""Return the set of all condition field names created by deferred OR regex expressions."""
result = set()
for field, count in cls.field_counts.items():
cleaned = cls.clean_field(field)
for i in range(1, count + 1):
suffix = "" if i == 1 else str(i)
result.add(f"{cleaned}Condition{suffix}")
return result

@classmethod
def reset(cls):
cls.field_counts = {}
Expand Down Expand Up @@ -188,6 +199,11 @@ class SplunkBackend(TextQueryBackend):
deferred_separator: ClassVar[str] = "\n| "
deferred_only_query: ClassVar[str] = "*"

# Pattern matching a leading field=value term in a query string.
_field_eq_val_re: ClassVar[Pattern] = re.compile(
r'([\w.]+)(?:="[^"]*"|=[^\s")]+)\s*'
)

# Correlations
correlation_methods: ClassVar[Dict[str, str]] = {
"stats": "Correlation using stats command (more efficient, static time window)",
Expand Down Expand Up @@ -330,14 +346,50 @@ def finish_query(
remaining_deferred.append(deferred_expression)

if deferred_regex_or_expressions:
# Collect all condition field names created by deferred OR
# regex expressions before resetting, so we can identify
# which leading query parts don't depend on them.
deferred_condition_fields = (
SplunkDeferredORRegularExpression.get_all_condition_fields()
)
SplunkDeferredORRegularExpression.reset()
state.deferred[:] = remaining_deferred
query = (
self.deferred_start
+ self.deferred_separator.join(deferred_regex_or_expressions)
+ "\n| search "
+ query
)

# Extract leading field=value conditions that don't reference
# any deferred condition field so they are placed before the
# deferred rex/eval pipeline commands instead of ending up
# inside the trailing "| search" clause.
prefix_parts = []
pos = 0
while pos < len(query):
m = self._field_eq_val_re.match(query, pos)
if m and m.group(1) not in deferred_condition_fields:
prefix_parts.append(m.group().strip())
pos = m.end()
else:
break

if prefix_parts:
prefix = " ".join(prefix_parts)
remaining_query = query[pos:]
query = (
prefix
+ self.deferred_start
+ self.deferred_separator.join(
deferred_regex_or_expressions
)
+ "\n| search "
+ remaining_query
)
else:
query = (
self.deferred_start
+ self.deferred_separator.join(
deferred_regex_or_expressions
)
+ "\n| search "
+ query
)

return super().finish_query(rule, query, state)

Expand Down
43 changes: 43 additions & 0 deletions tests/test_backend_splunk.py
Original file line number Diff line number Diff line change
Expand Up @@ -263,6 +263,49 @@ def test_splunk_regex_query_explicit_or_with_nested_fields():
]


def test_splunk_regex_query_explicit_or_with_add_condition():
"""Test that conditions added by processing pipelines (index, source) are
placed before deferred rex/eval pipeline commands instead of inside
the trailing '| search' clause."""

pipeline = ProcessingPipeline.from_yaml(
"""
name: Test
priority: 100
transformations:
- id: prefix_source_and_index
type: add_condition
conditions:
index: test
source: test
"""
)
splunk_backend = SplunkBackend(pipeline)

collection = SigmaCollection.from_yaml(
"""
title: Test
status: test
logsource:
category: test_category
product: test_product
detection:
selection:
EventID: 4688
CommandLine|re:
- "suspicious_command"
selection2:
Image|re:
- "suspicious_command"
condition: selection or selection2
"""
)

assert splunk_backend.convert(collection) == [
'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"'
]


def test_splunk_regex_group_name_is_capped_for_long_fields():
SplunkDeferredORRegularExpression.reset()
field = "msg_normalized_header_subject"
Expand Down
Loading