sigma2sql
Run Sigma detection rules as SQL on SQLite, PostgreSQL and DuckDB: hunt through log files without a SIEM, convert rules for the database your logs already live in, and unit-test rules before they reach production.
- Every SigmaHQ rule converts: 3162 / 3162 on SQLite and DuckDB, 3160 / 3162 on
PostgreSQL, plus all 45
count()aggregation rules SigmaHQ keeps underunsupported/. Every generated query is executed on all three databases in CI each week. - Checked against real events: all 476 of SigmaHQ's regression samples match as expected, and on those events sigma2sql agrees with pySigma on 3074 of 3076 rules; the two differences are IPv6 addresses pySigma's text matching misses (details).
- Correlations: event counts, distinct counts, sums and temporal sequences over a sliding window, as plain SQL.
- Your schema: profiles map Sigma fields to your columns or nested JSON (Sysmon, EVTX exports, Elastic/Winlogbeat ECS, OCSF built in) and limit each rule to the right log source.
Install
uv tool install "sigma2sql[duckdb] @ git+https://github.com/brkozer/sigma2sql"
The duckdb extra is needed for hunt; convert, test and check only need PyYAML.
Hunt through log files
git clone --depth 1 https://github.com/SigmaHQ/sigma
sigma2sql hunt sigma/rules --logs 'logs/*.json' --profile evtx --examples 1
hunt loads JSON, JSON Lines, Parquet or CSV files into an in-memory DuckDB database, runs
every rule and correlation, and lists the ones that matched, most severe first. On the
examples in this repository:
$ sigma2sql hunt examples/rules --logs examples/events.jsonl
LEVEL HITS RULE
high 2 PowerShell started with an encoded command (examples/rules/encoded_powershell.yml)
high 1 Brute force against one account (examples/rules/brute_force.yml)
high 1 Office application starting a shell (examples/rules/office_spawns_shell.yml)
medium 1 Successful RDP logon from a public address (examples/rules/rdp_from_outside.yml)
4 of 4 rules matched 12 events (0 rules skipped)
--examples N prints matching events (or correlation results such as
{"TargetUserName":"admin","window_start":"2026-11-01 03:12:01","window_end":"2026-11-01 03:12:17","value":3}),
--format json gives a machine-readable report and --fail-on high a non-zero exit code
when a rule at that level or above fires.
Convert rules
$ sigma2sql convert examples/rules/office_spawns_shell.yml -d postgres --json-column data
-- Office application starting a shell (8a7f3e91-2c44-4b8e-b0f6-3e5d9c1a7b02)
SELECT * FROM "events" WHERE (("data"->>'ParentImage' ILIKE '%\\winword.exe' ESCAPE '\' OR ...)
AND ("data"->>'Image' ILIKE '%\\cmd.exe' ESCAPE '\' OR ...));
A whole directory works the same way, e.g.
sigma2sql convert sigma/rules -d postgres -t security.events --json-column event --profile ecs-winlogbeat.
Without --json-column each Sigma field is a column. Unsupported rules are listed on stderr
with the reason, and the command exits non-zero.
| Option | Meaning |
|---|---|
-d, --dialect |
sqlite (default), postgres or duckdb |
-t, --table |
table name, schema.table allowed (default events) |
--json-column |
read fields from this JSON / JSONB column |
--keyword-column |
column searched by keyword rules (in JSON mode the whole document) |
-p, --profile |
built-in profile or a YAML file (see below) |
-m, --mapping |
YAML map from Sigma field to your column or key, applied over the profile |
--nested |
dotted names are paths into nested JSON |
--timestamp-field, --timestamp-format |
event time for correlations (iso, epoch_s, epoch_ms) |
Correlations
Sigma correlation rules and the older | count() by ... > N syntax both become SQL. With a
timespan, each event closes a window over the events of the same group before it; the
window's events are found with a self-join, which works the same on all three databases and
allows distinct counts:
title: Brute force against one account
correlation:
type: event_count # also value_count, value_sum, value_avg, temporal
rules: [failed_logon]
group-by: [TargetUserName]
timespan: 5m
condition: {gte: 3}
Each result row holds the group values, the window's start and end and the counted value.
Rules referenced by a correlation are not reported on their own unless it sets
generate: true. temporal_ordered is not supported yet.
Profiles
A profile describes where your events keep Sigma's fields and which events each log source
covers. sigma2sql profiles lists the built-in ones:
| Profile | For |
|---|---|
sysmon |
Sysmon events with their own field names; each category limited to its event ID |
evtx |
EVTX exported to JSON by evtx_dump or python-evtx (Event.System, Event.EventData) |
ecs-winlogbeat |
Windows events shipped by Winlogbeat in Elastic Common Schema |
ocsf |
OCSF 1.x events, time in epoch milliseconds |
Your own profile is a small YAML file:
nested: true
timestamp: "@timestamp"
default_field: "winlog.event_data.{field}" # fallback for unmapped fields
fields:
Image: process.executable
EventID: [event.code, winlog.event_id] # first one present wins
logsources:
- match: {product: windows, category: process_creation}
table: process_events # optional: route to a table
where: {event.code: "1"} # optional: extra condition
Without a profile, rules are not limited by log source: a network rule can then match a
process event that happens to have the same field values. The built-in profiles cover the
common Windows sources; for other data, add logsources entries.
Test rules
Put example events next to a rule, one JSON object per line:
rules/office_spawns_shell.yml
rules/office_spawns_shell.match.jsonl # every event must match
rules/office_spawns_shell.nomatch.jsonl # no event may match
$ sigma2sql check examples/rules --require-tests
ok examples/rules/brute_force.yml
ok examples/rules/encoded_powershell.yml
ok examples/rules/office_spawns_shell.yml
ok examples/rules/rdp_from_outside.yml
4 passed, 0 failed, 0 without tests
A failure names the rule, the file and the line, e.g. after adding an Outlook-spawned
cmd.exe to the match file of a rule that only watches Word, Excel and PowerPoint:
FAIL rules/office_spawns_shell.yml: Office application starting a shell: missed office_spawns_shell.match.jsonl event(s) 1
The same check as a GitHub Action:
- uses: brkozer/sigma2sql@v0.2.0
with:
path: rules
profile: sysmon # optional
require-tests: "true" # optional
sigma2sql test RULE EVENTS.jsonl prints the events (or correlation results) a single rule
produces.
Supported Sigma features
| Feature | SQLite | PostgreSQL | DuckDB |
|---|---|---|---|
maps, lists, keywords (with all, re and string modifiers) |
✓ | ✓ | ✓ |
and, or, not, 1 of x*, all of x*, them, condition lists |
✓ | ✓ | ✓ |
wildcards and escapes, contains, startswith, endswith, all, cased, windash |
✓ | ✓ | ✓ |
base64, base64offset, utf16, utf16le, utf16be, wide |
✓ | ✓ | ✓ |
re, re|i |
REGEXP ¹ |
~ ² |
regexp_matches ³ |
cidr (IPv4 and IPv6, any notation) |
cidr_match() ¹ |
inet <<= |
cidr_match() ¹ |
lt, lte, gt, gte, exists, fieldref, null |
✓ | ✓ | ✓ |
correlations, count() aggregations |
✓ | ✓ | ✓ |
expand (placeholders), temporal_ordered, filter rules |
– | – | – |
¹ Not built into the database. hunt, test and check register them; when you run the SQL
elsewhere, register regexp(pattern, value) and cidr_match(value, network) yourself
(sigma2sql.runner.connect() returns a SQLite connection with both).
² PostgreSQL rejects repetition counts above 255; those two rules are reported as unsupported
rather than silently changed.
³ RE2: no lookaround or backreferences; such rules are reported as unsupported on DuckDB.
Matching is case-insensitive by default (ASCII in SQLite, as its LIKE and NOCASE are);
values in a JSON column match whether they are stored as 4624 or "4624", and not on a
missing field is true, as Sigma intends.
How it works
flowchart LR
Y[rule files] --> L[loader<br/>rules, correlations,<br/>count aggregations]
L --> C[condition parser]
L --> D[search compiler<br/>modifiers, wildcards,<br/>base64, windash]
C --> E[match tree]
D --> E
P[profile<br/>fields, logsources,<br/>timestamp] --> R
E --> R[renderer<br/>SQLite · PostgreSQL · DuckDB]
R --> Q[SQL]
R --> K[correlation SQL<br/>self-join windows]
Q --> H[hunt / test / check]
K --> H
Library use
from sigma2sql import Target, load_rule_file, to_query
from sigma2sql.profile import apply_profile, load_profile
rule = load_rule_file("rule.yml")
target = apply_profile(
Target("postgres", "logs.windows", json_column="event"), load_profile("evtx")
)
print(to_query(rule, target))
Development
uv sync
uv run pytest # SQLite and DuckDB
SIGMA2SQL_PG_DSN=postgresql://postgres:test@localhost/postgres uv run pytest # + PostgreSQL
uv run python scripts/check_sigmahq.py sigma/rules --dsn ... [--profile evtx]
uv run python scripts/check_regression.py sigma
uv run --with pysigma --with pysigma-backend-sqlite python scripts/diff_pysigma.py sigma
uv run python scripts/demo_svg.py docs/demo.svg
The behaviour tests in tests/cases.py and tests/test_correlation.py run unchanged on all
three databases. The example rules and events in examples/ were written for this project.
License
MIT
Metadata
Release files for sigma2sql 0.2.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | Provenance | Actions |
|---|---|---|---|---|
| sigma2sql-0.2.0.tar.gz | 44.5 kB | With provenance | Details for sigma2sql-0.2.0.tar.gz |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Actions Reset |
|---|---|---|---|---|
| sigma2sql-0.2.0-py3-none-any.whl | Python 3 | none | any | Details for sigma2sql-0.2.0-py3-none-any.whl |
Total release size: 81.1 kB
Release files / sigma2sql-0.2.0.tar.gz
| Download URL | sigma2sql-0.2.0.tar.gz |
|---|---|
| Size | 44.5 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
d50977ae6c85b10415ebb5665526cd5d795c68d98fb1b0ac3f9de2b548bc8025
|
|
BLAKE2b-256 checksum How to use checksums |
b6730464939e3855d0dc70e610dc309e0aea76b4481711358fc4f1db6cc92ab4
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.16
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Nov 4, 2026.
Transparency logRelease files / sigma2sql-0.2.0-py3-none-any.whl
| Download URL | sigma2sql-0.2.0-py3-none-any.whl |
|---|---|
| Size | 36.5 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
e68ecf35bfabdcbaf3ceb0c99862b6e0b75106ddd5e6e8746f502df251499642
|
|
BLAKE2b-256 checksum How to use checksums |
9e7f77cb6429639d26ee7ded916a7ace453e5d471def6d621ffffffb7aec7651
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.16
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Nov 4, 2026.
Transparency logSecurity for sigma2sql 0.2.0
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
Full provenance
Both files in this release have been uploaded with provenance metadata.
Attestation for sigma2sql-0.2.0.tar.gz
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Nov 4, 2026.
Transparency logA note on package security
PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.
To learn more about protecting yourself from malicious actors, visit our package security documentation.