Microsoft Defender XDR Advanced Hunting Queries: 20 KQL Queries for Phishing, Identity and Endpoint Hunting

Microsoft Defender XDR advanced hunting queries KQL examples

When a phishing email lands in hundreds of inboxes, the clock starts. Who received it? Who clicked? Did anyone enter a password, and did the attacker sign in from somewhere new? In Microsoft Defender XDR (formerly Microsoft 365 Defender), advanced hunting queries written in KQL answer all of these in minutes, if you have the right queries ready.

This guide is a practical advanced hunting playbook: 20 ready-to-run KQL queries for phishing campaign scoping, click analysis, Entra ID sign-in investigation, device code phishing, lateral movement, endpoint hunting and daily alert triage. They come from hands-on incident response work in a Microsoft 365 environment, with company details replaced by examples. Copy a query, fill in the yellow values, and run it.

Who this is for: SOC analysts, Microsoft 365 and security admins, and anyone responding to phishing in Defender XDR. You need access to the Defender portal and the relevant licenses (see below).

What is advanced hunting in Microsoft Defender XDR?

Advanced hunting is the query-based threat hunting tool in the Microsoft Defender portal. It lets you search up to 30 days of raw data from email, identities, endpoints and cloud apps with Kusto Query Language (KQL), the same language used in Microsoft Sentinel and Azure Data Explorer. Unlike alerts, which only show what Microsoft detected, advanced hunting lets you ask your own questions across all that data at once.

  • Email: EmailEvents, EmailUrlInfo, UrlClickEvents and related tables (Defender for Office 365).
  • Identity: Entra ID sign-in events and identity logons.
  • Endpoints: DeviceProcessEvents, DeviceFileEvents, DeviceNetworkEvents and more (Defender for Endpoint).
  • Cloud apps: CloudAppEvents for Microsoft 365 activity (Defender for Cloud Apps).
  • Alerts: AlertInfo and AlertEvidence across all Defender products.

Licensing note: which tables you see depends on your licenses. Email tables need Defender for Office 365 Plan 2, device tables need Defender for Endpoint, and Microsoft 365 E5 includes them all. Check Microsoft's current licensing documentation for your tenant.

Quick links: Phishing scoping · Click analysis · Identity · Device code phishing · Lateral movement · Endpoint hunting · Daily triage · Reference

How to use this playbook

  • Where to run: Microsoft Defender portal → Investigation & response → Hunting → Advanced hunting.
  • One query at a time: paste one query, or select it and click Run query. If several queries are in the editor, only the last one returns results.
  • Placeholders: replace values in angle brackets, such as <subject> or <user@contoso.com>, before running. Most queries put these in let lines at the top.
  • Retention: Advanced hunting keeps 30 days of data. ago(30d) is the maximum look-back.
  • Time zone: the Timestamp column is stored in UTC. IST = UTC + 5:30. Where a query needs IST boundaries, it calculates them.
  • Result limit: 10,000 rows per query. For larger results, narrow the time range or split by day.
  • Export: use Export above the results grid to download CSV for reports or tickets.

Values you must fill in are listed in a yellow table under each query. Queries without a yellow table run as they are.

Tables used

TableWhat it contains
EmailEventsEvery email processed: sender, recipient, subject, delivery action and location
EmailUrlInfoURLs found in each email
UrlClickEventsSafe Links clicks: user, URL, allowed or blocked, clicked through
EntraIdSignInEventsEntra ID interactive and non-interactive sign-ins (older tenants: AADSignInEventsBeta)
IdentityInfoUser attributes from Entra ID: company, department, country, job title
AlertInfo / AlertEvidenceDefender alerts and the users, devices, emails and URLs attached to them
CloudAppEventsMicrosoft 365 activity: inbox rules, sharing, consent, admin actions
DeviceFileEvents / DeviceProcessEvents / DeviceNetworkEventsEndpoint file, process and network activity from Defender for Endpoint

1. Phishing campaign scoping

