Skip to content

Commit 9ccf249

Browse files
authored
Merge pull request #62 from SigmaHQ/copilot/add-spl2-support-backend
Fix SPL2 backend: CIDR matching, unbound keywords, regex flags, module fields, pipeline backends
2 parents 714fb03 + 5245ecd commit 9ccf249

4 files changed

Lines changed: 788 additions & 3 deletions

File tree

sigma/backends/splunk/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
from .splunk import SplunkBackend
2+
from .spl2 import SplunkSPL2Backend
23

34
backends = {
45
"splunk": SplunkBackend,
6+
"splunk_spl2": SplunkSPL2Backend,
57
}

sigma/backends/splunk/spl2.py

Lines changed: 136 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,136 @@
1+
import re
2+
from sigma.conversion.state import ConversionState
3+
from sigma.conversion.base import TextQueryBackend, DeferredQueryExpression
4+
from sigma.conditions import (
5+
ConditionAND,
6+
ConditionNOT,
7+
ConditionOR,
8+
ConditionItem,
9+
ConditionValueExpression,
10+
)
11+
from sigma.rule import SigmaRule
12+
from sigma.types import SigmaCompareExpression, SigmaString
13+
import sigma
14+
from typing import ClassVar, Dict, List, Optional, Pattern, Tuple, Union
15+
16+
17+
class SplunkSPL2Backend(TextQueryBackend):
18+
"""Splunk SPL2 backend."""
19+
20+
name: ClassVar[str] = "Splunk SPL2 queries"
21+
formats: ClassVar[Dict[str, str]] = {
22+
"default": "Plain SPL2 queries",
23+
"module": "SPL2 module output with $result = FROM ... assignments",
24+
}
25+
requires_pipeline: ClassVar[bool] = True
26+
27+
precedence: ClassVar[Tuple[ConditionItem, ConditionItem, ConditionItem]] = (
28+
ConditionNOT,
29+
ConditionOR,
30+
ConditionAND,
31+
)
32+
group_expression: ClassVar[str] = "({expr})"
33+
34+
bool_values = {True: "true", False: "false"}
35+
or_token: ClassVar[str] = "OR"
36+
and_token: ClassVar[str] = "AND"
37+
not_token: ClassVar[str] = "NOT"
38+
eq_token: ClassVar[str] = "="
39+
40+
field_quote: ClassVar[str] = "'"
41+
field_quote_pattern: ClassVar[Pattern] = re.compile(r"^[\w.]+$")
42+
43+
str_quote: ClassVar[str] = '"'
44+
escape_char: ClassVar[str] = "\\"
45+
wildcard_multi: ClassVar[str] = "%"
46+
wildcard_single: ClassVar[str] = "_"
47+
add_escaped: ClassVar[str] = "%_"
48+
49+
wildcard_match_expression: ClassVar[str] = "{field} LIKE {value}"
50+
51+
re_expression: ClassVar[str] = "match({field}, /(?i){regex}/)"
52+
re_escape_char: ClassVar[str] = "\\"
53+
re_escape: ClassVar[Tuple[str]] = ("/",)
54+
re_flag_prefix: ClassVar[bool] = False
55+
56+
cidr_expression: ClassVar[str] = 'cidrmatch("{value}", {field})'
57+
58+
compare_op_expression: ClassVar[str] = "{field}{operator}{value}"
59+
compare_operators: ClassVar[Dict[SigmaCompareExpression.CompareOperators, str]] = {
60+
SigmaCompareExpression.CompareOperators.LT: "<",
61+
SigmaCompareExpression.CompareOperators.LTE: "<=",
62+
SigmaCompareExpression.CompareOperators.GT: ">",
63+
SigmaCompareExpression.CompareOperators.GTE: ">=",
64+
}
65+
66+
field_equals_field_expression: ClassVar[str] = "{field1}={field2}"
67+
field_null_expression: ClassVar[str] = "{field} IS NULL"
68+
69+
convert_or_as_in: ClassVar[bool] = True
70+
convert_and_as_in: ClassVar[bool] = False
71+
in_expressions_allow_wildcards: ClassVar[bool] = False
72+
field_in_list_expression: ClassVar[str] = "{field} {op} ({list})"
73+
or_in_operator: ClassVar[Optional[str]] = "IN"
74+
list_separator: ClassVar[str] = ", "
75+
field_exists_expression: ClassVar[str] = "{field} IS NOT NULL"
76+
field_not_exists_expression: ClassVar[str] = "{field} IS NULL"
77+
78+
unbound_value_str_expression: ClassVar[str] = '_raw LIKE "%{value}%"'
79+
unbound_value_num_expression: ClassVar[str] = '_raw LIKE "%{value}%"'
80+
unbound_value_re_expression: ClassVar[str] = 'match(_raw, /{value}/)'
81+
82+
deferred_start: ClassVar[str] = "\n| "
83+
deferred_separator: ClassVar[str] = "\n| "
84+
deferred_only_query: ClassVar[str] = "*"
85+
86+
# Use FROM dataset WHERE query pattern
87+
query_expression: ClassVar[str] = "FROM {state[dataset]} WHERE {query}"
88+
state_defaults: ClassVar[Dict[str, str]] = {"dataset": "main"}
89+
90+
def __init__(
91+
self,
92+
processing_pipeline: Optional[
93+
"sigma.processing.pipeline.ProcessingPipeline"
94+
] = None,
95+
collect_errors: bool = False,
96+
dataset: str = "main",
97+
**kwargs,
98+
):
99+
super().__init__(processing_pipeline, collect_errors, **kwargs)
100+
self.dataset = dataset
101+
# Override state_defaults with the user-provided dataset
102+
self.state_defaults = {"dataset": dataset}
103+
104+
def convert_condition_val_str(
105+
self, cond: ConditionValueExpression, state: ConversionState
106+
) -> Union[str, DeferredQueryExpression]:
107+
"""Convert unbound string value expressions to _raw LIKE searches."""
108+
cond_value = cond.value
109+
if not isinstance(cond_value, SigmaString):
110+
raise TypeError(
111+
f"Expected SigmaString for cond.value, got {type(cond_value)}"
112+
)
113+
# Convert to plain string without quotes, escaping LIKE special chars
114+
converted = cond_value.convert(
115+
self.escape_char,
116+
self.wildcard_multi,
117+
self.wildcard_single,
118+
self.str_quote + self.add_escaped,
119+
self.filter_chars,
120+
)
121+
return f'_raw LIKE "%{converted}%"'
122+
123+
def finalize_query_default(
124+
self, rule: SigmaRule, query: str, index: int, state: ConversionState
125+
) -> str:
126+
table_fields = " | table " + ", ".join(rule.fields) if rule.fields else ""
127+
return query + table_fields
128+
129+
def finalize_query_module(
130+
self, rule: SigmaRule, query: str, index: int, state: ConversionState
131+
) -> str:
132+
table_fields = " | table " + ", ".join(rule.fields) if rule.fields else ""
133+
return "$result = " + query + table_fields
134+
135+
def finalize_output_module(self, queries: List[str]) -> List[str]:
136+
return queries

