Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
28 changes: 27 additions & 1 deletion sigma/backends/splunk/splunk.py
Original file line number Diff line number Diff line change
Expand Up @@ -457,7 +457,33 @@ def finalize_query_data_model(
"No fields specified by processing pipeline"
)

return f"""| tstats summariesonly=false allow_old_summaries=true fillnull_value="null" count min(_time) as firstTime max(_time) as lastTime from datamodel={data_model_set} where {query} by {fields}
# Separate deferred expressions (regex/rex/eval/where) from the WHERE clause.
# Deferred expressions cannot be placed inside a tstats WHERE clause.
deferred_part = ""
where_query = query

# Handle OR regex deferred expressions prepended by finish_query.
# Format: \n| rex ...\n| eval ...\n| search <query>
search_marker = "\n| search "
has_or_regex_deferred = search_marker in query
if has_or_regex_deferred:
marker_idx = query.index(search_marker)
deferred_part = query[:marker_idx]
where_query = query[marker_idx + len(search_marker) :]

# Handle simple deferred expressions appended by the parent's finish_query.
# Format: <query>\n| regex/where ...
deferred_suffix_marker = "\n| "
if deferred_suffix_marker in where_query:
idx = where_query.index(deferred_suffix_marker)
deferred_part += where_query[idx:]
where_query = where_query[:idx]

# Re-add the search command after deferred expressions for OR regex filtering
if has_or_regex_deferred:
deferred_part += search_marker + where_query

return f"""| tstats summariesonly=false allow_old_summaries=true fillnull_value="null" count min(_time) as firstTime max(_time) as lastTime from datamodel={data_model_set} where {where_query} by {fields}{deferred_part}
| `drop_dm_object_name({data_set})`
| convert timeformat="%Y-%m-%dT%H:%M:%S" ctime(firstTime)
| convert timeformat="%Y-%m-%dT%H:%M:%S" ctime(lastTime)
Expand Down
62 changes: 62 additions & 0 deletions tests/test_backend_splunk.py
Original file line number Diff line number Diff line change
Expand Up @@ -832,6 +832,68 @@ def test_splunk_data_model_dns_answer():
)
]

def test_splunk_data_model_process_creation_with_regex():
splunk_backend = SplunkBackend(processing_pipeline=splunk_cim_data_model())
rule = """
title: Test
status: test
logsource:
category: process_creation
product: windows
detection:
sel:
ParentImage|endswith: explorer.exe
CommandLine|re: foo.*bar
condition: sel
"""
result = splunk_backend.convert(SigmaCollection.from_yaml(rule), "data_model")
assert result == [
"""| tstats summariesonly=false allow_old_summaries=true fillnull_value="null" count min(_time) as firstTime max(_time) as lastTime from datamodel=Endpoint.Processes where
Processes.parent_process_path="*explorer.exe" by Processes.process Processes.dest Processes.process_current_directory Processes.process_path Processes.process_integrity_level Processes.original_file_name Processes.parent_process
Processes.parent_process_path Processes.parent_process_guid Processes.parent_process_id Processes.process_guid Processes.process_id Processes.user
| regex Processes.process="foo.*bar"
| `drop_dm_object_name(Processes)`
| convert timeformat="%Y-%m-%dT%H:%M:%S" ctime(firstTime)
| convert timeformat="%Y-%m-%dT%H:%M:%S" ctime(lastTime)
""".replace(
"\n", " "
)
]


def test_splunk_data_model_process_creation_with_or_regex():
splunk_backend = SplunkBackend(processing_pipeline=splunk_cim_data_model())
rule = """
title: Test
status: test
logsource:
category: process_creation
product: windows
detection:
sel_img:
ParentImage|endswith: explorer.exe
sel_cmd:
- CommandLine|contains: test_value
- CommandLine|re: foo.*bar
condition: all of sel_*
"""
result = splunk_backend.convert(SigmaCollection.from_yaml(rule), "data_model")
assert result == [
"""| tstats summariesonly=false allow_old_summaries=true fillnull_value="null" count min(_time) as firstTime max(_time) as lastTime from datamodel=Endpoint.Processes where
Processes.parent_process_path="*explorer.exe" Processes.process="*test_value*" OR processCondition="true" by Processes.process Processes.dest Processes.process_current_directory Processes.process_path Processes.process_integrity_level Processes.original_file_name Processes.parent_process
Processes.parent_process_path Processes.parent_process_guid Processes.parent_process_id Processes.process_guid Processes.process_id Processes.user
| rex field=Processes.process "(?<processMatch>foo.*bar)"
| eval processCondition=if(isnotnull(processMatch), "true", "false")
| search Processes.parent_process_path="*explorer.exe" Processes.process="*test_value*" OR processCondition="true"
| `drop_dm_object_name(Processes)`
| convert timeformat="%Y-%m-%dT%H:%M:%S" ctime(firstTime)
| convert timeformat="%Y-%m-%dT%H:%M:%S" ctime(lastTime)
""".replace(
"\n", " "
)
]


def test_splunk_data_model_no_data_model_specified():
splunk_backend = SplunkBackend()
rule = """
Expand Down