pysigma-backend-parseable 0.1.0__py3-none-any.whl

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.
@@ -0,0 +1,327 @@
1
+ Metadata-Version: 2.5
2
+ Name: pysigma-backend-parseable
3
+ Version: 0.1.0
4
+ Summary: pySigma backend for Parseable SQL queries
5
+ Project-URL: Homepage, https://github.com/parseablehq/pySigma-backend-parseable
6
+ Project-URL: Issues, https://github.com/parseablehq/pySigma-backend-parseable/issues
7
+ Author: Parseable pySigma contributors
8
+ License: MIT
9
+ License-File: LICENSE
10
+ Keywords: parseable,pysigma,security,sigma,sql
11
+ Requires-Python: >=3.10
12
+ Requires-Dist: pysigma<2,>=1.0.0
13
+ Provides-Extra: test
14
+ Requires-Dist: coverage[toml]>=7.8; extra == 'test'
15
+ Requires-Dist: pytest-cov>=6.1; extra == 'test'
16
+ Requires-Dist: pytest>=8.3; extra == 'test'
17
+ Requires-Dist: ruff>=0.12; extra == 'test'
18
+ Description-Content-Type: text/markdown
19
+
20
+ # pySigma backend for Parseable
21
+
22
+ Convert [Sigma](https://github.com/SigmaHQ/sigma) detection rules into SQL accepted by
23
+ [Parseable](https://github.com/parseablehq/parseable).
24
+
25
+ The backend generates SQL only. It does not create alerts, send queries, discover datasets,
26
+ or infer how fields are stored in your Parseable instance.
27
+
28
+ ## Requirements
29
+
30
+ - Python 3.10 or newer
31
+ - pySigma 1.x
32
+ - Parseable v3.2.1 or newer when executing generated CIDR queries
33
+ - A Parseable dataset when executing the generated SQL
34
+
35
+ ## Installation
36
+
37
+ The package is not yet published. Install it from a checkout:
38
+
39
+ ```bash
40
+ python -m venv .venv
41
+ source .venv/bin/activate
42
+ python -m pip install -e .
43
+ ```
44
+
45
+ For the `sigma` command-line interface:
46
+
47
+ ```bash
48
+ python -m pip install sigma-cli
49
+ ```
50
+
51
+ Confirm that the plugin is available:
52
+
53
+ ```bash
54
+ sigma list targets
55
+ sigma list pipelines parseable
56
+ ```
57
+
58
+ ## Quick start
59
+
60
+ Convert a rule into a complete query:
61
+
62
+ ```bash
63
+ sigma convert \
64
+ --target parseable \
65
+ --backend-option dataset=windows-events \
66
+ examples/powershell.yml
67
+ ```
68
+
69
+ Example output:
70
+
71
+ ```sql
72
+ SELECT "Image", "CommandLine", "User"
73
+ FROM "windows-events"
74
+ WHERE LOWER("Image") LIKE '%\\powershell.exe' ESCAPE '\'
75
+ AND LOWER("CommandLine") LIKE '%-encodedcommand%' ESCAPE '\'
76
+ ```
77
+
78
+ The backend quotes dataset and column names, so dotted names such as `service.name` are
79
+ rendered as one SQL identifier: `"service.name"`.
80
+
81
+ ### Python API
82
+
83
+ ```python
84
+ from pathlib import Path
85
+
86
+ from sigma.backends.parseable import ParseableBackend
87
+ from sigma.collection import SigmaCollection
88
+
89
+ rules = SigmaCollection.from_yaml(Path("rule.yml").read_text())
90
+ backend = ParseableBackend(dataset="windows-events")
91
+
92
+ for query in backend.convert(rules):
93
+ print(query)
94
+ ```
95
+
96
+ `convert()` returns a list because a Sigma document may contain multiple rules or
97
+ conditions.
98
+
99
+ ## Output formats
100
+
101
+ The default format produces a complete query and requires a dataset:
102
+
103
+ ```bash
104
+ sigma convert -t parseable -O dataset=windows-events rule.yml
105
+ ```
106
+
107
+ ```sql
108
+ SELECT * FROM "windows-events" WHERE "EventID" = 4625
109
+ ```
110
+
111
+ The `predicate` format produces only the condition for embedding in another query. It does
112
+ not require a dataset:
113
+
114
+ ```bash
115
+ sigma convert -t parseable -f predicate rule.yml
116
+ ```
117
+
118
+ ```sql
119
+ "EventID" = 4625
120
+ ```
121
+
122
+ ## Backend options
123
+
124
+ | Option | Default | Description |
125
+ | ----------------------- | ----------------------------- | ------------------------------------------------------------ |
126
+ | `dataset` | none | Dataset in the `FROM` clause; required for default output |
127
+ | `limit` | none | Positive integer appended as `LIMIT` |
128
+ | `default_search_fields` | `body,message,event.original` | Comma-separated columns searched by fieldless Sigma keywords |
129
+
130
+ Example:
131
+
132
+ ```bash
133
+ sigma convert \
134
+ -t parseable \
135
+ -O dataset=application-logs \
136
+ -O limit=500 \
137
+ -O default_search_fields=body,message,log \
138
+ rule.yml
139
+ ```
140
+
141
+ Every configured search field must exist in the target dataset. DataFusion rejects a query
142
+ that references a missing column.
143
+
144
+ ## Field mapping pipelines
145
+
146
+ Sigma rules use abstract field names. Parseable queries must use the exact columns created
147
+ at ingestion. Select the pipeline matching your stored event schema:
148
+
149
+ | Pipeline | Use when |
150
+ | ------------------ | --------------------------------------------------------------------- |
151
+ | `parseable_otlp` | OTLP log attributes are stored as literal semantic-convention columns |
152
+ | `parseable_ecs` | Nested ECS documents are flattened by Parseable using underscores |
153
+ | `parseable_sysmon` | Events use native Sysmon fields |
154
+
155
+ ### OpenTelemetry
156
+
157
+ ```bash
158
+ sigma convert \
159
+ -t parseable \
160
+ -p parseable_otlp \
161
+ -O dataset=otel-events \
162
+ rule.yml
163
+ ```
164
+
165
+ Representative mappings:
166
+
167
+ | Sigma | Parseable OTLP |
168
+ | ----------------- | ------------------------- |
169
+ | `Image` | `process.executable.path` |
170
+ | `CommandLine` | `process.command_line` |
171
+ | `ProcessId` | `process.pid` |
172
+ | `ParentProcessId` | `process.parent_pid` |
173
+ | `SourceIp` | `source.address` |
174
+ | `DestinationIp` | `destination.address` |
175
+ | `DestinationPort` | `destination.port` |
176
+ | `TargetFilename` | `file.path` |
177
+ | `QueryName` | `dns.question.name` |
178
+ | `Computer` | `host.name` |
179
+
180
+ Mappings are scoped by Sigma log source where field meaning changes. The pipeline does not
181
+ invent fields without a standard OpenTelemetry equivalent.
182
+
183
+ ### ECS
184
+
185
+ ```bash
186
+ sigma convert \
187
+ -t parseable \
188
+ -p parseable_ecs \
189
+ -O dataset=ecs-events \
190
+ rule.yml
191
+ ```
192
+
193
+ Nested ECS input such as `{"source":{"ip":"192.0.2.1"}}` becomes the Parseable column
194
+ `source_ip`.
195
+
196
+ | Sigma | Flattened ECS |
197
+ | ---------------- | --------------------------- |
198
+ | `EventID` | `event_code` |
199
+ | `Channel` | `winlog_channel` |
200
+ | `Image` | `process_executable` |
201
+ | `CommandLine` | `process_command_line` |
202
+ | `ParentImage` | `process_parent_executable` |
203
+ | `User` | `user_name` |
204
+ | `SourceIp` | `source_ip` |
205
+ | `DestinationIp` | `destination_ip` |
206
+ | `TargetFilename` | `file_path` |
207
+ | `QueryName` | `dns_question_name` |
208
+
209
+ Use a custom pipeline if your events contain literal dotted ECS keys or use different column
210
+ names.
211
+
212
+ ### Sysmon
213
+
214
+ ```bash
215
+ sigma convert \
216
+ -t parseable \
217
+ -p parseable_sysmon \
218
+ -O dataset=sysmon-events \
219
+ rule.yml
220
+ ```
221
+
222
+ This pipeline retains native Sysmon fields and adds the appropriate `Channel` and `EventID`
223
+ conditions for generic Windows log sources. For Sysmon normalized to nested ECS before
224
+ ingestion, chain the pipelines:
225
+
226
+ ```bash
227
+ sigma convert \
228
+ -t parseable \
229
+ -p parseable_sysmon \
230
+ -p parseable_ecs \
231
+ -O dataset=ecs-sysmon-events \
232
+ rule.yml
233
+ ```
234
+
235
+ ### Custom schemas
236
+
237
+ Built-in pipelines cannot know organization-specific column names. Define a pySigma pipeline
238
+ for the schema actually present in your dataset:
239
+
240
+ ```yaml
241
+ name: My Parseable field mapping
242
+ priority: 30
243
+ allowed_backends:
244
+ - parseable
245
+ transformations:
246
+ - id: organization_fields
247
+ type: field_name_mapping
248
+ mapping:
249
+ Image: exe_path
250
+ CommandLine: command
251
+ User: username
252
+ SourceIp: client_ip
253
+ ```
254
+
255
+ ```bash
256
+ sigma convert \
257
+ -t parseable \
258
+ -p company-parseable.yml \
259
+ -O dataset=company-events \
260
+ rule.yml
261
+ ```
262
+
263
+ Always compare generated columns with the Parseable dataset schema before deploying rules.
264
+
265
+ ## Supported Sigma features
266
+
267
+ The backend supports:
268
+
269
+ - Case-insensitive Sigma string matching and the `cased` modifier
270
+ - `contains`, `startswith`, `endswith`, `exists`, `fieldref`, and comparison modifiers
271
+ - Sigma `*` and `?` wildcards
272
+ - String and numeric lists
273
+ - Regular expressions bound to a field
274
+ - Null checks and Boolean conditions
275
+ - Fieldless string and numeric keyword searches
276
+ - IPv4 and IPv6 CIDR expressions through Parseable's `ip_in_cidr` SQL function
277
+
278
+ Unsupported constructs fail explicitly with `SigmaFeatureNotSupportedByBackendError`:
279
+
280
+ - Sigma correlation rules
281
+ - Timestamp-part modifiers such as `minute` and `hour`
282
+ - Fieldless regular expressions
283
+
284
+ CIDR conversion requires a Parseable deployment that provides `ip_in_cidr(ip, cidr)`. The
285
+ function correctly parses IPv4 and IPv6 rather than approximating address ranges as text.
286
+
287
+ ## Placeholders
288
+
289
+ Sigma placeholders such as `%Administrators%` are deployment-specific values. Resolve them
290
+ with a processing pipeline before conversion:
291
+
292
+ ```bash
293
+ sigma convert \
294
+ -t parseable \
295
+ -p examples/placeholder-pipeline.yml \
296
+ -O dataset=windows-events \
297
+ examples/placeholder-rule.yml
298
+ ```
299
+
300
+ Do not replace unknown placeholders with wildcards; that changes the detection's meaning.
301
+
302
+ ## Development
303
+
304
+ Install development dependencies and run local checks:
305
+
306
+ ```bash
307
+ python -m pip install -e '.[test]'
308
+ pytest tests -m 'not integration'
309
+ ruff check .
310
+ python -m build
311
+ ```
312
+
313
+ Live tests require `PARSEABLE_URL`, `PARSEABLE_INGESTION_URL`, and `PARSEABLE_API_KEY`.
314
+ They use fixture datasets and are intentionally excluded from the default test command:
315
+
316
+ ```bash
317
+ pytest tests/integration -m integration
318
+ ```
319
+
320
+ The GitHub Actions corpus job checks conversion against a pinned Sigma corpus. Detailed
321
+ results and regression thresholds live in [`reports/`](reports/) rather than this README.
322
+ Successful conversion means valid SQL was generated; it does not prove that a deployment has
323
+ matching columns or representative data.
324
+
325
+ ## License
326
+
327
+ MIT
@@ -0,0 +1,10 @@
1
+ sigma/backends/parseable/__init__.py,sha256=2bbzGrT_Bh4h2GdstDarL_3t-r095WPSr0S-X5L7cpA,72
2
+ sigma/backends/parseable/parseable.py,sha256=zPXuSUGJF3BLD-e2_hySrzb942dzwupSVwNd-R96GnU,14465
3
+ sigma/pipelines/parseable/__init__.py,sha256=54FY2hG-MsmuSaYr2PCD72DXrIYD_u9Kdy4yPNHQWj0,456
4
+ sigma/pipelines/parseable/ecs.py,sha256=HOHg-6e8qnHQvPU89Jxc-cKTzcsB0_g6AGVQAdp1-4Q,4933
5
+ sigma/pipelines/parseable/otlp.py,sha256=CsE9tDkfpjmnemC09xJbIf4xK3QG0Xm6dYWmfZ5UlaA,4575
6
+ sigma/pipelines/parseable/sysmon.py,sha256=0gaE5BqRVnY-5Rpi8eWXBbB2LHUN1ZEyRoNkU8iA2c8,2017
7
+ pysigma_backend_parseable-0.1.0.dist-info/METADATA,sha256=4p6GPyssSzC7nxD7Tb-_Hfh0SfpdhWWuOFGTnJUTL4s,9474
8
+ pysigma_backend_parseable-0.1.0.dist-info/WHEEL,sha256=zOwg4jB6zX2kU910N-cMawjivD6tO8NEWvE12je1bVk,87
9
+ pysigma_backend_parseable-0.1.0.dist-info/licenses/LICENSE,sha256=8LCWq38mm3iqMrEF6MqhTYvtESoOEomIv7Md-AlqV9Y,1087
10
+ pysigma_backend_parseable-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.32.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Parseable pySigma contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,3 @@
1
+ from .parseable import ParseableBackend
2
+
3
+ __all__ = ["ParseableBackend"]
@@ -0,0 +1,342 @@
1
+ """Convert Sigma rules to SQL accepted by Parseable's DataFusion query engine."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import re
6
+ from typing import Any, ClassVar
7
+
8
+ from sigma.conditions import (
9
+ ConditionAND,
10
+ ConditionFieldEqualsValueExpression,
11
+ ConditionItem,
12
+ ConditionNOT,
13
+ ConditionOR,
14
+ ConditionValueExpression,
15
+ )
16
+ from sigma.conversion.base import TextQueryBackend
17
+ from sigma.conversion.deferred import DeferredQueryExpression
18
+ from sigma.conversion.state import ConversionState
19
+ from sigma.exceptions import SigmaConfigurationError, SigmaFeatureNotSupportedByBackendError
20
+ from sigma.rule import SigmaRule
21
+ from sigma.types import (
22
+ SigmaBool,
23
+ SigmaCasedString,
24
+ SigmaCIDRExpression,
25
+ SigmaCompareExpression,
26
+ SigmaNumber,
27
+ SigmaRegularExpression,
28
+ SigmaString,
29
+ SpecialChars,
30
+ )
31
+
32
+
33
+ class ParseableBackend(TextQueryBackend):
34
+ """Parseable SQL backend.
35
+
36
+ Backend owns SQL syntax only. Processing pipelines remain responsible for mapping
37
+ abstract Sigma fields to columns present in a particular Parseable dataset.
38
+ """
39
+
40
+ name: ClassVar[str] = "parseable"
41
+ formats: ClassVar[dict[str, str]] = {
42
+ "default": "Complete Parseable SQL query",
43
+ "predicate": "Parseable SQL WHERE predicate",
44
+ }
45
+ requires_pipeline: ClassVar[bool] = False
46
+ correlation_methods: ClassVar[None] = None
47
+
48
+ precedence: ClassVar[tuple[type[ConditionItem], type[ConditionItem], type[ConditionItem]]] = (
49
+ ConditionNOT,
50
+ ConditionAND,
51
+ ConditionOR,
52
+ )
53
+ parenthesize: ClassVar[bool] = True
54
+ group_expression: ClassVar[str] = "({expr})"
55
+ token_separator: ClassVar[str] = " "
56
+ or_token: ClassVar[str] = "OR"
57
+ and_token: ClassVar[str] = "AND"
58
+ not_token: ClassVar[str] = "NOT"
59
+
60
+ field_quote: ClassVar[str] = '"'
61
+ field_quote_pattern: ClassVar[None] = None # Always quote identifiers.
62
+ field_escape: ClassVar[str] = '"' # SQL identifier quote escapes by doubling.
63
+ field_escape_quote: ClassVar[bool] = True
64
+ field_escape_pattern: ClassVar[None] = None
65
+
66
+ str_quote: ClassVar[str] = "'"
67
+ escape_char: ClassVar[str] = "\\"
68
+ wildcard_multi: ClassVar[str] = "%"
69
+ wildcard_single: ClassVar[str] = "_"
70
+ add_escaped: ClassVar[str] = "%_"
71
+ filter_chars: ClassVar[str] = ""
72
+ bool_values: ClassVar[dict[bool, str]] = {True: "TRUE", False: "FALSE"}
73
+
74
+ eq_token: ClassVar[str] = " = "
75
+ eq_expression: ClassVar[str] = "LOWER({field}) = {value}"
76
+ startswith_expression: ClassVar[str] = "LOWER({field}) LIKE {value} ESCAPE '\\'"
77
+ endswith_expression: ClassVar[str] = "LOWER({field}) LIKE {value} ESCAPE '\\'"
78
+ contains_expression: ClassVar[str] = "LOWER({field}) LIKE {value} ESCAPE '\\'"
79
+ wildcard_match_expression: ClassVar[str] = "LOWER({field}) LIKE {value} ESCAPE '\\'"
80
+
81
+ case_sensitive_match_expression: ClassVar[str] = "{field} = {value}"
82
+ case_sensitive_startswith_expression: ClassVar[str] = "{field} LIKE {value} ESCAPE '\\'"
83
+ case_sensitive_endswith_expression: ClassVar[str] = "{field} LIKE {value} ESCAPE '\\'"
84
+ case_sensitive_contains_expression: ClassVar[str] = "{field} LIKE {value} ESCAPE '\\'"
85
+
86
+ re_expression: ClassVar[str] = "regexp_like({field}, '{regex}')"
87
+ re_escape_char: ClassVar[str] = "\\"
88
+ re_escape: ClassVar[tuple[str, ...]] = ("'",)
89
+ re_escape_escape_char: ClassVar[bool] = False
90
+ re_flag_prefix: ClassVar[bool] = True
91
+
92
+ field_null_expression: ClassVar[str] = "{field} IS NULL"
93
+ field_exists_expression: ClassVar[str] = "{field} IS NOT NULL"
94
+ field_not_exists_expression: ClassVar[str] = "{field} IS NULL"
95
+
96
+ field_equals_field_expression: ClassVar[str] = "LOWER({field1}) = LOWER({field2})"
97
+ field_equals_field_startswith_expression: ClassVar[str] = (
98
+ "starts_with(LOWER({field1}), LOWER({field2}))"
99
+ )
100
+ field_equals_field_endswith_expression: ClassVar[str] = (
101
+ "ends_with(LOWER({field1}), LOWER({field2}))"
102
+ )
103
+ field_equals_field_contains_expression: ClassVar[str] = (
104
+ "strpos(LOWER({field1}), LOWER({field2})) > 0"
105
+ )
106
+ field_equals_field_escaping_quoting: ClassVar[tuple[bool, bool]] = (True, True)
107
+
108
+ compare_op_expression: ClassVar[str] = "{field} {operator} {value}"
109
+ compare_operators: ClassVar[dict[SigmaCompareExpression.CompareOperators, str]] = {
110
+ SigmaCompareExpression.CompareOperators.LT: "<",
111
+ SigmaCompareExpression.CompareOperators.LTE: "<=",
112
+ SigmaCompareExpression.CompareOperators.GT: ">",
113
+ SigmaCompareExpression.CompareOperators.GTE: ">=",
114
+ }
115
+
116
+ convert_or_as_in: ClassVar[bool] = True
117
+ convert_and_as_in: ClassVar[bool] = False
118
+ in_expressions_allow_wildcards: ClassVar[bool] = False
119
+ field_in_list_expression: ClassVar[str] = "{field} {op} ({list})"
120
+ or_in_operator: ClassVar[str] = "IN"
121
+ list_separator: ClassVar[str] = ", "
122
+
123
+ cidr_expression: ClassVar[None] = None
124
+ deferred_start: ClassVar[None] = None
125
+ deferred_separator: ClassVar[None] = None
126
+ deferred_only_query: ClassVar[None] = None
127
+
128
+ _SAFE_INTEGER = re.compile(r"^(0|[1-9][0-9]*)$")
129
+
130
+ def __init__(
131
+ self,
132
+ processing_pipeline: Any = None,
133
+ collect_errors: bool = False,
134
+ **backend_options: Any,
135
+ ) -> None:
136
+ super().__init__(processing_pipeline, collect_errors, **backend_options)
137
+ dataset = backend_options.get("dataset")
138
+ self.dataset = str(dataset).strip() if dataset is not None else ""
139
+ self.limit = self._parse_limit(backend_options.get("limit"))
140
+ search_fields = backend_options.get("default_search_fields", "body,message,event.original")
141
+ if isinstance(search_fields, str):
142
+ search_fields = [field.strip() for field in search_fields.split(",")]
143
+ self.default_search_fields = [str(field) for field in search_fields if str(field).strip()]
144
+
145
+ @classmethod
146
+ def _parse_limit(cls, value: Any) -> int | None:
147
+ if value is None or value == "":
148
+ return None
149
+ text = str(value)
150
+ if not cls._SAFE_INTEGER.fullmatch(text) or int(text) < 1:
151
+ raise SigmaConfigurationError("Parseable limit must be a positive integer.")
152
+ return int(text)
153
+
154
+ @staticmethod
155
+ def _sql_string(value: str) -> str:
156
+ return "'" + value.replace("'", "''") + "'"
157
+
158
+ def _render_string(self, value: SigmaString, *, like: bool, cased: bool) -> str:
159
+ if like:
160
+ rendered = value.convert("\\", "%", "_", "\\%_", "")
161
+ else:
162
+ rendered = value.convert("", "%", "_", "", "")
163
+ if not cased:
164
+ rendered = rendered.casefold()
165
+ return self._sql_string(rendered)
166
+
167
+ def convert_condition_field_eq_val_str(
168
+ self, cond: ConditionFieldEqualsValueExpression, state: ConversionState
169
+ ) -> str | DeferredQueryExpression:
170
+ value = cond.value
171
+ if not isinstance(value, SigmaString):
172
+ raise TypeError(f"Expected SigmaString, got {type(value)}")
173
+
174
+ field = self.escape_and_quote_field(cond.field)
175
+ if value.startswith(SpecialChars.WILDCARD_MULTI) and value.endswith(
176
+ SpecialChars.WILDCARD_MULTI
177
+ ):
178
+ expression, actual, like = self.contains_expression, value, True
179
+ elif value.endswith(SpecialChars.WILDCARD_MULTI):
180
+ expression, actual, like = self.startswith_expression, value, True
181
+ elif value.startswith(SpecialChars.WILDCARD_MULTI):
182
+ expression, actual, like = self.endswith_expression, value, True
183
+ elif value.contains_special():
184
+ expression, actual, like = self.wildcard_match_expression, value, True
185
+ else:
186
+ expression, actual, like = self.eq_expression, value, False
187
+
188
+ return expression.format(
189
+ field=field,
190
+ value=self._render_string(actual, like=like, cased=False),
191
+ backend=self,
192
+ )
193
+
194
+ def convert_condition_field_eq_val_str_case_sensitive(
195
+ self, cond: ConditionFieldEqualsValueExpression, state: ConversionState
196
+ ) -> str | DeferredQueryExpression:
197
+ value = cond.value
198
+ if not isinstance(value, SigmaString):
199
+ raise TypeError(f"Expected SigmaString, got {type(value)}")
200
+ field = self.escape_and_quote_field(cond.field)
201
+
202
+ if value.startswith(SpecialChars.WILDCARD_MULTI) and value.endswith(
203
+ SpecialChars.WILDCARD_MULTI
204
+ ):
205
+ expression, like = self.case_sensitive_contains_expression, True
206
+ elif value.endswith(SpecialChars.WILDCARD_MULTI):
207
+ expression, like = self.case_sensitive_startswith_expression, True
208
+ elif value.startswith(SpecialChars.WILDCARD_MULTI):
209
+ expression, like = self.case_sensitive_endswith_expression, True
210
+ elif value.contains_special():
211
+ expression, like = "{field} LIKE {value} ESCAPE '\\'", True
212
+ else:
213
+ expression, like = self.case_sensitive_match_expression, False
214
+
215
+ return expression.format(
216
+ field=field,
217
+ value=self._render_string(value, like=like, cased=True),
218
+ backend=self,
219
+ )
220
+
221
+ def convert_condition_as_in_expression(
222
+ self, cond: ConditionOR | ConditionAND, state: ConversionState
223
+ ) -> str | DeferredQueryExpression:
224
+ first = cond.args[0]
225
+ field = self.escape_and_quote_field(first.field)
226
+ cased = isinstance(first.value, SigmaCasedString)
227
+ if isinstance(first.value, SigmaString) and not cased:
228
+ field = f"LOWER({field})"
229
+ values = self.list_separator.join(
230
+ self._render_in_value(arg.value, cased=cased) for arg in cond.args
231
+ )
232
+ return self.field_in_list_expression.format(field=field, op=self.or_in_operator, list=values)
233
+
234
+ def _render_in_value(self, value: Any, *, cased: bool) -> str:
235
+ if isinstance(value, SigmaString):
236
+ return self._render_string(value, like=False, cased=cased)
237
+ if isinstance(value, SigmaNumber):
238
+ return str(value.number)
239
+ if isinstance(value, SigmaBool):
240
+ return self.bool_values[value.boolean]
241
+ raise TypeError(f"Unsupported Parseable IN-list value: {type(value)}")
242
+
243
+ def convert_value_re(
244
+ self, value: SigmaRegularExpression, state: ConversionState
245
+ ) -> str | DeferredQueryExpression:
246
+ # DataFusion uses SQL string literals around regexes. Preserve regex backslashes,
247
+ # but double apostrophes so regex content cannot terminate the literal.
248
+ return value.escape((), "\\", False, True).replace("'", "''")
249
+
250
+ def convert_condition_val_str(
251
+ self, cond: ConditionValueExpression, state: ConversionState
252
+ ) -> str | DeferredQueryExpression:
253
+ if not isinstance(cond.value, SigmaString):
254
+ raise TypeError(f"Expected SigmaString, got {type(cond.value)}")
255
+ if not self.default_search_fields:
256
+ raise SigmaConfigurationError(
257
+ "Value-only Sigma keywords require default_search_fields."
258
+ )
259
+ value = cond.value
260
+ if not value.contains_special():
261
+ wrapped = SigmaString()
262
+ wrapped.s = [SpecialChars.WILDCARD_MULTI, *value.s, SpecialChars.WILDCARD_MULTI]
263
+ else:
264
+ wrapped = value
265
+ rendered = self._render_string(wrapped, like=True, cased=False)
266
+ return "(" + " OR ".join(
267
+ f"LOWER({self.escape_and_quote_field(field)}) LIKE {rendered} ESCAPE '\\'"
268
+ for field in self.default_search_fields
269
+ ) + ")"
270
+
271
+ @staticmethod
272
+ def _unsupported(feature: str, source: Any = None) -> None:
273
+ raise SigmaFeatureNotSupportedByBackendError(
274
+ f"Parseable backend does not support {feature}.", source=source
275
+ )
276
+
277
+ def convert_condition_field_eq_val_cidr(
278
+ self, cond: ConditionFieldEqualsValueExpression, state: ConversionState
279
+ ) -> str | DeferredQueryExpression:
280
+ cidr = cond.value
281
+ if not isinstance(cidr, SigmaCIDRExpression):
282
+ raise TypeError(f"Expected SigmaCIDRExpression, got {type(cidr)}")
283
+ field = self.escape_and_quote_field(cond.field)
284
+ return f"ip_in_cidr({field}, {self._sql_string(str(cidr.network))})"
285
+
286
+ def convert_condition_field_eq_val_timestamp_part(
287
+ self, cond: ConditionFieldEqualsValueExpression, state: ConversionState
288
+ ) -> str | DeferredQueryExpression:
289
+ self._unsupported("timestamp-part modifiers", cond.source)
290
+
291
+ def convert_condition_val_num(
292
+ self, cond: ConditionValueExpression, state: ConversionState
293
+ ) -> str | DeferredQueryExpression:
294
+ if not self.default_search_fields:
295
+ raise SigmaConfigurationError(
296
+ "Value-only Sigma numeric keywords require default_search_fields."
297
+ )
298
+ value = str(cond.value)
299
+ pattern = f"(^|[^0-9.+-]){re.escape(value)}([^0-9.]|$)"
300
+ rendered = self._sql_string(pattern)
301
+ return "(" + " OR ".join(
302
+ f"regexp_like(CAST({self.escape_and_quote_field(field)} AS VARCHAR), {rendered})"
303
+ for field in self.default_search_fields
304
+ ) + ")"
305
+
306
+ def convert_condition_val_re(
307
+ self, cond: ConditionValueExpression, state: ConversionState
308
+ ) -> str | DeferredQueryExpression:
309
+ self._unsupported("unbound regular-expression searches", cond.source)
310
+
311
+ def convert_correlation_rule(
312
+ self,
313
+ rule: Any,
314
+ output_format: str | None = None,
315
+ method: str | None = None,
316
+ callback: Any = None,
317
+ ) -> list[Any]:
318
+ self._unsupported("Sigma correlation rules", getattr(rule, "source", None))
319
+
320
+ def finalize_query_default(
321
+ self, rule: SigmaRule, query: str, index: int, state: ConversionState
322
+ ) -> str:
323
+ if not self.dataset:
324
+ raise SigmaConfigurationError(
325
+ "Parseable dataset required. Pass -O dataset=<name>."
326
+ )
327
+ fields = rule.fields or ["*"]
328
+ selected = ", ".join(
329
+ field if field == "*" else self.escape_and_quote_field(field) for field in fields
330
+ )
331
+ sql = f"SELECT {selected} FROM {self.escape_and_quote_field(self.dataset)} WHERE {query}"
332
+ if self.limit is not None:
333
+ sql += f" LIMIT {self.limit}"
334
+ return sql
335
+
336
+ def finalize_query_predicate(
337
+ self, rule: SigmaRule, query: str, index: int, state: ConversionState
338
+ ) -> str:
339
+ return query
340
+
341
+ def finalize_output_predicate(self, queries: list[str]) -> list[str]:
342
+ return queries
@@ -0,0 +1,17 @@
1
+ """Built-in schema mappings for the Parseable backend."""
2
+
3
+ from .ecs import parseable_ecs_pipeline
4
+ from .otlp import parseable_otlp_pipeline
5
+ from .sysmon import parseable_sysmon_pipeline
6
+
7
+ pipelines = {
8
+ "parseable_otlp": parseable_otlp_pipeline,
9
+ "parseable_ecs": parseable_ecs_pipeline,
10
+ "parseable_sysmon": parseable_sysmon_pipeline,
11
+ }
12
+
13
+ __all__ = [
14
+ "parseable_ecs_pipeline",
15
+ "parseable_otlp_pipeline",
16
+ "parseable_sysmon_pipeline",
17
+ ]
@@ -0,0 +1,123 @@
1
+ """Mappings for nested Elastic Common Schema JSON flattened by Parseable."""
2
+
3
+ from sigma.pipelines.common import generate_windows_logsource_items
4
+ from sigma.processing.conditions import (
5
+ FieldNameProcessingItemAppliedCondition,
6
+ IncludeFieldCondition,
7
+ LogsourceCondition,
8
+ )
9
+ from sigma.processing.pipeline import ProcessingItem, ProcessingPipeline
10
+ from sigma.processing.transformations import (
11
+ AddFieldnamePrefixTransformation,
12
+ FieldMappingTransformation,
13
+ )
14
+
15
+ ECS_WINDOWS_FIELDS = {
16
+ "EventID": "event_code",
17
+ "Channel": "winlog_channel",
18
+ "Provider_Name": "winlog_provider_name",
19
+ "Computer": "host_name",
20
+ "ComputerName": "winlog_computer_name",
21
+ "ProcessGuid": "process_entity_id",
22
+ "ProcessId": "process_pid",
23
+ "Image": "process_executable",
24
+ "CommandLine": "process_command_line",
25
+ "CurrentDirectory": "process_working_directory",
26
+ "ParentProcessGuid": "process_parent_entity_id",
27
+ "ParentProcessId": "process_parent_pid",
28
+ "ParentImage": "process_parent_executable",
29
+ "ParentCommandLine": "process_parent_command_line",
30
+ "FileVersion": "process_pe_file_version",
31
+ "Description": "process_pe_description",
32
+ "Product": "process_pe_product",
33
+ "Company": "process_pe_company",
34
+ "OriginalFileName": "process_pe_original_file_name",
35
+ "TargetFilename": "file_path",
36
+ "FileName": "file_path",
37
+ "ImageLoaded": "file_path",
38
+ "Device": "file_path",
39
+ "Signed": "file_code_signature_signed",
40
+ "Signature": "file_code_signature_subject_name",
41
+ "SignatureStatus": "file_code_signature_status",
42
+ "Imphash": "file_pe_imphash",
43
+ "SourceIp": "source_ip",
44
+ "SourceHostname": "source_domain",
45
+ "SourcePort": "source_port",
46
+ "SourceAddress": "source_ip",
47
+ "ClientAddress": "source_ip",
48
+ "ClientName": "source_domain",
49
+ "IpAddress": "source_ip",
50
+ "IpPort": "source_port",
51
+ "WorkstationName": "source_domain",
52
+ "DestinationIp": "destination_ip",
53
+ "DestinationHostname": "destination_domain",
54
+ "DestinationPort": "destination_port",
55
+ "DestinationPortName": "network_protocol",
56
+ "DestinationAddress": "destination_ip",
57
+ "DestAddress": "destination_ip",
58
+ "DestPort": "destination_port",
59
+ "Protocol": "network_transport",
60
+ "SourceProcessGuid": "process_entity_id",
61
+ "SourceProcessId": "process_pid",
62
+ "SourceImage": "process_executable",
63
+ "SourceThreadId": "process_thread_id",
64
+ "TargetObject": "registry_path",
65
+ "PipeName": "file_name",
66
+ "Destination": "process_executable",
67
+ "QueryName": "dns_question_name",
68
+ "QueryStatus": "sysmon_dns_status",
69
+ "IsExecutable": "sysmon_file_is_executable",
70
+ "Archived": "sysmon_file_archived",
71
+ "AccountDomain": "user_domain",
72
+ "AccountName": "user_name",
73
+ "SubjectDomainName": "user_domain",
74
+ "SubjectUserName": "user_name",
75
+ "SubjectUserSid": "user_id",
76
+ "TargetDomainName": "user_target_domain",
77
+ "TargetUserName": "user_target_name",
78
+ "TargetUserSid": "user_target_id",
79
+ "User": "user_name",
80
+ "NewProcessId": "process_pid",
81
+ "NewProcessName": "process_executable",
82
+ "ParentProcessName": "process_parent_executable",
83
+ "ProcessName": "process_executable",
84
+ "ScriptName": "file_path",
85
+ "SequenceNumber": "event_sequence",
86
+ "HostApplication": "process_command_line",
87
+ "HostId": "process_entity_id",
88
+ "HostName": "process_title",
89
+ "CommandName": "powershell_command_name",
90
+ "CommandPath": "powershell_command_path",
91
+ "CommandType": "powershell_command_type",
92
+ "ScriptBlockText": "powershell_file_script_block_text",
93
+ "ScriptBlockId": "powershell_file_script_block_id",
94
+ "Payload": "powershell_file_script_block_text",
95
+ }
96
+
97
+
98
+ def parseable_ecs_pipeline() -> ProcessingPipeline:
99
+ """Map Windows Sigma fields to Parseable-flattened ECS columns."""
100
+ return ProcessingPipeline(
101
+ name="Parseable flattened ECS Windows mappings",
102
+ priority=20,
103
+ allowed_backends=frozenset({"parseable"}),
104
+ items=generate_windows_logsource_items("winlog_channel", "{source}")
105
+ + [
106
+ ProcessingItem(
107
+ identifier="parseable_ecs_windows_fields",
108
+ transformation=FieldMappingTransformation(ECS_WINDOWS_FIELDS),
109
+ rule_conditions=[LogsourceCondition(product="windows")],
110
+ ),
111
+ ProcessingItem(
112
+ identifier="parseable_ecs_windows_event_data",
113
+ transformation=AddFieldnamePrefixTransformation("winlog_event_data_"),
114
+ field_name_conditions=[
115
+ FieldNameProcessingItemAppliedCondition("parseable_ecs_windows_fields"),
116
+ IncludeFieldCondition(fields=[r"\w+[._]\w+"], mode="re"),
117
+ ],
118
+ field_name_condition_negation=True,
119
+ field_name_condition_linking=any,
120
+ rule_conditions=[LogsourceCondition(product="windows")],
121
+ ),
122
+ ],
123
+ )
@@ -0,0 +1,114 @@
1
+ """Mappings for OpenTelemetry semantic-convention attributes ingested through OTLP."""
2
+
3
+ from sigma.processing.conditions import LogsourceCondition
4
+ from sigma.processing.pipeline import ProcessingItem, ProcessingPipeline
5
+ from sigma.processing.transformations import FieldMappingTransformation
6
+
7
+ PROCESS_CATEGORIES = (
8
+ "process_creation",
9
+ "process_termination",
10
+ "process_access",
11
+ "process_tampering",
12
+ "create_remote_thread",
13
+ "raw_access_thread",
14
+ )
15
+ NETWORK_CATEGORIES = ("network_connection", "firewall", "proxy")
16
+ FILE_CATEGORIES = (
17
+ "file_access",
18
+ "file_change",
19
+ "file_create",
20
+ "file_delete",
21
+ "file_delete_detected",
22
+ "file_event",
23
+ "file_executable_detected",
24
+ "file_rename",
25
+ "image_load",
26
+ )
27
+ PROCESS_CONTEXT_CATEGORIES = tuple(
28
+ dict.fromkeys(PROCESS_CATEGORIES + NETWORK_CATEGORIES + FILE_CATEGORIES + ("dns_query",))
29
+ )
30
+
31
+
32
+ def _category_conditions(categories: tuple[str, ...]) -> list[LogsourceCondition]:
33
+ return [LogsourceCondition(category=category) for category in categories]
34
+
35
+
36
+ def parseable_otlp_pipeline() -> ProcessingPipeline:
37
+ """Map Sigma taxonomy fields to literal OTel attributes stored by Parseable OTLP."""
38
+ return ProcessingPipeline(
39
+ name="Parseable OTLP semantic-convention mappings",
40
+ priority=20,
41
+ allowed_backends=frozenset({"parseable"}),
42
+ items=[
43
+ ProcessingItem(
44
+ identifier="parseable_otlp_host_fields",
45
+ transformation=FieldMappingTransformation(
46
+ {
47
+ "Computer": "host.name",
48
+ "ComputerName": "host.name",
49
+ }
50
+ ),
51
+ ),
52
+ ProcessingItem(
53
+ identifier="parseable_otlp_process_fields",
54
+ transformation=FieldMappingTransformation(
55
+ {
56
+ "Image": "process.executable.path",
57
+ "ProcessName": "process.executable.path",
58
+ "CommandLine": "process.command_line",
59
+ "CurrentDirectory": "process.working_directory",
60
+ "ProcessId": "process.pid",
61
+ "ProcessID": "process.pid",
62
+ "ParentProcessId": "process.parent_pid",
63
+ "ParentProcessID": "process.parent_pid",
64
+ "User": "process.owner",
65
+ }
66
+ ),
67
+ rule_conditions=_category_conditions(PROCESS_CONTEXT_CATEGORIES),
68
+ rule_condition_linking=any,
69
+ ),
70
+ ProcessingItem(
71
+ identifier="parseable_otlp_network_fields",
72
+ transformation=FieldMappingTransformation(
73
+ {
74
+ "SourceIp": "source.address",
75
+ "SourceIP": "source.address",
76
+ "SourceAddress": "source.address",
77
+ "SourceHostname": "source.address",
78
+ "SourcePort": "source.port",
79
+ "DestinationIp": "destination.address",
80
+ "DestinationIP": "destination.address",
81
+ "DestinationAddress": "destination.address",
82
+ "DestinationHostname": "destination.address",
83
+ "DestinationPort": "destination.port",
84
+ "DestAddress": "destination.address",
85
+ "DestPort": "destination.port",
86
+ "Protocol": "network.transport",
87
+ }
88
+ ),
89
+ rule_conditions=_category_conditions(NETWORK_CATEGORIES),
90
+ rule_condition_linking=any,
91
+ ),
92
+ ProcessingItem(
93
+ identifier="parseable_otlp_file_fields",
94
+ transformation=FieldMappingTransformation(
95
+ {
96
+ "TargetFilename": "file.path",
97
+ "FileName": "file.path",
98
+ "FilePath": "file.path",
99
+ }
100
+ ),
101
+ rule_conditions=_category_conditions(FILE_CATEGORIES),
102
+ rule_condition_linking=any,
103
+ ),
104
+ ProcessingItem(
105
+ identifier="parseable_otlp_dns_fields",
106
+ transformation=FieldMappingTransformation(
107
+ {
108
+ "QueryName": "dns.question.name",
109
+ }
110
+ ),
111
+ rule_conditions=[LogsourceCondition(category="dns_query")],
112
+ ),
113
+ ],
114
+ )
@@ -0,0 +1,61 @@
1
+ """Canonical generic Windows log-source mappings for native Sysmon events."""
2
+
3
+ from sigma.processing.conditions import LogsourceCondition
4
+ from sigma.processing.pipeline import ProcessingItem, ProcessingPipeline
5
+ from sigma.processing.transformations import AddConditionTransformation
6
+
7
+ SYSMON_CHANNEL = "Microsoft-Windows-Sysmon/Operational"
8
+ SYSMON_EVENT_IDS: dict[str, int | list[int]] = {
9
+ "process_creation": 1,
10
+ "file_change": 2,
11
+ "network_connection": 3,
12
+ "sysmon_status": [4, 16],
13
+ "process_termination": 5,
14
+ "driver_load": 6,
15
+ "image_load": 7,
16
+ "create_remote_thread": 8,
17
+ "raw_access_thread": 9,
18
+ "process_access": 10,
19
+ "file_event": 11,
20
+ "registry_add": 12,
21
+ "registry_delete": 12,
22
+ "registry_set": 13,
23
+ "registry_rename": 14,
24
+ "registry_event": [12, 13, 14],
25
+ "create_stream_hash": 15,
26
+ "pipe_created": [17, 18],
27
+ "wmi_event": [19, 20, 21],
28
+ "dns_query": 22,
29
+ "file_delete": 23,
30
+ "clipboard_capture": 24,
31
+ "process_tampering": 25,
32
+ "file_delete_detected": 26,
33
+ "file_block_executable": 27,
34
+ "file_block_shredding": 28,
35
+ "file_executable_detected": 29,
36
+ "sysmon_error": 255,
37
+ }
38
+
39
+
40
+ def parseable_sysmon_pipeline() -> ProcessingPipeline:
41
+ """Add native Sysmon channel and event IDs to generic Windows Sigma rules."""
42
+ return ProcessingPipeline(
43
+ name="Parseable native Sysmon log-source mappings",
44
+ priority=10,
45
+ allowed_backends=frozenset({"parseable"}),
46
+ items=[
47
+ ProcessingItem(
48
+ identifier=f"parseable_sysmon_{category}",
49
+ transformation=AddConditionTransformation(
50
+ {
51
+ "Channel": SYSMON_CHANNEL,
52
+ "EventID": event_ids,
53
+ }
54
+ ),
55
+ rule_conditions=[
56
+ LogsourceCondition(product="windows", category=category)
57
+ ],
58
+ )
59
+ for category, event_ids in SYSMON_EVENT_IDS.items()
60
+ ],
61
+ )