UEBA
BASELINE
PEER GROUP
Z-SCORE
DetectionBlue TeamSOCOctober 12, 2026•9 min read

Detecting Per-User and Per-Host Data Volume Anomalies

Fixed thresholds fail in both directions. A per-entity baseline compared with a peer group is how you find the user who suddenly moves ten times their normal volume.

Scroll

A fixed 1 GB egress threshold is noisy for engineers and blind for a finance clerk. User and entity behaviour analytics replaces it with a baseline per entity, a comparison to peers with the same role, and awareness of weekly seasonality. You can build a defensible version in a SIEM without a dedicated UEBA product.

Signals at a Glance

SignalWhereATT&CK
Daily outbound bytes more than 3 standard deviations above the entity’s own baselineProxy, firewallT1030
Volume far above the entity’s peer group medianProxy, identity dataT1030
Download volume from file shares or SaaS spike before resignation or role changeFile audit, SaaS auditT1213
First-time large uploads to a category never used by the entityProxy categoriesT1567
Anomalous series flagged by seasonal decompositionTime-series analyticsT1030

Time-Series Anomalies

Per-entity series with weekly seasonality are straightforward in KQL. The function below decomposes each user’s daily outbound bytes and flags points the model cannot explain:

// Sentinel (KQL) - per-user daily upload anomalies with seasonality
// tune/validate: BytesSent and UserName depend on your proxy schema
let start = ago(45d);
let end = now();
ProxyLogs
| where TimeGenerated between (start .. end)
| make-series bytes = sum(BytesSent) on TimeGenerated from start to end step 1d by UserName
| extend (anomalies, score, baseline) = series_decompose_anomalies(bytes, 2.5, 7, "linefit")
| mv-expand TimeGenerated to typeof(datetime), bytes to typeof(long), anomalies to typeof(int),
            score to typeof(double), baseline to typeof(long)
| where anomalies == 1 and TimeGenerated > ago(2d) and bytes > 500000000
| project TimeGenerated, UserName, bytes, baseline, score
| order by score desc

A peer-group comparison catches the entity whose history is already inflated. Join on role or department and use a median-based score so a few heavy users do not mask each other:

index=proxy earliest=-7d@d
| stats sum(bytes_out) as user_bytes by user, _time span=1d
| lookup hr_roles user OUTPUT department
| eventstats median(user_bytes) as peer_median stdev(user_bytes) as peer_sd by department, _time
| eval peer_z = (user_bytes - peer_median) / max(peer_sd, 1)
| where peer_z > 4 AND user_bytes > 500000000
| sort - peer_z
``` tune/validate: the hr_roles lookup is yours to build from the directory ```

Operating the Detection

Vetted sources

Splunk ESCU analytic stories on data exfiltration and risk-based alerting, Microsoft Sentinel UEBA and KQL time-series functions, Elastic machine-learning job guidance, and MITRE CAR analytics on data volume. Baseline your own environment — model quality depends on clean role data and a history that is representative.

Build Detections That Fire

Mjolnir Security helps blue teams turn threat intelligence into high-fidelity detections across endpoint, network, identity and cloud telemetry.

Detection EngineeringManaged Detection & ResponseThreat HuntingIncident ResponsePurple Teaming
  • Detection Engineering We build and tune detections mapped to ATT&CK from your own telemetry.
  • Threat Hunting as a Service Hypothesis-driven hunts for the behaviours on this page.
  • 24/7 Incident Response Containment when a detection fires. Call +1 833 403 5875.
Written by: Mjolnir Threat Research (MTAC)  |  Published: October 12, 2026  |  Last reviewed: October 12, 2026