Skip to content

Commit

Permalink
Merge pull request #37 from arblade/main
Browse files Browse the repository at this point in the history
adding Oring regexes
  • Loading branch information
thomaspatzke authored Apr 2, 2024
2 parents 0f40b79 + 1a63781 commit fc6791f
Show file tree
Hide file tree
Showing 2 changed files with 125 additions and 20 deletions.
128 changes: 114 additions & 14 deletions sigma/backends/splunk/splunk.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import re
from sigma.conversion.state import ConversionState
from sigma.rule import SigmaRule
from sigma.conversion.base import TextQueryBackend
from sigma.modifiers import SigmaRegularExpression
from sigma.rule import SigmaRule, SigmaDetection
from sigma.conversion.base import TextQueryBackend, DeferredQueryExpression
from sigma.conversion.deferred import DeferredTextQueryExpression
from sigma.conditions import (
ConditionFieldEqualsValueExpression,
Expand All @@ -10,15 +11,15 @@
ConditionNOT,
ConditionItem,
)
from sigma.types import SigmaCompareExpression
from sigma.exceptions import SigmaFeatureNotSupportedByBackendError
from sigma.types import SigmaCompareExpression, SigmaString
from sigma.exceptions import SigmaFeatureNotSupportedByBackendError, SigmaError
from sigma.pipelines.splunk.splunk import (
splunk_sysmon_process_creation_cim_mapping,
splunk_windows_registry_cim_mapping,
splunk_windows_file_event_cim_mapping,
)
import sigma
from typing import Callable, ClassVar, Dict, List, Optional, Pattern, Tuple
from typing import Any, Callable, ClassVar, Dict, List, Optional, Pattern, Tuple, Union


class SplunkDeferredRegularExpression(DeferredTextQueryExpression):
Expand All @@ -30,6 +31,48 @@ class SplunkDeferredRegularExpression(DeferredTextQueryExpression):
default_field = "_raw"


class SplunkDeferredORRegularExpression(DeferredTextQueryExpression):
field_counts = {}
default_field = "_raw"
operators = {
True: "!=",
False: "=",
}

def __init__(self, state, field, arg) -> None:
SplunkDeferredORRegularExpression.add_field(field)
index_suffix = SplunkDeferredORRegularExpression.get_index_suffix(field)
self.template = (
'rex field={field} "(?<{field}Match'
+ index_suffix
+ '>{value})"\n| eval {field}Condition'
+ index_suffix
+ "=if(isnotnull({field}Match"
+ index_suffix
+ '), "true", "false")'
)
return super().__init__(state, field, arg)

@classmethod
def add_field(cls, field):
cls.field_counts[field] = (
cls.field_counts.get(field, 0) + 1
) # increment the field count

@classmethod
def get_index_suffix(cls, field):

index_suffix = cls.field_counts.get(field, 0)
if index_suffix == 1:
# return nothing for the first field use
return ""
return str(index_suffix)

@classmethod
def reset(cls):
cls.field_counts = {}


