KQL Threat Hunting in Microsoft Sentinel: Start Here

Most Sentinel workspaces don’t have a query problem. There are hundreds of hunting queries in the Content hub, half of them enabled, all of them written by someone else for someone else’s tenant. What’s missing is a reason to open the query bar on a quiet afternoon, and some idea of where to point it.

So here’s the short answer. Learn five operators against SigninLogs, then hunt identity attacks in this order: password spray, MFA fatigue, impossible travel. After that, look at what an attacker does once they’re in, which is usually handing themselves an admin role or hiding their tracks in a mailbox. Every hunt runs the same loop: look at the raw data, find the shape the attack leaves, add one refinement, and only then turn it into a detection.

The rest of this page is that order, with a full write-up for each step.

Why start with sign-in logs?

Because that’s where most intrusions start, and it’s a table you already have. Phishing, password spray and stolen tokens all end the same way: someone signs in who shouldn’t. SigninLogs records every attempt, with the result code, the IP address, the location and the app. If your users sign in through Entra ID and the connector is on, the data is already there waiting for you.

Learn to read one table properly before you try to read twenty badly.

What KQL do I actually need?

Less than you’d think. take, where, summarize, sort and project cover most of the hunting in this series, and the KQL primer walks through all five with one mental model: the pipe passes rows from one step to the next, and each step makes the pile smaller.

Two habits matter more than any operator. Filter early, with a where and a time bound before anything expensive, so you’re never summarising a month of data you were about to throw away. And remember that ResultType in SigninLogs is text, not a number: a successful sign-in is "0" in quotes, a wrong password is "50126". Leave the quotes off and the query won’t do what you think.

Not every non-zero code is a failure, either. Many are prompts doing their normal job. The ResultType codes reference (coming soon) covers which codes are noise and which deserve a look.

Which attacks should I hunt first?

Start with the three ways attackers get in:

  1. Password spray. One password tried against a great many accounts, a try or two each, so no single account ever locks. The signal is breadth: one source failing with 50126 against dozens of distinct usernames in minutes. This is the gentlest introduction to summarize and dcount().
  2. MFA fatigue. Someone has the password and keeps firing push prompts until the user taps Approve to make it stop. The signal is a burst of denied MFA (500121) on one account, followed by a success. This one adds your first join.
  3. Impossible travel. Two sign-ins for the same person, too far apart in distance and too close in time to be one human. This uses prev() to line up consecutive sign-ins and geo_distance_2points() to measure the gap. It’s the noisiest of the three, mostly thanks to VPNs, which makes it a good lesson in tuning.

Then the two things an attacker does once they’re inside:

  1. Privileged role changes (coming soon). New admin role assignments in AuditLogs, checked against a watchlist of who should actually hold them. Rare, loud, and the clearest sign that a foothold is turning into control.
  2. Malicious inbox rules (coming soon). Rules in OfficeActivity that forward mail out or quietly delete it. After a mailbox is compromised, hiding is usually the first job, and the rule it creates is often the only trace.

Why this order?

It follows the attack, and it follows the learning curve. Each hunt adds one technique to the last: a count, then a join, then prev(), then parsing JSON and a watchlist. By the fifth you’re writing the kind of query you’d have skipped in the Content hub because you couldn’t read it.

Identity comes first for a simpler reason. If you can’t see who’s coming through the door, everything downstream is guesswork.

How do I turn a hunt into a detection?

Slowly, and on purpose. Run it as a hunt for a couple of weeks first. Measure what normal looks like in your tenant, set the threshold just above it and write down why. Keep exclusions narrow, give each one an owner, and only then make it a scheduled analytics rule with entities mapped so the incident tells the analyst something useful.

The free KQL Detection Tuning Checklist walks one query through all of that, in order. Turning on two hundred Content hub rules in an afternoon is the other way to do it, and it’s how real alerts end up buried under noise nobody reads.

Where can I get the queries?

Every query from these posts is in kql-hunting-queries on GitHub: free, MIT-licensed, one file per query, with the table, the MITRE ATT&CK technique and a link back to the write-up in each header. Thresholds and lookback windows sit at the top as variables, so you can tune them without reading the whole thing.

What about the cost?

Hunting needs data kept long enough to hunt in, and that costs money. Sentinel's table rules (coming soon) covers filtering, splitting and the data lake tier, and why the savings are never quite as big as the slide said.

This page is updated as each new hunt goes live.