Ettin Sentry 17M: HTTP request classifier for a WAF
Repository id: MeerDevelopment/ettin-sentry-17m
A 17M-parameter encoder, fine-tuned from jhu-clsp/ettin-encoder-17m as a binary classifier, that scores a single HTTP request for how likely it is to be an attack or a probe. It reads four things: the method, the decoded path, the decoded query and the user agent. It returns one probability. Two thresholds turn that probability into a band (risky, dangerous), and a small per-client rule turns a client's recent bands into a verdict.
It ships as one FP32 ONNX file and is meant to run on ONNX Runtime, on the CUDA execution provider when a GPU is present and on the CPU provider otherwise. No PyTorch is needed to run it.
| Model version | ettin17m-fp32-ad059f51 |
| Checkpoint id | 40976031fc76260e |
| Training round | 4 (a window of about 12 days of traffic) |
| Risky threshold | 0.1062 |
| Dangerous threshold | 0.1484 |
| Maximum input | 128 tokens |
| Output | logits[:, 1] is the malicious class |
The checkpoint id is the first 16 hex characters of the sha256 of the PyTorch checkpoint the ONNX file was exported from. That checkpoint is not part of this release.
What it does and does not do
It does:
- score one request at a time, from the request line and user agent only
- flag attack syntax (traversal, LFI, RCE, SQL injection, XSS), probes for sensitive files, webshells, admin panels and appliances, scanner tools and spoofed crawlers
- give scores that are comparable across sites, because it never sees which site was hit
It does not:
- see request headers other than the user agent, cookies, the request body, the Host header, the client IP or the response
- know anything about the site it protects, its routes or its software
- block anything by itself. It is one signal inside a larger system that also applies exact rules and rate limits, and it is not a standalone web application firewall
Input format
The model input is one line of text, built the same way as in training and in the deployed scorer:
{METHOD} {decode_3x(path)}?{decode_3x(query)} [SEP] {user_agent}
- The
?is always present, also when the query is empty. decode_3xruns three passes of a fixed, partial percent-decode. In each pass, in this order:%25to%,%2e/%2Eto.,%2f/%2Fto/,%5c/%5Cto two backslash characters,%27to',%20to a space,%3c/%3Cto<,%3e/%3Eto>, and%00is removed. Nothing else is decoded:+stays+and other escapes stay as they are. Three passes undo double and triple encoding of the listed characters.- An empty or missing user agent becomes an empty string after
[SEP]. [SEP]is written literally; the tokenizer maps it to its separator token.- The tokenizer (the unchanged ModernBERT vocabulary of the base model) truncates at 128 tokens, special tokens included. Truncation cuts from the end, so a very long query can push the user agent, or the end of the query, out of the input.
Synthetic examples:
| method | path | query | user agent | model text |
|---|---|---|---|---|
| GET | /search |
q=%253Cscript%253Ealert(1)%253C%252Fscript%253E&page=2 |
Mozilla/5.0 (X11; Linux x86_64; rv:131.0) Gecko/20100101 Firefox/131.0 |
GET /search?q=<script>alert(1)</script>&page=2 [SEP] Mozilla/5.0 (X11; Linux x86_64; rv:131.0) Gecko/20100101 Firefox/131.0 |
| POST | /login |
(none) | curl/8.5.0 |
POST /login? [SEP] curl/8.5.0 |
| GET | /files/..%5C..%5Cwindows%5Cwin.ini |
(none) | (none) | GET /files/..\\..\\windows\\win.ini? [SEP] |
build_request_text in example.py is the reference implementation.
In training, query values that looked like secrets (tokens, keys, JWT-shaped values, matched on the value's shape together with the parameter name) were replaced by <REDACTED> before the text was built. Attack payloads were never redacted. You can apply the same kind of redaction before scoring. The effect of redaction on this model's recall was not measured.
Output, thresholds and the client rule
The ONNX graph takes input_ids and attention_mask (int64, shape [batch, seq]) and returns logits (shape [batch, 2]). Index 1 is the malicious class. The score is softmax(logits)[1], a number between 0 and 1. config.json has no label names, so tools that read it show the classes as LABEL_0 (benign) and LABEL_1 (malicious).
thresholds.json holds two cut points, both set on clean benign traffic ("golden" rows, see below) that was kept out of training:
| band | condition | how it was set |
|---|---|---|
| risky | score >= 0.1062 | the score that about 0.1% of golden benign requests reach (536 of 531,746) |
| dangerous | score >= 0.1484 | target fraction 10/47,708 (about 0.021%) of golden benign requests (120 of 531,746) |
The calibration rows are golden rows from the training and validation days (everything before the last two days of the window), scored with the PyTorch checkpoint, minus the golden rows of the 5,307 clients that belong to the clean evaluation population. A cutoff is the score at descending rank max(1, round(target_fraction * row_count)). Ties count as hits, so the achieved counts above are a little higher than the targets. Runtime uses the full-precision values in thresholds.json, not the rounded values shown here.
The two cutoffs are close together (dangerous is only about 0.04 above risky). See the limitations section for what that means in practice.
A single request is not a verdict. The system that uses the model keeps each client's scored requests for a sliding 10-minute window and decides per client:
- dangerous: at least 2 requests in the window score at or above the dangerous threshold
- risky: not dangerous, and at least 1 request in the window scores at or above the risky threshold
- none: otherwise
Requiring two dangerous requests is deliberate. One odd request from a real visitor should not get them blocked, while scanners almost always send many probes in a short time. What happens on a verdict (log, challenge, block) is a policy decision outside the model.
How to run
Download the folder (huggingface-cli download MeerDevelopment/ettin-sentry-17m --local-dir model), install onnxruntime, tokenizers and numpy, then from inside that folder:
import json
import numpy as np
import onnxruntime as ort
from tokenizers import Tokenizer
from example import build_request_text
tok = Tokenizer.from_file("tokenizer.json")
tok.enable_truncation(max_length=128)
tok.enable_padding(pad_id=tok.token_to_id("[PAD]"), pad_token="[PAD]")
session = ort.InferenceSession("model.onnx", providers=["CPUExecutionProvider"])
thresholds = json.load(open("thresholds.json"))
texts = [build_request_text("GET", "/download", "file=..%252f..%252fetc%252fpasswd", "curl/8.5.0")]
enc = tok.encode_batch(texts)
logits = session.run(None, {
"input_ids": np.array([e.ids for e in enc], dtype=np.int64),
"attention_mask": np.array([e.attention_mask for e in enc], dtype=np.int64),
})[0]
p = np.exp(logits - logits.max(axis=1, keepdims=True))
score = p[:, 1] / p.sum(axis=1)
print(score, score >= thresholds["risky_threshold"], score >= thresholds["dangerous_threshold"])
Use ["CUDAExecutionProvider", "CPUExecutionProvider"] with onnxruntime-gpu for GPU scoring. python example.py scores three synthetic requests (a plain home page visit, a double-encoded traversal and a webshell probe). Its output on CPU with this release:
risky >= 0.1062, dangerous >= 0.1484
0.0000 none GET /? [SEP] Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gec
1.0000 dangerous GET /download?file=../../../../etc/passwd [SEP] curl/8.5.0
0.0204 none GET /wp-content/plugins/hello/wso.php? [SEP] Mozilla/5.0
The webshell probe stays below risky in this example. Webshell probes are the family held out of training; see the evaluation for how the model does on that family as a whole.
manifest.json lists the sha256 of each release file except README.md and manifest.json itself. The reference scorer checks every file the manifest lists and refuses to load the model if one is missing or differs, so keep the folder complete.
| file | size | sha256 |
|---|---|---|
.gitattributes |
93 B | a8efccaf9e23c7d9aa3d1195c24949fe055bc424f02b2d75164f391baaf18abd |
LICENSE-base |
1.5 KB | f6a9db57b02f2f3381064bf7558f3b643c6baa4279cdb3e7ad9f1d6a411310e7 |
config.json |
1.6 KB | 40f57b2c1ff8903514fe92d8ba979b1bbbcb68a190d4b4ab28f01a88d9b8c767 |
example.py |
2.7 KB | 409ac3da19b3657ed592749929ef3b4d341431a6aa8b60969adbf121191bea0c |
model.onnx |
64.5 MB | ad059f51b033b19f0845e9fda427fd0acb08c6b9d1f14cebb92f6d16fc9486c6 |
thresholds.json |
89 B | 228aa69f06f52ca06923ef7e4bd3f9c4058a8e6a44bdca3df2654149987374b8 |
tokenizer.json |
3.4 MB | 8eb3950859984c5e7062c5e94d8d8026df86a527a5b6015f1d06d1a89cebd5f4 |
tokenizer_config.json |
378 B | 68acf857b869285d8c3ac2757ddd26e6f8030fb275d6800131521eb736084608 |
Training data
Described in aggregate only. No rows, site identifiers, hostnames, addresses or real request texts are part of this release, and the dataset is not released.
Sources
- Production HTTP access logs from real websites, all from one source of traffic, over a window of about 12 days spread across 13 calendar days, the first and last of them partial (29,041,448 requests before sampling). The sites are mostly PHP applications (Laravel, WordPress, Statamic and similar) and Node.js applications.
- A decoy web server we operate, with trap paths that no legitimate visitor has a reason to request: 70,852 requests collected over the same period, our own address excluded.
Labels. Binary target: malicious (recon_probe, exploit_attempt, spoofed_crawler, credential_abuse) against benign (benign, benign_bot). Requests labelled unknown are not trained on and are left out of the labelled metrics. Every row's label comes from the first of these that applies:
- Honeypot ground truth. A hit on an attack trap of the decoy server is malicious. Hits on neutral traps (home page, robots.txt, sitemap, a stylesheet, the login form) are not training attacks.
- Adjudicated override. Clusters of near-identical requests that the rules could not settle, or that caused known false alarms, were reviewed by LLM reviewers following a written guide. An override was accepted only when two reviewers agreed or a reconciling reviewer decided, and then applies to every row of the cluster. The snapshot holds 1,266 reviewer verdicts.
- Weak supervision ladder. A fixed precedence of labelling functions, most specific first: malformed methods; attack-syntax regex families; sensitive file, webshell, debug and CMS login paths (with a context check so a site's own admin traffic is not called a probe); known scanner tools; crawler claims checked against published IP ranges where they exist; known monitoring and automation agents; browser-style fetch metadata. A request only gets an attack label when its own text shows it. Client-level evidence can make a request
unknown, never malicious, because the model never sees client context.
Splits and holdouts.
- Time split: the first 10 days for training, the 11th day for validation, the last two days for testing.
- Held-out sites: a set of 13 held-out sites, chosen in an earlier round stratified by traffic volume, never in training, validation or test.
- Held-out attack family: webshell probes (a fixed rule over shell file names and their variants) are kept out of training, validation and checkpoint selection entirely. SSRF and VPN appliance probes, held out in earlier experiments, are training families in this release.
- Golden benign rows (strictly clean traffic per site and day, from clients that never sent anything suspicious) are excluded from training by row and used to set the thresholds.
- Clean evaluation clients (real browser sessions, a fixture group, and part of the clients behind known false alarms) are excluded from training by client.
Size. 2,503,015 training rows (649,383 malicious), 2,718,120 validation rows and 3,777,451 test rows (validation and test include unknown rows). Repeats of the same request signature are capped per day so one campaign cannot dominate.
Training. Full fine-tune of jhu-clsp/ettin-encoder-17m for binary sequence classification (mean pooling), maximum length 128, 1 epoch (19,555 steps) at batch 128, bf16 on one GPU. Checkpoints every 2,000 steps were compared on recall at 0.1% false positive rate on a fixed selection set drawn from the validation day (1,303 malicious and 50,000 benign rows); step 10,000 was kept. Seeds 7 and 42 were trained; this seed (42) was picked by the acceptance rules below. Exported to ONNX (opset 17, FP32, dynamic batch and sequence length).
Evaluation
Everything below comes from one full evaluation run that scored this model and the previous release (round 3) on the same, fixed evaluation files. Both models were scored through the deployed path: the FP32 ONNX file on ONNX Runtime with the CUDA execution provider at default settings (TF32 matmuls on), batches of 256. Round 3 used its own shipped files and thresholds. Rates are fractions of positive-labelled rows (recall) or of golden benign rows (false positive rate). Recall inherits the labelling noise of the weak labels.
Recall at matched false positive rate. Each model gets one threshold per nominal target (0.1% and 0.01%), derived from all 737,611 golden benign rows across all days and shared across evaluation sets (for this model 0.1024 and 0.1733). These are separate from the shipped risky and dangerous cutoffs.
| set | what it contains | positives | round 3 at 0.1% | this model at 0.1% | round 3 at 0.01% | this model at 0.01% |
|---|---|---|---|---|---|---|
| newest traffic | labelled test-day rows whose request signature first occurs in the whole data window, webshell holdout rows removed | 14,269 | 72.92% | 98.75% | 66.64% | 98.72% |
| held-out sites | labelled rows across all days from the 13 held-out sites never seen in training | 634,408 | 73.75% | 99.45% | 65.79% | 99.43% |
| held-out sites, text unseen | the rows above whose exact model text is not in round-4 training | 88,741 | 76.38% | 96.06% | 73.73% | 95.93% |
| webshell family, gated subset | held-out webshell probes whose path and query never occur in round-3 training | 56,898 | 68.43% | 91.09% | 55.64% | 90.64% |
| webshell family, whole | every trainable-source webshell probe, including texts round 3 trained on as benign | 78,647 | 54.79% | 83.94% | ||
| round-3 unseen-family file | the round-3 regression file (mostly webshell and LFI), rows not in round-4 training | 7,650 | 56.65% | 90.13% | ||
| round-3 unseen-family file, text unseen | rows whose model text is not in round-4 training | 9,863 | 63.92% | 87.43% |
Newest-traffic PR AUC: 0.9986 (round 3: 0.8726).
The gated webshell subset holds 56,898 rows but only 744 distinct path and query texts, and 5 texts make up half of the rows. A paired bootstrap over distinct texts (2,000 samples) puts this model's gain over round 3 at 0.1% FPR between 12.3 and 34.4 percentage points (95% interval). Averaged per text instead of per row, recall is 71.3% against 20.8% for round 3.
Trained families, not zero-shot. SSRF and VPN appliance probes were held out in earlier experiments and are trained on in this release. The cohort below is restricted to the validation and test days, but most of its texts also occur in the training window, so it measures a trained family on newer traffic. Round 3 did not train SSRF and did train VPN appliance probes.
| set | positives | round 3 at 0.1% | this model at 0.1% |
|---|---|---|---|
| former family holdout, from the validation day onward | 2,227 | 68.52% | 99.91% |
| of which SSRF | 1,089 | 38.57% | 99.82% |
| of which VPN appliance | 1,008 | 97.02% | 100.00% |
Recall by family at each model's own risky threshold, newest traffic only. Groups use the attack subclass, then the probe subclass, and are shown only with at least 20 positive rows. Small groups move a lot with a few rows.
| family or user-agent group | positives | round 3 | this model |
|---|---|---|---|
| filename probe | 8,897 | 68.18% | 99.83% |
| other | 3,702 | 100.00% | 100.00% |
| sqli | 1,106 | 15.01% | 87.16% |
| rce | 312 | 100.00% | 99.36% |
| traversal | 112 | 100.00% | 100.00% |
| vpn appliance probe | 43 | 100.00% | 100.00% |
| lfi | 38 | 65.79% | 100.00% |
| scanner tool user agent | 32 | 3.12% | 84.38% |
| attacks sent with a crawler user agent | 108 | 60.19% | 100.00% |
| attacks sent with any other user agent | 14,161 | 73.24% | 98.74% |
False positives on golden benign traffic. Check days are the last two days of the window (115,042 golden rows), which played no part in calibration.
| check | round 3 | this model |
|---|---|---|
| check-day golden requests at or above risky | 0.144% | 0.110% |
| check-day golden requests at or above dangerous | 0.0026% (3 rows) | 0.0165% (19 rows) |
| golden home page requests (root path, empty query, all days) at or above risky / dangerous | 0 / 0 of 15,106 | 0 / 0 of 15,106 |
ten fixed synthetic GET / requests with desktop and mobile browser user agents, at or above risky |
0 of 10 | 0 of 10 |
Clean clients. Client counts are mutually exclusive final verdicts, shown as risky / dangerous. Each client is judged over all of its rows in the evaluation window with the client rule above. The gated population is "decision B": clients with any attack-labelled row are removed, only benign-labelled rows decide, and unknown rows never count. The risky rate counts risky-only clients.
| population | clients | round 3 risky / dangerous | round 3 risky rate | this model risky / dangerous | this model risky rate |
|---|---|---|---|---|---|
| all clean clients, decision B (gated) | 7,398 | 94 / 12 | 1.27% | 39 / 6 | 0.53% |
| of which: held-back clients of known false-alarm routes | 5,378 | 74 / 12 | 1.38% | 11 / 4 | 0.20% |
| of which: held-out browser session clients | 1,773 | 18 / 0 | 1.02% | 23 / 2 | 1.30% |
| of which: fixture clients | 247 | 2 / 0 | 0.81% | 5 / 0 | 2.02% |
| decision B, rows whose text is not in round-4 training | 7,003 | 75 / 11 | 39 / 6 | ||
| earlier definition: every row of every clean client, whatever its label (not gated) | 7,402 | 110 / 53 | 1.49% | 49 / 21 | 0.66% |
| held-aside subset (not gated, see below) | 6,956 | 0 / 0 | 0.00% | 1 / 0 | 0.014% |
The improvement is not uniform. It comes from the false-alarm group; on the browser session and fixture groups this model has more risky clients than round 3, and 2 dangerous browser session clients where round 3 has none.
The held-aside subset is the decision B population minus every client that was risky or dangerous for any model in earlier experiments, and minus benign rows on the route shapes that guided training-data changes (Statamic Glide /img/, /_nuxt/, /!/, /dashboard with a query, common CMS paths and a podcast image proxy). It was fixed before this run. It says nothing about those route shapes, Glide included.
Fresh traffic after the data window. A cohort of about 25 hours of traffic right after the training and evaluation window, frozen before any model scored it. Same decision B rules, each model's own thresholds, CUDA scoring. Not gated.
| population | clients | round 3 risky / dangerous | round 3 risky rate | this model risky / dangerous | this model risky rate |
|---|---|---|---|---|---|
| (a) returning clean evaluation clients | 947 | 14 / 0 | 1.48% | 1 / 2 | 0.11% |
| (b) clients never seen in any round-4 file | 55,190 | 88 / 46 | 0.159% | 23 / 3 | 0.042% |
On (a) this model has 2 dangerous clients where round 3 has none, so it is not at least as good as round 3 on both fresh populations. (a) is small and the difference rests on two clients. Some benign-labelled drivers in both populations look like real probes (debug panel paths, backup file names), so part of these false alarms may be label noise.
Honeypot. Attack-trap rows at the risky threshold, all days (30,427 rows): 95.50% against 96.68% for round 3. Test-day attack-trap clients that reach a risky or dangerous verdict: 82 of 84 (97.6%) against 80 of 84 (95.2%). Honeypot rows from before the test days overlap training by design.
ONNX parity. Checked on 2,700 rows (2,000 labelled validation rows, 500 extra malicious validation rows, and the 200 longest distinct texts from the validation and benign evaluation sets, all of them at the full 128 tokens), padded batches of 64. No NaN scores, and no request changed band between ONNX (CPU or CUDA) and the PyTorch checkpoint.
| comparison | maximum absolute probability difference | limit |
|---|---|---|
| ONNX CPU vs PyTorch checkpoint | 6.8e-07 | 0.001 |
| ONNX CUDA (TF32 on) vs PyTorch checkpoint | 1.9e-04 | none, diagnostic |
| ONNX CUDA (TF32 on) vs ONNX CPU | 1.9e-04 | 0.01 |
| ONNX CUDA with TF32 off vs ONNX CPU | 2.6e-07 | 0.001 |
A separate CPU rescoring of the evaluation rows near either cutoff found differences up to 7.7e-4 between CPU and the CUDA acceptance scores. That matters here because of the close cutoffs; see the limitations.
CPU throughput. Not measured. It is pending an isolated single-core ONNX CPU measurement on the reference server. No number is given, and no GPU timing stands in for it.
Acceptance gates and amendments
This seed was selected because it passed every gate below on the run above. Gates and their values were fixed before that run started.
| gate | limit | round 3 | this model | result |
|---|---|---|---|---|
| newest traffic recall at 0.1% FPR | at most 1 point below round 3 | 72.92% | 98.75% | pass |
| held-out site recall at 0.1% FPR | at most 1 point below round 3 | 73.75% | 99.45% | pass |
| gated webshell family recall at 0.1% FPR | at most 1 point below round 3 | 68.43% | 91.09% | pass |
| check-day golden FPR at risky | at most 0.15% | 0.144% | 0.110% | pass |
| clean risky-only client rate, decision B | not above round 3 | 1.27% | 0.53% | pass |
| dangerous clean clients, decision B | not above round 3 (amended, see below) | 12 | 6 | pass |
| ONNX parity (CPU, default CUDA, TF32-off CUDA) | as in the parity table | pass | ||
| predeclared improvement: newest recall at 0.01% FPR | above round 3 | 66.64% | 98.72% | pass |
Among seeds that pass every gate, the one with the highest newest recall at 0.01% FPR is picked. Both seeds passed; seed 42 was picked.
Three changes were made to the acceptance rules before this run. They are listed because they make the gates easier to pass than the rules first written down.
- The gated family changed from SSRF and VPN appliance probes to webshell probes. In the first full round-4 run both seeds scored far below round 3 on the SSRF and VPN appliance holdout (11.04% and 17.74% against 80.66% at 0.1% FPR). That comparison was also lopsided, because round 3 had trained on VPN appliance probes. SSRF and VPN appliance moved into training, and the gate moved to webshell probes, a family round 3 never trained as attacks. The gate uses only webshell texts that never occur in round-3 training, because round 3 trained some webshell texts as benign, which would have favoured this model.
- The clean population became decision B. Before, every row of every clean client counted, whatever its label. Now clients with any attack-labelled row are removed and only benign-labelled rows decide. Numbers under the earlier definition are in the clean-client table.
- "Zero dangerous clean clients" became "no more dangerous clean clients than round 3". Clients of one route (
GET /dashboard/console-log?_rsc=<token>, a Next.js server component fetch) are dangerous for every model, round 3 included, and there is no admissible benign training data for that route: every such request sits in the golden or clean evaluation sets. Round 3 has 12 dangerous clean clients, this model 6. Under the original rule this model would fail.
Limitations and known weak spots
- Close cutoffs on a Statamic Glide cluster. The dangerous cutoff (0.1484) sits inside a dense cluster of scores of Statamic Glide image URLs (
/img/<asset>thumbnails). 18 golden rows lie within 2e-4 of it, and one clean client clears it by 1e-4. A page that loads several thumbnails can produce two near-cutoff hits in one view, which is how 3 of the 6 dangerous clean clients arise. The per-request golden FPR at dangerous on check days is about 6.3 times round 3's (0.0165% against 0.0026%; 17 of the 19 rows are Glide, 2 areHEAD /_next/data/...requests). Only 2 of 8,535 check-day golden clients reach a dangerous verdict (round 3: 1). Run this model in shadow (log-only) mode first on sites that use Statamic Glide. /dashboard/console-logwith a query (Next.js) is flagged dangerous by every model tested, round 3 included. It accounts for the other 3 dangerous clean clients.- Evaluation-guided training data. Several training-data changes were chosen after reading clean-client failures in earlier runs, so the clean-client numbers are partly in-sample: a draw exemption for benign Glide image rows; a cap on malicious rows per client, route and day aimed at a scanner campaign on Glide URLs; benign coverage for
/!/,/_nuxt/and/dashboardroutes with a query; a relabel of unknown CMS-route rows to benign (later narrowed); removal of training rows whose text or path and query also appears in the golden or clean sets; a rule that an XSS regex hit needs script syntax (C1, 22 training rows removed); and a rule that Next.js route segments such as[...slug]are not traversal evidence (C2, 48 training rows removed). SSRF and VPN admission and the webshell rules came from other gates. The held-aside subset and the fresh post-window cohort are the less biased checks, though the held-aside subset is not fully untouched: its v34 results were seen before the C2 data change was chosen, so the fresh post-window cohort is the cleanest evidence. - Shared text with training. About 24% of the rows that decide the clean gate (180,291 of 741,827 rows) have a model text that also occurs in training. Restricted to text-unseen rows, the result is similar (6 dangerous clients against 11 for round 3).
- Sensitive to small data changes. Removing the 48 C2 rows moved the clean risky-client rate from 30.89% and 14.15% (the two seeds of the previous run) to 0.53%. Seed and data variance were large in earlier runs; both seeds of this run agree.
- Other shell families are trainable. Only the gated webshell rule is held out. Other shell kits and file managers outside that rule stay in training, so recall on them is not zero-shot.
- Honeypot recall. Attack-trap row recall at risky is 1.2 points below round 3 (95.50% against 96.68%). This is not gated.
- Known mislabelled test rows. Evaluation labels were not edited after the C1 and C2 rules. The test files keep 14 rows (C1, also in newest traffic) and 16 rows (C2) that those rules would no longer call attacks; at most about 0.1 point of newest recall.
- CUDA and CPU differ near the cutoffs. ONNX Runtime CUDA uses TF32 matmuls by default. Acceptance used that path. On CPU the Glide rows score slightly lower: 5 dangerous clean clients instead of 6 (risky / dangerous 40 / 5 on CPU), and a check-day golden dangerous FPR of 0.0113% instead of 0.0165%. That comes from a partial CPU rescoring (rows near the cutoffs plus 3,000 random rows per file), under which the clean and check-day golden gates still hold; the recall gates were not recomputed on CPU. Expect verdicts on near-cutoff routes to depend on the backend.
- Tested scorer path. All numbers come from the FP32 ONNX file on ONNX Runtime. A PyTorch FP16 path was not tested with this model.
- Not measured. CPU throughput and the recall effect of redacting secret-shaped query values.
- Per-request view. Only method, path, query and user agent. Attacks in POST bodies, headers or cookies are invisible, and so is the Host header.
- No site context. The same path gets the same score on every site, so a site whose legitimate routes look like probes (custom admin paths, unusual API endpoints, hashed static assets) can see false risky scores.
- Shared clients. Many users behind one address (offices, mobile carriers, proxies) merge into one client, so one probing user can put a whole shared address in a band.
- Weak labels. Labels come from rules, honeypot traps and LLM review, not from a fully human-labelled set. Recall is partly measured against the same rules that made the training labels, which flatters it. Honeypot results are the most independent signal.
- Short window, one source. About 12 days of traffic from one source of traffic. Attack campaigns change quickly; expect drift.
- Truncation. Inputs are cut at 128 tokens. A long, padded query can hide the end of a payload or the user agent.
- Public weights. Anyone can probe this model offline and tune payloads until they score low. Do not let it be the only line of defence.
Intended use
- One scoring signal in a web application firewall or log monitoring pipeline, combined with exact rules, rate limits and a per-client rule like the one above.
- Triage of access logs: ranking clients and requests for human review.
- Research on learned request classification, as a small baseline that runs on CPU.
Out-of-scope use
- Blocking traffic on single-request scores without a per-client rule and human-reviewed policies.
- A replacement for patching, authentication or a rule-based WAF.
- Judging people: the score says something about a request's text, not about who sent it.
- Classifying anything other than HTTP requests in the input format above.
Privacy
The model was trained on production access logs. Before training, secret-shaped query values were redacted and client addresses were replaced by salted hashes, so the dataset holds no raw IP addresses. The training data, evaluation sets, scores and individual requests are not released. This repository contains only the ONNX model, its tokenizer and configuration, the thresholds, a manifest, a synthetic example and licence files. A pre-publication check scans every file, including printable strings in the ONNX file, for dataset fields, site identifiers, non-documentation IPv4 addresses, email addresses and unrecognised hostnames, and checks that the tokenizer vocabulary and merges equal the base model's. It is not proof against memorisation.
Licence
This model is released under the Creative Commons Attribution 4.0 International licence (CC BY 4.0). If you use or redistribute it, credit it like this:
Ettin Sentry 17M by MeerDevelopment, https://huggingface.co/MeerDevelopment/ettin-sentry-17m, licensed under CC BY 4.0.
The base model, jhu-clsp/ettin-encoder-17m, is released under the MIT licence. Its copyright notice and licence text are in LICENSE-base and apply to the base weights this model was derived from.
Citation
The base model:
@misc{weller2025seqvsseqopen,
title={Seq vs Seq: An Open Suite of Paired Encoders and Decoders},
author={Orion Weller and Kathryn Ricci and Marc Marone and Antoine Chaffin and Dawn Lawrie and Benjamin Van Durme},
year={2025},
eprint={2507.11412},
archivePrefix={arXiv},
primaryClass={cs.CL},
url={https://arxiv.org/abs/2507.11412},
}
- Downloads last month
- 10
Model tree for MeerDevelopment/ettin-sentry-17m
Base model
jhu-clsp/ettin-encoder-17m