Use these when a phishing email is reported. Start with 1.1 to get every recipient and their click status, then 1.2 for a per-company summary. Edit the badSubject and badDomain lines; leave one as "" if unknown.

1.1 Full recipient list with click status and company

One row per recipient. Clicked-through users sort to the top.

let lookback   = 30d;
let badSubject = "<unique part of subject>";
let badDomain  = "<sender domain>";
let msgs = EmailEvents
| where Timestamp > ago(lookback)
| where (isnotempty(badSubject) and Subject contains badSubject)
     or (isnotempty(badDomain) and (SenderFromDomain =~ badDomain or SenderMailFromDomain =~ badDomain))
| extend Recipient = tolower(RecipientEmailAddress);
let recipients = msgs
| summarize Received = min(Timestamp), Sender = take_any(SenderFromAddress),
            Subject = take_any(Subject), DeliveryAction = take_any(DeliveryAction),
            LatestDeliveryLocation = take_any(LatestDeliveryLocation)
  by Recipient;
let clicks = UrlClickEvents
| where Timestamp > ago(lookback)
| where NetworkMessageId in ((msgs | distinct NetworkMessageId))
| extend Recipient = tolower(AccountUpn)
| summarize FirstClick = min(Timestamp), TotalClicks = count(),
            ClickedThrough = countif(IsClickedThrough),
            ClickIPs = make_set(IPAddress, 5), ClickedUrls = make_set(Url, 3)
  by Recipient;
let company = IdentityInfo
| where Timestamp > ago(lookback)
| summarize arg_max(Timestamp, CompanyName, Department, Country, JobTitle)
  by Recipient = tolower(AccountUpn)
| project Recipient, CompanyName, Department, Country, JobTitle;
recipients
| join kind=leftouter clicks on Recipient
| join kind=leftouter company on Recipient
| extend Status = case(ClickedThrough > 0, "CLICKED THROUGH",
                       TotalClicks > 0,    "Clicked (blocked)",
                                           "No click"),
         Company = iff(isempty(CompanyName), "Unknown", CompanyName)
| project Received, Recipient, Company, Department, Country, JobTitle, Status,
          FirstClick, TotalClicks, ClickedThrough, DeliveryAction,
          LatestDeliveryLocation, Sender, Subject, ClickIPs, ClickedUrls
| order by Status asc, Company asc, Recipient asc
Fill inExample
badSubject"Document Requires Your Signature"
badDomain"fake-docs.example" (leave "" if unknown)

1.2 Company-wise summary

Run after 1.1 when management asks how many people per company were affected. Use the same let values.

let lookback   = 30d;
let badSubject = "<unique part of subject>";
let msgs = EmailEvents
| where Timestamp > ago(lookback)
| where Subject contains badSubject
| extend Recipient = tolower(RecipientEmailAddress)
| project NetworkMessageId, Recipient, LatestDeliveryLocation;
let clicks = UrlClickEvents
| where Timestamp > ago(lookback)
| where NetworkMessageId in ((msgs | distinct NetworkMessageId))
| extend Recipient = tolower(AccountUpn)
| summarize ClickedThrough = countif(IsClickedThrough) by Recipient;
let company = IdentityInfo
| where Timestamp > ago(lookback)
| summarize arg_max(Timestamp, CompanyName) by Recipient = tolower(AccountUpn)
| project Recipient, CompanyName;
msgs
| join kind=leftouter clicks on Recipient
| join kind=leftouter company on Recipient
| extend Company = iff(isempty(CompanyName), "Unknown", CompanyName)
| summarize Recipients     = dcount(Recipient),
            StillInInbox   = dcountif(Recipient, LatestDeliveryLocation == "Inbox/folder"),
            Clicked        = dcountif(Recipient, isnotnull(ClickedThrough)),
            ClickedThrough = dcountif(Recipient, ClickedThrough > 0)
  by Company
| order by Clicked desc
Fill inExample
badSubject"Document Requires Your Signature"

