Signalith query language¶
A query is a search followed by zero or more commands, each after a |.
The search selects events; commands reshape the results. It reads like Splunk's SPL,
but it is Signalith's own language and does not claim SPL compatibility.
datamodel=Authentication action=failure earliest=-24h
| stats count, dc(user) as users by src
| where count > 20
| sort -count
| head 10
Search¶
| Syntax | Meaning |
|---|---|
datamodel=Authentication |
Events of one data model: Authentication, Endpoint, Network_Traffic, Change, Malware (antivirus; see antivirus.md), Filesystem, Registry, Network_Resolution, Image_Load, Script, Inventory, Configuration_Check, Alerts, Other. datamodel=Risk searches risk events, which are left out otherwise |
index=signalith-authentication-2026.10.* |
One pattern of Signalith's daily indices, <prefix>-<datamodel>-YYYY.MM.DD. It must start with the configured opensearch.index_prefix and -; index=* means all events, as when index= is left out. Lists (a,b), exclusions (-x), other indices and system indices, other clusters (remote:x) and date math are refused |
field=value |
Exact match, case-insensitive |
field=val* |
Wildcard match with * and ? |
field=* |
The field has any value |
src=10.0.0.0/8 |
An IPv4 address range (CIDR) |
field!=value |
Does not match |
field>10, >=, <, <= |
Numeric (or text) comparison |
field IN (a, b, c) |
Any of several values |
word or "a phrase" |
Text anywhere in the raw event (_raw) |
NOT term, ( ... ) |
Negation and grouping |
a OR b |
Either. As in SPL, OR binds tighter than the implicit AND, so x a OR b means x AND (a OR b) |
earliest=-7d@d latest=now |
Time range; overrides the UI time picker |
Relative times: -30s, -15m, -4h, -7d, -2w, -1mon, -1y, optionally snapped
with @d, @h, @w. Absolute times are ISO 8601 (2026-10-01T00:00:00Z).
Quote values that contain spaces or symbols: process_path="C:\Windows\System32\cmd.exe".
Inside quotes, \" is a quote and \\ is one backslash; any other backslash is
kept as typed, so Windows paths work without doubling.
Commands¶
| Command | Example | Notes |
|---|---|---|
stats |
stats count, dc(user) as users, avg(bytes) by src, dest |
Functions: count, count(f), dc(f), sum(f), avg(f), min(f), max(f) |
timechart |
timechart span=1h count by action |
One aggregation when splitting by a field; span picked automatically if omitted |
top |
top limit=10 process_name by host |
Adds count and percent |
where |
where count > 20 AND users >= 2 |
AND binds tighter than OR here, as in most languages |
sort |
sort -count, src |
- for descending |
head |
head 20 |
Also limit |
table |
table _time, user, src |
Also fields; wildcards allowed |
rename |
rename src as source_ip |
|
enrich |
enrich or enrich user, dest |
Adds asset and identity context: <field>_priority, _category, _bunit, plus _owner, _location for assets and _name, _email, _manager, _watchlist for users. With no fields it enriches user, src_user, src, dest, host, src_host, dest_host and risk_object |
threatmatch |
threatmatch or threatmatch src, dest |
Keeps only events where a field equals a threat intel indicator. Runs inside OpenSearch, so it must come right after the search (before stats and other commands). Adds threat_match_field, threat_match_value, threat_indicator, threat_type, threat_source, threat_weight, threat_description and threat_target (the system on your side) to each event |
intel |
intel or intel src, url |
Adds <field>_intel_source, _intel_type, _intel_weight and _intel_description where a value is a known indicator. Works on any result, including after stats |
With no fields, threatmatch and intel check src, dest, src_ip, dest_ip,
dest_host, domain, query, url_domain, url, the hash fields
(process_sha256, process_sha1, process_md5, process_hash, file_hash,
file_sha256, file_sha1, file_md5, hash, md5, sha1, sha256) and the email fields (sender, recipient, src_user,
user). The threat_* columns are added after OpenSearch returns events, so
aggregate on event fields and add intel afterwards:
* | threatmatch src | stats count by src, dest | intel src.
Columns produced by stats are referenced by name in later commands, for example
sort -dc(user) or where avg(bytes) > 1000.
Asking in plain English¶
The box above the query editor (and signalith ask "...") turns a question into a
query. It understands:
| You write | It adds |
|---|---|
| logins, processes, connections, account changes, risk | datamodel=... |
| failed, successful, blocked, allowed, created, deleted | action=... |
| from 10.0.0.5, to web1, on dc01, by alice, port 22, user bob | field filters; not/except negates |
| powershell, cmd, certutil, mimikatz.exe, spawned by winword | process_name, parent_process_name |
| containing mimikatz, "quoted text" | process=*...* for processes, text search otherwise |
| last 4 hours, past 7 days, today, yesterday, this week | earliest= and latest= |
| how many, distinct users, total bytes, average bytes | stats count, dc(), sum(), avg() |
| per user, by source ip, for each host | by ... |
| top 10 users, most common processes, rare processes | top or stats ... \| sort \| head |
| more than 5, at least 3 hosts | where count>5; stats dc(dest) as distinct_dest ... \| where distinct_dest>=3 |
| per hour, over time, daily | timechart |
| show user, src and dest | table _time, user, src, dest |
Names from the asset and identity lists are recognized as users or hosts. When an AI model is configured, questions with unknown words go to it; see Writing queries in the README.
How queries run¶
- The search and any
wherecommands that come before the firststats,timechartortoprun inside OpenSearch, so they scale with the cluster. statsandtopuse composite aggregations and page through every group (up to 50,000 groups, then results are marked truncated).threatmatchbecomestermsfilters of indicator values (10,000 per clause) and prefix filters for networks, so matching runs inside OpenSearch too.- Commands after that run in the Signalith server on the aggregated rows.
- An event search without
statsreturns the newest 100 events, or up tosearch.max_events(at most 100,000) when later commands need to sort or filter them all. - Wildcard values (
*,?) can be up to 1,000 characters long, and the wildcard values inwherecommands up to 4,096 characters together (whereruns over every row in the server, so put long filters in the search before the first|). AnINlist holds at most 1,000 values. - At most 8 searches run at once (detections are counted separately), and one user's searches take at most 4 of those turns. Others wait their turn for up to 15 seconds, then fail with "Signalith is busy running other searches" (or "You already have several searches running"); try again shortly.
Use Explain in the UI, or signalith query -explain "<query>", to see the plain-English
steps and the exact OpenSearch request.