Match DSL
Full reference for the declarative match expression language
Match DSL
The match DSL is a declarative mini-language used in sentry.toml
([[rules.custom]] and [[policy.override]]) and on the CLI (sentry rules add --match, sentry rules test). The
parser
is a recursive-descent translator from text to the
RuleMatch
tree evaluated by the rules engine.
Grammar
expr := or_expr
or_expr := and_expr ('OR' and_expr)*
and_expr := not_expr ('AND' not_expr)*
not_expr := 'NOT' not_expr | primary
primary := '(' expr ')' | atom
atom := key op value- Logical operators are UPPERCASE words:
AND,OR,NOT. The tokenizer only recognizes the uppercase form — a lowercaseandbecomes part of a value and fails to parse. NOTbinds tightest, thenAND, thenOR. Parentheses group and override precedence.- Values are either bare words (they end at whitespace, parens or a
quote) or quoted strings (
"...") — required for patterns with spaces, regexes or=.
Conditions
| Key | RuleMatch variant | What it matches |
|---|---|---|
ip | Ip | Literal IP, CIDR (10.0.0.0/8) or range (10.0.0.1-10.0.0.99). v4 and v6. |
asn | Asn | AS number (u32). Only matches when the event carries an ASN (geo enrichment). |
country | Country | ISO 3166-1 alpha-2 code, case-insensitive compare. |
path | Path | Request path, URL-decoded (see operators below). |
method | Method | GET, POST, PUT, PATCH, DELETE, HEAD, OPTIONS, CONNECT, TRACE. Unknown methods become Other. |
status | Status | Exact HTTP status code. Only matches when the status is known (inline edge publishes the event after the response). |
query | Query | The whole query string, with string operators. |
header.<name> | Header | Header by (case-insensitive) name + string operator. Every http_* token of the nginx log_format becomes a header (e.g. header.cf-connecting-ip). |
ua (aliases user_agent, useragent) | UserAgent | User-Agent + string operator. |
body | Body | Request body when captured (inline sources). |
upload_filename | UploadFilename | (F10) fires when any multipart part declares a filename matching the operator. |
protocol | Protocol | http, http3, tcp, udp, tls, other (parsed case-insensitively). |
tls_ja3 | TlsFingerprint | JA3 hash (case-insensitive). TLS handshake events only (inline edge). |
tls_ja4 | TlsFingerprint | JA4 hash (case-insensitive). |
tls_sni | TlsSni | ClientHello SNI + string operator. A missing SNI never matches (the edge surfaces that as TlsSniMismatch instead). |
reputation | Reputation | Client IP reputation tier (values below). |
bot_verified | BotVerified | rDNS bot-verification outcome (values below). |
rate | Rate | Event count over a window and scope (syntax below). |
time | Time | Time-of-day window (see caveat below). |
String operators
Applies to ua, query, body, upload_filename, tls_sni and
header.<name>:
| Operator | Effect |
|---|---|
= value / eq / equals | Exact equality. |
contains value | Substring. |
starts_with value / startswith | Prefix. |
regex "<pattern>" | Regex (Rust regex, linear-time). Regexes compile once per process and are cached; an invalid pattern never matches. |
in "a" "b" "c" | Matches any value in the list. |
The inline key=value form (no operator word) is sugar for equals.
Path operators
| Form | Effect |
|---|---|
path=/admin/* | If it contains * → glob (* = any sequence, ? = one char; case-insensitive); otherwise exact equality. |
path regex "\.env" | Regex over the decoded path (use (?i) for case-insensitivity). |
path starts_with /admin (or startswith, prefix) | Prefix. |
path contains wp-login | Sugar for a regex with the value escaped. |
The path is URL-decoded before matching (%2e%2e%2f = ../,
+ = space), so encoding can't bypass rules. Decoding happens once per
evaluation, shared by every path condition.
reputation values
unknown, clean, suspicious, malicious, datacenter, vpn
(aliases proxy, vpn_proxy), tor, authorized, promiscuous.
authorized— scanner contracted by the organization (e.g. your pentest vendor); raises no signal — pair it with anallowrule.promiscuous— scanner that publishes recon data for anyone to query; each hit earns a smallPromiscuousScannersignal (weight 10).
Tiers come from reputation feeds
([[rules.feeds]] kind = "ip" with tier = "...").
bot_verified values
| Value | Matches when… |
|---|---|
true / verified | rDNS confirmed (BotStatus::Verified). |
false / spoofed | UA claims a crawler but forward-resolution failed (Spoofed). |
| engine | Verified as that engine: google, bing, yahoo, baidu, yandex. |
Pending/unknown never matches — verification only exists with
[bot_verification] enabled (opt-in).
rate
rate <count>/<window> [scope]- Window:
60s,5m,1h(default60s). - Scope:
per_ip(default),per_asn,per_path,global. - Requires the daemon's rate-limit backend (
InMemoryRateLimiteror Redis) — without a backend the condition never matches. The bucket identity includes the condition parameters, so two rules with equal thresholds share a counter and different thresholds stay independent. per_asnwithout an ASN on the event andper_pathoutside HTTP never match.
time
time inside(09:00-18:00 America/Sao_Paulo)
time outside(09:00-18:00 America/Sao_Paulo)The syntax parses but evaluation does not implement time windows yet — the condition never matches today (reserved for future work). Do not rely on it for enforcement.
JA3 datasets (ja3 in)
Membership in JA3 fingerprint sets
(RuleMatch::Ja3In)
is not expressible in the textual DSL: it is born from datasets
([[rules.feeds]] kind = "ja3" or DB-backed via sentry datasets import --kind ja3), which compile the whole list into a single rule. See
Default Rule Packs.
Parse errors
The parser returns
DslError:
unexpected token, missing value, missing closing parenthesis, invalid
number/IP, unknown key or empty expression. In [[rules.custom]] an error
fails only the offending rule (the name appears in the message); in
[[policy.override]] the error fails config loading.
Examples by category
[[rules.custom]]
name = "allow internal monitoring"
priority = 1
match = 'ip=10.0.0.0/8'
action = "allow"
log_level = "silent"
[[rules.custom]]
name = "block admin from RU"
priority = 10
match = 'country=RU AND path=/admin/*'
action = "block"
[[rules.custom]]
name = "block scanner UA on login"
priority = 12
match = 'ua regex "(?i)(sqlmap|nikto|nuclei)" AND path starts_with /login'
action = "block"
[[rules.custom]]
name = "block spoofed Googlebot"
priority = 12
match = 'bot_verified = "false"'
action = "block"
[[rules.custom]]
name = "flag odd cf-connecting-ip outside US"
priority = 20
match = 'header.cf-connecting-ip contains "1.2.3.4" AND NOT country=US'
action = "challenge"
[[rules.custom]]
name = "block double-extension uploads"
priority = 15
match = 'upload_filename contains ".php.jpg"'
action = "block"
[[rules.custom]]
name = "block known scanner JA3"
priority = 18
match = 'tls_ja4 = "t13d1516h2_8daaf6152771_e5627efa2ab1"'
action = "block"
[[rules.custom]]
name = "rate-limit 404 storms per IP"
priority = 15
match = 'status=404 AND rate 10/60s per_ip'
action = "rate_limit"CLI usage
sentry rules add --name "block admin from RU" \
--match 'country=RU AND path=/admin/*' --action block --priority 10
sentry rules test --path /admin --method GET --ip 1.2.3.4 --ua "sqlmap/1.0"sentry rules test evaluates the expression against a hypothetical event
(rate conditions without a backend and time never match in the
simulation) — validate before promoting to enforce.