Skip to content

Detections & Verdicts

Private beta

Email Security is in private beta. It is not generally available, and access is enabled per organization — if the ext-email-security extension is not in your catalog, this product is not turned on for you yet.

While it is in beta, expect the surface described here to move: commands, fields and event shapes may change between releases, and they may change in ways that are not backwards compatible. Pin a CLI version if you script against it, and re-read this page after upgrading.

Talk to us before relying on it in production.

Every message gets exactly one verdict, and the verdict always carries its reasons. This page explains how the reasons are produced, what the rules can see, and how to tune it.

The verdict

Verdict Meaning
malicious Score at or above the malicious threshold
suspicious Score at or above the suspicious threshold
graymail Bulk or marketing mail: neither an attack nor wanted
benign Below the suspicious threshold, and nothing said "graymail"
unknown No judgement was reached
error Judging failed

The verdict object on a message carries:

Field Meaning
verdict The class above
score 0–100
top_signals Up to five contributing rules, heaviest first, each with rule_id, name and weight. This is the "why this verdict" block
matched_signals Every rule id that matched, including suppressed ones — the hunting surface
tags The deduplicated, sorted tags of the rules that actually contributed
engine_version The rule-pack version that decided it
decided_at When
mode auto (the rule pack), analyst (a human override) or ai
campaign_id The campaign this message was clustered into, if any

A number alone is never the answer

A non-benign verdict always populates top_signals. The console renders it as Why this verdict in the message drawer, and the API returns it on both the index row (the single heaviest signal) and the detail response (the full list). A score with no explanation would not be actionable and is not offered.

Scoring

Each matching rule carries a weight (0–100, how much this evidence is worth) and a confidence (0–100, how often it is right when it fires). The score combines them with diminishing returns rather than a sum:

score = 100 × ( 1 − Π (1 − wᵢ/100 × cᵢ/100) )

Each signal removes a fraction of the remaining headroom. A sum would let five weak signals outscore one strong one and would need clamping at 100, which makes every heavily-signalled message look identical. One rule at weight N and confidence 100 scores exactly N, which is the identity every threshold is reasoned about against.

Threshold Default Where to change it
malicious_min 85 mailsec_policy/thresholds
suspicious_min 45 mailsec_policy/thresholds

malicious_min must stay above suspicious_min. Composed policy that inverts the pair is refused, and an inverted pair reaching the scorer any other way falls back to the defaults rather than making every suspicious message malicious.

The graymail lane

Graymail is a lane, not a score band. Rules classed graymail contribute no score at all; they set the verdict to graymail only when the score did not reach suspicious_min and no detection-class rule fired.

That last clause is the point: a newsletter that also carries a credential-phish link is a phish that happens to look like a newsletter, and filing it under "marketing" is filing it where nobody looks.

Exclusions

Exclusions are applied before scoring, so a suppressed rule contributes nothing to the score and never appears in top_signals. It still appears in matched_signals, so a suppression is auditable rather than invisible.

Exclusions can be scoped by rule id, sender address, sender domain or mailbox, require a written reason, and can carry an expiry — after which they become inert without anyone deleting them. See Policy Reference → Exclusions.

What the rules can read

Rules are standard D&R detect blocks evaluated against the Message Data Model — the parsed message — plus the enrichments the pipeline stamped onto it. Because the enrichments are in the message, a rule reads them as ordinary paths and a re-evaluation later sees exactly what the pipeline saw.

The parsed message

headers (including the raw header list, decomposed sender and recipient addresses, and every domain and IP found), sender (with reply_to_mismatch, free-mail and disposable flags), recipients, subject, body (HTML and plain, extracted display text, thread segmentation into the current reply and previous quoted threads, hidden-text detection), links, attachments, auth (parsed SPF / DKIM / DMARC / ARC results with alignment) and hops (the parsed Received chain).

Enrichments