1.3 Sender domain activity over 30 days

Shows every subject (campaign wave) sent from a domain, with sender IPs and authentication results.

EmailEvents
| where Timestamp > ago(30d)
| where SenderFromDomain =~ "<sender domain>" or SenderMailFromDomain =~ "<sender domain>"
| summarize Emails = count(), Recipients = dcount(RecipientEmailAddress),
            FirstSeen = min(Timestamp), LastSeen = max(Timestamp),
            Delivered = countif(DeliveryAction == "Delivered"),
            StillInInbox = countif(LatestDeliveryLocation == "Inbox/folder"),
            SenderIPs = make_set(SenderIPv4, 10), Verdicts = make_set(ThreatTypes, 5),
            AuthResults = make_set(AuthenticationDetails, 3)
  by Subject
| order by FirstSeen asc
Fill inExample
<sender domain> (2 places)fake-docs.example

If Company shows Unknown, the Company attribute is empty in Entra ID; try Department, or match the export against AD. Emails still in Inbox/folder must be removed through Email & collaboration → Explorer → Take action → Soft delete.

2. Click analysis and URLs

2.1 Users who clicked, with URLs and IPs

Fastest way to list only the users who clicked.

let msgIds = EmailEvents
| where Timestamp > ago(30d)
| where Subject contains "<unique part of subject>"
| distinct NetworkMessageId;
UrlClickEvents
| where Timestamp > ago(30d)
| where NetworkMessageId in (msgIds)
| summarize FirstClick = min(Timestamp), Clicks = count(),
            ClickedThrough = countif(IsClickedThrough == true),
            IPs = make_set(IPAddress, 5), Urls = make_set(Url, 5)
  by AccountUpn
| extend Status = iff(ClickedThrough > 0, "CLICKED THROUGH", "Clicked (blocked)")
| order by Status asc, FirstClick asc
Fill inExample
<unique part of subject>Payment Batch Scheduled

2.2 URLs inside the phishing email

Use the domains found here for blocking and endpoint hunting.

EmailEvents
| where Timestamp > ago(30d)
| where Subject contains "<unique part of subject>"
| join kind=inner (EmailUrlInfo | where Timestamp > ago(30d)) on NetworkMessageId
| summarize Emails = dcount(NetworkMessageId) by Url, UrlDomain
| order by Emails desc
Fill inExample
<unique part of subject>Payment Batch Scheduled

2.3 Is the sender domain also used for legitimate mail?

Run before blocking a whole domain. If only phishing subjects appear, block the domain; if normal business mail also appears, block only the sender IP and the URL.

EmailEvents
| where Timestamp > ago(30d)
| where SenderFromDomain =~ "<sender domain>"
| summarize Emails = count(), Recipients = dcount(RecipientEmailAddress),
            Phish = countif(ThreatTypes has "Phish"),
            SenderIPs = make_set(SenderIPv4, 10)
  by Subject
| order by Emails desc
Fill inExample
<sender domain>mail.example.net

Note: UrlClickEvents only records clicks that pass through Safe Links. A link copied into another browser or opened on a personal device is not recorded, so also run the endpoint queries in section 6 for clicked domains.

3. Identity investigation (Entra ID sign-ins)

Use for any user who clicked through, or who has a risky sign-in alert. Older tenants may need AADSignInEventsBeta instead of EntraIdSignInEvents.

3.1 A user's sign-in locations (last 7 days)

Builds the user's normal pattern: office IP, home ISP, mobile carrier. Anything outside it needs review.

EntraIdSignInEvents
| where Timestamp > ago(7d)
| where AccountUpn =~ "<user@contoso.com>"
| summarize SignIns = count(), Success = countif(ErrorCode == 0),
            First = min(Timestamp), Last = max(Timestamp),
            Apps = make_set(Application, 5), UserAgents = make_set(UserAgent, 3)
  by IPAddress, Country, City
| order by First asc
Fill inExample
<user@contoso.com>firstname.lastname@contoso.com

