|
| 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 |
0 commit comments