sigma2sql 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.
Files changed (53) hide show
  1. sigma2sql-0.2.0/.gitignore +8 -0
  2. sigma2sql-0.2.0/CHANGELOG.md +29 -0
  3. sigma2sql-0.2.0/LICENSE +21 -0
  4. sigma2sql-0.2.0/PKG-INFO +277 -0
  5. sigma2sql-0.2.0/README.md +247 -0
  6. sigma2sql-0.2.0/examples/events.jsonl +12 -0
  7. sigma2sql-0.2.0/examples/rules/brute_force.match.jsonl +3 -0
  8. sigma2sql-0.2.0/examples/rules/brute_force.nomatch.jsonl +5 -0
  9. sigma2sql-0.2.0/examples/rules/brute_force.yml +26 -0
  10. sigma2sql-0.2.0/examples/rules/encoded_powershell.match.jsonl +3 -0
  11. sigma2sql-0.2.0/examples/rules/encoded_powershell.nomatch.jsonl +3 -0
  12. sigma2sql-0.2.0/examples/rules/encoded_powershell.yml +21 -0
  13. sigma2sql-0.2.0/examples/rules/office_spawns_shell.match.jsonl +2 -0
  14. sigma2sql-0.2.0/examples/rules/office_spawns_shell.nomatch.jsonl +2 -0
  15. sigma2sql-0.2.0/examples/rules/office_spawns_shell.yml +22 -0
  16. sigma2sql-0.2.0/examples/rules/rdp_from_outside.match.jsonl +2 -0
  17. sigma2sql-0.2.0/examples/rules/rdp_from_outside.nomatch.jsonl +3 -0
  18. sigma2sql-0.2.0/examples/rules/rdp_from_outside.yml +19 -0
  19. sigma2sql-0.2.0/pyproject.toml +78 -0
  20. sigma2sql-0.2.0/scripts/check_regression.py +107 -0
  21. sigma2sql-0.2.0/scripts/check_sigmahq.py +76 -0
  22. sigma2sql-0.2.0/scripts/demo_svg.py +40 -0
  23. sigma2sql-0.2.0/scripts/diff_pysigma.py +145 -0
  24. sigma2sql-0.2.0/src/sigma2sql/__init__.py +21 -0
  25. sigma2sql-0.2.0/src/sigma2sql/__main__.py +3 -0
  26. sigma2sql-0.2.0/src/sigma2sql/cli.py +320 -0
  27. sigma2sql-0.2.0/src/sigma2sql/collection.py +62 -0
  28. sigma2sql-0.2.0/src/sigma2sql/condition.py +119 -0
  29. sigma2sql-0.2.0/src/sigma2sql/correlation.py +82 -0
  30. sigma2sql-0.2.0/src/sigma2sql/detection.py +230 -0
  31. sigma2sql-0.2.0/src/sigma2sql/errors.py +6 -0
  32. sigma2sql-0.2.0/src/sigma2sql/expr.py +140 -0
  33. sigma2sql-0.2.0/src/sigma2sql/hunt.py +116 -0
  34. sigma2sql-0.2.0/src/sigma2sql/profile.py +119 -0
  35. sigma2sql-0.2.0/src/sigma2sql/profiles/__init__.py +0 -0
  36. sigma2sql-0.2.0/src/sigma2sql/profiles/ecs-winlogbeat.yml +43 -0
  37. sigma2sql-0.2.0/src/sigma2sql/profiles/evtx.yml +71 -0
  38. sigma2sql-0.2.0/src/sigma2sql/profiles/ocsf.yml +26 -0
  39. sigma2sql-0.2.0/src/sigma2sql/profiles/sysmon.yml +26 -0
  40. sigma2sql-0.2.0/src/sigma2sql/py.typed +0 -0
  41. sigma2sql-0.2.0/src/sigma2sql/rule.py +260 -0
  42. sigma2sql-0.2.0/src/sigma2sql/runner.py +82 -0
  43. sigma2sql-0.2.0/src/sigma2sql/sql.py +325 -0
  44. sigma2sql-0.2.0/tests/cases.py +228 -0
  45. sigma2sql-0.2.0/tests/test_check.py +54 -0
  46. sigma2sql-0.2.0/tests/test_cli.py +74 -0
  47. sigma2sql-0.2.0/tests/test_correlation.py +281 -0
  48. sigma2sql-0.2.0/tests/test_duckdb.py +25 -0
  49. sigma2sql-0.2.0/tests/test_hunt.py +72 -0
  50. sigma2sql-0.2.0/tests/test_parse.py +109 -0
  51. sigma2sql-0.2.0/tests/test_postgres.py +44 -0
  52. sigma2sql-0.2.0/tests/test_profile.py +193 -0
  53. sigma2sql-0.2.0/tests/test_sqlite.py +22 -0
