Logwatcher's Zenit #03: Histogram, the Weight Measurement of Logs
Bin it. Chart it. Peek at the peaks.
Bin it. Chart it. Peek at the peaks.
At the summit of signal and noise lies the Logwatcher’s Zenit — a quiet place for analysts who squint at timestamps and whisper to correlation engines. Bring your coffee, leave your assumptions at home, and fire up your favourite log parser.
Introduction
Histograms — the underrated champions of shape and structure — can tip the scales of any investigation. While your data might weigh in at petabytes, it’s these humble bins that are David when you’re wrestling logs the size of Goliath.
In this post, we explore how histograms in KQL (and beyond) help analysts reveal periodicity, detect anomalies, and catch that beaconing malware that always checks in at exactly 10-minute intervals like it’s punching a cursed time clock or just being an insanely slow drum machine someone forgot to sync, or turn off.
What Is a Histogram?
A histogram isn’t just a bar chart — it’s a story. A tale of counts over time. A saga of spikes, and silence. When you bin data over time intervals, you begin to see the rhythm of your logs: the heartbeat of systems, the subtle pulse of attackers, the glaring gaps when something suddenly… stops.
Think of it this way: if raw logs are atoms, then a histogram is the periodic table.
Let’s start by looking at some neat examples in Sentinel. Most of them will work perfectly fine in Microsoft Defender XDR as well, unless I make a note of it. We don’t want you experience angry error messages. I’ll go through the syntax you need and will follow up with a couple of use cases.
The Basic Spell: summarize count() by bin(Timestamp, Timespan)
It’s now time to link up with your inner Histogram Magician. Well, it’s not really magic because magic doesn’t exist. But your colleagues might think it is, so why not get that magic wand after all?
— “It’s dangerous to go alone. Take this. 🪄”
With great power comes great responsibility, so wave and whisk it wisely and make sure to not knock things over with it. From a magician to another; use your wrist rather than your arm.
To get the DeviceNetworkEvents
from Microsoft Defender XDR, you need to connect the Microsoft Defender XDR data source in Sentinel. And I assume you know how to do it. Let me know in the comment section. I could turn the whole data connector thingy into a blog post if requested.
Another thing to note when mixing Defender XDR and Sentinel data is that Defender XDR calls an endpointDeviceName
while Sentinel calls them Computer
.
Now we’re ready.
1
2
3
4
DeviceLogonEvents
| where Timestamp > ago(1d)
| summarize count() by bin(Timestamp, 10m)
| render timechart
This shows how many logon events occurred every 10 minutes. Great for spotting brute force attempts, scripted logons, or just… weirdly regular behaviour that smells like a bot, walks like a bot, and logs on like a bot.
Use Case 1: Spotting Beaconing Behaviour
Attackers love regularity. You know who else does? Histograms.
1
2
3
4
5
DeviceNetworkEvents
| where Timestamp > ago(1d)
| where RemoteUrl has "example.com"
| summarize count() by bin(Timestamp, 5m)
| render timechart
If that line is suspiciously flat across the timeline, congrats: you’ve found a digital metronome. Beaconing C2? Scheduled task gone rogue? Only one way to find out. But first — admire the histogram. They can be such beautiful visuals.
Pro Tip: When in doubt, look for periodicity. If your logs resemble a Japanese train schedule, someone’s probably up to no good.
Use Case 2: Idle Systems, Active Minds
In 1967, The Tremeloes sang “Silence is golden”. But sometimes, silence is actually the anomaly. Silence could be the one thing we didn’t know we were looking for — but with a histogram it became perfectly clear.
Here’s a Sentinel only KQL query using the Heartbeat table and TimeGenerated instead of Defender XDR’s Timestamp.
1
2
3
4
Heartbeat
| where TimeGenerated > ago(3d)
| summarize count() by bin(TimeGenerated, 1h)
| render timechart
Got a host that usually phones home every 5 minutes but suddenly drops off the radar for a few hours? Might be a network glitch. Might be a sleep schedule. Might be compromise o’clock.
Zen Reminder: It’s not just about the peaks. The valleys speak too. And valleys means silence. 🧘♂️
Bonus: Building Custom Buckets
Who said time is the only dimension? Let’s bucket by another field:
1
2
3
4
DeviceEvents
| where Timestamp > ago(1h)
| summarize count() by bin(Timestamp, 5m), ActionType
| render timechart
Want to see how many ProcessCreated
vs FileDeleted
events happen over time? This will give you a layered view, and if one layer starts growing like mushrooms after rain — it’s time to investigate.
Visual Density vs Visual Clarity
A quick word of caution: don’t over-bin it.
bin(Timestamp, 1s)
Sure, you can bin at one-second intervals. But unless you’re hunting nation-state actors who operate on atomic clocks, that’s just visual noise. Balance is key. Choose your resolution based on your context.
Final Example: Histogram + Join = Love
This query will only work in Sentinel since we’ll utilise the Heartbeat
table again.
1
2
3
4
5
6
7
8
let SuspiciousHosts =
DeviceNetworkEvents
| where RemoteUrl endswith ".xyz"
| summarize by DeviceName;
Heartbeat
| where Computer in (SuspiciousHosts)
| summarize count() by bin(TimeGenerated, 1h), Computer
| render timechart
Now we’re filtering the heartbeat by a suspicious host list. If they start skipping beats… don’t ignore it.
🧘 Conclusion: Read Between the Bins
A histogram is more than just a tool — it’s a lens. One that lets you step back, squint, and see.
It won’t solve the case for you. But it’ll show you where the rhythm breaks, where anomalies bloom, and where your attention should wander.
So next time you’re staring at raw logs and feeling like you’re drowning in entropy — bin it. Chart it. Read between the bars or under the lines. Maybe your math teacher was right after all? The area under the curve could perhaps tell a store and be that Integral part of your investigation.
Because in the end, histograms could actually be heavier than kilograms.