Path What it carries
enrichments/sender_profile This organization's history with the sender address: first_seen_ts, days_known, msg_count_30d, flagged_count_180d, prevalence (none / new / rare / common)
enrichments/domain_profile The same, keyed on the sender's registrable domain
enrichments/sender_domain The sender domain's registration age, from RDAP with a bounded global cache
enrichments/link_features[] Per link, aligned with links[]: domain, domain_age_days, popularity_bucket (top1k / top100k / top1m / unranked), in_urlhaus, mixed_script (a homograph label mixing writing systems), credentials_in_url (the https://apple.com@evil.example/ trick)
enrichments/lookalike vip_hit (display_name:<name> when the display name matches a VIP whose address does not, email:<addr> when the sender is the VIP), org_domain_distance and brand_domain_distance — edit distances against your own domains and known brands
attachments[].explode Attachment explosion: recursive children with their own names, hashes, magic types and depth; archive (encrypted, file_count, max_depth_hit); vba (auto_exec, suspicious, hex_strings); qr[].url; ocr_excerpt; yara_matches; the scanners that ran

Attachment explosion is bounded — a per-message time budget and size and event caps — and can only ever fail toward "not scanned". explode.scanners names what actually ran for that attachment, so a rule that depends on a particular kind of evidence can tell "the scanner found nothing" from "that scanner did not run".

Absent is not benign

An enrichment that could not be resolved is absent, never a reassuring value. domain_age_days is missing when the registry lookup was unavailable, rate-limited or a cache miss — which is common — and a newly-registered-domain rule must therefore test presence as well as a threshold. Likewise popularity_bucket is empty when the lookup did not run, which is a different fact from unranked, a positive finding that the domain really is not in the list. A missing lookup must never become a suspicion.

The sender-history feedback loop, and why it is closed

Sender profiles are read before the message is counted, so the stamp answers "what did this organization know about this sender before this message arrived". Counting first would make msg_count_30d never zero and first contact indistinguishable from second contact.

More importantly, the profile's flagged_count_180d counter is only incremented for verdicts that flag independently of prevalence signals. Rules whose evidence is the accumulated history carry a prevalence tag, and the counter is computed with those rules removed.

The reason is a loop observed live before the contract existed: the sender-history rule alone can cross the suspicious threshold, and if a flagged verdict fed the counter, one false positive would re-flag that sender forever — each flag re-incrementing the counter that caused it, never decaying, and in enforce mode quarantining a legitimate sender silently. Counting only the independent lane makes a history rule an amplifier of other evidence and never of itself.

The managed rule pack

A packaged, versioned set of rules ships with the product and its version is stamped into every verdict as engine_version. The current pack:

Rule id Class Weight What it says
ms-sender-first-contact signal 30 First message ever from this sender (prevalence: none)
ms-sender-known-bad-history signal 65 This sender has been independently flagged before
ms-sender-domain-newly-registered signal 45 The sender's domain was registered in the last week
ms-auth-dmarc-fail signal 50 DMARC failed
ms-auth-spf-fail-inbound signal 40 SPF failed on inbound mail
ms-impersonation-vip-display-name signal 55 Display name matches a VIP but the address does not
ms-impersonation-org-domain-lookalike signal 70 Sender domain is one or two edits from one of your domains
ms-impersonation-exact-org-domain-external detection 85 Claims one of your domains but arrived from outside
ms-impersonation-reply-to-mismatch signal 35 Reply-To points at a different organization than From
ms-link-display-href-mismatch signal 60 A link's visible text names a different site than its destination
ms-link-credentials-in-url signal 75 A link embeds credentials before the host
ms-link-mixed-script-domain detection 80 A link's domain mixes writing systems within one label
ms-link-unranked-domain signal 30 A link points at a domain absent from the top-1M list
ms-link-known-malicious-url detection 95 A link matches the managed malicious-URL feed
ms-graymail-list-unsubscribe graymail Bulk mail carrying List-Unsubscribe
ms-graymail-precedence-bulk graymail The message declares itself bulk

Rule ids are stable and are never renamed — that is the only reason an exclusion or a per-rule override can be persisted at all.

You can disable a packaged rule or replace its weight for your organization without forking anything, through mailsec_policy/thresholdsrule_overrides.

Judge a message without ingesting it

POST /mailsec/{oid}/analyze (limacharlie mailsec analyze --file suspect.eml) parses a raw message you supply and runs the enrichers and the packaged rules against default policy. Nothing is ingested or stored: no index row is written, no raw copy is kept, and the organization's mail history is unchanged. It is how you test a rule change, or analyze a sample that was never in the tenant. The tenant-specific context it cannot have — your sender history, your VIP list — is named explicitly in the response rather than silently missing.

Two seats for rules

Signal rules run in the collector, before the verdict is emitted. Platform D&R rules run afterwards, on the emitted events, with the whole response arsenal. Same syntax, different seat. See Custom Rules and Events & Automation.