pySigma-backend-sqlite 0.1.1__tar.gz → 0.2.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,19 +1,19 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: pySigma-backend-sqlite
3
- Version: 0.1.1
3
+ Version: 0.2.0
4
4
  Summary: pySigma sqlite backend
5
5
  Home-page: https://github.com/SigmaHQ/pySigma-backend-sqlite
6
6
  License: LGPL-3.0-only
7
7
  Author: wagga
8
- Requires-Python: >=3.8,<4.0
8
+ Requires-Python: >=3.9,<4.0
9
9
  Classifier: License :: OSI Approved :: GNU Lesser General Public License v3 (LGPLv3)
10
10
  Classifier: Programming Language :: Python :: 3
11
- Classifier: Programming Language :: Python :: 3.8
12
11
  Classifier: Programming Language :: Python :: 3.9
13
12
  Classifier: Programming Language :: Python :: 3.10
14
13
  Classifier: Programming Language :: Python :: 3.11
15
14
  Classifier: Programming Language :: Python :: 3.12
16
- Requires-Dist: pysigma (>=0.10.5,<0.11.0)
15
+ Classifier: Programming Language :: Python :: 3.13
16
+ Requires-Dist: pysigma (>=0.11.17,<0.12.0)
17
17
  Project-URL: Repository, https://github.com/SigmaHQ/pySigma-backend-sqlite
18
18
  Description-Content-Type: text/markdown
19
19
 
@@ -1,6 +1,6 @@
1
1
  [tool.poetry]
2
2
  name = "pySigma-backend-sqlite"
3
- version = "0.1.1"
3
+ version = "0.2.0"
4
4
  description = "pySigma sqlite backend"
5
5
  readme = "README.md"
6
6
  authors = ["wagga"]
@@ -11,13 +11,14 @@ packages = [
11
11
  ]
12
12
 
13
13
  [tool.poetry.dependencies]
14
- python = "^3.8"
15
- pysigma = "^0.10.5"
14
+ python = "^3.9"
15
+ pysigma = "^0.11.17"
16
16
 
17
17
  [tool.poetry.group.dev.dependencies]
18
- pytest = "^7.4.0"
19
- pytest-cov = "^4.1.0"
20
- coverage = "^7.3.0"
18
+ black = "^24.1"
19
+ pytest = "^8.0"
20
+ pytest-cov = "^4.1"
21
+ coverage = "^7.4"
21
22
 
22
23
 
23
24
  [tool.pdm]
@@ -27,7 +28,7 @@ dev = []
27
28
  [tool.pdm.build]
28
29
  includes = ["sigma"]
29
30
  [build-system]
30
- requires = ["poetry-core>=1.0.0"]
31
+ requires = ["poetry-core>=1.8.1"]
31
32
  build-backend = "poetry.core.masonry.api"
32
33
 
33
34
  [project]
@@ -35,18 +36,19 @@ authors = [
35
36
  {name = "wagga", email = "wagga"},
36
37
  ]
37
38
  license = {text = "LGPL-3.0-only"}
38
- requires-python = ">=3.8,<4.0"
39
+ requires-python = ">=3.9,<4.0"
39
40
  dependencies = [
40
- "pysigma>=0.10.2",
41
+ "pysigma>=0.11.17",
41
42
  ]
43
+
42
44
  name = "pySigma-backend-sqlite"
43
- version = "0.1.1"
45
+ version = "0.2.0"
44
46
  description = "pySigma sqlite backend"
45
47
 
46
48
  [project.urls]
47
49
  repository = "https://github.com/SigmaHQ/pySigma-backend-sqlite"
48
50
 
49
51
  [project.group.dev.dependencies]
