Microsoft 365 advanced hunting is the query surface over the raw telemetry the Defender products collect, and Kusto Query Language is what you write those queries in. Most introductions to it get the vocabulary wrong, which matters more than it sounds: calling a function an operator sends you looking for the wrong thing in the reference. This page fixes the terminology, sets out the limits the service actually enforces, and shows where a hunting query stops being a hunt and becomes a detection.
Where Microsoft 365 advanced hunting lives and what it can see
Advanced hunting runs in the Microsoft Defender portal. Per the advanced hunting overview on Microsoft Learn, checked 19 August 2026, it queries data from Microsoft Defender for Endpoint, Microsoft Defender for Office 365, Microsoft Defender for Cloud Apps, Microsoft Defender for Identity and Microsoft Sentinel. That breadth is the whole point: one query can connect a sign in with the mail activity around it and the device the session came from. How that endpoint telemetry gets shaped in the first place is a separate job, covered in Defender controlled configuration in Intune.
Native retention is 30 days of raw data. That is the single most important number on this page and the one most guides omit. If an investigation needs to reach further back, the data has to be somewhere else already, which in practice means a Microsoft Sentinel workspace or a streaming API export set up before you needed it. Retention is not something you can decide on the morning of the incident.
There are two query modes. Guided mode gives a query builder for people who do not write KQL. Advanced mode is a blank editor. Guided mode is genuinely useful for shaping a first query and then reading the KQL it produces, which is a faster way to learn the schema than the reference is.
Operators and functions are not the same thing
KQL originates in Azure Data Explorer and is declarative: a query states which data is wanted rather than how to fetch it. A query is a pipeline. A tabular source flows through a chain of operators separated by the pipe character, and functions are called inside those operators to compute values.
That distinction is where most write ups, including an earlier version of this one, go wrong. Two corrections worth stating plainly:
project is an operator, and it does not raise alerts. Per the project operator reference, checked 19 August 2026, it selects the columns to include, renames them, drops the ones not listed, and inserts computed columns. The order of the arguments sets the order of the columns in the result. Every column you do not name is dropped. It is a shaping operator and nothing more.
iff is a function, not an operator. Per the iff function reference, checked 19 August 2026, it takes a condition and returns one of two values, and the documentation states explicitly that iff and iif are equivalent. You call it inside extend or project. You do not pipe into it.
The same applies to the other names that get muddled. where, summarize, extend, parse, join and union are operators. isempty, ago, make_set and arg_max are functions. Getting this right is not pedantry: the Kusto reference is organised into operator pages and function pages, so a wrong guess costs you a search every time.
A query that shows the difference
This runs against the DeviceEvents table and uses both. iff computes a value inside extend, and project shapes the output.
// Antivirus detections in the last day, tagged by whether the event
// carries a device name. iff() is a function called inside extend().
// project() is an operator: it selects and orders the output columns.
DeviceEvents
| where Timestamp > ago(24h)
| where ActionType == "AntivirusDetection"
| extend Attribution = iff(isempty(DeviceName), "unattributed", "attributed")
| project Timestamp, DeviceId, DeviceName, ActionType, Attribution
Nothing in that query alerts anybody. It returns rows in the portal and stops. Turning it into something that wakes a person up is a separate step with its own rules.
Where a hunt becomes a detection
A saved hunting query and a custom detection rule are different objects. The rule is what generates alerts and can take automated response actions, and Microsoft’s custom detection rules documentation, checked 19 August 2026, sets conditions on the query before it will accept one.
The query has to return a timestamp column, either Timestamp or TimeGenerated, which sets the alert time. For Defender for Endpoint tables it should return DeviceId or DeviceName, so alerts land in the right device group scope and the process tree renders. For the other Defender tables it needs Timestamp and ReportId together, so the service can find the originating event again. To map impacted assets automatically it also wants entity identifier columns: DeviceId, DeviceName or RemoteDeviceName for devices, RecipientEmailAddress and the sender address columns for mailboxes, AccountObjectId, AccountSid or AccountUpn for accounts.
Frequencies are fixed: every 24 hours with a 30 day lookback, every 12 hours with 48, every 3 hours with 12, hourly with 4, or continuous near real time. Custom intervals from five minutes to fourteen days exist but only for Microsoft Sentinel data. Each rule generates a maximum of 150 alerts per run.
That 150 alert ceiling is worth designing around rather than discovering. A rule that is too broad does not fail loudly. It silently truncates, and the thing you were hunting may be in the part that got cut.
The failure modes
Writing the query in local time. Queries are written in UTC. Results are converted to the timezone configured in the portal. A hunt scoped with ago() is fine, but the moment someone hardcodes a window around an incident report written in British Summer Time, the query silently covers the wrong hour.
Hitting a quota and reading it as a clean result. The documented limits are 100,000 rows returned, a 10 minute timeout, and a 64 MB result size. CPU is metered too: exceed roughly 10 per cent of the tenant allocation in a 15 minute cycle and you get a warning, and at 100 per cent queries are blocked until the cycle refreshes. A truncated result set looks exactly like a small one.
Assuming 30 days is negotiable. It is not, for native Defender data. Every retrospective hunt on a breach discovered late runs into this, and the answer is always the same: the export you did not configure last quarter.
Hunting for the thing instead of the pattern. A query pinned to one sender address or one file hash catches that one incident. A query built on behaviour, such as an inbox rule created shortly after an unfamiliar sign in, catches the next one too. That distinction is the same one that separates useful and useless controls in phishing defence and in mail bombing, where the individual indicators change constantly and the shape does not.
What I would do differently
My tenant is a lab with no enrolled devices, so I have not run these queries against a real incident and I am not going to quote a hunt time or a detection rate I did not measure. What follows is judgement about the approach.
I would spend the first week on the schema rather than on the language. KQL is small and reads well within an afternoon. Knowing which table holds the field you want, and which of the five products fills it, is the part that actually slows people down, and no amount of syntax practice substitutes for it.
I would also treat the 30 day retention as an architecture decision made now rather than a limit met later. Working out what needs to survive past a month, and getting it into a Sentinel workspace, is worth more than any query you will write in the same week.
On access, hunting is read heavy and tempting to hand out widely. It is telemetry across identity, mail and endpoint, which makes it exactly the sort of capability that should be scoped deliberately rather than bundled into an existing admin role. The reasoning is the same as for any other broad read permission, and it is set out in least privilege. The mail side of the same estate is covered in email security.
Last verified: 19 August 2026.



