DSL de Match
Referência completa da mini-linguagem declarativa das expressões de match
DSL de Match
A DSL de match é uma mini-linguagem declarativa usada no sentry.toml
([[rules.custom]] e [[policy.override]]) e na CLI (sentry rules add --match, sentry rules test). O
parser
é um recursive-descent que traduz o texto para a árvore
RuleMatch
avaliada pelo rules engine.
Gramática
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- Operadores lógicos são palavras em MAIÚSCULAS:
AND,OR,NOT. O tokenizer só reconhece a forma uppercase —andvira parte de um valor e vira erro de parse. NOTtem a maior precedência, depoisAND, depoisOR. Parênteses agrupam e sobrepõem a precedência.- Valores podem ser palavras nuas (terminam em espaço, parêntese ou
aspas) ou strings entre aspas (
"...") — obrigatório para padrões com espaço, regex ou=.
Condições
| Chave | Variação de RuleMatch | O que casa |
|---|---|---|
ip | Ip | IP literal, CIDR (10.0.0.0/8) ou range (10.0.0.1-10.0.0.99). v4 e v6. |
asn | Asn | Número do AS (u32). Só casa quando o evento tem ASN (enrichment geo). |
country | Country | Código ISO 3166-1 alpha-2, comparação case-insensitive. |
path | Path | Path do request URL-decodificado (ver operadores abaixo). |
method | Method | GET, POST, PUT, PATCH, DELETE, HEAD, OPTIONS, CONNECT, TRACE. Método desconhecido vira Other. |
status | Status | Código HTTP exato. Só casa quando o status é conhecido (na edge inline o evento é publicado depois da resposta). |
query | Query | Query string inteira, com operadores de string. |
header.<nome> | Header | Header por nome (case-insensitive) + operador de string. Todo token http_* do log_format nginx vira header (ex.: header.cf-connecting-ip). |
ua (aliases user_agent, useragent) | UserAgent | User-Agent + operador de string. |
body | Body | Corpo da requisição quando capturado (fontes inline). |
upload_filename | UploadFilename | (F10) dispara quando qualquer parte multipart declara um filename que casa com o operador. |
protocol | Protocol | http, http3, tcp, udp, tls, other (parse case-insensitive). |
tls_ja3 | TlsFingerprint | Hash JA3 (case-insensitive). Só em eventos de handshake TLS (edge inline). |
tls_ja4 | TlsFingerprint | Hash JA4 (case-insensitive). |
tls_sni | TlsSni | SNI do ClientHello + operador de string. SNI ausente nunca casa (a edge sinaliza TlsSniMismatch à parte). |
reputation | Reputation | Tier de reputação do IP (valores abaixo). |
bot_verified | BotVerified | Resultado da verificação rDNS de bots (valores abaixo). |
rate | Rate | Contador de eventos por janela e escopo (sintaxe abaixo). |
time | Time | Janela de horário (ver ressalva abaixo). |
Operadores de string
Aplicáveis a ua, query, body, upload_filename, tls_sni e
header.<nome>:
| Operador | Efeito |
|---|---|
= valor / eq / equals | Igualdade exata. |
contains valor | Substring. |
starts_with valor / startswith | Prefixo. |
regex "<padrão>" | Regex (Rust regex, linear-time). Regexes são compiladas 1× por processo e cacheadas; padrão inválido nunca casa. |
in "a" "b" "c" | Casa contra qualquer valor da lista. |
Forma inline chave=valor (sem operador) equivale a equals.
Operadores de path
| Forma | Efeito |
|---|---|
path=/admin/* | Se contém * → glob (* = qualquer sequência, ? = 1 char; case-insensitive); senão igualdade exata. |
path regex "\.env" | Regex sobre o path decodificado (use (?i) para ignorar caixa). |
path starts_with /admin (ou startswith, prefix) | Prefixo. |
path contains wp-login | Açúcar para regex com o valor escapado. |
O path é URL-decodificado antes do match (%2e%2e%2f = ../,
+ = espaço), então encoding não burla regras. O decode acontece 1× por
avaliação, compartilhado entre todas as condições path.
Valores de reputation
unknown, clean, suspicious, malicious, datacenter, vpn
(aliases proxy, vpn_proxy), tor, authorized, promiscuous.
authorized— scanner contratado pela organização (ex.: fornecedor de pentest); não gera sinal — combine com uma regraallow.promiscuous— scanner que publica os dados de recon para qualquer um; cada hit rende um pequeno sinalPromiscuousScanner(peso 10).
Os tiers vêm das reputation feeds
([[rules.feeds]] kind = "ip" com tier = "...").
Valores de bot_verified
| Valor | Casa quando… |
|---|---|
true / verified | rDNS confirmado (BotStatus::Verified). |
false / spoofed | UA alega crawler mas o forward-resolve falhou (Spoofed). |
| engine | Verificado como aquele engine: google, bing, yahoo, baidu, yandex. |
Pendente/desconhecido nunca casa — verificação só existe com
[bot_verification] habilitado (opt-in).
rate
rate <count>/<janela> [escopo]- Janela:
60s,5m,1h(default60s). - Escopo:
per_ip(default),per_asn,per_path,global. - Requer o rate-limit backend do daemon (
InMemoryRateLimiterou Redis) — sem backend, a condição nunca casa. O bucket inclui os parâmetros da condição, então duas regras com thresholds iguais compartilham contador e thresholds diferentes ficam independentes. per_asnsem ASN no evento eper_pathfora de HTTP nunca casam.
time
time inside(09:00-18:00 America/Sao_Paulo)
time outside(09:00-18:00 America/Sao_Paulo)A sintaxe parseia mas a avaliação ainda não implementa janelas de horário — a condição nunca casa hoje (reservada para evolução futura). Não dependa dela para enforcement.
Datasets JA3 (ja3 in)
A membership em conjuntos de fingerprints JA3
(RuleMatch::Ja3In)
não é expressa na DSL textual: ela nasce de datasets ([[rules.feeds]] kind = "ja3" ou DB-backed via sentry datasets import --kind ja3), que
compilam a lista inteira numa única regra. Ver
Default Rule Packs.
Erros de parse
O parser retorna
DslError:
token inesperado, valor faltando, parêntese sem fechar, número/IP inválido,
chave desconhecida ou expressão vazia. Em [[rules.custom]] um erro derruba
só a regra culpada (o nome aparece na mensagem); em [[policy.override]]
o erro falha o carregamento do config.
Exemplos por categoria
[[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"Uso na CLI
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 avalia a expressão contra um evento hipotético (regras
de rate sem backend e time nunca casam na simulação) — valide antes de
promover para enforce.