50
- pytest = "^7.4.0"
51
- pytest-cov = "^4.1.0"
52
- coverage = "^7.3.0"
52
+ pytest = "^8.0"
53
+ pytest-cov = "^4.1"
54
+ coverage = "^7.4"
@@ -0,0 +1,5 @@
1
+ from .sqlite import sqliteBackend
2
+
3
+ backends = { # Mapping between backend identifiers and classes. This is used by the pySigma plugin system to recognize backends and expose them with the identifier.
4
+ "sqlite": sqliteBackend,
5
+ }
@@ -0,0 +1,431 @@
1
+ from sigma.conversion.deferred import DeferredQueryExpression
2
+ from sigma.conversion.state import ConversionState
3
+ from sigma.exceptions import SigmaFeatureNotSupportedByBackendError
4
+ from sigma.rule import SigmaRule
5
+ from sigma.conversion.base import TextQueryBackend
6
+ from sigma.conditions import (
7
+ ConditionItem,
8
+ ConditionAND,
9
+ ConditionOR,
10
+ ConditionNOT,
11
+ ConditionValueExpression,
12
+ ConditionFieldEqualsValueExpression,
13
+ )
14
+ from sigma.types import (
15
+ SigmaCompareExpression,
16
+ SigmaString,
17
+ SpecialChars,
18
+ SigmaRegularExpressionFlag,
19
+ SigmaCIDRExpression,
20
+ )
21
+
22
+ import re
23
+ import json
24
+ from typing import ClassVar, Dict, List, Optional, Pattern, Tuple, Union, Any
25
+
26
+
27
+ class sqliteBackend(TextQueryBackend):
28
+ """SQLite backend."""
29
+
30
+ # Operator precedence: tuple of Condition{AND,OR,NOT} in order of precedence.
31
+ # The backend generates grouping if required
32
+ name: ClassVar[str] = "SQLite backend"
33
+ formats: Dict[str, str] = {
34
+ "default": "Plain SQLite queries",
35
+ "zircolite": "Zircolite JSON format",
36
+ }
37
+ requires_pipeline: bool = (
38
+ False # TODO: does the backend requires that a processing pipeline is provided? This information can be used by user interface programs like Sigma CLI to warn users about inappropriate usage of the backend.
39
+ )
40
+
41
+ precedence: ClassVar[Tuple[ConditionItem, ConditionItem, ConditionItem]] = (
42
+ ConditionNOT,
43
+ ConditionAND,
44
+ ConditionOR,
45
+ )
46
+ parenthesize: bool = True
47
+ group_expression: ClassVar[str] = (
48
+ "({expr})" # Expression for precedence override grouping as format string with {expr} placeholder
49
+ )
50
+
51
+ # Generated query tokens
52
+ token_separator: str = " " # separator inserted between all boolean operators
53
+ or_token: ClassVar[str] = "OR"
54
+ and_token: ClassVar[str] = "AND"
55
+ not_token: ClassVar[str] = "NOT"
56
+ eq_token: ClassVar[str] = (
57
+ "=" # Token inserted between field and value (without separator)
58
+ )
59
+
60
+ # String output
61
+ ## Fields
62
+ ### Quoting
63
+
64
+ # SQLite correct way to handle field name is detailed here : https://sqlite.org/lang_keywords.html.
65
+ # Double-quoting should be the way to go. But since in some case quotes are interpreted as literals we need to find an alternative.
66
+ # Obviously, we cannot use "[" and "]", because it is 2 different characters, so we are left with "`" (MySQL).
67
+ field_quote: ClassVar[str] = (
68
+ "`" # Character used to quote field characters if field_quote_pattern matches (or not, depending on field_quote_pattern_negation). No field name quoting is done if not set.
69
+ )
70
+ field_quote_pattern: ClassVar[Pattern] = re.compile(
71
+ "^[a-zA-Z0-9_]*$"
72
+ ) # Quote field names if this pattern (doesn't) matches, depending on field_quote_pattern_negation. Field name is always quoted if pattern is not set.
73
+ field_quote_pattern_negation: ClassVar[bool] = (
74
+ True # Negate field_quote_pattern result. Field name is quoted if pattern doesn't matches if set to True (default).
75
+ )
76
+
77
+ ## Values
78
+ str_quote: ClassVar[str] = (
79
+ "'" # string quoting character (added as escaping character)
80
+ )
81
+
82
+ escape_char: ClassVar[str] = (
83
+ "\\" # Escaping character for special characters inside string
84
+ )
85
+ wildcard_multi: ClassVar[str] = "%" # Character used as multi-character wildcard
86
+ wildcard_single: ClassVar[str] = "_" # Character used as single-character wildcard
87
+
88
+ # Special case for case sensitive string matching
89
+ wildcard_glob : ClassVar[str] = "*" # Character used as glob wildcard
90
+ wildcard_glob_single : ClassVar[str] = "?" # Character used as glob wildcard
91
+
92
+ add_escaped: ClassVar[str] = (
93
+ "\\" # Characters quoted in addition to wildcards and string quote
94
+ )
95
+ # filter_chars : ClassVar[str] = "" # Characters filtered
96
+ bool_values: ClassVar[Dict[bool, str]] = (
97
+ { # Values to which boolean values are mapped.
98
+ True: "true",
99
+ False: "false",
100
+ }
101
+ )
102
+
103
+ # String matching operators. if none is appropriate eq_token is used.
104
+ startswith_expression: ClassVar[str] = "{field} LIKE '{value}%' ESCAPE '\\'"
105
+ endswith_expression: ClassVar[str] = "{field} LIKE '%{value}' ESCAPE '\\'"
106
+ contains_expression: ClassVar[str] = "{field} LIKE '%{value}%' ESCAPE '\\'"
107
+ wildcard_match_expression: ClassVar[str] = (
108
+ "{field} LIKE '{value}' ESCAPE '\\'" # Special expression if wildcards can't be matched with the eq_token operator
109
+ )
110
+
111
+ field_exists_expression: ClassVar[str] = "{field} = {field}"
112
+
113
+ # Special expression if wildcards can't be matched with the eq_token operator
114
+ wildcard_match_str_expression: ClassVar[str] = "{field} LIKE '{value}' ESCAPE '\\'"
115
+ # wildcard_match_num_expression: ClassVar[str] = "{field} LIKE '%{value}%'"
116
+
117
+ # Regular expressions
118
+ # Regular expression query as format string with placeholders {field}, {regex}, {flag_x} where x
119
+ # is one of the flags shortcuts supported by Sigma (currently i, m and s) and refers to the
120
+ # token stored in the class variable re_flags.
121
+ re_expression: ClassVar[str] = "{field} REGEXP '{regex}'"
122
+ re_escape_char: ClassVar[str] = (
123
+ "" # Character used for escaping in regular expressions
124
+ )
125
+ re_escape: ClassVar[Tuple[str]] = () # List of strings that are escaped
126
+ re_escape_escape_char: bool = True # If True, the escape character is also escaped
127
+ re_flag_prefix: bool = (
128
+ True # If True, the flags are prepended as (?x) group at the beginning of the regular expression, e.g. (?i). If this is not supported by the target, it should be set to False.
129
+ )
130
+
131
+ # Mapping from SigmaRegularExpressionFlag values to static string templates that are used in
132
+ # flag_x placeholders in re_expression template.
133
+ # By default, i, m and s are defined. If a flag is not supported by the target query language,
134
+ # remove it from re_flags or don't define it to ensure proper error handling in case of appearance.
135
+ # re_flags : Dict[SigmaRegularExpressionFlag, str] = {}
136
+
137
+ case_sensitive_match_expression: ClassVar[str] = "{field} GLOB {value} ESCAPE '\\'"
138
+
139
+ # Numeric comparison operators
140
+ compare_op_expression: ClassVar[str] = (
141
+ "{field} {operator} {value}" # Compare operation query as format string with placeholders {field}, {operator} and {value}
142
+ )
143
+ # Mapping between CompareOperators elements and strings used as replacement for {operator} in compare_op_expression
144
+ compare_operators: ClassVar[Dict[SigmaCompareExpression.CompareOperators, str]] = {
145
+ SigmaCompareExpression.CompareOperators.LT: "<",
146
+ SigmaCompareExpression.CompareOperators.LTE: "<=",
147
+ SigmaCompareExpression.CompareOperators.GT: ">",
148
+ SigmaCompareExpression.CompareOperators.GTE: ">=",
149
+ }
150
+
151
+ # Expression for comparing two event fields
152
+ field_equals_field_expression: ClassVar[Optional[str]] = (
153
+ None # Field comparison expression with the placeholders {field1} and {field2} corresponding to left field and right value side of Sigma detection item
154
+ )
155
+ field_equals_field_escaping_quoting: Tuple[bool, bool] = (
156
+ True,
157
+ True,
158
+ ) # If regular field-escaping/quoting is applied to field1 and field2. A custom escaping/quoting can be implemented in the convert_condition_field_eq_field_escape_and_quote method.
159
+
160
+ # Null/None expressions
161
+ field_null_expression: ClassVar[str] = (
162
+ "{field} IS NULL" # Expression for field has null value as format string with {field} placeholder for field name
163
+ )
164
+
165
+ # Field existence condition expressions.
166
+ # field_exists_expression : ClassVar[str] = "exists({field})" # Expression for field existence as format string with {field} placeholder for field name
167
+ # field_not_exists_expression : ClassVar[str] = "notexists({field})" # Expression for field non-existence as format string with {field} placeholder for field name. If not set, field_exists_expression is negated with boolean NOT.
168
+
169
+ # Field value in list, e.g. "field in (value list)" or "field containsall (value list)"
170
+ convert_or_as_in: ClassVar[bool] = False # Convert OR as in-expression
171
+ convert_and_as_in: ClassVar[bool] = False # Convert AND as in-expression
172
+ in_expressions_allow_wildcards: ClassVar[bool] = (
173
+ False # Values in list can contain wildcards. If set to False (default) only plain values are converted into in-expressions.
174
+ )
175
+ field_in_list_expression: ClassVar[str] = (
176
+ "{field} {op} ({list})" # Expression for field in list of values as format string with placeholders {field}, {op} and {list}
177
+ )
178
+ or_in_operator: ClassVar[str] = (
179
+ "IN" # Operator used to convert OR into in-expressions. Must be set if convert_or_as_in is set
180
+ )
181
+ # and_in_operator : ClassVar[str] = "contains-all" # Operator used to convert AND into in-expressions. Must be set if convert_and_as_in is set
182
+ list_separator: ClassVar[str] = ", " # List element separator
183
+
184
+ # Value not bound to a field
185
+
186
+ # TODO : SQlite only handles FTS ("MATCH") with virtual tables. Not Handled for now.
187
+ # unbound_value_str_expression : ClassVar[str] = "MATCH {value}" # Expression for string value not bound to a field as format string with placeholder {value}
188
+ # unbound_value_num_expression : ClassVar[str] = 'MATCH {value}' # Expression for number value not bound to a field as format string with placeholder {value}
189
+
190
+ # Query finalization: appending and concatenating deferred query part
191
+ deferred_start: ClassVar[str] = (
192
+ "" # String used as separator between main query and deferred parts
193
+ )
194
+ deferred_separator: ClassVar[str] = (
195
+ "" # String used to join multiple deferred query parts
196
+ )
197
+ deferred_only_query: ClassVar[str] = (
198
+ "" # String used as query if final query only contains deferred expression
199
+ )
200
+
201
+ table = "<TABLE_NAME>"
202
+
203
+ def convert_value_str(
204
+ self, s: SigmaString, state: ConversionState, no_quote: bool = False, glob_wildcards: bool = False
205
+ ) -> str:
206
+ """Convert a SigmaString into a plain string which can be used in query."""
207
+
208
+ if glob_wildcards:
209
+ converted = s.convert(
210
+ escape_char=self.escape_char,
211
+ wildcard_multi=self.wildcard_glob,
212
+ wildcard_single=self.wildcard_glob_single,
213
+ add_escaped=self.add_escaped,
214
+ filter_chars=self.filter_chars,
215
+ )
216
+ else:
217
+ converted = s.convert(
218
+ escape_char=self.escape_char,
219
+ wildcard_multi=self.wildcard_multi,
220
+ wildcard_single=self.wildcard_single,
221
+ add_escaped=self.add_escaped,
222
+ filter_chars=self.filter_chars,
223
+ )
224
+
225
+ converted = converted.replace(
226
+ "'", "''"
227
+ ) # Doubling single quote in SQL is mandatory
228
+
229
+ if self.decide_string_quoting(s) and not no_quote:
230
+ return self.quote_string(converted)
231
+ else:
232
+ return converted
233
+
234
+ def convert_condition_field_eq_val_str(
235
+ self, cond: ConditionFieldEqualsValueExpression, state: ConversionState
236
+ ) -> Union[str, DeferredQueryExpression]:
237
+ """Conversion of field = string value expressions"""
238
+ try:
239
+ remove_quote = True # Expressions that use "LIKE" (starswith, endswith, ...) don't need supplemental quotes
240
+
241
+ if ( # Check conditions for usage of 'startswith' operator
242
+ self.startswith_expression
243
+ is not None # 'startswith' operator is defined in backend
244
+ and cond.value.endswith(
245
+ SpecialChars.WILDCARD_MULTI
246
+ ) # String ends with wildcard
247
+ and not cond.value[
248
+ :-1
249
+ ].contains_special() # Remainder of string doesn't contains special characters
250
+ ):
251
+ expr = (
252
+ self.startswith_expression
253
+ ) # If all conditions are fulfilled, use 'startswith' operartor instead of equal token
254
+ value = cond.value[:-1]
255
+ elif ( # Same as above but for 'endswith' operator: string starts with wildcard and doesn't contains further special characters
256
+ self.endswith_expression is not None
257
+ and cond.value.startswith(SpecialChars.WILDCARD_MULTI)
258
+ and not cond.value[1:].contains_special()
259
+ ):
260
+ expr = self.endswith_expression
261
+ value = cond.value[1:]
262
+ elif ( # contains: string starts and ends with wildcard
263
+ self.contains_expression is not None
264
+ and cond.value.startswith(SpecialChars.WILDCARD_MULTI)
265
+ and cond.value.endswith(SpecialChars.WILDCARD_MULTI)
266
+ and not cond.value[1:-1].contains_special()
267
+ ):
268
+ expr = self.contains_expression
269
+ value = cond.value[1:-1]
270
+ elif ( # wildcard match expression: string contains wildcard
271
+ self.wildcard_match_expression is not None
272
+ and (
273
+ cond.value.contains_special()
274
+ or self.wildcard_multi in cond.value
275
+ or self.wildcard_single in cond.value
276
+ or self.escape_char in cond.value
277
+ )
278
+ ):
279
+ expr = self.wildcard_match_expression
280
+ value = cond.value
281
+ else:
282
+ expr = "{field}" + self.eq_token + "{value}"
283
+ value = cond.value
284
+ remove_quote = False
285
+
286
+ if remove_quote:
287
+ return expr.format(
288
+ field=self.escape_and_quote_field(cond.field),
289
+ value=self.convert_value_str(value, state, remove_quote),
290
+ )
291
+ else:
292
+ return expr.format(
293
+ field=self.escape_and_quote_field(cond.field),
294
+ value=self.convert_value_str(value, state),
295
+ )
296
+ except TypeError: # pragma: no cover
297
+ raise NotImplementedError(
298
+ "Field equals string value expressions with strings are not supported by the backend."
299
+ )
300
+
301
+ def convert_condition_field_eq_val_str_case_sensitive(
302
+ self, cond: ConditionFieldEqualsValueExpression, state: ConversionState
303
+ ) -> Union[str, DeferredQueryExpression]:
304
+ """Conversion of case-sensitive field = string value expressions"""
305
+ try:
306
+ if ( # Check conditions for usage of 'startswith' operator
307
+ self.case_sensitive_startswith_expression
308
+ is not None # 'startswith' operator is defined in backend
309
+ and cond.value.endswith(SpecialChars.WILDCARD_MULTI) # String ends with wildcard
310
+ and (
311
+ self.case_sensitive_startswith_expression_allow_special
312
+ or not cond.value[:-1].contains_special()
313
+ ) # Remainder of string doesn't contains special characters or it's allowed
314
+ ):
315
+ expr = (
316
+ self.case_sensitive_startswith_expression
317
+ ) # If all conditions are fulfilled, use 'startswith' operator instead of equal token
318
+ value = cond.value[:-1]
319
+ elif ( # Same as above but for 'endswith' operator: string starts with wildcard and doesn't contains further special characters
320
+ self.case_sensitive_endswith_expression is not None
321
+ and cond.value.startswith(SpecialChars.WILDCARD_MULTI)
322
+ and (
323
+ self.case_sensitive_endswith_expression_allow_special
324
+ or not cond.value[1:].contains_special()
325
+ )
326
+ ):
327
+ expr = self.case_sensitive_endswith_expression
328
+ value = cond.value[1:]
329
+ elif ( # contains: string starts and ends with wildcard
330
+ self.case_sensitive_contains_expression is not None
331
+ and cond.value.startswith(SpecialChars.WILDCARD_MULTI)
332
+ and cond.value.endswith(SpecialChars.WILDCARD_MULTI)
333
+ and (
334
+ self.case_sensitive_contains_expression_allow_special
335
+ or not cond.value[1:-1].contains_special()
336
+ )
337
+ ):
338
+ expr = self.case_sensitive_contains_expression
339
+ value = cond.value[1:-1]
340
+ elif self.case_sensitive_match_expression is not None:
341
+ expr = self.case_sensitive_match_expression
342
+ value = cond.value
343
+ else:
344
+ raise NotImplementedError(
345
+ "Case-sensitive string matching is not supported by backend."
346
+ )
347
+
348
+ return expr.format(
349
+ field=self.escape_and_quote_field(cond.field),
350
+ value=self.convert_value_str(value, state, no_quote=False, glob_wildcards=True),
351
+ regex=self.convert_value_re(value.to_regex(self.add_escaped_re), state),
352
+ )
353
+ except TypeError: # pragma: no cover
354
+ raise NotImplementedError(
355
+ "Case-sensitive field equals string value expressions with strings are not supported by the backend."
356
+ )
357
+
358
+ def convert_condition_field_eq_val_cidr(
359
+ self, cond: ConditionFieldEqualsValueExpression, state: ConversionState
360
+ ) -> Union[str, DeferredQueryExpression]:
361
+ """Conversion of field matches CIDR value expressions."""
362
+ cidr: SigmaCIDRExpression = cond.value
363
+ expanded = cidr.expand()
364
+ expanded_cond = ConditionOR(
365
+ [
366
+ ConditionFieldEqualsValueExpression(cond.field, SigmaString(network))
367
+ for network in expanded
368
+ ],
369
+ cond.source,
370
+ )
371
+ return self.convert_condition(expanded_cond, state)
372
+
373
+ def finalize_query_default(
374
+ self, rule: SigmaRule, query: str, index: int, state: ConversionState
375
+ ) -> Any:
376
+
377
+ # TODO : fields support will be handled with a backend option (all fields by default)
378
+ # fields = "*" if len(rule.fields) == 0 else f"*, {', '.join(rule.fields)}"
379
+
380
+ # TODO : table name will be handled with a backend option
381
+ sqlite_query = f"SELECT * FROM {self.table} WHERE {query}"
382
+
383
+ return sqlite_query
384
+
385
+ def finalize_query_zircolite(
386
+ self, rule: SigmaRule, query: str, index: int, state: ConversionState
387
+ ) -> Any:
388
+
389
+ sqlite_query = f"SELECT * FROM logs WHERE {query}"
390
+
391
+ rule_as_dict = rule.to_dict()
392
+
393
+ zircolite_rule = {
394
+ "title": rule_as_dict["title"],
395
+ "id": rule_as_dict["id"] if "id" in rule_as_dict else "",
396
+ "status": rule_as_dict["status"] if "status" in rule_as_dict else "",
397
+ "description": (
398
+ rule_as_dict["description"] if "description" in rule_as_dict else ""
399
+ ),
400
+ "author": rule_as_dict["author"] if "author" in rule_as_dict else "",
401
+ "tags": rule_as_dict["tags"] if "tags" in rule_as_dict else [],
402
+ "falsepositives": (
403
+ rule_as_dict["falsepositives"]
404
+ if "falsepositives" in rule_as_dict
405
+ else []
406
+ ),
407
+ "level": rule_as_dict["level"] if "level" in rule_as_dict else "",
408
+ "rule": [sqlite_query],
409
+ "filename": "",
410
+ }
411
+ return zircolite_rule
412
+
413
+ def finalize_output_zircolite(self, queries: List[Dict]) -> str:
414
+ return json.dumps(list(queries))
415
+
416
+ # TODO : SQlite only handles FTS ("MATCH") with virtual tables. Not Handled for now.
417
+ def convert_condition_val_str(
418
+ self, cond: ConditionValueExpression, state: ConversionState
419
+ ) -> Union[str, DeferredQueryExpression]:
420
+ """Conversion of value-only strings."""
421
+ raise SigmaFeatureNotSupportedByBackendError(
422
+ "Value-only string expressions (i.e Full Text Search or 'keywords' search) are not supported by the backend."
423
+ )
424
+
425
+ def convert_condition_val_num(
426
+ self, cond: ConditionValueExpression, state: ConversionState
427
+ ) -> Union[str, DeferredQueryExpression]:
428
+ """Conversion of value-only numbers."""
429
+ raise SigmaFeatureNotSupportedByBackendError(
430
+ "Value-only number expressions (i.e Full Text Search or 'keywords' search) are not supported by the backend."
431
+ )
@@ -1,5 +0,0 @@
1
- from .sqlite import sqliteBackend
2
-
3
- backends = { # Mapping between backend identifiers and classes. This is used by the pySigma plugin system to recognize backends and expose them with the identifier.
4
- "sqlite": sqliteBackend,
5
- }
@@ -1,260 +0,0 @@
1
- from sigma.conversion.deferred import DeferredQueryExpression
2
- from sigma.conversion.state import ConversionState
3
- from sigma.exceptions import SigmaFeatureNotSupportedByBackendError
4
- from sigma.rule import SigmaRule
5
- from sigma.conversion.base import TextQueryBackend
6
- from sigma.conditions import ConditionItem, ConditionAND, ConditionOR, ConditionNOT, ConditionValueExpression, ConditionFieldEqualsValueExpression
7
- from sigma.types import SigmaCompareExpression, SigmaString, SpecialChars, SigmaRegularExpressionFlag, SigmaCIDRExpression
8
-
9
- import re
10
- import json
11
- from typing import ClassVar, Dict, List, Optional, Pattern, Tuple, Union, Any
12
-
13
- class sqliteBackend(TextQueryBackend):
14
- """SQLite backend."""
15
-
16
- # Operator precedence: tuple of Condition{AND,OR,NOT} in order of precedence.
17
- # The backend generates grouping if required
18
- name : ClassVar[str] = "SQLite backend"
19
- formats : Dict[str, str] = {
20
- "default": "Plain SQLite queries",
21
- "zircolite": "Zircolite JSON format",
22
- }
23
- requires_pipeline : bool = False # TODO: does the backend requires that a processing pipeline is provided? This information can be used by user interface programs like Sigma CLI to warn users about inappropriate usage of the backend.
24
-
25
- precedence : ClassVar[Tuple[ConditionItem, ConditionItem, ConditionItem]] = (ConditionNOT, ConditionAND, ConditionOR)
26
- parenthesize : bool = True
27
- group_expression : ClassVar[str] = "({expr})" # Expression for precedence override grouping as format string with {expr} placeholder
28
-
29
- # Generated query tokens
30
- token_separator : str = " " # separator inserted between all boolean operators
31
- or_token : ClassVar[str] = "OR"
32
- and_token : ClassVar[str] = "AND"
33
- not_token : ClassVar[str] = "NOT"
34
- eq_token : ClassVar[str] = "=" # Token inserted between field and value (without separator)
35
-
36
- # String output
37
- ## Fields
38
- ### Quoting
39
-
40
- # SQLite correct way to handle field name is detailed here : https://sqlite.org/lang_keywords.html.
41
- # Double-quoting should be the way to go. But since in some case quotes are interpreted as literals we need to find an alternative.
42
- # Obviously, we cannot use "[" and "]", because it is 2 different characters, so we are left with "`" (MySQL).
43
- field_quote : ClassVar[str] = "`" # Character used to quote field characters if field_quote_pattern matches (or not, depending on field_quote_pattern_negation). No field name quoting is done if not set.
44
- field_quote_pattern : ClassVar[Pattern] = re.compile("^[a-zA-Z0-9_]*$") # Quote field names if this pattern (doesn't) matches, depending on field_quote_pattern_negation. Field name is always quoted if pattern is not set.
45
- field_quote_pattern_negation : ClassVar[bool] = True # Negate field_quote_pattern result. Field name is quoted if pattern doesn't matches if set to True (default).
46
-
47
- ### Escaping
48
- #field_escape : ClassVar[str] = "\\" # Character to escape particular parts defined in field_escape_pattern.
49
- #field_escape_quote : ClassVar[bool] = True # Escape quote string defined in field_quote
50
- #field_escape_pattern : ClassVar[Pattern] = re.compile("\\s") # All matches of this pattern are prepended with the string contained in field_escape.
51
-
52
- ## Values
53
- str_quote : ClassVar[str] = "'" # string quoting character (added as escaping character)
54
- #str_quote_pattern: ClassVar[Pattern] = re.compile(r"^.*%{1,}.*|.*_{1,}.*$")
55
- #str_quote_pattern_negation: ClassVar[bool] = True
56
-
57
- escape_char : ClassVar[str] = "\\" # Escaping character for special characters inside string
58
- wildcard_multi : ClassVar[str] = "%" # Character used as multi-character wildcard
59
- wildcard_single : ClassVar[str] = "_" # Character used as single-character wildcard
60
- add_escaped : ClassVar[str] = "\\" # Characters quoted in addition to wildcards and string quote
61
- #filter_chars : ClassVar[str] = "" # Characters filtered
62
- bool_values : ClassVar[Dict[bool, str]] = { # Values to which boolean values are mapped.
63
- True: "true",
64
- False: "false",
65
- }
66
-
67
- # String matching operators. if none is appropriate eq_token is used.
68
- startswith_expression : ClassVar[str] = "{field} LIKE '{value}%' ESCAPE '\\'"
69
- endswith_expression : ClassVar[str] = "{field} LIKE '%{value}' ESCAPE '\\'"
70
- contains_expression : ClassVar[str] = "{field} LIKE '%{value}%' ESCAPE '\\'"
71
- wildcard_match_expression : ClassVar[str] = "{field} LIKE '{value}' ESCAPE '\\'" # Special expression if wildcards can't be matched with the eq_token operator
72
-
73
- # Special expression if wildcards can't be matched with the eq_token operator
74
- wildcard_match_str_expression: ClassVar[str] = "{field} LIKE '{value}' ESCAPE '\\'"
75
- #wildcard_match_num_expression: ClassVar[str] = "{field} LIKE '%{value}%'"
76
-
77
- # Regular expressions
78
- # Regular expression query as format string with placeholders {field}, {regex}, {flag_x} where x
79
- # is one of the flags shortcuts supported by Sigma (currently i, m and s) and refers to the
80
- # token stored in the class variable re_flags.
81
- re_expression : ClassVar[str] = "{field} REGEXP '{regex}'"
82
- re_escape_char : ClassVar[str] = "" # Character used for escaping in regular expressions
83
- re_escape : ClassVar[Tuple[str]] = () # List of strings that are escaped
84
- re_escape_escape_char : bool = True # If True, the escape character is also escaped
85
- re_flag_prefix : bool = True # If True, the flags are prepended as (?x) group at the beginning of the regular expression, e.g. (?i). If this is not supported by the target, it should be set to False.
86
-
87
- # Mapping from SigmaRegularExpressionFlag values to static string templates that are used in
88
- # flag_x placeholders in re_expression template.
89
- # By default, i, m and s are defined. If a flag is not supported by the target query language,
90
- # remove it from re_flags or don't define it to ensure proper error handling in case of appearance.
91
- # re_flags : Dict[SigmaRegularExpressionFlag, str] = {}
92
-
93
- # Numeric comparison operators
94
- compare_op_expression : ClassVar[str] = "{field} {operator} {value}" # Compare operation query as format string with placeholders {field}, {operator} and {value}
95
- # Mapping between CompareOperators elements and strings used as replacement for {operator} in compare_op_expression
96
- compare_operators : ClassVar[Dict[SigmaCompareExpression.CompareOperators, str]] = {
97
- SigmaCompareExpression.CompareOperators.LT : "<",
98
- SigmaCompareExpression.CompareOperators.LTE : "<=",
99
- SigmaCompareExpression.CompareOperators.GT : ">",
100
- SigmaCompareExpression.CompareOperators.GTE : ">=",
101
- }
102
-
103
- # Expression for comparing two event fields
104
- field_equals_field_expression : ClassVar[Optional[str]] = None # Field comparison expression with the placeholders {field1} and {field2} corresponding to left field and right value side of Sigma detection item
105
- field_equals_field_escaping_quoting : Tuple[bool, bool] = (True, True) # If regular field-escaping/quoting is applied to field1 and field2. A custom escaping/quoting can be implemented in the convert_condition_field_eq_field_escape_and_quote method.
106
-
107
- # Null/None expressions
108
- field_null_expression : ClassVar[str] = "{field} IS NULL" # Expression for field has null value as format string with {field} placeholder for field name
109
-
110
- # Field existence condition expressions.
111
- #field_exists_expression : ClassVar[str] = "exists({field})" # Expression for field existence as format string with {field} placeholder for field name
112
- #field_not_exists_expression : ClassVar[str] = "notexists({field})" # Expression for field non-existence as format string with {field} placeholder for field name. If not set, field_exists_expression is negated with boolean NOT.
113
-
114
- # Field value in list, e.g. "field in (value list)" or "field containsall (value list)"
115
- convert_or_as_in : ClassVar[bool] = False # Convert OR as in-expression
116
- convert_and_as_in : ClassVar[bool] = False # Convert AND as in-expression
117
- in_expressions_allow_wildcards : ClassVar[bool] = False # Values in list can contain wildcards. If set to False (default) only plain values are converted into in-expressions.
118
- field_in_list_expression : ClassVar[str] = "{field} {op} ({list})" # Expression for field in list of values as format string with placeholders {field}, {op} and {list}
119
- or_in_operator : ClassVar[str] = "IN" # Operator used to convert OR into in-expressions. Must be set if convert_or_as_in is set
120
- #and_in_operator : ClassVar[str] = "contains-all" # Operator used to convert AND into in-expressions. Must be set if convert_and_as_in is set
121
- list_separator : ClassVar[str] = ", " # List element separator
122
-
123
- # Value not bound to a field
124
-
125
- # TODO : SQlite only handles FTS ("MATCH") with virtual tables. Not Handled for now.
126
- #unbound_value_str_expression : ClassVar[str] = "MATCH {value}" # Expression for string value not bound to a field as format string with placeholder {value}
127
- #unbound_value_num_expression : ClassVar[str] = 'MATCH {value}' # Expression for number value not bound to a field as format string with placeholder {value}
128
-
129
- # Query finalization: appending and concatenating deferred query part
130
- deferred_start : ClassVar[str] = "" # String used as separator between main query and deferred parts
131
- deferred_separator : ClassVar[str] = "" # String used to join multiple deferred query parts
132
- deferred_only_query : ClassVar[str] = "" # String used as query if final query only contains deferred expression
133
-
134
- table = '<TABLE_NAME>'
135
-
136
- def convert_value_str(self, s: SigmaString, state: ConversionState, no_quote: bool=False) -> str:
137
- """Convert a SigmaString into a plain string which can be used in query."""
138
- converted = s.convert(
139
- self.escape_char,
140
- self.wildcard_multi,
141
- self.wildcard_single,
142
- self.add_escaped,
143
- self.filter_chars,
144
- )
145
-
146
- converted = converted.replace("\'", "\'\'") # Doubling single quote in SQL is mandatory
147
-
148
- if self.decide_string_quoting(s) and not no_quote:
149
- return self.quote_string(converted)
150
- else:
151
- return converted
152
-
153
- def convert_condition_field_eq_val_str(
154
- self, cond: ConditionFieldEqualsValueExpression, state: ConversionState
155
- ) -> Union[str, DeferredQueryExpression]:
156
- """Conversion of field = string value expressions"""
157
- try:
158
- remove_quote = True # Expressions that use "LIKE" (starswith, endswith, ...) don't need supplemental quotes
159
-
160
- if ( # Check conditions for usage of 'startswith' operator
161
- self.startswith_expression is not None # 'startswith' operator is defined in backend
162
- and cond.value.endswith(SpecialChars.WILDCARD_MULTI) # String ends with wildcard
163
- and not cond.value[:-1].contains_special() # Remainder of string doesn't contains special characters
164
- ):
165
- expr = (self.startswith_expression) # If all conditions are fulfilled, use 'startswith' operartor instead of equal token
166
- value = cond.value[:-1]
167
- elif ( # Same as above but for 'endswith' operator: string starts with wildcard and doesn't contains further special characters
168
- self.endswith_expression is not None
169
- and cond.value.startswith(SpecialChars.WILDCARD_MULTI)
170
- and not cond.value[1:].contains_special()
171
- ):
172
- expr = self.endswith_expression
173
- value = cond.value[1:]
174
- elif ( # contains: string starts and ends with wildcard
175
- self.contains_expression is not None
176
- and cond.value.startswith(SpecialChars.WILDCARD_MULTI)
177
- and cond.value.endswith(SpecialChars.WILDCARD_MULTI)
178
- and not cond.value[1:-1].contains_special()
179
- ):
180
- expr = self.contains_expression
181
- value = cond.value[1:-1]
182
- elif ( # wildcard match expression: string contains wildcard
183
- self.wildcard_match_expression is not None and (
184
- cond.value.contains_special()
185
- or self.wildcard_multi in cond.value
186
- or self.wildcard_single in cond.value
187
- or self.escape_char in cond.value
188
- )
189
- ):
190
- expr = self.wildcard_match_expression
191
- value = cond.value
192
- else:
193
- expr = "{field}" + self.eq_token + "{value}"
194
- value = cond.value
195
- remove_quote = False
196
-
197
- if remove_quote:
198
- return expr.format(
199
- field=self.escape_and_quote_field(cond.field),
200
- value=self.convert_value_str(value, state, remove_quote),
201
- )
202
- else:
203
- return expr.format(
204
- field=self.escape_and_quote_field(cond.field),
205
- value=self.convert_value_str(value, state),
206
- )
207
- except TypeError: # pragma: no cover
208
- raise NotImplementedError(
209
- "Field equals string value expressions with strings are not supported by the backend."
210
- )
211
-
212
- def convert_condition_field_eq_val_cidr(self, cond : ConditionFieldEqualsValueExpression, state : ConversionState) -> Union[str, DeferredQueryExpression]:
213
- """Conversion of field matches CIDR value expressions."""
214
- cidr : SigmaCIDRExpression = cond.value
215
- expanded = cidr.expand()
216
- expanded_cond = ConditionOR([ ConditionFieldEqualsValueExpression(cond.field, SigmaString(network)) for network in expanded ], cond.source)
217
- return self.convert_condition(expanded_cond, state)
218
-
219
- def finalize_query_default(self, rule: SigmaRule, query: str, index: int, state: ConversionState) -> Any:
220
-
221
- # TODO : fields support will be handled with a backend option (all fields by default)
222
- #fields = "*" if len(rule.fields) == 0 else f"*, {', '.join(rule.fields)}"
223
-
224
- # TODO : table name will be handled with a backend option
225
- sqlite_query = f'SELECT * FROM {self.table} WHERE {query}'
226
-
227
- return sqlite_query
228
-
229
-
230
- def finalize_query_zircolite(self, rule: SigmaRule, query: str, index: int, state: ConversionState) -> Any:
231
-
232
- sqlite_query = f'SELECT * FROM logs WHERE {query}'
233
-
234
- rule_as_dict = rule.to_dict()
235
-
236
- zircolite_rule = {
237
- "title": rule_as_dict["title"],
238
- "id": rule_as_dict["id"] if "id" in rule_as_dict else "",
239
- "status": rule_as_dict["status"] if "status" in rule_as_dict else "",
240
- "description": rule_as_dict["description"] if "description" in rule_as_dict else "",
241
- "author": rule_as_dict["author"] if "author" in rule_as_dict else "",
242
- "tags": rule_as_dict["tags"] if "tags" in rule_as_dict else [],
243
- "falsepositives": rule_as_dict["falsepositives"] if "falsepositives" in rule_as_dict else [],
244
- "level": rule_as_dict["level"] if "level" in rule_as_dict else "",
245
- "rule": [sqlite_query],
246
- "filename": ""
247
- }
248
- return zircolite_rule
249
-
250
- def finalize_output_zircolite(self, queries: List[Dict]) -> str:
251
- return json.dumps(list(queries))
252
-
253
- # TODO : SQlite only handles FTS ("MATCH") with virtual tables. Not Handled for now.
254
- def convert_condition_val_str(self, cond : ConditionValueExpression, state : ConversionState) -> Union[str, DeferredQueryExpression]:
255
- """Conversion of value-only strings."""
256
- raise SigmaFeatureNotSupportedByBackendError("Value-only string expressions (i.e Full Text Search or 'keywords' search) are not supported by the backend.")
257
-
258
- def convert_condition_val_num(self, cond : ConditionValueExpression, state : ConversionState) -> Union[str, DeferredQueryExpression]:
259
- """Conversion of value-only numbers."""
260
- raise SigmaFeatureNotSupportedByBackendError("Value-only number expressions (i.e Full Text Search or 'keywords' search) are not supported by the backend.")