3.2 Successful sign-ins from unknown IPs

List the user's known IPs from 3.1, then find any success from elsewhere.

let knownIPs = dynamic(["<office IP>", "<home IP>"]);
EntraIdSignInEvents
| where Timestamp > datetime(<incident start, e.g. 2026-10-12 18:00>)
| where AccountUpn =~ "<user@contoso.com>"
| where ErrorCode == 0 and IPAddress !in (knownIPs)
| summarize SignIns = count(), Apps = make_set(Application, 10),
            First = min(Timestamp), Last = max(Timestamp)
  by IPAddress, Country, City
Fill inExample
knownIPsThe user's normal IPs from 3.1, e.g. "203.0.113.10", "198.51.100.25"
<incident start>2026-10-12 18:00 (UTC)
<user@contoso.com>firstname.lastname@contoso.com

3.3 Detailed sign-ins with error codes

Shows whether the attacker had the password (50074/50076) or not (50126). See section 8 for codes.

EntraIdSignInEvents
| where Timestamp > ago(7d)
| where AccountUpn =~ "<user@contoso.com>"
| where IPAddress in ("<suspicious IP 1>", "<suspicious IP 2>")
| project Timestamp, IPAddress, ErrorCode, Application, LogonType,
          ConditionalAccessStatus, AuthenticationRequirement,
          DeviceName, IsManaged, IsCompliant, UserAgent
| order by Timestamp asc
Fill inExample
<user@contoso.com>firstname.lastname@contoso.com
<suspicious IP 1>, <suspicious IP 2>203.0.113.45, 198.51.100.77

3.4 Attacker IPs across all accounts

Answers "did the attacker get into any other account?" Any other account with Success > 0 is compromised.

let iocIPs = dynamic(["<IP 1>", "<IP 2>", "<IP 3>"]);
EntraIdSignInEvents
| where Timestamp > ago(30d)
| where IPAddress in (iocIPs)
| summarize Attempts = count(), Success = countif(ErrorCode == 0),
            Apps = make_set(Application, 5), First = min(Timestamp)
  by AccountUpn, IPAddress
| order by Success desc
Fill inExample
iocIPsAll attacker IPs found, e.g. "203.0.113.45", "192.0.2.15"

3.5 Sign-ins from a suspicious device

An attacker-registered device can sign in from any new IP. This query follows the device, not the IP.

EntraIdSignInEvents
| where Timestamp > datetime(<incident start>)
| where DeviceName =~ "<DEVICE-NAME>"
| project Timestamp, AccountUpn, IPAddress, Country, Application, ErrorCode, LogonType
| order by Timestamp asc
Fill inExample
<incident start>2026-10-12 18:00 (UTC)
<DEVICE-NAME>The unknown device name from Entra, e.g. SUSPECT-DEVICE-01

3.6 Password spray and scripted sign-in attempts

Automated clients such as Apache-HttpAsyncClient or python-requests indicate credential stuffing.

EntraIdSignInEvents
| where Timestamp > ago(7d)
| where UserAgent has_any ("Apache-HttpAsyncClient", "python-requests", "axios", "curl", "Go-http-client")
     or IPAddress == "<suspicious IP>"
| summarize Attempts = count(), Success = countif(ErrorCode == 0),
            ErrorCodes = make_set(ErrorCode), Users = dcount(AccountUpn),
            SampleUsers = make_set(AccountUpn, 20)
  by IPAddress, Country, UserAgent
| order by Users desc
Fill inExample
<suspicious IP> (optional)203.0.113.88, or delete the or IPAddress == ... line

4. Device code phishing and device registration

Device code phishing gets a token without breaking MFA: the user enters an attacker's code at microsoft.com/devicelogin, and the token goes to the attacker. The attacker often then joins a device to Entra ID to obtain a Primary Refresh Token.

4.1 Users with device code or device registration alerts

