API — sentry-core
Public API reference for sentry-core: domain model, signals, plugin traits, rules engine, policy, pipeline and trackers
API — sentry-core
sentry-core is the contracts crate: domain model, plugin traits, rules engine, heuristics and trackers. It is pure — no I/O, no HTTP, no DB — and declares #![forbid(unsafe_code)]. Every plugin crate depends only on it.
Domain model — module event
Event
Event is the normalized, protocol-agnostic event that flows through the whole pipeline. Common fields live on the struct; protocol specifics live in protocol: ProtocolData.
pub struct Event {
pub id: Uuid,
pub timestamp: DateTime<Utc>,
pub source: SourceKind,
pub transport: Transport,
pub client_ip: IpAddr,
pub geo: Option<GeoInfo>,
pub asn: Option<u32>,
/* ... bytes, duration, reputation, bot ... */
pub protocol: ProtocolData,
}Accessors that pattern-match the payload — heuristics and rules use them to opt into protocol-specific logic without assuming every event is HTTP:
| Method | Returns |
|---|---|
http() / http_mut() | Option<&HttpData> |
tcp() | Option<&TcpData> |
tls() | Option<&TlsData> |
syslog() | Option<&SyslogData> |
is_http() / protocol_kind() | bool / ProtocolKind |
Event::new creates with a fresh id/timestamp; Event::canonical_ip normalizes IPv4-mapped IPv6 (::ffff:a.b.c.d) to plain IPv4 — the same client maps to a single key across blocks, storage and the CLI.
ProtocolData
ProtocolData is the enum that keeps the Event extensible. Adding a protocol = a new variant + a helper on impl Event + updating protocol_kind().
| Variant | Payload |
|---|---|
Http(HttpData) | HTTP/1.x or HTTP/2 request |
Tcp(TcpData) | Non-HTTP TCP stream (capture, edge-tcp) |
Udp(UdpData) | UDP datagram (UdpData, decodes DNS when recognized) |
TlsHandshake(TlsData) | ClientHello telemetry (SNI/JA3/JA4) |
Syslog(SyslogData) | RFC 5424/3164 message |
Raw(RawData) | Fallback (RawData) |
HttpData, UploadInfo and the smaller enums
HttpData carries method/scheme/host/path/query/status/user_agent/headers/cookies/body/uploads. body is only populated by inline sources and falls back to the capture cap after the pipeline runs. UploadInfo is upload metadata (F10) — content never persists; UploadKind classifies by magic bytes (Text/Image/Archive/Executable/Pdf/Unknown).
Other types in the module: TcpData (flags, reassembled payload, SYN fingerprint, burst syn_count), TcpFlags, TcpStage, TlsData (sni/ja3/ja4/cipher/version/alpn), SyslogData, SourceKind, Transport, HttpMethod, GeoInfo and ReputationInfo. RawEvent is what the Source trait emits, before enrichment; into_event promotes it to Event.
Signals and verdicts — module analysis
Signal and SignalKind
Signal is the unit of detection — the "why" behind a score:
pub struct Signal {
pub kind: SignalKind,
pub weight: u8,
pub detail: Option<String>,
}SignalKind is the closed catalog of detections. Default weights (the [scorer.weights] config overrides per key, except posture_advisory, which is pinned to 0):
| Signal | Default weight | Emitted by |
|---|---|---|
Log4Shell | 80 | heuristic |
Rce (cmd injection) | 70 | heuristic |
SqlInjection | 60 | heuristic |
UploadPolyglot | 60 | UPLOAD_POLYGLOT_WEIGHT |
Lfi | 50 | heuristic |
KnownBadIp | 50 | KNOWN_BAD_IP_WEIGHT |
UploadExecutable | 50 | UPLOAD_EXECUTABLE_WEIGHT |
Xss | 45 | heuristic |
SuspiciousLoginSuccess | 45 | SUSPICIOUS_LOGIN_SUCCESS_WEIGHT |
BadCrawler | 40 | heuristic |
PathTraversal | 40 | heuristic |
CredentialStuffing | 40 | CREDENTIAL_STUFFING_WEIGHT |
AuthBruteForce | 35 | AUTH_BRUTE_FORCE_WEIGHT |
ScanBehavior | 35 | SCAN_BEHAVIOR_WEIGHT |
SpoofedBot | 35 | SPOOFED_BOT_WEIGHT |
SensitivePath | 30 | heuristic |
TcpScanner | 30 | heuristic |
UploadTypeMismatch | 30 | UPLOAD_TYPE_MISMATCH_WEIGHT |
UploadDisallowed | 30 | UPLOAD_DISALLOWED_WEIGHT |
DirectoryBruteForce | 30 | DIRECTORY_BRUTE_FORCE_WEIGHT |
RandomScan | 25 | RANDOM_SCAN_WEIGHT |
UploadFlood | 25 | UPLOAD_FLOOD_WEIGHT |
ProtocolViolation | 25 | PROTOCOL_VIOLATION_WEIGHT (schemas override per policy) |
VpnProxy | 20 | VPN_PROXY_WEIGHT |
TlsSniMismatch | 20 | TLS_SNI_MISMATCH_WEIGHT |
UploadOversize | 20 | UPLOAD_OVERSIZE_WEIGHT |
ScanAttackCorrelation | 20 | SCAN_ATTACK_CORRELATION_WEIGHT |
TorExitNode | 15 | TOR_EXIT_NODE_WEIGHT |
MethodNotAllowed | 10 | RouteValidator |
SuspiciousUA (empty UA) | 10 | heuristic |
PromiscuousScanner | 10 | PROMISCUOUS_SCANNER_WEIGHT |
UnknownRoute | 8 | RouteValidator |
AnomalousPayload | 25 | [ai] signal_weight (default) |
LlmMalicious | dynamic | LLM score × confidence |
ExternalReputation | scaled | provider abuseConfidenceScore |
RuleHit | 100/50/30 | rule short-circuit, per verdict |
PostureAdvisory | 0 (pinned) | web posture, never re-scores the visitor |
Verdict, RiskLevel, AnalysisResult and Decision
Verdict is the recommended action: Allow / RateLimit / Challenge / Block / Quarantine. RiskLevel buckets the 0–100 score into five fixed levels (from_score: Info 0–9, Low 10–29, Medium 30–49, High 50–74, Critical 75+).
AnalysisResult is the detection (score + signals + pre-policy verdict); Decision is the final response after policy, with override_reason and an optional log_level. RuleLogLevel controls console verbosity for short-circuited events (silent|info|warn|error).
Plugin traits
Source
Implemented by every data origin (source.rs):
#[async_trait]
pub trait Source: Send + Sync {
fn name(&self) -> &'static str;
async fn stream(&self) -> Result<mpsc::Receiver<RawEvent>>;
async fn shutdown(&self) -> Result<()> { Ok(()) }
}The origin is opened inside stream, which returns the receiver end of a bounded channel; dropping the sender signals end-of-stream. Module helpers: event_channel (standard sizing) and send_or_log (push with a per-source log throttle when the channel fills).
Action, ActionContext and ActionDispatch
Implemented by every response (action.rs):
#[async_trait]
pub trait Action: Send + Sync {
fn name(&self) -> &'static str;
async fn execute(&self, evt: &Event, decision: &Decision) -> Result<()>;
async fn execute_with_context(&self, evt: &Event, decision: &Decision, ctx: &ActionContext) -> Result<()>;
fn applies_to(&self, decision: &Decision) -> bool;
fn dispatch(&self) -> ActionDispatch;
}Implementations must be idempotent (replay after a restart is a no-op, not an error). ActionContext carries the incident_id for bidirectional alerting (F4.5). ActionDispatch decides where the action runs: Inline (local containment, hot path) or Deferred (default; network I/O goes to post-processing workers).
ChallengeProvider
The provider-agnostic edge abstraction (challenge.rs) — mirrors the LlmProvider pattern:
#[async_trait]
pub trait ChallengeProvider: Send + Sync {
fn name(&self) -> &'static str;
async fn apply(&self, ip: IpAddr, verdict: Verdict, opts: &EdgeOptions) -> Result<()>;
}ChallengeAction is the single Action edge providers need: it filters the Block/Challenge/RateLimit verdicts (skips Allow/Quarantine) and delegates to the provider. EdgeMode is the flavor (block/js_challenge/managed_challenge/rate_limit) and EdgeOptions carries TTL + preferred mode. A new edge provider (AWS WAF, Fastly…) = implement the trait + one arm in daemon::build_challenge_action.
AI providers (crate sentry-ai)
LlmProvider
The LLM classification contract (llm.rs):
#[async_trait]
pub trait LlmProvider: Send + Sync {
fn name(&self) -> &'static str;
fn model_id(&self) -> &str;
async fn classify(&self, req: ClassifyRequest) -> anyhow::Result<ClassifyResponse>;
async fn explain(&self, req: ExplainRequest) -> anyhow::Result<String>;
}Bundled adapters: OpenRouterProvider (default, SENTRY_LLM_KEY), OllamaProvider (local, keyless) and MockLlmProvider. ClassifyResponse carries verdict, score, confidence and token usage.
IpLookupProvider
External per-IP reputation lookup (F7.5, lookup.rs):
#[async_trait]
pub trait IpLookupProvider: Send + Sync {
fn name(&self) -> &'static str;
async fn check(&self, ip: IpAddr) -> Result<IpLookupResult, String>;
}AbuseIpDbLookup implements GET /api/v2/check; the returned score scales the ExternalReputation signal weight. The daemon's async fork only queries the gray band (score above the trigger, verdict ≠ Block), with caching and quota.
ThreatModel
The local classic model sits behind ThreatModel — implemented by OnnxThreatModel when the onnx feature is enabled. Training/export details in AI.
Rules engine — module rules
Rule, RuleSet and SharedRuleSet
pub struct Rule {
pub id: RuleId,
pub priority: i32, // lower = evaluated first
pub enabled: bool,
pub match_: RuleMatch,
pub action: RuleAction,
pub ttl: Option<Duration>,
pub source: RuleSource,
/* tags, log_level, created_at */
}Rule is evaluated on the fast path, before heuristics and AI. RuleSet indexes rules by IP/ASN/country for fast lookup; SharedRuleSet is the Arc<RwLock<>> handle that hot-reload swaps (shared()).
RuleAction and RuleSource
RuleAction: Allow (full bypass) > Block/Challenge/RateLimit (short-circuit) > Log/Tag (annotate and continue). RuleSource records the origin (config/db/cloudflare_sync/feed/auto_learned/default_pack).
RuleMatch
RuleMatch is the declarative condition, composable with All/Any/Not: Ip(cidr), Asn, Country, Path{op, pattern}, Method, Header{name, op}, UserAgent, Query, Body, UploadFilename, Protocol, TlsFingerprint{ja3, ja4}, TlsSni, Ja3In(set), Reputation(tier), BotVerified, Status, Rate{count, per_secs, scope}, Time.
DSL
The match = "..." text in TOML is compiled by dsl::parse (recursive descent, AND/OR/NOT/parentheses) into a RuleMatch. Typos fail at load time, not at runtime. rules_from_config compiles each [[rules.custom]] independently — one invalid expression skips just that rule. ReputationTier (malicious/tor/vpn_proxy/promiscuous/authorized) feeds Reputation(tier).
Policy — module policy
VerdictPolicy
VerdictPolicy is the configurable detection→action mapping. The default preserves the historical mapping (Info/Low→Allow, Medium→RateLimit, High→Challenge, Critical→Block); each level is remappable in TOML and [[policy.override]] forces a verdict via DSL — the first matching override wins and populates Decision.override_reason.
pub fn from_config(cfg: &PolicyConfig) -> Result<Self>;
pub fn decide(&self, level: RiskLevel, evt: &Event) -> (Verdict, Option<String>);Pipeline — module pipeline
Pipeline
Pipeline orchestrates rules → heuristics → routes → scorer → policy → escalation. Built through chained builders:
| Method | Arms |
|---|---|
with_config | shared ruleset + routes + scorer + policy |
configure_uploads | body/upload inspection (F10) |
with_trust | never-ban/blacklist/shadow (F7.2) |
with_rate_limiter | backend for Rate conditions |
with_offender | repeat-offender escalation |
with_scan_tracker / with_behavior_tracker / with_correlation_tracker | behavioral detectors |
with_bot_verifier | rDNS bot verification (F7.10) |
Execution and hot-reload methods: process (synchronous, returns ProcessedEvent), rescore_from (re-integrates AI/LLM/lookup fork signals — can only raise, never lower), observe_response (per-IP response-signal queue, F3.9.1), swap_rules / swap_routes, prune_pending.
RouteValidator and RouteDef
RouteValidator validates known routes: path outside the list → UnknownRoute; unsupported method → MethodNotAllowed. RouteDef accepts templates (/users/{id}, trailing /*); RouteValidator::merge unions config ∪ DB (dedupe by path, config wins).
State trackers
All are per-IP with sliding windows and bounded history; the daemon prunes periodically.
| Tracker | File | Detects |
|---|---|---|
OffenderTracker | offender.rs | Repeat offenders: strikes per window; record_and_escalate only climbs the verdict ladder RateLimit→Challenge→Block |
ScanTracker | scan.rs | 4xx bursts: ≥N distinct paths → RandomScan; ≥N 4xx → ScanBehavior |
BehaviorTracker | behavior.rs | Login brute force, credential stuffing (distinct UAs), wordlist 404s, suspicious success after failures |
CorrelationTracker | correlation.rs | A scan from a different IP on the same /24, /64 or ASN shortly before the attack → ScanAttackCorrelation |
BotVerifier | botverify.rs | rDNS crawler cache (Verified/Spoofed/Unknown); DNS injected via BotDnsResolver, pure flow in verify_with |
BlockTable | blocks.rs | Shared blocked-IP table with optional TTL; the edge fast-path denies before the pipeline (is_blocked, seed, reload) |
TrustSet / SharedTrustSet | trust.rs | [real_ip] trusted proxies, never-ban, blacklist and shadow lists; embedded Cloudflare ranges (cloudflare_ranges) |
Config — module config
SentryConfig is the typed schema of sentry.toml (serde; loading with figment + the SENTRY_ env overlay lives in the CLI). Every section has its own struct (CoreConfig, EdgeConfig, UploadsConfig, PolicyConfig, …), all re-exported at the crate root. The full key/default reference lives in the Configuration section — here is just the contract: the action type is the ActionKind enum, not a string, so typos fail at load.
Heuristics — module heuristics
Heuristic and HeuristicEngine
pub trait Heuristic: Send + Sync {
fn name(&self) -> &'static str;
fn gate_bit(&self) -> Option<u8> { None }
fn analyze(&self, evt: &Event, text: &DecodedHttp<'_>) -> Vec<Signal>;
}Heuristic is a regex detector; HeuristicEngine runs them all (analyze). Performance (F5): an Aho-Corasick automaton of trigger literals (gate) gates the regexes — clean traffic costs one SIMD pass. Bundled families: SqlInjection, Xss, PathTraversal, Lfi, Log4Shell, CmdInjection, SensitivePath, BadCrawler, EmptyUserAgent, TcpScanner + the upload ones (F10, with gate_bit() = None because they operate on body content, not path text).
DecodedHttp
DecodedHttp carries the URL-decoded path and query (%27→', +/%20→space) computed once per event, plus parsed multipart/urlencoded content. When writing a new heuristic, analyze text.path/text.query — never the raw http.path, or encoded payloads bypass you. (The decode happens in the internal http_text helper.)
reload_dataset_lists
pub fn reload_dataset_lists(user_agents: &[String], paths: &[String]);reload_dataset_lists merges literals from DB-backed datasets (user_agent/path) into the prefilter gates via ArcSwap — hot-reload without a restart (F7.7).