sigma2sql 0.2.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,71 @@
1
+ description: Windows event logs exported to JSON by evtx_dump or python-evtx (Event.System / Event.EventData)
2
+ nested: true
3
+ timestamp: "Event.System.TimeCreated.#attributes.SystemTime"
4
+ # Some providers (e.g. Defender) name fields with spaces: "New Value" for NewValue.
5
+ default_field: ["Event.EventData.{field}", "Event.EventData.{field_words}"]
6
+ fields:
7
+ # EventID carries Qualifiers on some logs and then becomes {"#attributes": ..., "#text": N}.
8
+ EventID: [Event.System.EventID.#text, Event.System.EventID]
9
+ Channel: Event.System.Channel
10
+ Computer: Event.System.Computer
11
+ Provider_Name: Event.System.Provider.#attributes.Name
12
+ Level: Event.System.Level
13
+ Keywords: Event.System.Keywords
14
+ # WMI-Activity events keep their data under UserData, one element per event type.
15
+ Operation:
16
+ - Event.EventData.Operation
17
+ - Event.UserData.Operation_ClientFailure.Operation
18
+ - Event.UserData.Operation_StartedOperational.Operation
19
+ ClientMachine: [Event.EventData.ClientMachine, Event.UserData.Operation_ClientFailure.ClientMachine]
20
+ PossibleCause: [Event.EventData.PossibleCause, Event.UserData.Operation_ClientFailure.PossibleCause]
21
+ Consumer: [Event.EventData.Consumer, Event.UserData.Operation_ESStoConsumerBinding.CONSUMER]
22
+ Query: [Event.EventData.Query, Event.UserData.Operation_TemporaryEssStarted.Query]
23
+ logsources:
24
+ # Sysmon categories: the Sysmon channel plus the category's event ID.
25
+ - {match: {product: windows, category: process_creation}, where: {Event.System.Channel: Microsoft-Windows-Sysmon/Operational, Event.System.EventID: 1}}
26
+ - {match: {product: windows, category: file_change}, where: {Event.System.Channel: Microsoft-Windows-Sysmon/Operational, Event.System.EventID: 2}}
27
+ - {match: {product: windows, category: network_connection}, where: {Event.System.Channel: Microsoft-Windows-Sysmon/Operational, Event.System.EventID: 3}}
28
+ - {match: {product: windows, category: process_termination}, where: {Event.System.Channel: Microsoft-Windows-Sysmon/Operational, Event.System.EventID: 5}}
29
+ - {match: {product: windows, category: driver_load}, where: {Event.System.Channel: Microsoft-Windows-Sysmon/Operational, Event.System.EventID: 6}}
30
+ - {match: {product: windows, category: image_load}, where: {Event.System.Channel: Microsoft-Windows-Sysmon/Operational, Event.System.EventID: 7}}
31
+ - {match: {product: windows, category: create_remote_thread}, where: {Event.System.Channel: Microsoft-Windows-Sysmon/Operational, Event.System.EventID: 8}}
32
+ - {match: {product: windows, category: raw_access_thread}, where: {Event.System.Channel: Microsoft-Windows-Sysmon/Operational, Event.System.EventID: 9}}
33
+ - {match: {product: windows, category: process_access}, where: {Event.System.Channel: Microsoft-Windows-Sysmon/Operational, Event.System.EventID: 10}}
34
+ - {match: {product: windows, category: file_event}, where: {Event.System.Channel: Microsoft-Windows-Sysmon/Operational, Event.System.EventID: 11}}
35
+ - {match: {product: windows, category: registry_add}, where: {Event.System.Channel: Microsoft-Windows-Sysmon/Operational, Event.System.EventID: 12}}
36
+ - {match: {product: windows, category: registry_delete}, where: {Event.System.Channel: Microsoft-Windows-Sysmon/Operational, Event.System.EventID: 12}}
37
+ - {match: {product: windows, category: registry_set}, where: {Event.System.Channel: Microsoft-Windows-Sysmon/Operational, Event.System.EventID: 13}}
38
+ - {match: {product: windows, category: registry_rename}, where: {Event.System.Channel: Microsoft-Windows-Sysmon/Operational, Event.System.EventID: 14}}
39
+ - {match: {product: windows, category: registry_event}, where: {Event.System.Channel: Microsoft-Windows-Sysmon/Operational, Event.System.EventID: [12, 13, 14]}}
40
+ - {match: {product: windows, category: create_stream_hash}, where: {Event.System.Channel: Microsoft-Windows-Sysmon/Operational, Event.System.EventID: 15}}
41
+ - {match: {product: windows, category: pipe_created}, where: {Event.System.Channel: Microsoft-Windows-Sysmon/Operational, Event.System.EventID: [17, 18]}}
42
+ - {match: {product: windows, category: wmi_event}, where: {Event.System.Channel: Microsoft-Windows-Sysmon/Operational, Event.System.EventID: [19, 20, 21]}}
43
+ - {match: {product: windows, category: dns_query}, where: {Event.System.Channel: Microsoft-Windows-Sysmon/Operational, Event.System.EventID: 22}}
44
+ - {match: {product: windows, category: file_delete}, where: {Event.System.Channel: Microsoft-Windows-Sysmon/Operational, Event.System.EventID: [23, 26]}}
45
+ - {match: {product: windows, category: clipboard_capture}, where: {Event.System.Channel: Microsoft-Windows-Sysmon/Operational, Event.System.EventID: 24}}
46
+ - {match: {product: windows, category: process_tampering}, where: {Event.System.Channel: Microsoft-Windows-Sysmon/Operational, Event.System.EventID: 25}}
47
+ - {match: {product: windows, category: file_executable_detected}, where: {Event.System.Channel: Microsoft-Windows-Sysmon/Operational, Event.System.EventID: 29}}
48
+ # PowerShell categories.
49
+ - {match: {product: windows, category: ps_script}, where: {Event.System.Channel: Microsoft-Windows-PowerShell/Operational, Event.System.EventID: 4104}}
50
+ - {match: {product: windows, category: ps_module}, where: {Event.System.Channel: Microsoft-Windows-PowerShell/Operational, Event.System.EventID: 4103}}
51
+ - {match: {product: windows, category: ps_classic_start}, where: {Event.System.Channel: Windows PowerShell, Event.System.EventID: 400}}
52
+ - {match: {product: windows, category: ps_classic_provider_start}, where: {Event.System.Channel: Windows PowerShell, Event.System.EventID: 600}}
53
+ # Services map to their channel.
54
+ - {match: {product: windows, service: security}, where: {Event.System.Channel: Security}}
55
+ - {match: {product: windows, service: system}, where: {Event.System.Channel: System}}
56
+ - {match: {product: windows, service: application}, where: {Event.System.Channel: Application}}
57
+ - {match: {product: windows, service: sysmon}, where: {Event.System.Channel: Microsoft-Windows-Sysmon/Operational}}
58
+ - {match: {product: windows, service: powershell}, where: {Event.System.Channel: Microsoft-Windows-PowerShell/Operational}}
59
+ - {match: {product: windows, service: powershell-classic}, where: {Event.System.Channel: Windows PowerShell}}
60
+ - {match: {product: windows, service: taskscheduler}, where: {Event.System.Channel: Microsoft-Windows-TaskScheduler/Operational}}
61
+ - {match: {product: windows, service: wmi}, where: {Event.System.Channel: Microsoft-Windows-WMI-Activity/Operational}}
62
+ - {match: {product: windows, service: windefend}, where: {Event.System.Channel: Microsoft-Windows-Windows Defender/Operational}}
63
+ - {match: {product: windows, service: bits-client}, where: {Event.System.Channel: Microsoft-Windows-Bits-Client/Operational}}
64
+ - {match: {product: windows, service: firewall-as}, where: {Event.System.Channel: Microsoft-Windows-Windows Firewall With Advanced Security/Firewall}}
65
+ - {match: {product: windows, service: appxdeployment-server}, where: {Event.System.Channel: Microsoft-Windows-AppXDeploymentServer/Operational}}
66
+ - {match: {product: windows, service: codeintegrity-operational}, where: {Event.System.Channel: Microsoft-Windows-CodeIntegrity/Operational}}
67
+ - {match: {product: windows, service: dns-client}, where: {Event.System.Channel: Microsoft-Windows-DNS Client Events/Operational}}
68
+ - {match: {product: windows, service: driver-framework}, where: {Event.System.Channel: Microsoft-Windows-DriverFrameworks-UserMode/Operational}}
69
+ - {match: {product: windows, service: ntlm}, where: {Event.System.Channel: Microsoft-Windows-NTLM/Operational}}
70
+ - {match: {product: windows, service: terminalservices-localsessionmanager}, where: {Event.System.Channel: Microsoft-Windows-TerminalServices-LocalSessionManager/Operational}}
71
+ - {match: {product: windows, service: openssh}, where: {Event.System.Channel: OpenSSH/Operational}}
@@ -0,0 +1,26 @@
1
+ description: OCSF 1.x events (nested JSON, time in epoch milliseconds), routed by class_uid
2
+ nested: true
3
+ timestamp: time
4
+ timestamp_format: epoch_ms
5
+ fields:
6
+ Image: process.file.path
7
+ CommandLine: process.cmd_line
8
+ ProcessId: process.pid
9
+ CurrentDirectory: process.file.parent_folder
10
+ ParentImage: actor.process.file.path
11
+ ParentCommandLine: actor.process.cmd_line
12
+ ParentProcessId: actor.process.pid
13
+ User: actor.user.name
14
+ Computer: device.hostname
15
+ TargetFilename: file.path
16
+ DestinationIp: dst_endpoint.ip
17
+ DestinationPort: dst_endpoint.port
18
+ DestinationHostname: dst_endpoint.hostname
19
+ SourceIp: src_endpoint.ip
20
+ SourcePort: src_endpoint.port
21
+ QueryName: query.hostname
22
+ logsources:
23
+ - {match: {category: process_creation}, where: {class_uid: 1007, activity_id: 1}}
24
+ - {match: {category: file_event}, where: {class_uid: 1001}}
25
+ - {match: {category: network_connection}, where: {class_uid: 4001}}
26
+ - {match: {category: dns_query}, where: {class_uid: 4003}}
@@ -0,0 +1,26 @@
1
+ description: Sysmon events with their original field names; each Sigma category is limited to its Sysmon event ID
2
+ timestamp: UtcTime
3
+ logsources:
4
+ - {match: {product: windows, category: process_creation}, where: {EventID: 1}}
5
+ - {match: {product: windows, category: file_change}, where: {EventID: 2}}
6
+ - {match: {product: windows, category: network_connection}, where: {EventID: 3}}
7
+ - {match: {product: windows, category: process_termination}, where: {EventID: 5}}
8
+ - {match: {product: windows, category: driver_load}, where: {EventID: 6}}
9
+ - {match: {product: windows, category: image_load}, where: {EventID: 7}}
10
+ - {match: {product: windows, category: create_remote_thread}, where: {EventID: 8}}
11
+ - {match: {product: windows, category: raw_access_thread}, where: {EventID: 9}}
12
+ - {match: {product: windows, category: process_access}, where: {EventID: 10}}
13
+ - {match: {product: windows, category: file_event}, where: {EventID: 11}}
14
+ - {match: {product: windows, category: registry_add}, where: {EventID: 12}}
15
+ - {match: {product: windows, category: registry_delete}, where: {EventID: 12}}
16
+ - {match: {product: windows, category: registry_set}, where: {EventID: 13}}
17
+ - {match: {product: windows, category: registry_rename}, where: {EventID: 14}}
18
+ - {match: {product: windows, category: registry_event}, where: {EventID: [12, 13, 14]}}
19
+ - {match: {product: windows, category: create_stream_hash}, where: {EventID: 15}}
20
+ - {match: {product: windows, category: pipe_created}, where: {EventID: [17, 18]}}
21
+ - {match: {product: windows, category: wmi_event}, where: {EventID: [19, 20, 21]}}
22
+ - {match: {product: windows, category: dns_query}, where: {EventID: 22}}
23
+ - {match: {product: windows, category: file_delete}, where: {EventID: [23, 26]}}
24
+ - {match: {product: windows, category: clipboard_capture}, where: {EventID: 24}}
25
+ - {match: {product: windows, category: process_tampering}, where: {EventID: 25}}
26
+ - {match: {product: windows, category: file_executable_detected}, where: {EventID: 29}}
sigma2sql/py.typed ADDED
File without changes
sigma2sql/rule.py ADDED
@@ -0,0 +1,260 @@
1
+ from __future__ import annotations
2
+
3
+ import dataclasses
4
+ import re
5
+ from collections.abc import Mapping
6
+ from dataclasses import dataclass, replace
7
+ from pathlib import Path
8
+ from typing import Any, Literal
9
+
10
+ import yaml
11
+
12
+ from sigma2sql.condition import compile_condition
13
+ from sigma2sql.detection import compile_search
14
+ from sigma2sql.errors import RuleError, UnsupportedRule
15
+ from sigma2sql.expr import Expr, any_of
16
+
17
+ CorrelationType = Literal["event_count", "value_count", "value_sum", "value_avg", "temporal"]
18
+ CORRELATION_TYPES: tuple[CorrelationType, ...] = (
19
+ "event_count",
20
+ "value_count",
21
+ "value_sum",
22
+ "value_avg",
23
+ "temporal",
24
+ )
25
+ OPS = {"gt": ">", "gte": ">=", "lt": "<", "lte": "<=", "eq": "="}
26
+ LEGACY_OPS = {">": "gt", ">=": "gte", "<": "lt", "<=": "lte", "=": "eq", "==": "eq"}
27
+ TIMESPAN = re.compile(r"^\s*(\d+)\s*([smhdw])\s*$")
28
+ UNIT = {"s": 1, "m": 60, "h": 3600, "d": 86400, "w": 604800}
29
+ # `selection | count(field) by a, b > 5`, the pre-correlation aggregation syntax.
30
+ AGGREGATION = re.compile(
31
+ r"^count\(\s*(?P<field>[\w.-]*)\s*\)"
32
+ r"(?:\s+by\s+(?P<by>[\w.-]+(?:\s*,\s*[\w.-]+)*))?"
33
+ r"\s*(?P<op>>=|<=|==|=|>|<)\s*(?P<value>\d+)$"
34
+ )
35
+
36
+ Condition = tuple[tuple[str, float], ...]
37
+
38
+
39
+ @dataclass(frozen=True)
40
+ class Rule:
41
+ title: str
42
+ id: str | None
43
+ level: str | None
44
+ searches: dict[str, Any]
45
+ conditions: tuple[str, ...]
46
+ logsource: Mapping[str, str] = dataclasses.field(default_factory=dict)
47
+ name: str | None = None
48
+ # Set for rules written with the legacy `| count() ...` condition syntax.
49
+ aggregation: Correlation | None = None
50
+ path: Path | None = dataclasses.field(default=None, compare=False)
51
+
52
+ def compile(self) -> Expr:
53
+ """Build the match tree. Searches the condition never uses are not compiled."""
54
+ cache: dict[str, Expr] = {}
55
+
56
+ def resolve(name: str) -> Expr:
57
+ if name not in cache:
58
+ cache[name] = compile_search(name, self.searches[name])
59
+ return cache[name]
60
+
61
+ names = list(self.searches)
62
+ return any_of([compile_condition(c, names, resolve) for c in self.conditions])
63
+
64
+ @property
65
+ def ref(self) -> str:
66
+ """The name correlations use for this rule."""
67
+ return self.name or self.id or self.title
68
+
69
+
70
+ @dataclass(frozen=True)
71
+ class Correlation:
72
+ """A Sigma correlation rule: a count or sequence over the matches of other rules."""
73
+
74
+ title: str
75
+ id: str | None
76
+ level: str | None
77
+ type: CorrelationType
78
+ rules: tuple[str, ...]
79
+ group_by: tuple[str, ...] = ()
80
+ timespan: int | None = None # seconds
81
+ condition: Condition = ()
82
+ field: str | None = None
83
+ aliases: Mapping[str, Mapping[str, str]] = dataclasses.field(default_factory=dict)
84
+ generate: bool = False
85
+ name: str | None = None
86
+ path: Path | None = dataclasses.field(default=None, compare=False)
87
+
88
+
89
+ Document = Rule | Correlation
90
+
91
+
92
+ def load_documents(text: str, path: Path | None = None) -> list[Document]:
93
+ try:
94
+ docs = [d for d in yaml.safe_load_all(text) if d is not None]
95
+ except yaml.YAMLError as exc:
96
+ raise RuleError(f"invalid YAML: {exc}") from exc
97
+ if not docs:
98
+ raise RuleError("empty rule file")
99
+ out: list[Document] = []
100
+ for doc in docs:
101
+ if not isinstance(doc, dict):
102
+ raise RuleError("a rule must be a YAML map")
103
+ if "action" in doc:
104
+ raise UnsupportedRule("legacy rule collections ('action: global') are not supported")
105
+ if "correlation" in doc:
106
+ out.append(_correlation(doc, path))
107
+ elif "filter" in doc:
108
+ raise UnsupportedRule("filter rules are not supported")
109
+ else:
110
+ out.append(_rule(doc, path))
111
+ return out
112
+
113
+
114
+ def load_file(path: Path) -> list[Document]:
115
+ return load_documents(path.read_text(encoding="utf-8"), path)
116
+
117
+
118
+ def load_rule(text: str, path: Path | None = None) -> Rule:
119
+ """Load a file that holds exactly one detection rule."""
120
+ docs = load_documents(text, path)
121
+ if len(docs) != 1 or not isinstance(docs[0], Rule):
122
+ raise UnsupportedRule("expected a single detection rule")
123
+ return docs[0]
124
+
125
+
126
+ def load_rule_file(path: Path) -> Rule:
127
+ return load_rule(path.read_text(encoding="utf-8"), path)
128
+
129
+
130
+ def _meta(doc: dict[str, Any], path: Path | None) -> dict[str, Any]:
131
+ def opt(key: str) -> str | None:
132
+ value = doc.get(key)
133
+ return str(value) if value is not None else None
134
+
135
+ return {
136
+ "title": str(doc.get("title") or (path.stem if path else "untitled")),
137
+ "id": opt("id"),
138
+ "level": opt("level"),
139
+ "name": opt("name"),
140
+ "path": path,
141
+ }
142
+
143
+
144
+ def _rule(doc: dict[str, Any], path: Path | None) -> Rule:
145
+ detection = doc.get("detection")
146
+ if not isinstance(detection, dict):
147
+ raise RuleError("missing 'detection' section")
148
+
149
+ condition = detection.get("condition")
150
+ if isinstance(condition, str):
151
+ conditions: tuple[str, ...] = (condition,)
152
+ elif isinstance(condition, list) and condition and all(isinstance(c, str) for c in condition):
153
+ conditions = tuple(condition)
154
+ else:
155
+ raise RuleError("missing or invalid 'detection.condition'")
156
+
157
+ searches = {str(k): v for k, v in detection.items() if k not in ("condition", "timeframe")}
158
+ if not searches:
159
+ raise RuleError("detection has no searches")
160
+
161
+ logsource = doc.get("logsource") or {}
162
+ if not isinstance(logsource, dict):
163
+ raise RuleError("'logsource' must be a map")
164
+
165
+ rule = Rule(
166
+ searches=searches,
167
+ conditions=conditions,
168
+ logsource={str(k): str(v) for k, v in logsource.items() if v is not None},
169
+ **_meta(doc, path),
170
+ )
171
+ if not any("|" in c for c in conditions):
172
+ return rule
173
+ if len(conditions) != 1:
174
+ raise UnsupportedRule("aggregations need a single condition")
175
+ base, aggregation = (part.strip() for part in conditions[0].split("|", 1))
176
+ timeframe = detection.get("timeframe")
177
+ rule = replace(rule, conditions=(base,))
178
+ return replace(rule, aggregation=_aggregation(rule, aggregation, timeframe))
179
+
180
+
181
+ def _aggregation(rule: Rule, text: str, timeframe: Any) -> Correlation:
182
+ m = AGGREGATION.match(text)
183
+ if not m:
184
+ raise UnsupportedRule(f"aggregation {text!r} is not supported (only count)")
185
+ by = tuple(g.strip() for g in m["by"].split(",")) if m["by"] else ()
186
+ return Correlation(
187
+ title=rule.title,
188
+ id=rule.id,
189
+ level=rule.level,
190
+ type="value_count" if m["field"] else "event_count",
191
+ rules=(rule.ref,),
192
+ group_by=by,
193
+ timespan=_timespan(timeframe) if timeframe is not None else None,
194
+ condition=((LEGACY_OPS[m["op"]], float(m["value"])),),
195
+ field=m["field"] or None,
196
+ path=rule.path,
197
+ )
198
+
199
+
200
+ def _correlation(doc: dict[str, Any], path: Path | None) -> Correlation:
201
+ spec = doc["correlation"]
202
+ if not isinstance(spec, dict):
203
+ raise RuleError("'correlation' must be a map")
204
+ kind = spec.get("type")
205
+ if kind == "temporal_ordered":
206
+ raise UnsupportedRule("temporal_ordered correlations are not supported")
207
+ if kind not in CORRELATION_TYPES:
208
+ raise UnsupportedRule(f"correlation type {kind!r} is not supported")
209
+
210
+ rules = spec.get("rules")
211
+ if isinstance(rules, str):
212
+ rules = [rules]
213
+ if not isinstance(rules, list) or not rules:
214
+ raise RuleError("correlation needs a list of 'rules'")
215
+
216
+ group_by = spec.get("group-by") or []
217
+ if isinstance(group_by, str):
218
+ group_by = [group_by]
219
+
220
+ raw_condition = spec.get("condition") or {}
221
+ if not isinstance(raw_condition, dict):
222
+ raise RuleError("correlation 'condition' must be a map")
223
+ value_field = raw_condition.get("field", spec.get("field"))
224
+ condition: list[tuple[str, float]] = []
225
+ for op, value in raw_condition.items():
226
+ if op == "field":
227
+ continue
228
+ if op not in OPS or isinstance(value, bool) or not isinstance(value, int | float):
229
+ raise RuleError(f"invalid correlation condition {op}: {value!r}")
230
+ condition.append((op, float(value)))
231
+ if kind == "temporal" and not condition:
232
+ condition = [("gte", float(len(rules)))]
233
+ if not condition:
234
+ raise RuleError("correlation needs a condition")
235
+ if kind in ("value_count", "value_sum", "value_avg") and not value_field:
236
+ raise RuleError(f"{kind} correlations need a condition field")
237
+
238
+ aliases = spec.get("aliases") or {}
239
+ if not isinstance(aliases, dict) or not all(isinstance(v, dict) for v in aliases.values()):
240
+ raise RuleError("correlation 'aliases' must map names to {rule: field}")
241
+
242
+ timespan = spec.get("timespan")
243
+ return Correlation(
244
+ type=kind,
245
+ rules=tuple(str(r) for r in rules),
246
+ group_by=tuple(str(g) for g in group_by),
247
+ timespan=_timespan(timespan) if timespan is not None else None,
248
+ condition=tuple(condition),
249
+ field=str(value_field) if value_field else None,
250
+ aliases={str(k): {str(r): str(f) for r, f in v.items()} for k, v in aliases.items()},
251
+ generate=bool(spec.get("generate", False)),
252
+ **_meta(doc, path),
253
+ )
254
+
255
+
256
+ def _timespan(value: Any) -> int:
257
+ m = TIMESPAN.match(str(value))
258
+ if not m:
259
+ raise RuleError(f"invalid timespan {value!r}; use e.g. 30s, 5m, 1h, 1d")
260
+ return int(m[1]) * UNIT[m[2]]
sigma2sql/runner.py ADDED
@@ -0,0 +1,82 @@
1
+ """Run rules against sample events in an in-memory SQLite database."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import ipaddress
6
+ import json
7
+ import re
8
+ import sqlite3
9
+ from contextlib import closing
10
+ from dataclasses import replace
11
+ from typing import Any
12
+
13
+ from sigma2sql.rule import Rule
14
+ from sigma2sql.sql import Target, to_query
15
+
16
+ TABLE = "events"
17
+ COLUMN = "data"
18
+
19
+
20
+ def connect() -> sqlite3.Connection:
21
+ """SQLite connection with the REGEXP and cidr_match functions the sqlite dialect uses."""
22
+ conn = sqlite3.connect(":memory:")
23
+ conn.create_function("regexp", 2, _regexp, deterministic=True)
24
+ conn.create_function("cidr_match", 2, _cidr_match, deterministic=True)
25
+ return conn
26
+
27
+
28
+ def target(base: Target | None = None) -> Target:
29
+ """`base` (mapping, timestamp settings) pointed at the runner's events table."""
30
+ base = base or Target()
31
+ routes = tuple(replace(r, table=None) for r in base.routes)
32
+ return replace(base, dialect="sqlite", table=TABLE, json_column=COLUMN, routes=routes)
33
+
34
+
35
+ def load(events: list[dict[str, Any]]) -> sqlite3.Connection:
36
+ conn = connect()
37
+ conn.execute(f"CREATE TABLE {TABLE} ({COLUMN} TEXT NOT NULL)")
38
+ conn.executemany(
39
+ f"INSERT INTO {TABLE} (rowid, {COLUMN}) VALUES (?, ?)",
40
+ [(i + 1, json.dumps(e)) for i, e in enumerate(events)],
41
+ )
42
+ return conn
43
+
44
+
45
+ def matching_events(
46
+ rule: Rule, events: list[dict[str, Any]], base: Target | None = None
47
+ ) -> list[int]:
48
+ """Return the indexes of the events the rule matches."""
49
+ query = to_query(rule, target(base), columns="rowid")
50
+ with closing(load(events)) as conn:
51
+ return [row[0] - 1 for row in conn.execute(query + " ORDER BY rowid")]
52
+
53
+
54
+ def rows(query: str, events: list[dict[str, Any]]) -> list[dict[str, Any]]:
55
+ """Run a query (e.g. a correlation) over the events and return named rows."""
56
+ with closing(load(events)) as conn:
57
+ cur = conn.execute(query)
58
+ names = [d[0] for d in cur.description]
59
+ return [dict(zip(names, row, strict=True)) for row in cur.fetchall()]
60
+
61
+
62
+ def read_jsonl(text: str) -> list[dict[str, Any]]:
63
+ events: list[dict[str, Any]] = []
64
+ for n, line in enumerate(text.splitlines(), 1):
65
+ if not line.strip():
66
+ continue
67
+ event = json.loads(line)
68
+ if not isinstance(event, dict):
69
+ raise ValueError(f"line {n}: each event must be a JSON object")
70
+ events.append(event)
71
+ return events
72
+
73
+
74
+ def _regexp(pattern: str, value: Any) -> bool:
75
+ return value is not None and re.search(pattern, str(value)) is not None
76
+
77
+
78
+ def _cidr_match(value: Any, network: str) -> bool:
79
+ try:
80
+ return ipaddress.ip_address(str(value)) in ipaddress.ip_network(network)
81
+ except ValueError:
82
+ return False