AlertInfo
| where Timestamp > ago(14d)
| where Title has_any ("device code", "token exchange", "Entra device join", "Device registration after")
| join kind=inner (AlertEvidence | where EntityType == "User" | project AlertId, AccountUpn, AccountName) on AlertId
| summarize Alerts = make_set(Title, 10), First = min(Timestamp) by AccountName, AccountUpn
| order by First desc

4.2 What to check in the Entra portal

Advanced hunting does not show every device code field, so confirm in Entra admin center → Identity → Monitoring & health → Sign-in logs:

  1. Filter User = the affected user and Authentication protocol = Device code.
  2. Open each entry and record IP address, Application, Resource, Original transfer method, MFA result and Conditional Access status.
  3. Treat as phishing when the application is Microsoft Authentication Broker with resource Device Registration Service, the IPs are external, and each success follows error 50199.
  4. Then go to Identity → Devices → All devices, search the user, and compare device names, join type and MDM with the user's known devices.

4.3 Immediate actions when confirmed

  1. Record the device ID and object ID, then Disable the unknown device.
  2. Revoke sessions for the user and reset the password.
  3. Review authentication methods and remove unknown ones.
  4. Check Defender → Action center → History for Attack Disruption actions.
  5. Run 3.4 and 3.5 with the attacker IPs and device name.

5. Post-compromise and lateral movement

Run these for every account confirmed or suspected compromised. If all are clean and 3.4 shows no other accounts, record "No evidence of lateral movement identified."

5.1 Mail sent from the compromised mailbox

Internal phishing from a trusted colleague is the most common next step.

EmailEvents
| where Timestamp > datetime(<incident start>)
| where SenderFromAddress =~ "<user@contoso.com>"
| where EmailDirection in ("Intra-org", "Outbound")
| summarize Recipients = dcount(RecipientEmailAddress),
            SampleRecipients = make_set(RecipientEmailAddress, 20),
            Sent = min(Timestamp)
  by Subject, EmailDirection
| order by Recipients desc
Fill inExample
<incident start>2026-10-12 18:00 (UTC)
<user@contoso.com>firstname.lastname@contoso.com

5.2 Who clicked links in mail from that mailbox

let msgs = EmailEvents
| where Timestamp > datetime(<incident start>)
| where SenderFromAddress =~ "<user@contoso.com>"
| distinct NetworkMessageId;
UrlClickEvents
| where Timestamp > datetime(<incident start>)
| where NetworkMessageId in (msgs)
| summarize Clicks = count(), ClickedThrough = countif(IsClickedThrough), Urls = make_set(Url, 5)
  by AccountUpn
Fill inExample
<incident start> (2 places)2026-10-12 18:00 (UTC)
<user@contoso.com>firstname.lastname@contoso.com

5.3 Microsoft 365 actions by the account

Use the user's Entra object ID (Entra → Users → user → Overview).

CloudAppEvents
| where Timestamp > datetime(<incident start>)
| where AccountObjectId == "<user object ID>"
| summarize Count = count(), IPs = make_set(IPAddress, 5), First = min(Timestamp)
  by Application, ActionType
| order by First asc
Fill inExample
<incident start>2026-10-12 18:00 (UTC)
<user object ID>Entra → Users → user → Object ID, e.g. 6f4ec9a9-...
ActionTypeWhat it means
New-InboxRule, Set-InboxRule, UpdateInboxRulesRule created to hide or forward mail
Send, SendAs, SendOnBehalfMail sent in the user's name
AnonymousLinkCreated, SharingSet, AddedToGroupFiles shared outside or access widened
Consent to application.OAuth app given access to the account
Reset user password., Add member to role., Update user.Another account changed; possible only with an admin role

Also check Entra → Users → user → Assigned roles. If the user holds any admin role, review Entra audit logs filtered by Initiated by = the user.

6. Endpoint hunting: remote access tools and payloads

Some phishing links deliver a remote management (RMM) installer such as ScreenConnect instead of a credential page. Confirm first whether the tool is approved at your organization; if not, any hit is malicious.

