Local ONNX Model
Classic ThreatModel (features → sklearn → ONNX) via ort, with hot-swap retraining
Local ONNX Model
Sentry's local model is a classic-ML classifier (logistic regression) that
runs as an async fork beside the hot path. Inference is local —
no network, deterministic, typically sub-ms — via ort (ONNX Runtime),
behind the optional onnx Cargo feature (disabled by default).
The ThreatModel trait
/// A local, deterministic, no-network threat model.
#[async_trait]
pub trait ThreatModel: Send + Sync {
fn name(&self) -> &'static str;
fn version(&self) -> &str;
async fn analyze(&self, evt: &Event) -> anyhow::Result<Vec<Signal>>;
}Defined in
sentry-ai/src/threat.rs
— the pipeline depends on a Box<dyn ThreatModel> without pulling ort
into every build. The ONNX implementation is
OnnxThreatModel
(onnx feature), loaded by the daemon in
build_ai_fork
when [ai] enabled = true. Without the feature, the stage stays disabled
with a warning — the rest of Sentry keeps working.
load
validates the model's input width against FEATURE_NAMES: a model trained
against a different feature version fails at startup with retraining
instructions — it never runs out of sync. Anomaly probability above
threshold emits an AnomalousPayload signal weighted signal_weight.
Features — the single source of truth
sentry-ai/src/features.rs
is the single source of truth for the model input: the same extract
function runs at inference time (daemon fork) and at training export time
(sentry model export writes a CSV whose header is FEATURE_NAMES). The
training script validates the CSV header and fails loudly if the two sides
drift — training and inference can never diverge.
There are 25 features, all normalized to 0.0–1.0 (fixed-length vector;
non-HTTP events extract an all-zero vector). Summary by group:
| Group | Features |
|---|---|
| Path structure | path_len, query_len, path_depth, path_digit_ratio, path_special_ratio, path_entropy, path_encoded_count, path_dot_dot, path_null_byte |
| Attack tokens (over URL-decoded, lowercased text) | sqli_token_score, xss_token_score, traversal_token_score, jndi_token, cmd_token_score |
| File extension | has_file_ext, ext_is_script, ext_is_sensitive |
| HTTP method | method_is_get, method_is_post |
| User-Agent | ua_len, ua_is_empty, ua_bot_token |
| Response status | status_4xx, status_5xx |
| Query string | query_param_count |
Retraining loop
Feature extraction happens exclusively in Rust — the Python script only consumes exported vectors. The full cycle:
- Export —
sentry model export(handler, flags) writes a CSV (FEATURE_NAMES,label) with features extracted by Rust over recent events from Postgres:--hours N— look-back window (default 720 = 30 days);--out <file>— destination (defaultdataset.csv);--synthetic— synthetic seed dataset without Postgres (--rows, default 4000, half benign);--confirmed— labels from confirmed incidents (incident-linked event = 1, everything else = 0). Without the flag, positives are events with verdictblock/challenge/rate_limit.
- Train —
tools/train_model.py(tools/train_model.py):StandardScaler + LogisticRegression(sklearn,class_weight="balanced") evaluated on a holdout split, exported to ONNX withzipmap: false(probabilities as a plain float tensor). - Replace — swap the file pointed to by
model_path(the committed seed ismodels/anomaly_v1.onnx). - Hot swap —
sentry model reload(handler) sendsNOTIFY sentry_model_changedthrough Postgres; the daemon reloads the file from disk and swaps the model hot (listener), with the live model inArc<RwLock<Arc<dyn ThreatModel>>>. A failed load keeps the previous model. - Status —
sentry model status(handler) shows enabled, path, mode/trigger/threshold, whether the file loads (with--features onnx) and the active LLM provider.
sentry model export --synthetic
python tools/train_model.py --csv dataset.csv --out models/anomaly_v1.onnx
sentry model reload # requires Postgres: the daemon listens on NOTIFY sentry_model_changed
sentry model statusThe build needs the feature: cargo build --release --features sentry-cli/onnx.
[ai] configuration
[ai]
enabled = false
model_path = "models/anomaly_v1.onnx"
threshold = 0.7 # anomaly probability that emits the signal
signal_weight = 25 # AnomalousPayload weight (override: [scorer.weights])
mode = "fork" # fork | inline | shadow
trigger = "above_score" # above_score | always | quarantine_only
min_score = 20 # hot-path score gate (above_score)
concurrency = 4 # max parallel inferences (fork mode)
cache_ttl_secs = 300 # result cache keyed by payload hashKeys and defaults in AiConfig.
What mode/trigger/cache do at runtime is described in
AI Layer.