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.
- pysigma_backend_parseable-0.1.0.dist-info/METADATA +327 -0
- pysigma_backend_parseable-0.1.0.dist-info/RECORD +10 -0
- pysigma_backend_parseable-0.1.0.dist-info/WHEEL +4 -0
- pysigma_backend_parseable-0.1.0.dist-info/licenses/LICENSE +21 -0
- sigma/backends/parseable/__init__.py +3 -0
- sigma/backends/parseable/parseable.py +342 -0
- sigma/pipelines/parseable/__init__.py +17 -0
- sigma/pipelines/parseable/ecs.py +123 -0
- sigma/pipelines/parseable/otlp.py +114 -0
- sigma/pipelines/parseable/sysmon.py +61 -0
|
@@ -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,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,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
|
+
)
|