Profiles¶
A profile says where your events keep Sigma's fields and which events each log source covers.
It is a small YAML file. convert, hunt, test and check all take one with -p, either
by built-in name or as a path:
sigma2sql convert rules/ -d postgres --json-column data -p evtx
sigma2sql check rules/ -p ./acme.yml
Without a profile each Sigma field is a column (or a top-level JSON key) of the same name, and rules are not limited by log source: a network rule can then match a process event that happens to have the same field values.
A value with a .yml or .yaml suffix is always read as a file. Any other value is read as a
file if one of that name exists, and is a built-in name otherwise. So a file called sysmon
in the working directory hides the built-in profile.
Built-in profiles¶
$ sigma2sql profiles
ecs-winlogbeat Windows events shipped by Winlogbeat in Elastic Common Schema (nested JSON); unmapped fields fall back to winlog.event_data
evtx Windows event logs exported to JSON by evtx_dump or python-evtx (Event.System / Event.EventData)
ocsf OCSF 1.x events (nested JSON, time in epoch milliseconds), routed by class_uid
sysmon Sysmon events with their original field names; each Sigma category is limited to its Sysmon event ID
| Profile | Layout | Event time | product |
Fallback for unmapped fields |
|---|---|---|---|---|
sysmon |
flat | UtcTime |
windows |
none |
evtx |
nested | Event.System.TimeCreated.#attributes.SystemTime |
windows |
Event.EventData.{field}, then Event.EventData.{field_words} |
ecs-winlogbeat |
nested | @timestamp |
windows |
winlog.event_data.{field} |
ocsf |
nested | time, epoch milliseconds |
none | none |
No built-in profile names a JSON column, because that depends on your table. hunt, test and
check have their own, but convert needs --json-column with the nested profiles. Without
it a dotted path becomes one column name, which is rarely what you want. reg.yml is shown
below:
$ sigma2sql convert reg.yml -p evtx
-- Value set by reg.exe
SELECT * FROM "events" WHERE (("Event.System.Channel" = 'Microsoft-Windows-Sysmon/Operational' COLLATE NOCASE AND "Event.System.EventID" = 13) AND ("Event.EventData.Image" LIKE '%\\reg.exe' ESCAPE '\' AND COALESCE("Event.EventData.NewValue", "Event.EventData.New Value") = 'evil' COLLATE NOCASE));
$ sigma2sql convert reg.yml -p evtx --json-column data
-- Value set by reg.exe
SELECT * FROM "events" WHERE ((json_extract("data", '$."Event"."System"."Channel"') = 'Microsoft-Windows-Sysmon/Operational' COLLATE NOCASE AND CAST(json_extract("data", '$."Event"."System"."EventID"') AS TEXT) = '13') AND (json_extract("data", '$."Event"."EventData"."Image"') LIKE '%\\reg.exe' ESCAPE '\' AND COALESCE(json_extract("data", '$."Event"."EventData"."NewValue"'), json_extract("data", '$."Event"."EventData"."New Value"')) = 'evil' COLLATE NOCASE));
sysmon¶
For Sysmon events that keep their own field names at the top level, so nothing is mapped. The profile only limits each Sigma category to its event ID:
| Category | EventID |
Category | EventID |
|---|---|---|---|
process_creation |
1 | registry_set |
13 |
file_change |
2 | registry_rename |
14 |
network_connection |
3 | registry_event |
12, 13, 14 |
process_termination |
5 | create_stream_hash |
15 |
driver_load |
6 | pipe_created |
17, 18 |
image_load |
7 | wmi_event |
19, 20, 21 |
create_remote_thread |
8 | dns_query |
22 |
raw_access_thread |
9 | file_delete |
23, 26 |
process_access |
10 | clipboard_capture |
24 |
file_event |
11 | process_tampering |
25 |
registry_add |
12 | file_executable_detected |
29 |
registry_delete |
12 |
Every entry needs product: windows on the rule as well. Rules for a service, such as
security, get no extra condition.
evtx¶
For Windows events as evtx_dump or python-evtx export them to JSON. This is the layout hunt
produces from .evtx files, and it uses this profile by itself when you pass no other
(details).
| Sigma field | Path in the event |
|---|---|
EventID |
Event.System.EventID.#text, then Event.System.EventID |
Channel |
Event.System.Channel |
Computer |
Event.System.Computer |
Provider_Name |
Event.System.Provider.#attributes.Name |
Level |
Event.System.Level |
Keywords |
Event.System.Keywords |
Operation |
Event.EventData.Operation, then under Event.UserData.Operation_ClientFailure and Event.UserData.Operation_StartedOperational |
ClientMachine, PossibleCause |
Event.EventData, then Event.UserData.Operation_ClientFailure |
Consumer |
Event.EventData.Consumer, then Event.UserData.Operation_ESStoConsumerBinding.CONSUMER |
Query |
Event.EventData.Query, then Event.UserData.Operation_TemporaryEssStarted.Query |
Every other field is read from Event.EventData. EventID has two paths because it carries
Qualifiers on some logs and then becomes an object with a #text key. The {field_words}
fallback exists for Defender, which writes New Value for NewValue, and the UserData
paths for WMI-Activity. Both came out of the
regression samples.
The log sources:
- The 23 Sysmon categories in the table above, each limited to the channel
Microsoft-Windows-Sysmon/Operationaland its event ID. ps_script(4104) andps_module(4103) onMicrosoft-Windows-PowerShell/Operational;ps_classic_start(400) andps_classic_provider_start(600) onWindows PowerShell.- Services, each limited to its channel:
| Service | Event.System.Channel |
|---|---|
security, system, application |
Security, System, Application |
sysmon |
Microsoft-Windows-Sysmon/Operational |
powershell |
Microsoft-Windows-PowerShell/Operational |
powershell-classic |
Windows PowerShell |
taskscheduler |
Microsoft-Windows-TaskScheduler/Operational |
wmi |
Microsoft-Windows-WMI-Activity/Operational |
windefend |
Microsoft-Windows-Windows Defender/Operational |
bits-client |
Microsoft-Windows-Bits-Client/Operational |
firewall-as |
Microsoft-Windows-Windows Firewall With Advanced Security/Firewall |
appxdeployment-server |
Microsoft-Windows-AppXDeploymentServer/Operational |
codeintegrity-operational |
Microsoft-Windows-CodeIntegrity/Operational |
dns-client |
Microsoft-Windows-DNS Client Events/Operational |
driver-framework |
Microsoft-Windows-DriverFrameworks-UserMode/Operational |
ntlm |
Microsoft-Windows-NTLM/Operational |
terminalservices-localsessionmanager |
Microsoft-Windows-TerminalServices-LocalSessionManager/Operational |
openssh |
OpenSSH/Operational |
ecs-winlogbeat¶
For Windows events shipped by Winlogbeat in Elastic Common Schema. Fields without an ECS home
fall back to winlog.event_data.
| Sigma field | ECS field |
|---|---|
EventID |
event.code |
Channel |
winlog.channel |
Provider_Name |
winlog.provider_name |
Computer |
host.name |
Image |
process.executable |
CommandLine |
process.command_line |
ProcessId |
process.pid |
CurrentDirectory |
process.working_directory |
OriginalFileName |
process.pe.original_file_name |
Product, Company, Description |
process.pe.product, .company, .description |
ParentImage |
process.parent.executable |
ParentCommandLine |
process.parent.command_line |
ParentProcessId |
process.parent.pid |
User |
user.name |
TargetFilename, ImageLoaded |
file.path |
DestinationIp, DestinationPort |
destination.ip, destination.port |
DestinationHostname |
destination.domain |
SourceIp, SourcePort |
source.ip, source.port |
QueryName |
dns.question.name |
TargetObject |
registry.path |
The Sysmon categories assume the Winlogbeat sysmon module, where event.code holds the event
ID as a string. Each category is also limited to the channel
Microsoft-Windows-Sysmon/Operational:
| Log source | Condition |
|---|---|
process_creation |
event.code 1 |
network_connection |
event.code 3 |
image_load |
event.code 7 |
process_access |
event.code 10 |
file_event |
event.code 11 |
registry_set |
event.code 13 |
registry_event |
event.code 12, 13 or 14 |
dns_query |
event.code 22 |
service security |
winlog.channel is Security |
service system |
winlog.channel is System |
service powershell |
winlog.channel is Microsoft-Windows-PowerShell/Operational |
ocsf¶
For OCSF 1.x events, with the time in epoch milliseconds in time.
| Sigma field | OCSF field |
|---|---|
Image |
process.file.path |
CommandLine |
process.cmd_line |
ProcessId |
process.pid |
CurrentDirectory |
process.file.parent_folder |
ParentImage |
actor.process.file.path |
ParentCommandLine |
actor.process.cmd_line |
ParentProcessId |
actor.process.pid |
User |
actor.user.name |
Computer |
device.hostname |
TargetFilename |
file.path |
DestinationIp, DestinationPort |
dst_endpoint.ip, dst_endpoint.port |
DestinationHostname |
dst_endpoint.hostname |
SourceIp, SourcePort |
src_endpoint.ip, src_endpoint.port |
QueryName |
query.hostname |
| Category | Condition |
|---|---|
process_creation |
class_uid 1007 and activity_id 1 |
file_event |
class_uid 1001 |
network_connection |
class_uid 4001 |
dns_query |
class_uid 4003 |
These entries match on the category alone and the profile sets no product, so hunt runs
every rule with this profile.
Write your own¶
# acme.yml
description: Our Windows events
json_column: event # used by convert only
nested: true # dotted names are paths into nested JSON
timestamp: ts # event time for correlations
timestamp_format: epoch_s # iso (default), epoch_s or epoch_ms
product: windows # hunt runs only rules for this product
default_field: ["attrs.{field}", "attrs.{field_words}"]
fields:
Image: [proc.path, proc.image] # first one present wins
logsources:
- match: {product: windows, category: registry_set}
table: registry_events # used by convert only
where: {kind: set}
- match: {product: windows}
where: {source|startswith: win}
Every key is optional, and an empty file is a valid profile.
| Key | Type | Default | Meaning |
|---|---|---|---|
description |
string | empty | The line sigma2sql profiles shows for a built-in profile |
json_column |
string | none | JSON column to read from; without it each field is a column |
nested |
boolean | false |
Split names on dots and follow them into nested JSON |
timestamp |
string | timestamp |
Column or path of the event time, see Event time |
timestamp_format |
string | iso |
iso, epoch_s or epoch_ms |
product |
string | none | See product scoping |
default_field |
string or list | none | Templates for fields not in fields |
fields |
map | empty | Sigma field to a name, or a list of names |
logsources |
list | empty | See log sources |
There is no top-level table: the table comes from -t or from a logsources entry.
Field lookup¶
Each Sigma field is looked up in this order, and the first step that knows the field decides:
- The
-mmapping file. - The profile's
fields. - The profile's
default_fieldtemplates. - The field's own name.
The event time has an order of its own, see Event time.
A list in fields and several default_field templates both give a chain of candidates, which
becomes COALESCE. The first one that is not NULL wins, so a key that is present with a JSON
null falls through to the next.
In a template, {field} is the Sigma name and {field_words} the same name with a space
before each capital that follows a lower-case letter or a digit: NewValue becomes New Value
and IPAddress stays as it is. Every template must contain one of the two.
When both templates give the same name, as they do for Image, it is used once.
With acme.yml from above, Image comes from the list in fields and NewValue from the
two templates:
# reg.yml
title: Value set by reg.exe
logsource: {product: windows, category: registry_set}
detection:
sel:
Image|endswith: '\reg.exe'
NewValue: evil
condition: sel
$ sigma2sql convert reg.yml -d postgres -p acme.yml
-- Value set by reg.exe
SELECT * FROM "registry_events" WHERE (LOWER("event"->>'kind') = 'set' AND (COALESCE("event"#>>'{"proc","path"}', "event"#>>'{"proc","image"}') ILIKE '%\\reg.exe' ESCAPE '\' AND LOWER(COALESCE("event"#>>'{"attrs","NewValue"}', "event"#>>'{"attrs","New Value"}')) = 'evil'));
The -m mapping file¶
-m is a flat YAML map from Sigma field to your column or key, applied over the profile. It
is the place for the few fields that differ in your data. Each value must be one string; a
list is only allowed in a profile's fields.
$ sigma2sql convert reg.yml -p acme.yml -m mapping.yml
-- Value set by reg.exe
SELECT * FROM "registry_events" WHERE (json_extract("event", '$."kind"') = 'set' COLLATE NOCASE AND (json_extract("event", '$."exe_path"') LIKE '%\\reg.exe' ESCAPE '\' AND COALESCE(json_extract("event", '$."attrs"."NewValue"'), json_extract("event", '$."attrs"."New Value"')) = 'evil' COLLATE NOCASE));
Image now reads exe_path and the other field is unchanged. With nested, a mapped name is
still split on dots. The mapping does not reach the where of a log source, which is already
in your own field names.
Event time¶
Correlations read the event time from the path timestamp names. timestamp_format says
how to read it: iso for text such as 2026-11-01T03:12:01Z, epoch_s and epoch_ms for
numbers. The minute to year modifiers use the same format on the field they are written
on. --timestamp-field and --timestamp-format override both keys.
The time is read in this order, and the first step that knows the field decides:
- An entry for the time field in the
-mmapping file or the profile'sfields. - The path
timestampnames and then, with a JSON column, thedefault_fieldtemplates. These candidates becomeCOALESCE, as for any field.
With this profile and a JSON column, the time is read from ts, then from attrs.ts:
Without a JSON column only the templates apply, so the same profile reads the column attrs.ts.
The full path in a built-in profile comes first, so default_field does not move it. Here
failed.yml asks for three failed logons in five minutes, and evtx.jsonl holds three such
events in the evtx layout:
$ sigma2sql hunt failed.yml --logs evtx.jsonl -p evtx -e 1
LEVEL HITS RULE
high 1 Repeated failed logons (failed.yml)
{"TargetUserName":"admin","window_start":"2026-11-01 03:12:00","window_end":"2026-11-01 03:12:02","value":3}
1 of 1 rules matched 3 events (0 rules skipped)
An entry in fields or -m wins over both. moved.jsonl holds the same events with the time
in Event.EventData.LogonTime:
$ sigma2sql hunt failed.yml --logs moved.jsonl -p evtx
0 of 1 rules matched 3 events (0 rules skipped)
$ sigma2sql hunt failed.yml --logs moved.jsonl -p evtx -m moved.yml -e 1
LEVEL HITS RULE
high 1 Repeated failed logons (failed.yml)
{"TargetUserName":"admin","window_start":"2026-11-01 03:12:00","window_end":"2026-11-01 03:12:02","value":3}
1 of 1 rules matched 3 events (0 rules skipped)
--timestamp-field follows the same order. With -p evtx a name such as LogonTime or
Sysmon's UtcTime is enough, as the default_field templates lead to Event.EventData:
$ sigma2sql hunt failed.yml --logs moved.jsonl -p evtx --timestamp-field LogonTime -e 1
LEVEL HITS RULE
high 1 Repeated failed logons (failed.yml)
{"TargetUserName":"admin","window_start":"2026-11-01 03:12:00","window_end":"2026-11-01 03:12:02","value":3}
1 of 1 rules matched 3 events (0 rules skipped)
Log sources¶
Each logsources entry has a match map and can have a table and a where.
matchapplies when every key in it is on the rule'slogsourcewith the same value. The case of the value does not matter, but the key must be spelled as in the rule. An empty map applies to every rule.- The first entry that applies wins. The others are not looked at, so put specific entries before general ones.
whereis a Sigma search map, added to the rule withAND. It can use lists (any of the values) and modifiers. Its keys are your own field names, as they are in the event: neitherfields,default_fieldnor-mapplies to them.nestedand the JSON column do.tablesends the rule to another table. Onlyconvertuses it, sincehunt,testandcheckkeep all events in one table. They still applywhere.- A rule that no entry matches runs on the default table with no extra condition.
In acme.yml, reg.yml took the first entry: table registry_events and kind = 'set'. A
rule for service: security falls through to the second entry:
$ sigma2sql convert other.yml -p acme.yml
-- Any Windows rule
SELECT * FROM "events" WHERE (json_extract("event", '$."source"') LIKE 'win%' ESCAPE '\' AND CAST(COALESCE(json_extract("event", '$."attrs"."EventID"'), json_extract("event", '$."attrs"."Event ID"')) AS TEXT) = '4688');
Product scoping¶
product tells hunt that the logs only hold this logsource.product, so it leaves out rules
for anything else. Without it, a rule of the form not <address range> for another product
matches every event, because the field it tests is missing and
not on a missing field is true. Here the Zeek rule
is not a network range, and the log holds one Windows event:
$ sigma2sql hunt zeek.yml win.yml --logs events.jsonl
LEVEL HITS RULE
- 1 Traffic outside the private range (zeek.yml)
- 1 Windows logon (win.yml)
2 of 2 rules matched 1 events (0 rules skipped)
$ sigma2sql hunt zeek.yml win.yml --logs events.jsonl -p sysmon
LEVEL HITS RULE
- 1 Windows logon (win.yml)
1 of 1 rules matched 1 events (0 rules skipped)
1 rules are for other log sources
The comparison ignores case. A rule with no product at all is also left out. A correlation
stays if at least one of the rules it refers to has the product. With the three Windows
profiles a directory of all SigmaHQ rules can be used as it is.
Only hunt scopes by product. convert, test and check take the rules you give them.
Options and profiles together¶
| Setting | What wins |
|---|---|
| Field names | -m over fields |
| Event time | --timestamp-field and --timestamp-format over the profile |
nested |
On if either the profile or --nested says so; there is no option to turn it off |
JSON column on convert |
The profile's json_column over --json-column |
Table on convert |
A log source's table over -t |
Table and JSON column on hunt, test, check |
Their own, whatever the profile says |
Errors¶
An unknown profile name or key stops the command before any rule runs, with exit code 2:
$ sigma2sql convert reg.yml -p sysmn
sigma2sql: unknown profile 'sysmn' (built in: ecs-winlogbeat, evtx, ocsf, sysmon)
$ sigma2sql convert reg.yml -p typo.yml
sigma2sql: unknown profile keys: default_fields
The same goes for a file that cannot be read or is not valid YAML, a fields that is not a map,
a list in fields that is empty or holds anything but strings, a default_field without
{field} or {field_words}, a timestamp_format other than the three above, a logsources
entry without a match map, a where that is not a map, and a -m file that is not a flat
map of strings.