Skip to content

Commit 210a51f

Browse files
authored
Merge pull request #47 from arblade:main
Improving regex oring
2 parents 2685a3e + f514e76 commit 210a51f

File tree

2 files changed

+130
-34
lines changed

2 files changed

+130
-34
lines changed

sigma/backends/splunk/splunk.py

Lines changed: 42 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -41,41 +41,53 @@ class SplunkDeferredORRegularExpression(DeferredTextQueryExpression):
4141
}
4242

4343
def __init__(self, state, field, arg) -> None:
44-
SplunkDeferredORRegularExpression.add_field(field)
45-
index_suffix = SplunkDeferredORRegularExpression.get_index_suffix(field)
46-
self.template = (
47-
'rex field={field} "(?<{field}Match'
48-
+ index_suffix
49-
+ '>{value})"\n| eval {field}Condition'
50-
+ index_suffix
51-
+ "=if(isnotnull({field}Match"
52-
+ index_suffix
53-
+ '), "true", "false")'
44+
self.add_field(field)
45+
field_condition = self.get_field_condition(field)
46+
field_match = self.get_field_match(field)
47+
self.template = 'rex field={{field}} "(?<{field_match}>{{value}})"\n| eval {field_condition}=if(isnotnull({field_match}), "true", "false")'.format(
48+
field_match=field_match, field_condition=field_condition
5449
)
5550
return super().__init__(state, field, arg)
5651

52+
@staticmethod
53+
def clean_field(field):
54+
# splunk does not allow dots in regex group, so we need to clean variables
55+
return re.sub(".*\\.", "", field)
56+
5757
@classmethod
5858
def add_field(cls, field):
5959
cls.field_counts[field] = (
6060
cls.field_counts.get(field, 0) + 1
6161
) # increment the field count
6262

6363
@classmethod
64-
def get_index_suffix(cls, field):
65-
66-
index_suffix = cls.field_counts.get(field, 0)
64+
def get_field_suffix(cls, field):
65+
index_suffix = cls.field_counts.get(field, "")
6766
if index_suffix == 1:
68-
# return nothing for the first field use
69-
return ""
70-
return str(index_suffix)
67+
index_suffix = ""
68+
return index_suffix
69+
70+
@classmethod
71+
def construct_field_variable(cls, field, variable):
72+
cleaned_field = cls.clean_field(field)
73+
index_suffix = cls.get_field_suffix(field)
74+
return f"{cleaned_field}{variable}{index_suffix}"
75+
76+
@classmethod
77+
def get_field_match(cls, field):
78+
return cls.construct_field_variable(field, "Match")
79+
80+
@classmethod
81+
def get_field_condition(cls, field):
82+
return cls.construct_field_variable(field, "Condition")
7183

7284
@classmethod
7385
def reset(cls):
7486
cls.field_counts = {}
7587

7688

77-
class SplunkDeferredCIDRExpression(DeferredTextQueryExpression):
78-
template = 'where {op}cidrmatch("{value}", {field})'
89+
class SplunkDeferredFieldRefExpression(DeferredTextQueryExpression):
90+
template = "where {op}'{field}'='{value}'"
7991
operators = {
8092
True: "NOT ",
8193
False: "",
@@ -107,6 +119,7 @@ class SplunkBackend(TextQueryBackend):
107119
)
108120
group_expression: ClassVar[str] = "({expr})"
109121

122+
bool_values = {True: "true", False: "false"}
110123
or_token: ClassVar[str] = "OR"
111124
and_token: ClassVar[str] = " "
112125
not_token: ClassVar[str] = "NOT"
@@ -125,7 +138,7 @@ class SplunkBackend(TextQueryBackend):
125138
re_escape_char: ClassVar[str] = "\\"
126139
re_escape: ClassVar[Tuple[str]] = ('"',)
127140

128-
cidr_expression: ClassVar[str] = "{value}"
141+
cidr_expression: ClassVar[str] = '{field}="{value}"'
129142

130143
compare_op_expression: ClassVar[str] = "{field}{operator}{value}"
131144
compare_operators: ClassVar[Dict[SigmaCompareExpression.CompareOperators, str]] = {
@@ -135,6 +148,7 @@ class SplunkBackend(TextQueryBackend):
135148
SigmaCompareExpression.CompareOperators.GTE: ">=",
136149
}
137150

151+
field_equals_field_expression: ClassVar[str] = "{field2}"
138152
field_null_expression: ClassVar[str] = "NOT {field}=*"
139153

140154
convert_or_as_in: ClassVar[bool] = True
@@ -248,9 +262,7 @@ def convert_condition_field_eq_val_re(
248262
).postprocess(None, cond)
249263

250264
cond_true = ConditionFieldEqualsValueExpression(
251-
cond.field
252-
+ "Condition"
253-
+ str(SplunkDeferredORRegularExpression.get_index_suffix(cond.field)),
265+
SplunkDeferredORRegularExpression.get_field_condition(cond.field),
254266
SigmaString("true"),
255267
)
256268
# returning fieldX=true
@@ -259,19 +271,19 @@ def convert_condition_field_eq_val_re(
259271
state, cond.field, super().convert_condition_field_eq_val_re(cond, state)
260272
).postprocess(None, cond)
261273

262-
def convert_condition_field_eq_val_cidr(
274+
def convert_condition_field_eq_field(
263275
self,
264276
cond: ConditionFieldEqualsValueExpression,
265277
state: "sigma.conversion.state.ConversionState",
266-
) -> SplunkDeferredCIDRExpression:
267-
"""Defer CIDR network range matching to pipelined where cidrmatch command after main search expression."""
278+
) -> SplunkDeferredFieldRefExpression:
279+
"""Defer FieldRef matching to pipelined with `where` command after main search expression."""
268280
if cond.parent_condition_chain_contains(ConditionOR):
269281
raise SigmaFeatureNotSupportedByBackendError(
270-
"ORing CIDR matching is not yet supported by Splunk backend",
282+
"ORing FieldRef matching is not yet supported by Splunk backend",
271283
source=cond.source,
272284
)
273-
return SplunkDeferredCIDRExpression(
274-
state, cond.field, super().convert_condition_field_eq_val_cidr(cond, state)
285+
return SplunkDeferredFieldRefExpression(
286+
state, cond.field, super().convert_condition_field_eq_field(cond, state)
275287
).postprocess(None, cond)
276288

277289
def finalize_query(
@@ -381,13 +393,11 @@ def finalize_query_data_model(
381393
cim_fields = " ".join(
382394
splunk_sysmon_process_creation_cim_mapping.values()
383395
)
384-
396+
385397
elif rule.logsource.category == "proxy":
386398
data_model = "Web"
387399
data_set = "Proxy"
388-
cim_fields = " ".join(
389-
splunk_web_proxy_cim_mapping.values()
390-
)
400+
cim_fields = " ".join(splunk_web_proxy_cim_mapping.values())
391401

392402
try:
393403
data_model_set = state.processing_state["data_model_set"]

tests/test_backend_splunk.py

Lines changed: 88 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
import pytest
33
from sigma.backends.splunk import SplunkBackend
44
from sigma.collection import SigmaCollection
5+
from sigma.processing.pipeline import ProcessingPipeline
56
from sigma.pipelines.splunk import splunk_cim_data_model
67

78

@@ -224,6 +225,43 @@ def test_splunk_regex_query_explicit_or(splunk_backend: SplunkBackend):
224225
)
225226

226227

228+
def test_splunk_regex_query_explicit_or_with_nested_fields():
229+
230+
pipeline = ProcessingPipeline.from_yaml(
231+
"""
232+
name: Test
233+
priority: 100
234+
transformations:
235+
- id: field_mapping
236+
type: field_name_mapping
237+
mapping:
238+
fieldA: Event.EventData.fieldA
239+
fieldB: Event.EventData.fieldB
240+
"""
241+
)
242+
splunk_backend = SplunkBackend(pipeline)
243+
244+
collection = SigmaCollection.from_yaml(
245+
"""
246+
title: Test
247+
status: test
248+
logsource:
249+
category: test_category
250+
product: test_product
251+
detection:
252+
sel1:
253+
fieldA|re: foo.*bar
254+
sel2:
255+
fieldB|re: boo.*foo
256+
condition: sel1 or sel2
257+
"""
258+
)
259+
260+
assert splunk_backend.convert(collection) == [
261+
'\n| rex field=Event.EventData.fieldA "(?<fieldAMatch>foo.*bar)"\n| eval fieldACondition=if(isnotnull(fieldAMatch), "true", "false")\n| rex field=Event.EventData.fieldB "(?<fieldBMatch>boo.*foo)"\n| eval fieldBCondition=if(isnotnull(fieldBMatch), "true", "false")\n| search fieldACondition="true" OR fieldBCondition="true"'
262+
]
263+
264+
227265
def test_splunk_single_regex_query(splunk_backend: SplunkBackend):
228266
assert (
229267
splunk_backend.convert(
@@ -264,12 +302,12 @@ def test_splunk_cidr_query(splunk_backend: SplunkBackend):
264302
"""
265303
)
266304
)
267-
== ['fieldB="foo" fieldC="bar"\n| where cidrmatch("192.168.0.0/16", fieldA)']
305+
== ['fieldA="192.168.0.0/16" fieldB="foo" fieldC="bar"']
268306
)
269307

270308

271309
def test_splunk_cidr_or(splunk_backend: SplunkBackend):
272-
with pytest.raises(SigmaFeatureNotSupportedByBackendError, match="ORing CIDR"):
310+
assert (
273311
splunk_backend.convert(
274312
SigmaCollection.from_yaml(
275313
"""
@@ -289,6 +327,54 @@ def test_splunk_cidr_or(splunk_backend: SplunkBackend):
289327
"""
290328
)
291329
)
330+
== ['fieldA="192.168.0.0/16" OR fieldA="10.0.0.0/8" fieldB="foo" fieldC="bar"']
331+
)
332+
333+
334+
def test_splunk_fieldref_query(splunk_backend: SplunkBackend):
335+
assert (
336+
splunk_backend.convert(
337+
SigmaCollection.from_yaml(
338+
"""
339+
title: Test
340+
status: test
341+
logsource:
342+
category: test_category
343+
product: test_product
344+
detection:
345+
sel:
346+
fieldA|fieldref: fieldD
347+
fieldB: foo
348+
fieldC: bar
349+
condition: sel
350+
"""
351+
)
352+
)
353+
== ["fieldB=\"foo\" fieldC=\"bar\"\n| where 'fieldA'='fieldD'"]
354+
)
355+
356+
357+
def test_splunk_fieldref_or(splunk_backend: SplunkBackend):
358+
with pytest.raises(SigmaFeatureNotSupportedByBackendError, match="ORing FieldRef"):
359+
splunk_backend.convert(
360+
SigmaCollection.from_yaml(
361+
"""
362+
title: Test
363+
status: test
364+
logsource:
365+
category: test_category
366+
product: test_product
367+
detection:
368+
sel:
369+
fieldA|fieldref:
370+
- fieldD
371+
- fieldE
372+
fieldB: foo
373+
fieldC: bar
374+
condition: sel
375+
"""
376+
)
377+
)
292378

293379

294380
def test_splunk_fields_output(splunk_backend: SplunkBackend):

0 commit comments

Comments
 (0)