6.1 Download, execution and network activity for a tool or domain

Replace ScreenConnect and the domain with what you found in 2.2.

let tool   = "ScreenConnect";
let domain = "<malicious domain>";
union
(DeviceFileEvents
 | where Timestamp > ago(7d)
 | where FileName has tool or FileOriginUrl has domain or FileOriginUrl has tool
 | project Timestamp, DeviceName, User = InitiatingProcessAccountUpn, Event = "File",
           Detail = strcat(FolderPath, " | ", FileOriginUrl), SHA256),
(DeviceProcessEvents
 | where Timestamp > ago(7d)
 | where FileName has tool or ProcessCommandLine has tool
 | project Timestamp, DeviceName, User = AccountUpn, Event = "Process",
           Detail = ProcessCommandLine, SHA256),
(DeviceNetworkEvents
 | where Timestamp > ago(7d)
 | where RemoteUrl has domain or RemoteUrl has tool
 | project Timestamp, DeviceName, User = InitiatingProcessAccountUpn, Event = "Network",
           Detail = strcat(RemoteUrl, ":", RemotePort), SHA256 = "")
| order by DeviceName, Timestamp asc
Fill inExample
tool"ScreenConnect" (or the RMM tool found)
domain"attacker-tenant.screenconnect.com"
EventMeaningAction
FileInstaller downloadedDelete the file, check for Process events
ProcessInstaller or client service ranDevice compromised: Isolate device, uninstall, reset user credentials
NetworkConnection to the relayAttacker may have live remote access: isolate immediately

Also add the domain under Settings → Endpoints → Indicators → URLs/Domains with action Block.

7. Daily alert triage

Advanced hunting has no incidents table; incidents are built from alerts, so these queries use AlertInfo. Time boundaries are calculated in IST (yesterday 00:00 IST to now).

7.1 Today's and yesterday's alerts with users and devices

let IST = 5h + 30m;
let todayStartIST = startofday(now() + IST) - IST;
let fromTime = todayStartIST - 1d;
AlertInfo
| where Timestamp >= fromTime
| join kind=leftouter (
    AlertEvidence
    | where Timestamp >= fromTime
    | summarize Users = make_set_if(AccountUpn, isnotempty(AccountUpn), 10),
                Devices = make_set_if(DeviceName, isnotempty(DeviceName), 10)
      by AlertId
  ) on AlertId
| extend Day = iff(Timestamp >= todayStartIST, "Today", "Yesterday"),
         TimeIST = Timestamp + IST
| project TimeIST, Day, Severity, Title, Category, ServiceSource,
          DetectionSource, AttackTechniques, Users, Devices, AlertId
| order by TimeIST desc

7.2 Alert summary by title and source

let IST = 5h + 30m;
let todayStartIST = startofday(now() + IST) - IST;
let fromTime = todayStartIST - 1d;
AlertInfo
| where Timestamp >= fromTime
| extend Day = iff(Timestamp >= todayStartIST, "Today", "Yesterday")
| summarize Alerts = count(),
            High = countif(Severity == "High"), Medium = countif(Severity == "Medium"),
            Low = countif(Severity == "Low"), Info = countif(Severity == "Informational")
  by Day, Title, ServiceSource
| order by Day asc, High desc, Alerts desc

7.3 Incidents list (only if Microsoft Sentinel is connected)

If this returns "table not found", Sentinel is not connected; use 7.1 and 7.2.

let IST = 5h + 30m;
let fromTime = startofday(now() + IST) - IST - 1d;
SecurityIncident
| where CreatedTime >= fromTime
| summarize arg_max(LastModifiedTime, *) by IncidentNumber
| project CreatedIST = CreatedTime + IST, IncidentNumber, Title, Severity,
          Status, Classification, Owner = tostring(Owner.assignedTo),
          AlertCount = array_length(AlertIds)
| order by CreatedIST desc

8. Reference: reading results and fixing errors

Click status