class SplunkDeferredCIDRExpression(DeferredTextQueryExpression):
template = 'where {op}cidrmatch("{value}", {field})'
operators = {
Expand Down Expand Up @@ -111,19 +154,23 @@ class SplunkBackend(TextQueryBackend):
# Correlations
correlation_methods: ClassVar[Dict[str, str]] = {
"stats": "Correlation using stats command (more efficient, static time window)",
#"transaction": "Correlation using transaction command (less efficient, sliding time window",
# "transaction": "Correlation using transaction command (less efficient, sliding time window",
}
default_correlation_method: ClassVar[str] = "stats"
default_correlation_query: ClassVar[str] = {"stats": "{search}\n\n{aggregate}\n\n{condition}"}
default_correlation_query: ClassVar[str] = {
"stats": "{search}\n\n{aggregate}\n\n{condition}"
}

correlation_search_single_rule_expression: ClassVar[str] = "{query}"
correlation_search_multi_rule_expression: ClassVar[str] = "| multisearch\n{queries}"
correlation_search_multi_rule_query_expression: ClassVar[
str
] = '[ search {query} | eval event_type="{ruleid}"{normalization} ]'
correlation_search_multi_rule_query_expression: ClassVar[str] = (
'[ search {query} | eval event_type="{ruleid}"{normalization} ]'
)
correlation_search_multi_rule_query_expression_joiner: ClassVar[str] = "\n"

correlation_search_field_normalization_expression: ClassVar[str] = " | rename {field} as {alias}"
correlation_search_field_normalization_expression: ClassVar[str] = (
" | rename {field} as {alias}"
)
correlation_search_field_normalization_expression_joiner: ClassVar[str] = ""

event_count_aggregation_expression: ClassVar[Dict[str, str]] = {
Expand Down Expand Up @@ -190,11 +237,23 @@ def convert_condition_field_eq_val_re(
state: "sigma.conversion.state.ConversionState",
) -> SplunkDeferredRegularExpression:
"""Defer regular expression matching to pipelined regex command after main search expression."""

if cond.parent_condition_chain_contains(ConditionOR):
raise SigmaFeatureNotSupportedByBackendError(
"ORing regular expressions is not yet supported by Splunk backend",
source=cond.source,
# adding the deferred to the state
SplunkDeferredORRegularExpression(
state,
cond.field,
super().convert_condition_field_eq_val_re(cond, state),
).postprocess(None, cond)

cond_true = ConditionFieldEqualsValueExpression(
cond.field
+ "Condition"
+ str(SplunkDeferredORRegularExpression.get_index_suffix(cond.field)),
SigmaString("true"),
)
# returning fieldX=true
return super().convert_condition_field_eq_val_str(cond_true, state)
return SplunkDeferredRegularExpression(
state, cond.field, super().convert_condition_field_eq_val_re(cond, state)
).postprocess(None, cond)
Expand All @@ -214,6 +273,47 @@ def convert_condition_field_eq_val_cidr(
state, cond.field, super().convert_condition_field_eq_val_cidr(cond, state)
).postprocess(None, cond)

def finalize_query(
self,
rule: SigmaRule,
query: Union[str, DeferredQueryExpression],
index: int,
state: ConversionState,
output_format: str,
) -> Union[str, DeferredQueryExpression]:

if state.has_deferred():
deferred_regex_or_expressions = []
no_regex_oring_deferred_expressions = []

for index, deferred_expression in enumerate(state.deferred):

if type(deferred_expression) == SplunkDeferredORRegularExpression:
deferred_regex_or_expressions.append(
deferred_expression.finalize_expression()
)
else:
no_regex_oring_deferred_expressions.append(deferred_expression)

if len(deferred_regex_or_expressions) > 0:
SplunkDeferredORRegularExpression.reset() # need to reset class for potential future conversions
# remove deferred oring regex expressions from the state
# as they will be taken into account by the super().finalize_query
state.deferred = no_regex_oring_deferred_expressions

return super().finalize_query(
rule,
self.deferred_start
+ self.deferred_separator.join(deferred_regex_or_expressions)
+ "\n| search "
+ query,
index,
state,
output_format,
)

return super().finalize_query(rule, query, index, state, output_format)

def finalize_query_default(
self, rule: SigmaRule, query: str, index: int, state: ConversionState
) -> str:
Expand Down
17 changes: 11 additions & 6 deletions tests/test_backend_splunk.py
Original file line number Diff line number Diff line change
Expand Up @@ -172,9 +172,7 @@ def test_splunk_regex_query(splunk_backend: SplunkBackend):


def test_splunk_regex_query_implicit_or(splunk_backend: SplunkBackend):
with pytest.raises(
SigmaFeatureNotSupportedByBackendError, match="ORing regular expressions"
):
assert (
splunk_backend.convert(
SigmaCollection.from_yaml(
"""
Expand All @@ -194,12 +192,15 @@ def test_splunk_regex_query_implicit_or(splunk_backend: SplunkBackend):
"""
)
)
== [
'\n| rex field=fieldA "(?<fieldAMatch>foo.*bar)"\n| eval fieldACondition=if(isnotnull(fieldAMatch), "true", "false")\n| rex field=fieldA "(?<fieldAMatch2>boo.*foo)"\n| eval fieldACondition2=if(isnotnull(fieldAMatch2), "true", "false")\n| search fieldACondition="true" OR fieldACondition2="true" fieldB="foo" fieldC="bar"'
]
)


def test_splunk_regex_query_explicit_or(splunk_backend: SplunkBackend):
with pytest.raises(
SigmaFeatureNotSupportedByBackendError, match="ORing regular expressions"
):

assert (
splunk_backend.convert(
SigmaCollection.from_yaml(
"""
Expand All @@ -217,6 +218,10 @@ def test_splunk_regex_query_explicit_or(splunk_backend: SplunkBackend):
"""
)
)
== [
'\n| rex field=fieldA "(?<fieldAMatch>foo.*bar)"\n| eval fieldACondition=if(isnotnull(fieldAMatch), "true", "false")\n| rex field=fieldB "(?<fieldBMatch>boo.*foo)"\n| eval fieldBCondition=if(isnotnull(fieldBMatch), "true", "false")\n| search fieldACondition="true" OR fieldBCondition="true"'
]
)


def test_splunk_single_regex_query(splunk_backend: SplunkBackend):
Expand Down

0 comments on commit fc6791f

Please sign in to comment.