@@ -0,0 +1,8 @@
1
+ .venv/
2
+ __pycache__/
3
+ .pytest_cache/
4
+ .mypy_cache/
5
+ .ruff_cache/
6
+ dist/
7
+ AGENTS.md
8
+ CLAUDE.md
@@ -0,0 +1,29 @@
1
+ # Changelog
2
+
3
+ ## 0.2.0 (2026-11-05)
4
+
5
+ - `hunt`: run a rule directory over JSON, JSON Lines, Parquet or CSV logs with DuckDB, no
6
+ database needed; table or JSON report, `--fail-on LEVEL` for CI.
7
+ - DuckDB dialect next to SQLite and PostgreSQL.
8
+ - Correlation rules (`event_count`, `value_count`, `value_sum`, `value_avg`, `temporal`, with
9
+ `group-by`, `timespan` and `aliases`) and legacy `| count() by ... > N` aggregations. All 45
10
+ `count()` rules SigmaHQ keeps under `unsupported/` and `deprecated/` convert and run.
11
+ - Profiles: field mapping, logsource routing (per-category table and/or extra conditions),
12
+ nested JSON paths, fallback field templates and timestamp settings. Built in: `sysmon`,
13
+ `evtx`, `ecs-winlogbeat`, `ocsf`.
14
+ - Modifiers `base64`, `base64offset`, `utf16`, `utf16le`, `utf16be`, `wide`, and keyword
15
+ searches with modifiers (`'|all'`, `'|re'`). SigmaHQ coverage is now 3162/3162 on SQLite and
16
+ DuckDB, 3160/3162 on PostgreSQL.
17
+ - `check` command and a GitHub Action that test rules against
18
+ `<rule>.match.jsonl` / `<rule>.nomatch.jsonl` files.
19
+ - Correctness checks in CI: SigmaHQ regression samples (476/476 match as expected) and a
20
+ rule-by-rule comparison with pySigma's SQLite backend on real events (see
21
+ `docs/correctness.md`).
22
+ - Fix: `windash` now also expands values written with `/` (e.g. ` /accepteula`), and every
23
+ flag in a value.
24
+ - `--version`.
25
+
26
+ ## 0.1.0 (2026-11-05)
27
+
28
+ - First release: Sigma rules to SQLite and PostgreSQL SQL, field mapping, JSON column mode,
29
+ and `test` to run a rule against JSON Lines events.
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Ahmet Burak Özer
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,277 @@
1
+ Metadata-Version: 2.5
2
+ Name: sigma2sql
3
+ Version: 0.2.0
4
+ Summary: Sigma rules as SQL for SQLite, PostgreSQL and DuckDB: convert, hunt through log files and test rules
5
+ Project-URL: Homepage, https://github.com/brkozer/sigma2sql
6
+ Project-URL: Issues, https://github.com/brkozer/sigma2sql/issues
7
+ Project-URL: Changelog, https://github.com/brkozer/sigma2sql/blob/main/CHANGELOG.md
8
+ Author: Ahmet Burak Özer
9
+ License-Expression: MIT
10
+ License-File: LICENSE
11
+ Keywords: detection-engineering,duckdb,postgresql,siem,sigma,sql,threat-hunting
12
+ Classifier: Development Status :: 4 - Beta
13
+ Classifier: Environment :: Console
14
+ Classifier: Intended Audience :: Information Technology
15
+ Classifier: Intended Audience :: System Administrators
16
+ Classifier: License :: OSI Approved :: MIT License
17
+ Classifier: Operating System :: OS Independent
18
+ Classifier: Programming Language :: Python :: 3
19
+ Classifier: Programming Language :: Python :: 3.12
20
+ Classifier: Programming Language :: Python :: 3.13
21
+ Classifier: Topic :: Database
22
+ Classifier: Topic :: Security
23
+ Classifier: Typing :: Typed
24
+ Requires-Python: >=3.12
25
+ Requires-Dist: pyyaml>=6.0.2
26
+ Provides-Extra: duckdb
27
+ Requires-Dist: duckdb>=1.1; extra == 'duckdb'
28
+ Requires-Dist: numpy>=2.0; extra == 'duckdb'
29
+ Description-Content-Type: text/markdown
30
+
31
+ # sigma2sql
32
+
33
+ [![ci](https://github.com/brkozer/sigma2sql/actions/workflows/ci.yml/badge.svg)](https://github.com/brkozer/sigma2sql/actions/workflows/ci.yml)
34
+ ![python](https://img.shields.io/badge/python-3.12%2B-3776AB)
35
+ ![license](https://img.shields.io/badge/license-MIT-green)
36
+
37
+ Run [Sigma](https://sigmahq.io) detection rules as SQL on **SQLite**, **PostgreSQL** and
38
+ **DuckDB**: hunt through log files without a SIEM, convert rules for the database your logs
39
+ already live in, and unit-test rules before they reach production.
40
+
41
+ - **Every SigmaHQ rule converts**: 3162 / 3162 on SQLite and DuckDB, 3160 / 3162 on
42
+ PostgreSQL, plus all 45 `count()` aggregation rules SigmaHQ keeps under `unsupported/`.
43
+ Every generated query is executed on all three databases in CI each week.
44
+ - **Checked against real events**: all 476 of SigmaHQ's regression samples match as expected,
45
+ and on those events sigma2sql agrees with pySigma on 3074 of 3076 rules; the two
46
+ differences are IPv6 addresses pySigma's text matching misses
47
+ ([details](docs/correctness.md)).
48
+ - **Correlations**: event counts, distinct counts, sums and temporal sequences over a sliding
49
+ window, as plain SQL.
50
+ - **Your schema**: profiles map Sigma fields to your columns or nested JSON (Sysmon, EVTX
51
+ exports, Elastic/Winlogbeat ECS, OCSF built in) and limit each rule to the right log source.
52
+
53
+ <p align="center"><img src="https://raw.githubusercontent.com/brkozer/sigma2sql/main/docs/demo.svg" alt="sigma2sql hunting through sample logs, checking rule tests and converting a rule to PostgreSQL" width="100%"></p>
54
+
55
+ ## Install
56
+
57
+ ```bash
58
+ uv tool install "sigma2sql[duckdb] @ git+https://github.com/brkozer/sigma2sql"
59
+ ```
60
+
61
+ The `duckdb` extra is needed for `hunt`; `convert`, `test` and `check` only need PyYAML.
62
+
63
+ ## Hunt through log files
64
+
65
+ ```bash
66
+ git clone --depth 1 https://github.com/SigmaHQ/sigma
67
+ sigma2sql hunt sigma/rules --logs 'logs/*.json' --profile evtx --examples 1
68
+ ```
69
+
70
+ `hunt` loads JSON, JSON Lines, Parquet or CSV files into an in-memory DuckDB database, runs
71
+ every rule and correlation, and lists the ones that matched, most severe first. On the
72
+ examples in this repository:
73
+
74
+ ```console
75
+ $ sigma2sql hunt examples/rules --logs examples/events.jsonl
76
+ LEVEL HITS RULE
77
+ high 2 PowerShell started with an encoded command (examples/rules/encoded_powershell.yml)
78
+ high 1 Brute force against one account (examples/rules/brute_force.yml)
79
+ high 1 Office application starting a shell (examples/rules/office_spawns_shell.yml)
80
+ medium 1 Successful RDP logon from a public address (examples/rules/rdp_from_outside.yml)
81
+ 4 of 4 rules matched 12 events (0 rules skipped)
82
+ ```
83
+
84
+ `--examples N` prints matching events (or correlation results such as
85
+ `{"TargetUserName":"admin","window_start":"2026-11-01 03:12:01","window_end":"2026-11-01 03:12:17","value":3}`),
86
+ `--format json` gives a machine-readable report and `--fail-on high` a non-zero exit code
87
+ when a rule at that level or above fires.
88
+
89
+ ## Convert rules
90
+
91
+ ```console
92
+ $ sigma2sql convert examples/rules/office_spawns_shell.yml -d postgres --json-column data
93
+ -- Office application starting a shell (8a7f3e91-2c44-4b8e-b0f6-3e5d9c1a7b02)
94
+ SELECT * FROM "events" WHERE (("data"->>'ParentImage' ILIKE '%\\winword.exe' ESCAPE '\' OR ...)
95
+ AND ("data"->>'Image' ILIKE '%\\cmd.exe' ESCAPE '\' OR ...));
96
+ ```
97
+
98
+ A whole directory works the same way, e.g.
99
+ `sigma2sql convert sigma/rules -d postgres -t security.events --json-column event --profile ecs-winlogbeat`.
100
+ Without `--json-column` each Sigma field is a column. Unsupported rules are listed on stderr
101
+ with the reason, and the command exits non-zero.
102
+
103
+ | Option | Meaning |
104
+ | --- | --- |
105
+ | `-d, --dialect` | `sqlite` (default), `postgres` or `duckdb` |
106
+ | `-t, --table` | table name, `schema.table` allowed (default `events`) |
107
+ | `--json-column` | read fields from this JSON / JSONB column |
108
+ | `--keyword-column` | column searched by keyword rules (in JSON mode the whole document) |
109
+ | `-p, --profile` | built-in profile or a YAML file (see below) |
110
+ | `-m, --mapping` | YAML map from Sigma field to your column or key, applied over the profile |
111
+ | `--nested` | dotted names are paths into nested JSON |
112
+ | `--timestamp-field`, `--timestamp-format` | event time for correlations (`iso`, `epoch_s`, `epoch_ms`) |
113
+
114
+ ## Correlations
115
+
116
+ Sigma correlation rules and the older `| count() by ... > N` syntax both become SQL. With a
117
+ `timespan`, each event closes a window over the events of the same group before it; the
118
+ window's events are found with a self-join, which works the same on all three databases and
119
+ allows distinct counts:
120
+
121
+ ```yaml
122
+ title: Brute force against one account
123
+ correlation:
124
+ type: event_count # also value_count, value_sum, value_avg, temporal
125
+ rules: [failed_logon]
126
+ group-by: [TargetUserName]
127
+ timespan: 5m
128
+ condition: {gte: 3}
129
+ ```
130
+
131
+ Each result row holds the group values, the window's start and end and the counted value.
132
+ Rules referenced by a correlation are not reported on their own unless it sets
133
+ `generate: true`. `temporal_ordered` is not supported yet.
134
+
135
+ ## Profiles
136
+
137
+ A profile describes where your events keep Sigma's fields and which events each log source
138
+ covers. `sigma2sql profiles` lists the built-in ones:
139
+
140
+ | Profile | For |
141
+ | --- | --- |
142
+ | `sysmon` | Sysmon events with their own field names; each category limited to its event ID |
143
+ | `evtx` | EVTX exported to JSON by evtx_dump or python-evtx (`Event.System`, `Event.EventData`) |
144
+ | `ecs-winlogbeat` | Windows events shipped by Winlogbeat in Elastic Common Schema |
145
+ | `ocsf` | OCSF 1.x events, time in epoch milliseconds |
146
+
147
+ Your own profile is a small YAML file:
148
+
149
+ ```yaml
150
+ nested: true
151
+ timestamp: "@timestamp"
152
+ default_field: "winlog.event_data.{field}" # fallback for unmapped fields
153
+ fields:
154
+ Image: process.executable
155
+ EventID: [event.code, winlog.event_id] # first one present wins
156
+ logsources:
157
+ - match: {product: windows, category: process_creation}
158
+ table: process_events # optional: route to a table
159
+ where: {event.code: "1"} # optional: extra condition
160
+ ```
161
+
162
+ Without a profile, rules are not limited by log source: a network rule can then match a
163
+ process event that happens to have the same field values. The built-in profiles cover the
164
+ common Windows sources; for other data, add `logsources` entries.
165
+
166
+ ## Test rules
167
+
168
+ Put example events next to a rule, one JSON object per line:
169
+
170
+ ```
171
+ rules/office_spawns_shell.yml
172
+ rules/office_spawns_shell.match.jsonl # every event must match
173
+ rules/office_spawns_shell.nomatch.jsonl # no event may match
174
+ ```
175
+
176
+ ```console
177
+ $ sigma2sql check examples/rules --require-tests
178
+ ok examples/rules/brute_force.yml
179
+ ok examples/rules/encoded_powershell.yml
180
+ ok examples/rules/office_spawns_shell.yml
181
+ ok examples/rules/rdp_from_outside.yml
182
+ 4 passed, 0 failed, 0 without tests
183
+ ```
184
+
185
+ A failure names the rule, the file and the line, e.g. after adding an Outlook-spawned
186
+ `cmd.exe` to the match file of a rule that only watches Word, Excel and PowerPoint:
187
+
188
+ ```console
189
+ FAIL rules/office_spawns_shell.yml: Office application starting a shell: missed office_spawns_shell.match.jsonl event(s) 1
190
+ ```
191
+
192
+ The same check as a GitHub Action:
193
+
194
+ ```yaml
195
+ - uses: brkozer/sigma2sql@v0.2.0
196
+ with:
197
+ path: rules
198
+ profile: sysmon # optional
199
+ require-tests: "true" # optional
200
+ ```
201
+
202
+ `sigma2sql test RULE EVENTS.jsonl` prints the events (or correlation results) a single rule
203
+ produces.
204
+
205
+ ## Supported Sigma features
206
+
207
+ | Feature | SQLite | PostgreSQL | DuckDB |
208
+ | --- | --- | --- | --- |
209
+ | maps, lists, keywords (with `all`, `re` and string modifiers) | ✓ | ✓ | ✓ |
210
+ | `and`, `or`, `not`, `1 of x*`, `all of x*`, `them`, condition lists | ✓ | ✓ | ✓ |
211
+ | wildcards and escapes, `contains`, `startswith`, `endswith`, `all`, `cased`, `windash` | ✓ | ✓ | ✓ |
212
+ | `base64`, `base64offset`, `utf16`, `utf16le`, `utf16be`, `wide` | ✓ | ✓ | ✓ |
213
+ | `re`, `re\|i` | `REGEXP` ¹ | `~` ² | `regexp_matches` ³ |
214
+ | `cidr` (IPv4 and IPv6, any notation) | `cidr_match()` ¹ | `inet <<=` | `cidr_match()` ¹ |
215
+ | `lt`, `lte`, `gt`, `gte`, `exists`, `fieldref`, `null` | ✓ | ✓ | ✓ |
216
+ | correlations, `count()` aggregations | ✓ | ✓ | ✓ |
217
+ | `expand` (placeholders), `temporal_ordered`, filter rules | – | – | – |
218
+
219
+ ¹ Not built into the database. `hunt`, `test` and `check` register them; when you run the SQL
220
+ elsewhere, register `regexp(pattern, value)` and `cidr_match(value, network)` yourself
221
+ (`sigma2sql.runner.connect()` returns a SQLite connection with both).<br>
222
+ ² PostgreSQL rejects repetition counts above 255; those two rules are reported as unsupported
223
+ rather than silently changed.<br>
224
+ ³ RE2: no lookaround or backreferences; such rules are reported as unsupported on DuckDB.
225
+
226
+ Matching is case-insensitive by default (ASCII in SQLite, as its `LIKE` and `NOCASE` are);
227
+ values in a JSON column match whether they are stored as `4624` or `"4624"`, and `not` on a
228
+ missing field is true, as Sigma intends.
229
+
230
+ ## How it works
231
+
232
+ ```mermaid
233
+ flowchart LR
234
+ Y[rule files] --> L[loader<br/>rules, correlations,<br/>count aggregations]
235
+ L --> C[condition parser]
236
+ L --> D[search compiler<br/>modifiers, wildcards,<br/>base64, windash]
237
+ C --> E[match tree]
238
+ D --> E
239
+ P[profile<br/>fields, logsources,<br/>timestamp] --> R
240
+ E --> R[renderer<br/>SQLite · PostgreSQL · DuckDB]
241
+ R --> Q[SQL]
242
+ R --> K[correlation SQL<br/>self-join windows]
243
+ Q --> H[hunt / test / check]
244
+ K --> H
245
+ ```
246
+
247
+ ## Library use
248
+
249
+ ```python
250
+ from sigma2sql import Target, load_rule_file, to_query
251
+ from sigma2sql.profile import apply_profile, load_profile
252
+
253
+ rule = load_rule_file("rule.yml")
254
+ target = apply_profile(
255
+ Target("postgres", "logs.windows", json_column="event"), load_profile("evtx")
256
+ )
257
+ print(to_query(rule, target))
258
+ ```
259
+
260
+ ## Development
261
+
262
+ ```bash
263
+ uv sync
264
+ uv run pytest # SQLite and DuckDB
265
+ SIGMA2SQL_PG_DSN=postgresql://postgres:test@localhost/postgres uv run pytest # + PostgreSQL
266
+ uv run python scripts/check_sigmahq.py sigma/rules --dsn ... [--profile evtx]
267
+ uv run python scripts/check_regression.py sigma
268
+ uv run --with pysigma --with pysigma-backend-sqlite python scripts/diff_pysigma.py sigma
269
+ uv run python scripts/demo_svg.py docs/demo.svg
270
+ ```
271
+
272
+ The behaviour tests in `tests/cases.py` and `tests/test_correlation.py` run unchanged on all
273
+ three databases. The example rules and events in `examples/` were written for this project.
274
+
275
+ ## License
276
+
277
+ MIT
@@ -0,0 +1,247 @@
1
+ # sigma2sql
2
+
3
+ [![ci](https://github.com/brkozer/sigma2sql/actions/workflows/ci.yml/badge.svg)](https://github.com/brkozer/sigma2sql/actions/workflows/ci.yml)
4
+ ![python](https://img.shields.io/badge/python-3.12%2B-3776AB)
5
+ ![license](https://img.shields.io/badge/license-MIT-green)
6
+
7
+ Run [Sigma](https://sigmahq.io) detection rules as SQL on **SQLite**, **PostgreSQL** and
8
+ **DuckDB**: hunt through log files without a SIEM, convert rules for the database your logs
9
+ already live in, and unit-test rules before they reach production.
10
+
11
+ - **Every SigmaHQ rule converts**: 3162 / 3162 on SQLite and DuckDB, 3160 / 3162 on
12
+ PostgreSQL, plus all 45 `count()` aggregation rules SigmaHQ keeps under `unsupported/`.
13
+ Every generated query is executed on all three databases in CI each week.
14
+ - **Checked against real events**: all 476 of SigmaHQ's regression samples match as expected,
15
+ and on those events sigma2sql agrees with pySigma on 3074 of 3076 rules; the two
16
+ differences are IPv6 addresses pySigma's text matching misses
17
+ ([details](docs/correctness.md)).
18
+ - **Correlations**: event counts, distinct counts, sums and temporal sequences over a sliding
19
+ window, as plain SQL.
20
+ - **Your schema**: profiles map Sigma fields to your columns or nested JSON (Sysmon, EVTX
21
+ exports, Elastic/Winlogbeat ECS, OCSF built in) and limit each rule to the right log source.
22
+
23
+ <p align="center"><img src="https://raw.githubusercontent.com/brkozer/sigma2sql/main/docs/demo.svg" alt="sigma2sql hunting through sample logs, checking rule tests and converting a rule to PostgreSQL" width="100%"></p>
24
+
25
+ ## Install
26
+
27
+ ```bash
28
+ uv tool install "sigma2sql[duckdb] @ git+https://github.com/brkozer/sigma2sql"
29
+ ```
30
+
31
+ The `duckdb` extra is needed for `hunt`; `convert`, `test` and `check` only need PyYAML.
32
+
33
+ ## Hunt through log files
34
+
35
+ ```bash
36
+ git clone --depth 1 https://github.com/SigmaHQ/sigma
37
+ sigma2sql hunt sigma/rules --logs 'logs/*.json' --profile evtx --examples 1
38
+ ```
39
+
40
+ `hunt` loads JSON, JSON Lines, Parquet or CSV files into an in-memory DuckDB database, runs
41
+ every rule and correlation, and lists the ones that matched, most severe first. On the
42
+ examples in this repository:
43
+
44
+ ```console
45
+ $ sigma2sql hunt examples/rules --logs examples/events.jsonl
46
+ LEVEL HITS RULE
47
+ high 2 PowerShell started with an encoded command (examples/rules/encoded_powershell.yml)
48
+ high 1 Brute force against one account (examples/rules/brute_force.yml)
49
+ high 1 Office application starting a shell (examples/rules/office_spawns_shell.yml)
50
+ medium 1 Successful RDP logon from a public address (examples/rules/rdp_from_outside.yml)
51
+ 4 of 4 rules matched 12 events (0 rules skipped)
52
+ ```
53
+
54
+ `--examples N` prints matching events (or correlation results such as
55
+ `{"TargetUserName":"admin","window_start":"2026-11-01 03:12:01","window_end":"2026-11-01 03:12:17","value":3}`),
56
+ `--format json` gives a machine-readable report and `--fail-on high` a non-zero exit code
57
+ when a rule at that level or above fires.
58
+
59
+ ## Convert rules
60
+
61
+ ```console
62
+ $ sigma2sql convert examples/rules/office_spawns_shell.yml -d postgres --json-column data
63
+ -- Office application starting a shell (8a7f3e91-2c44-4b8e-b0f6-3e5d9c1a7b02)
64
+ SELECT * FROM "events" WHERE (("data"->>'ParentImage' ILIKE '%\\winword.exe' ESCAPE '\' OR ...)
65
+ AND ("data"->>'Image' ILIKE '%\\cmd.exe' ESCAPE '\' OR ...));
66
+ ```
67
+
68
+ A whole directory works the same way, e.g.
69
+ `sigma2sql convert sigma/rules -d postgres -t security.events --json-column event --profile ecs-winlogbeat`.
70
+ Without `--json-column` each Sigma field is a column. Unsupported rules are listed on stderr
71
+ with the reason, and the command exits non-zero.
72
+
73
+ | Option | Meaning |
74
+ | --- | --- |
75
+ | `-d, --dialect` | `sqlite` (default), `postgres` or `duckdb` |
76
+ | `-t, --table` | table name, `schema.table` allowed (default `events`) |
77
+ | `--json-column` | read fields from this JSON / JSONB column |
78
+ | `--keyword-column` | column searched by keyword rules (in JSON mode the whole document) |
79
+ | `-p, --profile` | built-in profile or a YAML file (see below) |
80
+ | `-m, --mapping` | YAML map from Sigma field to your column or key, applied over the profile |
81
+ | `--nested` | dotted names are paths into nested JSON |
82
+ | `--timestamp-field`, `--timestamp-format` | event time for correlations (`iso`, `epoch_s`, `epoch_ms`) |
83
+
84
+ ## Correlations
85
+
86
+ Sigma correlation rules and the older `| count() by ... > N` syntax both become SQL. With a
87
+ `timespan`, each event closes a window over the events of the same group before it; the
88
+ window's events are found with a self-join, which works the same on all three databases and
89
+ allows distinct counts:
90
+
91
+ ```yaml
92
+ title: Brute force against one account
93
+ correlation:
94
+ type: event_count # also value_count, value_sum, value_avg, temporal
95
+ rules: [failed_logon]
96
+ group-by: [TargetUserName]
97
+ timespan: 5m
98
+ condition: {gte: 3}
99
+ ```
100
+
101
+ Each result row holds the group values, the window's start and end and the counted value.
102
+ Rules referenced by a correlation are not reported on their own unless it sets
103
+ `generate: true`. `temporal_ordered` is not supported yet.
104
+
105
+ ## Profiles
106
+
107
+ A profile describes where your events keep Sigma's fields and which events each log source
108
+ covers. `sigma2sql profiles` lists the built-in ones:
109
+
110
+ | Profile | For |
111
+ | --- | --- |
112
+ | `sysmon` | Sysmon events with their own field names; each category limited to its event ID |
113
+ | `evtx` | EVTX exported to JSON by evtx_dump or python-evtx (`Event.System`, `Event.EventData`) |
114
+ | `ecs-winlogbeat` | Windows events shipped by Winlogbeat in Elastic Common Schema |
115
+ | `ocsf` | OCSF 1.x events, time in epoch milliseconds |
116
+
117
+ Your own profile is a small YAML file:
118
+
119
+ ```yaml
120
+ nested: true
121
+ timestamp: "@timestamp"
122
+ default_field: "winlog.event_data.{field}" # fallback for unmapped fields
123
+ fields:
124
+ Image: process.executable
125
+ EventID: [event.code, winlog.event_id] # first one present wins
126
+ logsources:
127
+ - match: {product: windows, category: process_creation}
128
+ table: process_events # optional: route to a table
129
+ where: {event.code: "1"} # optional: extra condition
130
+ ```
131
+
132
+ Without a profile, rules are not limited by log source: a network rule can then match a
133
+ process event that happens to have the same field values. The built-in profiles cover the
134
+ common Windows sources; for other data, add `logsources` entries.
135
+
136
+ ## Test rules
137
+
138
+ Put example events next to a rule, one JSON object per line:
139
+
140
+ ```
141
+ rules/office_spawns_shell.yml
142
+ rules/office_spawns_shell.match.jsonl # every event must match
143
+ rules/office_spawns_shell.nomatch.jsonl # no event may match
144
+ ```
145
+
146
+ ```console
147
+ $ sigma2sql check examples/rules --require-tests
148
+ ok examples/rules/brute_force.yml
149
+ ok examples/rules/encoded_powershell.yml
150
+ ok examples/rules/office_spawns_shell.yml
151
+ ok examples/rules/rdp_from_outside.yml
152
+ 4 passed, 0 failed, 0 without tests
153
+ ```
154
+
155
+ A failure names the rule, the file and the line, e.g. after adding an Outlook-spawned
156
+ `cmd.exe` to the match file of a rule that only watches Word, Excel and PowerPoint:
157
+
158
+ ```console
159
+ FAIL rules/office_spawns_shell.yml: Office application starting a shell: missed office_spawns_shell.match.jsonl event(s) 1
160
+ ```
161
+
162
+ The same check as a GitHub Action:
163
+
164
+ ```yaml
165
+ - uses: brkozer/sigma2sql@v0.2.0
166
+ with:
167
+ path: rules
168
+ profile: sysmon # optional
169
+ require-tests: "true" # optional
170
+ ```
171
+
172
+ `sigma2sql test RULE EVENTS.jsonl` prints the events (or correlation results) a single rule
173
+ produces.
174
+
175
+ ## Supported Sigma features
176
+
177
+ | Feature | SQLite | PostgreSQL | DuckDB |
178
+ | --- | --- | --- | --- |
179
+ | maps, lists, keywords (with `all`, `re` and string modifiers) | ✓ | ✓ | ✓ |
180
+ | `and`, `or`, `not`, `1 of x*`, `all of x*`, `them`, condition lists | ✓ | ✓ | ✓ |
181
+ | wildcards and escapes, `contains`, `startswith`, `endswith`, `all`, `cased`, `windash` | ✓ | ✓ | ✓ |
182
+ | `base64`, `base64offset`, `utf16`, `utf16le`, `utf16be`, `wide` | ✓ | ✓ | ✓ |
183
+ | `re`, `re\|i` | `REGEXP` ¹ | `~` ² | `regexp_matches` ³ |
184
+ | `cidr` (IPv4 and IPv6, any notation) | `cidr_match()` ¹ | `inet <<=` | `cidr_match()` ¹ |
185
+ | `lt`, `lte`, `gt`, `gte`, `exists`, `fieldref`, `null` | ✓ | ✓ | ✓ |
186
+ | correlations, `count()` aggregations | ✓ | ✓ | ✓ |
187
+ | `expand` (placeholders), `temporal_ordered`, filter rules | – | – | – |
188
+
189
+ ¹ Not built into the database. `hunt`, `test` and `check` register them; when you run the SQL
190
+ elsewhere, register `regexp(pattern, value)` and `cidr_match(value, network)` yourself
191
+ (`sigma2sql.runner.connect()` returns a SQLite connection with both).<br>
192
+ ² PostgreSQL rejects repetition counts above 255; those two rules are reported as unsupported
193
+ rather than silently changed.<br>
194
+ ³ RE2: no lookaround or backreferences; such rules are reported as unsupported on DuckDB.
195
+
196
+ Matching is case-insensitive by default (ASCII in SQLite, as its `LIKE` and `NOCASE` are);
197
+ values in a JSON column match whether they are stored as `4624` or `"4624"`, and `not` on a
198
+ missing field is true, as Sigma intends.
199
+
200
+ ## How it works
201
+
202
+ ```mermaid
203
+ flowchart LR
204
+ Y[rule files] --> L[loader<br/>rules, correlations,<br/>count aggregations]
205
+ L --> C[condition parser]
206
+ L --> D[search compiler<br/>modifiers, wildcards,<br/>base64, windash]
207
+ C --> E[match tree]
208
+ D --> E
209
+ P[profile<br/>fields, logsources,<br/>timestamp] --> R
210
+ E --> R[renderer<br/>SQLite · PostgreSQL · DuckDB]
211
+ R --> Q[SQL]
212
+ R --> K[correlation SQL<br/>self-join windows]
213
+ Q --> H[hunt / test / check]
214
+ K --> H
215
+ ```
216
+
217
+ ## Library use
218
+
219
+ ```python
220
+ from sigma2sql import Target, load_rule_file, to_query
221
+ from sigma2sql.profile import apply_profile, load_profile
222
+
223
+ rule = load_rule_file("rule.yml")
224
+ target = apply_profile(
225
+ Target("postgres", "logs.windows", json_column="event"), load_profile("evtx")
226
+ )
227
+ print(to_query(rule, target))
228
+ ```
229
+
230
+ ## Development
231
+
232
+ ```bash
233
+ uv sync
234
+ uv run pytest # SQLite and DuckDB
235
+ SIGMA2SQL_PG_DSN=postgresql://postgres:test@localhost/postgres uv run pytest # + PostgreSQL
236
+ uv run python scripts/check_sigmahq.py sigma/rules --dsn ... [--profile evtx]
237
+ uv run python scripts/check_regression.py sigma
238
+ uv run --with pysigma --with pysigma-backend-sqlite python scripts/diff_pysigma.py sigma
239
+ uv run python scripts/demo_svg.py docs/demo.svg
240
+ ```
241
+
242
+ The behaviour tests in `tests/cases.py` and `tests/test_correlation.py` run unchanged on all
243
+ three databases. The example rules and events in `examples/` were written for this project.
244
+
245
+ ## License
246
+
247
+ MIT
@@ -0,0 +1,12 @@
1
+ {"EventID": 1, "Image": "C:\\Windows\\System32\\WindowsPowerShell\\v1.0\\powershell.exe", "ParentImage": "C:\\Windows\\explorer.exe", "CommandLine": "powershell.exe -NoProfile -enc SQBFAFgA"}
2
+ {"EventID": 1, "Image": "C:\\Windows\\System32\\WindowsPowerShell\\v1.0\\powershell.exe", "ParentImage": "C:\\Program Files\\Microsoft Configuration Manager\\bin\\ccmexec.exe", "CommandLine": "powershell.exe -enc SQBFAFgA"}
3
+ {"EventID": 1, "Image": "C:\\Program Files\\PowerShell\\7\\pwsh.exe", "ParentImage": "C:\\Windows\\explorer.exe", "CommandLine": "pwsh.exe /ec SQBFAFgA"}
4
+ {"EventID": 1, "Image": "C:\\Windows\\System32\\cmd.exe", "ParentImage": "C:\\Program Files\\Microsoft Office\\root\\Office16\\WINWORD.EXE", "CommandLine": "cmd.exe /c whoami"}
5
+ {"EventID": 1, "Image": "C:\\Windows\\System32\\notepad.exe", "ParentImage": "C:\\Windows\\explorer.exe", "CommandLine": "notepad.exe report.txt"}
6
+ {"EventID": 4624, "LogonType": 10, "IpAddress": "203.0.113.45", "TargetUserName": "admin"}
7
+ {"EventID": "4624", "LogonType": "10", "IpAddress": "192.168.1.20", "TargetUserName": "admin"}
8
+ {"EventID": 4624, "LogonType": 3, "IpAddress": "198.51.100.7", "TargetUserName": "svc_backup"}
9
+ {"EventID": 4625, "LogonType": 3, "IpAddress": "198.51.100.23", "TargetUserName": "admin", "timestamp": "2026-11-01T03:12:01Z"}
10
+ {"EventID": 4625, "LogonType": 3, "IpAddress": "198.51.100.23", "TargetUserName": "admin", "timestamp": "2026-11-01T03:12:09Z"}
11
+ {"EventID": 4625, "LogonType": 3, "IpAddress": "198.51.100.23", "TargetUserName": "admin", "timestamp": "2026-11-01T03:12:17Z"}
12
+ {"EventID": 4625, "LogonType": 3, "IpAddress": "198.51.100.40", "TargetUserName": "svc_backup", "timestamp": "2026-11-01T09:00:00Z"}
@@ -0,0 +1,3 @@
1
+ {"EventID": 4625, "TargetUserName": "admin", "timestamp": "2026-11-01T03:12:01Z"}
2
+ {"EventID": 4625, "TargetUserName": "admin", "timestamp": "2026-11-01T03:13:30Z"}
3
+ {"EventID": 4625, "TargetUserName": "admin", "timestamp": "2026-11-01T03:16:00Z"}
@@ -0,0 +1,5 @@
1
+ {"EventID": 4625, "TargetUserName": "admin", "timestamp": "2026-11-01T03:12:01Z"}
2
+ {"EventID": 4625, "TargetUserName": "admin", "timestamp": "2026-11-01T03:15:00Z"}
3
+ {"EventID": 4625, "TargetUserName": "admin", "timestamp": "2026-11-01T03:18:00Z"}
4
+ {"EventID": 4625, "TargetUserName": "alice", "timestamp": "2026-11-01T03:12:02Z"}
5
+ {"EventID": 4624, "TargetUserName": "admin", "timestamp": "2026-11-01T03:12:03Z"}
@@ -0,0 +1,26 @@
1
+ title: Failed logon
2
+ id: 3b1f6c0e-7a52-4f3e-9d0a-1c2b3d4e5f04
3
+ name: failed_logon
4
+ status: experimental
5
+ description: A failed Windows logon. Used by the correlation below; not alerted on its own.
6
+ logsource:
7
+ product: windows
8
+ service: security
9
+ detection:
10
+ selection:
11
+ EventID: 4625
12
+ condition: selection
13
+ level: informational
14
+ ---
15
+ title: Brute force against one account
16
+ id: 9e8d7c6b-5a49-4382-a1b0-c9d8e7f6a505
17
+ status: experimental
18
+ description: Three or more failed logons for the same user within five minutes.
19
+ correlation:
20
+ type: event_count
21
+ rules: [failed_logon]
22
+ group-by: [TargetUserName]
23
+ timespan: 5m
24
+ condition:
25
+ gte: 3
26
+ level: high
@@ -0,0 +1,3 @@
1
+ {"EventID": 1, "Image": "C:\\Windows\\System32\\WindowsPowerShell\\v1.0\\powershell.exe", "ParentImage": "C:\\Windows\\explorer.exe", "CommandLine": "powershell.exe -NoProfile -enc SQBFAFgA"}
2
+ {"EventID": 1, "Image": "C:\\Program Files\\PowerShell\\7\\pwsh.exe", "ParentImage": "C:\\Windows\\explorer.exe", "CommandLine": "pwsh.exe /ec SQBFAFgA"}
3
+ {"EventID": 1, "Image": "C:\\Windows\\System32\\WindowsPowerShell\\v1.0\\POWERSHELL.EXE", "ParentImage": "C:\\Windows\\System32\\cmd.exe", "CommandLine": "POWERSHELL -EncodedCommand SQBFAFgA "}
@@ -0,0 +1,3 @@
1
+ {"EventID": 1, "Image": "C:\\Windows\\System32\\WindowsPowerShell\\v1.0\\powershell.exe", "ParentImage": "C:\\Program Files\\Microsoft Configuration Manager\\bin\\ccmexec.exe", "CommandLine": "powershell.exe -enc SQBFAFgA"}
2
+ {"EventID": 1, "Image": "C:\\Windows\\System32\\WindowsPowerShell\\v1.0\\powershell.exe", "ParentImage": "C:\\Windows\\explorer.exe", "CommandLine": "powershell.exe -File C:\\scripts\\encode.ps1"}
3
+ {"EventID": 1, "Image": "C:\\Windows\\System32\\cmd.exe", "ParentImage": "C:\\Windows\\explorer.exe", "CommandLine": "cmd.exe /c echo -enc "}
@@ -0,0 +1,21 @@
1
+ title: PowerShell started with an encoded command
2
+ id: 5f0c2b2e-1d55-4c1a-9a51-7d1b0e3c6a01
3
+ status: experimental
4
+ description: Flags powershell.exe or pwsh.exe launched with -EncodedCommand or one of its abbreviations.
5
+ logsource:
6
+ product: windows
7
+ category: process_creation
8
+ detection:
9
+ selection_image:
10
+ Image|endswith:
11
+ - '\powershell.exe'
12
+ - '\pwsh.exe'
13
+ selection_flag:
14
+ CommandLine|windash|contains:
15
+ - ' -enc '
16
+ - ' -encodedcommand '
17
+ - ' -ec '
18
+ filter_trusted_parent:
19
+ ParentImage|startswith: 'C:\Program Files\Microsoft Configuration Manager\'
20
+ condition: all of selection_* and not filter_trusted_parent
21
+ level: high