sigma/pipelines/splunk/splunk.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -78,7 +78,7 @@
7878
def splunk_windows_pipeline():
7979
return ProcessingPipeline(
8080
name="Splunk Windows log source conditions",
81-
allowed_backends={"splunk"},
81+
allowed_backends={"splunk", "splunk_spl2"},
8282
priority=20,
8383
items=generate_windows_logsource_items("source", "WinEventLog:{source}")
8484
+ [
@@ -97,7 +97,7 @@ def splunk_windows_pipeline():
9797
def splunk_windows_sysmon_acceleration_keywords():
9898
return ProcessingPipeline(
9999
name="Splunk Windows Sysmon search acceleration keywords",
100-
allowed_backends={"splunk"},
100+
allowed_backends={"splunk", "splunk_spl2"},
101101
priority=25,
102102
items=[
103103
ProcessingItem( # Some optimizations searching for characteristic keyword for specific log sources
@@ -123,7 +123,7 @@ def splunk_windows_sysmon_acceleration_keywords():
123123
def splunk_cim_data_model():
124124
return ProcessingPipeline(
125125
name="Splunk CIM Data Model Mapping",
126-
allowed_backends={"splunk"},
126+
allowed_backends={"splunk", "splunk_spl2"},
127127
priority=20,
128128
items=[
129129
ProcessingItem(

0 commit comments

Comments
 (0)