pySigma-backend-sqlite 1.2.4__tar.gz → 2.0.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.
@@ -0,0 +1,279 @@
1
+ Metadata-Version: 2.4
2
+ Name: pySigma-backend-sqlite
3
+ Version: 2.0.0
4
+ Summary: pySigma sqlite backend
5
+ License-Expression: LGPL-3.0-only
6
+ License-File: LICENSE
7
+ Author: wagga
8
+ Requires-Python: >=3.10,<4.0
9
+ Classifier: Programming Language :: Python :: 3
10
+ Classifier: Programming Language :: Python :: 3.10
11
+ Classifier: Programming Language :: Python :: 3.11
12
+ Classifier: Programming Language :: Python :: 3.12
13
+ Classifier: Programming Language :: Python :: 3.13
14
+ Classifier: Programming Language :: Python :: 3.14
15
+ Classifier: Programming Language :: Python :: 3.15
16
+ Requires-Dist: pysigma (>=1.5.1,<2.0)
17
+ Project-URL: Repository, https://github.com/SigmaHQ/pySigma-backend-sqlite
18
+ Description-Content-Type: text/markdown
19
+
20
+ ![Tests](https://github.com/wagga40/pySigma-backend-sqlite/actions/workflows/test.yml/badge.svg)
21
+ ![Coverage Badge](https://img.shields.io/endpoint?url=https://gist.githubusercontent.com/wagga40/2ec45ded898fa11f2c42bcb9d2b163cf/raw/test.json)
22
+ ![Status](https://img.shields.io/badge/Status-pre--release-orange)
23
+
24
+ # pySigma SQLite Backend
25
+
26
+ This is the SQLite backend for pySigma. It provides the package `sigma.backends.sqlite` with the `sqliteBackend` class.
27
+
28
+ This backend also aims to be compatible with [Zircolite](https://github.com/wagga40/Zircolite) which uses **pure SQLite queries** to perform SIGMA-based detection on EVTX, Auditd, Sysmon for linux, XML or JSONL/NDJSON Logs.
29
+
30
+ It supports the following output formats:
31
+
32
+ * **default**: plain SQLite queries
33
+ * **zircolite** : SQLite queries in JSON format for Zircolite
34
+
35
+ This backend is currently maintained by:
36
+
37
+ * [wagga](https://github.com/wagga40/)
38
+
39
+ ## Requirements
40
+
41
+ * Python 3.10 or later
42
+ * [pySigma](https://github.com/SigmaHQ/pySigma) `>= 1.5.1, < 2.0`
43
+
44
+ ## Supported Features
45
+
46
+ ### Sigma Modifiers
47
+
48
+ | Modifier | Description | SQLite Implementation |
49
+ |----------|-------------|----------------------|
50
+ | `contains` | Substring matching | `LIKE '%value%'` |
51
+ | `startswith` | Prefix matching | `LIKE 'value%'` |
52
+ | `endswith` | Suffix matching | `LIKE '%value'` |
53
+ | `all` | All values must match | Multiple `AND` conditions |
54
+ | `re` | Regular expressions | `REGEXP` |
55
+ | `cidr` | CIDR network matching | Expanded to `LIKE` patterns |
56
+ | `cased` | Case-sensitive matching | `GLOB` |
57
+ | `fieldref` | Compare two fields | `field1=field2` or with `LIKE` for startswith/endswith/contains |
58
+ | `exists` | Field existence check | `field IS NOT NULL` / `field IS NULL` |
59
+ | `gt`, `gte`, `lt`, `lte` | Numeric comparisons | `>`, `>=`, `<`, `<=` |
60
+ | `neq` | Not equal | `NOT COALESCE((field='value'), 0)` |
61
+ | `hour`, `minute`, `day`, `week`, `month`, `year` | Timestamp part extraction | `strftime()` |
62
+ | `base64`, `base64offset`, `wide`, `utf16`, `utf16be`, `windash` | Value expansion | handled by pySigma, emitted as ordinary string matches |
63
+
64
+ > **Note on `cased`:** SQLite's `GLOB` is a native 2-argument operator with no `ESCAPE` clause, so queries are emitted as `field GLOB 'pattern'` and backslashes are kept literal (e.g. Windows paths match as-is). A *literal* `*`, `?` or `[` in a `cased` value is escaped as the single-member character class `[*]`, `[?]` or `[[]`, which is the escape `GLOB` does have.
65
+
66
+ ### Query semantics
67
+
68
+ The generated SQL is meant to mean what the Sigma rule means, without the caller having to repair it first.
69
+
70
+ #### Negation and absent fields
71
+
72
+ Sigma reads a condition on a field the event does not carry as **false**, so `selection and not filter` still matches when the filter names such a field. SQLite evaluates that comparison to `NULL`, `NOT NULL` is `NULL`, and the row is dropped. Every generated `NOT` therefore wraps its operand:
73
+
74
+ ```sql
75
+ SELECT * FROM logs WHERE EventID=1 AND (NOT COALESCE((CommandLine LIKE '%foo%' ESCAPE '\'), 0))
76
+ ```
77
+
78
+ The wrapper is applied at every `NOT`, not only the outermost, which is what makes nested negations come out right. Roughly one in five SigmaHQ rules contains a negation.
79
+
80
+ #### Case sensitivity
81
+
82
+ Sigma compares strings case-insensitively. SQLite's `=` does not, unless the column is declared `COLLATE NOCASE`. Against a table that does not, set `collate_nocase = True` and literal string equality is emitted as `field='value' COLLATE NOCASE`. It is off by default because a collation on an already-`NOCASE` column is noise and can cost an index seek. Comparisons emitted as `LIKE` are case-insensitive either way; `|cased` uses `GLOB`, which is case-sensitive by definition and is never collated. Note that SQLite's `NOCASE` folds ASCII only.
83
+
84
+ #### Booleans
85
+
86
+ A flattener has to choose how to store a JSON boolean: Zircolite stores the strings `'true'`/`'false'` because that is what Sigma rules compare against, while a hand-built table is as likely to hold `1`/`0` — and SQLite reads the bare keyword `true` as the integer `1`, which never equals the text `'true'`. Both are accepted:
87
+
88
+ ```sql
89
+ SELECT * FROM logs WHERE (Flag='true' OR Flag=1)
90
+ ```
91
+
92
+ #### Long value lists
93
+
94
+ SQLite parses `a OR b OR c` into a left-deep tree whose height is the operand count, and rejects anything over `SQLITE_MAX_EXPR_DEPTH` (1000 by default) with *"Expression tree is too large"*. Chains longer than `max_flat_operands` (100) are re-associated into nested groups, which is the same boolean value at a bounded height. SigmaHQ's *Vulnerable Driver Load*, with 4431 boolean operators, is rejected at prepare time without this and parses with it.
95
+
96
+ ### Correlation rules (version 2)
97
+
98
+ Correlation output is an **alert summary**, with an occurrence time and evidence,
99
+ not an arbitrary event or a list of group keys. The following eight types run on
100
+ SQLite: `event_count`, `value_count`, `value_sum`, `value_avg`, `value_median`,
101
+ `value_percentile`, `temporal`, and `temporal_ordered`. The last two also accept
102
+ pySigma's extended Boolean conditions; `temporal_extended` and
103
+ `temporal_ordered_extended` are internal conversion methods, not YAML types.
104
+
105
+ - Counts and statistics use backward, inclusive `[t - timespan, t]` windows at
106
+ distinct matching event times. Separate qualifying times remain separate alerts.
107
+ - Ordered stages must have **strictly increasing timestamps**. The compiler finds
108
+ a valid sequence even if earlier, out-of-order occurrences are present. Ties
109
+ cannot establish order. Extended OR branches are evaluated independently and
110
+ their evidence is merged when they describe the same window.
111
+ - A condition containing absence, such as `a and not b`, opens a forward window
112
+ from a positive event and fires at its deadline. `observation_end` is Unix
113
+ seconds; by default the horizon is the latest valid timestamp in the input
114
+ tables, including events that match no detection. Unexpired windows are
115
+ reported as `incomplete_window`, not successful absence alerts. Pure-negative
116
+ conditions use observed events as context; an empty input invents no groups.
117
+ - String grouping keys are normalized to lowercase using SQLite's ASCII folding;
118
+ original values remain in evidence. Value counts use NOCASE comparisons.
119
+ - Aliases are applied to individual reference projections. Chained correlations
120
+ consume child alert occurrences, preserve child metrics and evidence, and
121
+ honour `generate`. Parent fields outside a child's grouping keys are projected
122
+ from its contributing events; a child counts once per parent group.
123
+ - An event matching several rules or condition branches counts once in an
124
+ `event_count`. Temporal membership is retained separately.
125
+ - Invalid timestamps and missing grouping keys are excluded consistently and
126
+ counted by the execution plan's diagnostics. Ordinary rules can still match
127
+ such events. Numeric statistics ignore NULL, invalid and nonfinite values.
128
+ Numeric text must be a valid JSON number (for example `12`, `-2.5`, `1e3`).
129
+ - Percentiles use linear interpolation at rank `(n - 1) * p / 100`. Percentile 50
130
+ equals the median, including even sample sizes. Sums use SQLite REAL arithmetic
131
+ to avoid integer accumulator overflow; large integers can lose precision.
132
+
133
+ SQLite **3.38.0 or newer, including its JSON functions**, is required for
134
+ correlations. No custom SQL function is required by the correlation engine.
135
+ The default ISO timestamp expression retains millisecond precision, the precision
136
+ of SQLite's date parser. Provide a numeric seconds expression to retain finer
137
+ precision. Plain detection SQL still only needs the features it uses; REGEXP
138
+ continues to require a registered function.
139
+
140
+ ```python
141
+ backend = sqliteBackend(
142
+ table="logs",
143
+ timestamp_field="SystemTime",
144
+ event_id_field="row_id", # default: SQLite rowid
145
+ observation_end=1704067500, # optional explicit observation horizon
146
+ )
147
+ # For epoch seconds (including fractional seconds):
148
+ backend = sqliteBackend(timestamp_field="event_time", timestamp_format="unix")
149
+ ```
150
+
151
+ Documented settings are constructor keywords. Existing instance-attribute
152
+ configuration remains supported; `timestamp_format` selects its expression at construction. `table` defaults to `logs` for both output formats; pipeline `setState`
153
+ values take precedence. Other settings are `timestamp_field` (default `timestamp`),
154
+ `timestamp_seconds_expression`, `event_id_field` (default `rowid`),
155
+ `timestamp_format` (`iso`, `unix`, `unix_ms`, `unix_us`),
156
+ `observation_end`, `collate_nocase` (default `False`), and `max_flat_operands`
157
+ (default `100`). Event identifiers must be non-NULL and unique within a table.
158
+
159
+ #### Result and execution contracts
160
+
161
+ The standalone SQL returns these columns:
162
+
163
+ | Column | Meaning |
164
+ |---|---|
165
+ | `alert_id` | Deterministic occurrence identifier within the input snapshot |
166
+ | `group_keys` | JSON object of grouping keys; `{}` for ungrouped correlations |
167
+ | `occurrence_time`, `window_start`, `window_end` | Unix seconds, including fractional seconds |
168
+ | `metric_name`, `metric_value` | Correlation type and aggregate value |
169
+ | `event_count` | Number of contributing physical events, separate from the metric |
170
+ | `event_ids` | JSON array of table-scoped source event identifiers |
171
+ | `child_alert_ids` | JSON array of child occurrence identifiers |
172
+
173
+ IDs refer to the current input snapshot, not a global event store. A pure-negative
174
+ alert's evidence records the observed context that started its timer.
175
+
176
+ The `zircolite` format retains the `rule` list of standalone SELECT statements
177
+ and adds `schema_version: 2`, `result_type`, and compiler-derived `required_fields`.
178
+ Ordinary rules also carry `source_table` and `logsource`. Correlations carry a
179
+ `correlation_plan` with version, ordered SELECT preparation stages and indexes,
180
+ result SQL, diagnostic SQL, source-table identities, and cleanup names.
181
+
182
+ ```python
183
+ import json
184
+ from sigma.backends.sqlite.runtime import execute_plan
185
+
186
+ entry = json.loads(backend.convert(collection, "zircolite"))[-1]
187
+ alerts, diagnostics = execute_plan(connection, entry["correlation_plan"])
188
+ # alerts contain parsed group_keys/event_ids and full source event evidence.
189
+ ```
190
+
191
+ `execute_plan` widens missing event fields from compiler metadata, materializes
192
+ indexed temporary relations, retrieves results and evidence, then removes the
193
+ temporary relations. Widening is a schema change to the source table itself:
194
+ `execute_plan` (like `ensure_fields`) adds each absent field as a NULL
195
+ `TEXT COLLATE NOCASE` column. It never commits the caller's transaction. Callers own
196
+ connection-level cancellation and transaction recovery after SQLite interrupts.
197
+ Set `include_events=False` to omit expanded source records, and `limit=N` to
198
+ retrieve at most `N + 1` summaries, earliest occurrences first, for a
199
+ discard-noisy-rule policy.
200
+
201
+ The CTE and indexed paths compile from the same relations. The indexed path avoids
202
+ full scans for each bounded lookup. Dense windows can nevertheless have quadratic
203
+ **evidence volume**, and medians/percentiles sort each qualifying sample. Use
204
+ selective base rules, result limits and measured runtime budgets; see
205
+ `tools/benchmark_correlations.py`.
206
+
207
+ See [the migration guide](docs/migration-v2.md).
208
+
209
+ ### Other Features
210
+
211
+ * **NULL value handling**: `field: null` → `field IS NULL`
212
+ * **Boolean values**: matched against both text and numeric storage (see *Booleans* above)
213
+ * **Field name quoting**: Special characters in field names are quoted with backticks
214
+ * **Wildcard escaping**: Proper escaping of `%` and `_` characters in values, including values read from a second field by `|fieldref`
215
+ * **Table name**: `logs` for both output formats; both honour a `setState` transformation on the `table` key
216
+
217
+ ## Known issues/limitations
218
+
219
+ * Full text search support will need some work and is not a priority since it needs virtual tables on SQLite side. Sigma rules using value-only (`keywords`) detections — about 3% of SigmaHQ — raise `SigmaFeatureNotSupportedByBackendError`
220
+ * `|re` is emitted as `field REGEXP 'pattern'`. SQLite has no built-in `REGEXP`, so the caller must register one; it should return false for a `NULL` input, as Zircolite's does, so that a regex on an absent field is false rather than an error
221
+ * `|cidr` is expanded into `LIKE` prefixes, since SQLite has no network type. For IPv6 this only matches addresses written in the same canonical form as the expansion
222
+ * The backend cannot know the target schema, so a rule naming a column the database does not have fails to prepare, taking its other branches with it. Zircolite widens its table with the missing columns as `NULL`; another consumer has to do the same
223
+
224
+ # Quick Start
225
+
226
+ ## Example script (default output) with sysmon pipeline
227
+
228
+ ### Add pipelines
229
+
230
+ ```shell
231
+ poetry add pysigma-pipeline-sysmon
232
+ poetry add pysigma-pipeline-windows
233
+ ```
234
+
235
+ ### Convert a rule
236
+
237
+ ```python
238
+ from sigma.collection import SigmaCollection
239
+ from sigma.backends.sqlite import sqliteBackend
240
+ from sigma.pipelines.sysmon import sysmon_pipeline
241
+ from sigma.pipelines.windows import windows_logsource_pipeline
242
+
243
+ # Combine pipelines to map both Channel and EventID:
244
+ # 1. sysmon_pipeline: maps category (e.g., process_creation) -> EventID (e.g., 1)
245
+ # and changes logsource to service=sysmon
246
+ # 2. windows_logsource_pipeline: maps service=sysmon -> Channel
247
+ #
248
+ # For process_creation/windows, this produces:
249
+ # Channel='Microsoft-Windows-Sysmon/Operational' AND EventID=1
250
+ combined_pipeline = sysmon_pipeline() + windows_logsource_pipeline()
251
+ sqlite_backend = sqliteBackend(combined_pipeline)
252
+ # Set the table name for the generated SQL queries
253
+ sqlite_backend.table = "logs"
254
+
255
+
256
+ rule = SigmaCollection.from_yaml(
257
+ r"""
258
+ title: Test
259
+ status: test
260
+ logsource:
261
+ category: test_category
262
+ product: test_product
263
+ detection:
264
+ sel:
265
+ fieldA: valueA
266
+ fieldB: valueB
267
+ condition: sel
268
+ """)
269
+
270
+ print(sqlite_backend.convert(rule)[0])
271
+
272
+ ```
273
+
274
+ ## Running
275
+
276
+ ```shell
277
+ poetry run python3 example.py
278
+ ```
279
+
@@ -0,0 +1,259 @@
1
+ ![Tests](https://github.com/wagga40/pySigma-backend-sqlite/actions/workflows/test.yml/badge.svg)
2
+ ![Coverage Badge](https://img.shields.io/endpoint?url=https://gist.githubusercontent.com/wagga40/2ec45ded898fa11f2c42bcb9d2b163cf/raw/test.json)
3
+ ![Status](https://img.shields.io/badge/Status-pre--release-orange)
4
+
5
+ # pySigma SQLite Backend
6
+
7
+ This is the SQLite backend for pySigma. It provides the package `sigma.backends.sqlite` with the `sqliteBackend` class.
8
+
9
+ This backend also aims to be compatible with [Zircolite](https://github.com/wagga40/Zircolite) which uses **pure SQLite queries** to perform SIGMA-based detection on EVTX, Auditd, Sysmon for linux, XML or JSONL/NDJSON Logs.
10
+
11
+ It supports the following output formats:
12
+
13
+ * **default**: plain SQLite queries
14
+ * **zircolite** : SQLite queries in JSON format for Zircolite
15
+
16
+ This backend is currently maintained by:
17
+
18
+ * [wagga](https://github.com/wagga40/)
19
+
20
+ ## Requirements
21
+
22
+ * Python 3.10 or later
23
+ * [pySigma](https://github.com/SigmaHQ/pySigma) `>= 1.5.1, < 2.0`
24
+
25
+ ## Supported Features
26
+
27
+ ### Sigma Modifiers
28
+
29
+ | Modifier | Description | SQLite Implementation |
30
+ |----------|-------------|----------------------|
31
+ | `contains` | Substring matching | `LIKE '%value%'` |
32
+ | `startswith` | Prefix matching | `LIKE 'value%'` |
33
+ | `endswith` | Suffix matching | `LIKE '%value'` |
34
+ | `all` | All values must match | Multiple `AND` conditions |
35
+ | `re` | Regular expressions | `REGEXP` |
36
+ | `cidr` | CIDR network matching | Expanded to `LIKE` patterns |
37
+ | `cased` | Case-sensitive matching | `GLOB` |
38
+ | `fieldref` | Compare two fields | `field1=field2` or with `LIKE` for startswith/endswith/contains |
39
+ | `exists` | Field existence check | `field IS NOT NULL` / `field IS NULL` |
40
+ | `gt`, `gte`, `lt`, `lte` | Numeric comparisons | `>`, `>=`, `<`, `<=` |
41
+ | `neq` | Not equal | `NOT COALESCE((field='value'), 0)` |
42
+ | `hour`, `minute`, `day`, `week`, `month`, `year` | Timestamp part extraction | `strftime()` |
43
+ | `base64`, `base64offset`, `wide`, `utf16`, `utf16be`, `windash` | Value expansion | handled by pySigma, emitted as ordinary string matches |
44
+
45
+ > **Note on `cased`:** SQLite's `GLOB` is a native 2-argument operator with no `ESCAPE` clause, so queries are emitted as `field GLOB 'pattern'` and backslashes are kept literal (e.g. Windows paths match as-is). A *literal* `*`, `?` or `[` in a `cased` value is escaped as the single-member character class `[*]`, `[?]` or `[[]`, which is the escape `GLOB` does have.
46
+
47
+ ### Query semantics
48
+
49
+ The generated SQL is meant to mean what the Sigma rule means, without the caller having to repair it first.
50
+
51
+ #### Negation and absent fields
52
+
53
+ Sigma reads a condition on a field the event does not carry as **false**, so `selection and not filter` still matches when the filter names such a field. SQLite evaluates that comparison to `NULL`, `NOT NULL` is `NULL`, and the row is dropped. Every generated `NOT` therefore wraps its operand:
54
+
55
+ ```sql
56
+ SELECT * FROM logs WHERE EventID=1 AND (NOT COALESCE((CommandLine LIKE '%foo%' ESCAPE '\'), 0))
57
+ ```
58
+
59
+ The wrapper is applied at every `NOT`, not only the outermost, which is what makes nested negations come out right. Roughly one in five SigmaHQ rules contains a negation.
60
+
61
+ #### Case sensitivity
62
+
63
+ Sigma compares strings case-insensitively. SQLite's `=` does not, unless the column is declared `COLLATE NOCASE`. Against a table that does not, set `collate_nocase = True` and literal string equality is emitted as `field='value' COLLATE NOCASE`. It is off by default because a collation on an already-`NOCASE` column is noise and can cost an index seek. Comparisons emitted as `LIKE` are case-insensitive either way; `|cased` uses `GLOB`, which is case-sensitive by definition and is never collated. Note that SQLite's `NOCASE` folds ASCII only.
64
+
65
+ #### Booleans
66
+
67
+ A flattener has to choose how to store a JSON boolean: Zircolite stores the strings `'true'`/`'false'` because that is what Sigma rules compare against, while a hand-built table is as likely to hold `1`/`0` — and SQLite reads the bare keyword `true` as the integer `1`, which never equals the text `'true'`. Both are accepted:
68
+
69
+ ```sql
70
+ SELECT * FROM logs WHERE (Flag='true' OR Flag=1)
71
+ ```
72
+
73
+ #### Long value lists
74
+
75
+ SQLite parses `a OR b OR c` into a left-deep tree whose height is the operand count, and rejects anything over `SQLITE_MAX_EXPR_DEPTH` (1000 by default) with *"Expression tree is too large"*. Chains longer than `max_flat_operands` (100) are re-associated into nested groups, which is the same boolean value at a bounded height. SigmaHQ's *Vulnerable Driver Load*, with 4431 boolean operators, is rejected at prepare time without this and parses with it.
76
+
77
+ ### Correlation rules (version 2)
78
+
79
+ Correlation output is an **alert summary**, with an occurrence time and evidence,
80
+ not an arbitrary event or a list of group keys. The following eight types run on
81
+ SQLite: `event_count`, `value_count`, `value_sum`, `value_avg`, `value_median`,
82
+ `value_percentile`, `temporal`, and `temporal_ordered`. The last two also accept
83
+ pySigma's extended Boolean conditions; `temporal_extended` and
84
+ `temporal_ordered_extended` are internal conversion methods, not YAML types.
85
+
86
+ - Counts and statistics use backward, inclusive `[t - timespan, t]` windows at
87
+ distinct matching event times. Separate qualifying times remain separate alerts.
88
+ - Ordered stages must have **strictly increasing timestamps**. The compiler finds
89
+ a valid sequence even if earlier, out-of-order occurrences are present. Ties
90
+ cannot establish order. Extended OR branches are evaluated independently and
91
+ their evidence is merged when they describe the same window.
92
+ - A condition containing absence, such as `a and not b`, opens a forward window
93
+ from a positive event and fires at its deadline. `observation_end` is Unix
94
+ seconds; by default the horizon is the latest valid timestamp in the input
95
+ tables, including events that match no detection. Unexpired windows are
96
+ reported as `incomplete_window`, not successful absence alerts. Pure-negative
97
+ conditions use observed events as context; an empty input invents no groups.
98
+ - String grouping keys are normalized to lowercase using SQLite's ASCII folding;
99
+ original values remain in evidence. Value counts use NOCASE comparisons.
100
+ - Aliases are applied to individual reference projections. Chained correlations
101
+ consume child alert occurrences, preserve child metrics and evidence, and
102
+ honour `generate`. Parent fields outside a child's grouping keys are projected
103
+ from its contributing events; a child counts once per parent group.
104
+ - An event matching several rules or condition branches counts once in an
105
+ `event_count`. Temporal membership is retained separately.
106
+ - Invalid timestamps and missing grouping keys are excluded consistently and
107
+ counted by the execution plan's diagnostics. Ordinary rules can still match
108
+ such events. Numeric statistics ignore NULL, invalid and nonfinite values.
109
+ Numeric text must be a valid JSON number (for example `12`, `-2.5`, `1e3`).
110
+ - Percentiles use linear interpolation at rank `(n - 1) * p / 100`. Percentile 50
111
+ equals the median, including even sample sizes. Sums use SQLite REAL arithmetic
112
+ to avoid integer accumulator overflow; large integers can lose precision.
113
+
114
+ SQLite **3.38.0 or newer, including its JSON functions**, is required for
115
+ correlations. No custom SQL function is required by the correlation engine.
116
+ The default ISO timestamp expression retains millisecond precision, the precision
117
+ of SQLite's date parser. Provide a numeric seconds expression to retain finer
118
+ precision. Plain detection SQL still only needs the features it uses; REGEXP
119
+ continues to require a registered function.
120
+
121
+ ```python
122
+ backend = sqliteBackend(
123
+ table="logs",
124
+ timestamp_field="SystemTime",
125
+ event_id_field="row_id", # default: SQLite rowid
126
+ observation_end=1704067500, # optional explicit observation horizon
127
+ )
128
+ # For epoch seconds (including fractional seconds):
129
+ backend = sqliteBackend(timestamp_field="event_time", timestamp_format="unix")
130
+ ```
131
+
132
+ Documented settings are constructor keywords. Existing instance-attribute
133
+ configuration remains supported; `timestamp_format` selects its expression at construction. `table` defaults to `logs` for both output formats; pipeline `setState`
134
+ values take precedence. Other settings are `timestamp_field` (default `timestamp`),
135
+ `timestamp_seconds_expression`, `event_id_field` (default `rowid`),
136
+ `timestamp_format` (`iso`, `unix`, `unix_ms`, `unix_us`),
137
+ `observation_end`, `collate_nocase` (default `False`), and `max_flat_operands`
138
+ (default `100`). Event identifiers must be non-NULL and unique within a table.
139
+
140
+ #### Result and execution contracts
141
+
142
+ The standalone SQL returns these columns:
143
+
144
+ | Column | Meaning |
145
+ |---|---|
146
+ | `alert_id` | Deterministic occurrence identifier within the input snapshot |
147
+ | `group_keys` | JSON object of grouping keys; `{}` for ungrouped correlations |
148
+ | `occurrence_time`, `window_start`, `window_end` | Unix seconds, including fractional seconds |
149
+ | `metric_name`, `metric_value` | Correlation type and aggregate value |
150
+ | `event_count` | Number of contributing physical events, separate from the metric |
151
+ | `event_ids` | JSON array of table-scoped source event identifiers |
152
+ | `child_alert_ids` | JSON array of child occurrence identifiers |
153
+
154
+ IDs refer to the current input snapshot, not a global event store. A pure-negative
155
+ alert's evidence records the observed context that started its timer.
156
+
157
+ The `zircolite` format retains the `rule` list of standalone SELECT statements
158
+ and adds `schema_version: 2`, `result_type`, and compiler-derived `required_fields`.
159
+ Ordinary rules also carry `source_table` and `logsource`. Correlations carry a
160
+ `correlation_plan` with version, ordered SELECT preparation stages and indexes,
161
+ result SQL, diagnostic SQL, source-table identities, and cleanup names.
162
+
163
+ ```python
164
+ import json
165
+ from sigma.backends.sqlite.runtime import execute_plan
166
+
167
+ entry = json.loads(backend.convert(collection, "zircolite"))[-1]
168
+ alerts, diagnostics = execute_plan(connection, entry["correlation_plan"])
169
+ # alerts contain parsed group_keys/event_ids and full source event evidence.
170
+ ```
171
+
172
+ `execute_plan` widens missing event fields from compiler metadata, materializes
173
+ indexed temporary relations, retrieves results and evidence, then removes the
174
+ temporary relations. Widening is a schema change to the source table itself:
175
+ `execute_plan` (like `ensure_fields`) adds each absent field as a NULL
176
+ `TEXT COLLATE NOCASE` column. It never commits the caller's transaction. Callers own
177
+ connection-level cancellation and transaction recovery after SQLite interrupts.
178
+ Set `include_events=False` to omit expanded source records, and `limit=N` to
179
+ retrieve at most `N + 1` summaries, earliest occurrences first, for a
180
+ discard-noisy-rule policy.
181
+
182
+ The CTE and indexed paths compile from the same relations. The indexed path avoids
183
+ full scans for each bounded lookup. Dense windows can nevertheless have quadratic
184
+ **evidence volume**, and medians/percentiles sort each qualifying sample. Use
185
+ selective base rules, result limits and measured runtime budgets; see
186
+ `tools/benchmark_correlations.py`.
187
+
188
+ See [the migration guide](docs/migration-v2.md).
189
+
190
+ ### Other Features
191
+
192
+ * **NULL value handling**: `field: null` → `field IS NULL`
193
+ * **Boolean values**: matched against both text and numeric storage (see *Booleans* above)
194
+ * **Field name quoting**: Special characters in field names are quoted with backticks
195
+ * **Wildcard escaping**: Proper escaping of `%` and `_` characters in values, including values read from a second field by `|fieldref`
196
+ * **Table name**: `logs` for both output formats; both honour a `setState` transformation on the `table` key
197
+
198
+ ## Known issues/limitations
199
+
200
+ * Full text search support will need some work and is not a priority since it needs virtual tables on SQLite side. Sigma rules using value-only (`keywords`) detections — about 3% of SigmaHQ — raise `SigmaFeatureNotSupportedByBackendError`
201
+ * `|re` is emitted as `field REGEXP 'pattern'`. SQLite has no built-in `REGEXP`, so the caller must register one; it should return false for a `NULL` input, as Zircolite's does, so that a regex on an absent field is false rather than an error
202
+ * `|cidr` is expanded into `LIKE` prefixes, since SQLite has no network type. For IPv6 this only matches addresses written in the same canonical form as the expansion
203
+ * The backend cannot know the target schema, so a rule naming a column the database does not have fails to prepare, taking its other branches with it. Zircolite widens its table with the missing columns as `NULL`; another consumer has to do the same
204
+
205
+ # Quick Start
206
+
207
+ ## Example script (default output) with sysmon pipeline
208
+
209
+ ### Add pipelines
210
+
211
+ ```shell
212
+ poetry add pysigma-pipeline-sysmon
213
+ poetry add pysigma-pipeline-windows
214
+ ```
215
+
216
+ ### Convert a rule
217
+
218
+ ```python
219
+ from sigma.collection import SigmaCollection
220
+ from sigma.backends.sqlite import sqliteBackend
221
+ from sigma.pipelines.sysmon import sysmon_pipeline
222
+ from sigma.pipelines.windows import windows_logsource_pipeline
223
+
224
+ # Combine pipelines to map both Channel and EventID:
225
+ # 1. sysmon_pipeline: maps category (e.g., process_creation) -> EventID (e.g., 1)
226
+ # and changes logsource to service=sysmon
227
+ # 2. windows_logsource_pipeline: maps service=sysmon -> Channel
228
+ #
229
+ # For process_creation/windows, this produces:
230
+ # Channel='Microsoft-Windows-Sysmon/Operational' AND EventID=1
231
+ combined_pipeline = sysmon_pipeline() + windows_logsource_pipeline()
232
+ sqlite_backend = sqliteBackend(combined_pipeline)
233
+ # Set the table name for the generated SQL queries
234
+ sqlite_backend.table = "logs"
235
+
236
+
237
+ rule = SigmaCollection.from_yaml(
238
+ r"""
239
+ title: Test
240
+ status: test
241
+ logsource:
242
+ category: test_category
243
+ product: test_product
244
+ detection:
245
+ sel:
246
+ fieldA: valueA
247
+ fieldB: valueB
248
+ condition: sel
249
+ """)
250
+
251
+ print(sqlite_backend.convert(rule)[0])
252
+
253
+ ```
254
+
255
+ ## Running
256
+
257
+ ```shell
258
+ poetry run python3 example.py
259
+ ```
@@ -1,6 +1,8 @@
1
1
  [project]
2
2
  name = "pySigma-backend-sqlite"
3
- version = "1.2.4"
3
+ version = "2.0.0"
4
+ requires-python = ">=3.10,<4.0"
5
+ dependencies = ["pysigma>=1.5.1,<2.0"]
4
6
  description = "pySigma sqlite backend"
5
7
  readme = "README.md"
6
8
  authors = [
@@ -13,10 +15,6 @@ packages = [
13
15
  { include = "sigma" }
14
16
  ]
15
17
 
16
- [tool.poetry.dependencies]
17
- python = "^3.10"
18
- pysigma = "^1"
19
-
20
18
  [tool.poetry.group.dev.dependencies]
21
19
  black = "^24.1"
22
20
  pytest = "^8.0"
@@ -25,6 +23,7 @@ coverage = "^7.4"
25
23
 
26
24
 
27
25
  [tool.pytest.ini_options]
26
+ testpaths = ["tests"]
28
27
  filterwarnings = [
29
28
  # Suppress pyparsing deprecation warning from pySigma library
30
29
  "ignore:'parseString' deprecated:DeprecationWarning",
@@ -37,17 +36,8 @@ dev = []
37
36
  [tool.pdm.build]
38
37
  includes = ["sigma"]
39
38
  [build-system]
40
- requires = ["poetry-core>=1.8.1"]
39
+ requires = ["poetry-core>=2.0.0"]
41
40
  build-backend = "poetry.core.masonry.api"
42
41
 
43
42
  [project.urls]
44
43
  Repository = "https://github.com/SigmaHQ/pySigma-backend-sqlite"
45
-
46
- [project.group.dev.dependencies]
47
- pytest = "^8.0"
48
- pytest-cov = "^4.1"
49
- coverage = "^7.4"
50
-
51
-
52
-
53
-