BYTES OUT
BASELINE
NEW DESTINATION
EGRESS
DetectionBlue TeamSOCOctober 12, 2026•9 min read

Detecting Large Outbound Transfers: Bytes-Out Baselining

Almost every exfiltration eventually shows up as bytes leaving the network. The hard part is knowing what normal looks like for each host.

Scroll

Whatever tool the adversary uses, the stolen data has to cross your perimeter. Volume alone is a noisy signal, but volume measured against a per-host baseline, weighted by destination rarity and time of day, is one of the most reliable egress detections you can build from telemetry you already collect.

Signals at a Glance

SignalWhereATT&CK
Hourly bytes-out far above the host’s own 14-day baselineFirewall, Zeek conn.logT1030
Large upload to a destination never seen from this hostFirewall, proxyT1041
Server or workstation with an upload/download ratio inverted from its roleFlow dataT1041
Many equal-sized transfers just under a size thresholdFlow data, proxyT1030
Outbound volume outside business hoursFirewall, proxyT1041

Building the Baseline

Baseline each internal source on its own history rather than against a global threshold. A build server and a receptionist’s laptop have nothing in common. Compare the current hour against the host’s trailing percentile and require a rare destination before you alert:

// Sentinel (KQL) - per-host bytes-out vs 14-day baseline
// tune/validate: field names assume CEF firewall logs in CommonSecurityLog
let baseline = CommonSecurityLog
| where TimeGenerated between (ago(14d) .. ago(1d))
| where DeviceAction == "allow" and ipv4_is_private(DestinationIP) == false
| summarize hourly = sum(SentBytes) by SourceIP, bin(TimeGenerated, 1h)
| summarize p95 = percentile(hourly, 95), mean = avg(hourly), sd = stdev(hourly) by SourceIP;
let seen = CommonSecurityLog
| where TimeGenerated between (ago(14d) .. ago(1d))
| summarize by SourceIP, DestinationIP;
CommonSecurityLog
| where TimeGenerated > ago(1h)
| where DeviceAction == "allow" and ipv4_is_private(DestinationIP) == false
| summarize bytesOut = sum(SentBytes) by SourceIP, DestinationIP
| join kind=leftanti seen on SourceIP, DestinationIP
| join kind=inner baseline on SourceIP
| extend z = (bytesOut - mean) / max_of(sd, 1.0)
| where bytesOut > 100000000 and bytesOut > 3 * p95 and z > 4
| order by bytesOut desc

Zeek gives you the same view without a firewall licence. Sum orig_bytes per originator and rank by the number of distinct external responders it fed:

index=zeek sourcetype="bro:conn:json" local_orig=true local_resp=false
| rename "id.orig_h" as src, "id.resp_h" as dest
| stats sum(orig_bytes) as bytes_out dc(dest) as dests values(service) as services by src
| eval mb_out = round(bytes_out / 1048576, 1)
| where mb_out > 500
| sort - mb_out
``` tune/validate: add a lookup of approved backup and sync targets, then exclude them ```

Catching Size-Capped Egress

Operators who know about volume alerts split the data. The result is many transfers of similar size just under a limit, or a steady stream to one destination. Look at counts and size variance as well as totals:

Vetted sources

Start from SigmaHQ and Elastic network rules for anomalous egress, MITRE CAR analytics on data transfer volume, and Zeek conn.log semantics (orig_bytes, local_orig). Baseline your own environment first — backup, sync and telemetry traffic is the dominant false-positive source.

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