StatusMeaningAction
No clickUser did not click the linkRemove the email from the inbox
Clicked (blocked)Safe Links blocked the click; user did not reach the siteRemove the email; send an awareness reminder
CLICKED THROUGHUser reached the malicious site (click allowed, or warning overridden)Run sections 3 and 6; reset password and revoke sessions if any suspicious sign-in

Entra ID sign-in error codes

CodeMeaningRisk
0Successful sign-inCheck IP, app and location
50126Invalid username or passwordLow: attacker does not have the password
50053Account locked (smart lockout)Low: spray was stopped
50074 / 50076Password correct, MFA requiredHigh: password is exposed, reset it
53003Blocked by Conditional AccessPolicy worked; check why it was attempted
50140"Stay signed in" interruptNormal
50158External security challengeUsually normal
50199User confirmation prompt ("Are you trying to sign in to...?")Before a device code success, indicates the user approved an attacker's code

Common query problems

ProblemFix
Only one query returns resultsRun one query at a time, or select the query before Run query
Failed to resolve table EntraIdSignInEventsUse AADSignInEventsBeta
SecurityIncident not foundSentinel is not connected; use section 7.1
0 rows for a subject searchUse contains instead of has for multi-word text; shorten the phrase
0 rows for a time windowTimestamps are UTC; widen with ago(1d) or convert IST to UTC
Company shows UnknownCompany attribute is empty in Entra ID; use Department or match against AD
Results capped at 10,000 rowsNarrow the time range or split the query by day
Email delivered despite Phish verdictCheck Final system override; an organization policy or third-party filter rule may bypass Defender

Notes

  • Coordinate tenant-level changes (Conditional Access, Tenant Allow/Block List, device settings) with your IT team and change control.
  • Attach exported CSVs and screenshots to the incident ticket as evidence.
Learn the building blocks: new to KQL? Start with KQL for beginners, get to know the most important email table in EmailEvents table explained, and turn these queries into automatic alerts with custom detection rules in Microsoft Defender.

Tips for writing better advanced hunting queries

  • Filter on Timestamp first. Put where Timestamp > ago(...) right after the table name; it makes queries much faster.
  • Use has instead of contains when matching whole words. It uses the index and is faster.
  • Use let statements for values you change often, as in the queries above, so one edit updates the whole query.
  • Turn proven queries into custom detection rules so they run on a schedule and create alerts automatically.
  • Check the schema (the panel on the left of the query editor) when a column or table name fails; Microsoft renames tables over time, for example the move from AADSignInEventsBeta to EntraIdSignInEvents.
  • Export evidence early. Data is only kept for 30 days, so download CSVs during an investigation.

Want to automate the follow-up steps? See our PowerShell automation scripts for IT admins, and our real case of device code phishing on Microsoft 365 for the attack behind section 4.

FAQ

What are advanced hunting queries in Microsoft Defender XDR?

They are KQL queries you run in the Defender portal (Hunting > Advanced hunting) to search raw email, identity, endpoint and cloud app data from the last 30 days, for threat hunting and incident investigation.

Is Microsoft 365 Defender the same as Microsoft Defender XDR?

Yes. Microsoft renamed Microsoft 365 Defender to Microsoft Defender XDR. Advanced hunting works the same way, and older queries usually still run, apart from renamed tables.

Where do I find the advanced hunting page?

In the Microsoft Defender portal, go to Investigation & response > Hunting > Advanced hunting.

Why does my query say "Failed to resolve table"?

Either the table needs a license your tenant doesn't have, or it has been renamed. For sign-in data, try AADSignInEventsBeta instead of EntraIdSignInEvents.

Can advanced hunting queries run automatically?

Yes. Save a query as a custom detection rule with a schedule, and Defender will create alerts (and optionally take actions) when it returns results.

How far back can I search?

Advanced hunting keeps up to 30 days of data. For longer retention, stream the data to Microsoft Sentinel or another SIEM.

Post a Comment

0 Comments