PRACTITIONER GUIDE | THREAT HUNTING
Practitioner GuideUpdated 14 min read

Microsoft Sentinel KQL Threat Hunting: 12 Queries That Surface Attacker Behavior Across Azure, M365, and On-Premises Logs

MITRE ATT&CK
Sentinel's built-in MITRE coverage map shows which techniques your current detection rules cover -- threat hunting fills the gaps in yellow (partial) and white (uncovered) cells
90 days
is the default Log Analytics retention window -- extend to 2 years for compliance environments using archive tier storage at reduced cost
SigninLogs
and AADNonInteractiveUserSignInLogs are the two Entra ID sign-in tables -- non-interactive logins (service principals, token refresh) are a common blind spot in hunting queries that only check SigninLogs
Bookmark
Sentinel hunting bookmarks tag suspicious query results for follow-up investigation and can be promoted to incidents, connecting hunting findings to the full Sentinel investigation workflow

SponsoredRetool

Retool's new app builder is where AI-generated code ships safely

Building apps with AI is easy. Getting them to production safely is another story.

Start building for free today

Threat hunting in Sentinel starts with a hypothesis about attacker behavior and a KQL query designed to surface evidence of that behavior. These 12 queries cover the most frequently observed cloud attacker TTPs across Entra ID, Azure Resource Manager, Microsoft 365, and on-premises Windows events forwarded to Sentinel. Each query includes the MITRE ATT&CK technique it targets and the false positive sources to tune out.

Entra ID Sign-In Anomaly Queries

Query 1: Impossible travel (same user, different country within 1 hour)

let timeWindow = 1h;
SigninLogs
| where TimeGenerated > ago(7d)
| where ResultType == "0"  // Successful sign-ins only
| extend Country = tostring(LocationDetails.countryOrRegion)
| summarize
    Countries = make_set(Country),
    IPs = make_set(IPAddress),
    FirstSeen = min(TimeGenerated),
    LastSeen = max(TimeGenerated)
  by UserPrincipalName, bin(TimeGenerated, timeWindow)
| where array_length(Countries) > 1
| project UserPrincipalName, Countries, IPs, FirstSeen, LastSeen
// MITRE: T1078 Valid Accounts | FP: VPN users, travelers near borders

Query 2: Sign-in from new country (not seen in last 30 days for this user)

let baseline = SigninLogs
| where TimeGenerated between (ago(37d) .. ago(7d))
| where ResultType == "0"
| summarize KnownCountries = make_set(tostring(LocationDetails.countryOrRegion)) by UserPrincipalName;
SigninLogs
| where TimeGenerated > ago(7d)
| where ResultType == "0"
| extend Country = tostring(LocationDetails.countryOrRegion)
| join kind=leftouter baseline on UserPrincipalName
| where Country !in (KnownCountries)
| project TimeGenerated, UserPrincipalName, Country, IPAddress, AppDisplayName
// MITRE: T1078 | FP: New hires, business travel

Query 3: Token replay -- device code authentication from unusual IP after long delay

SigninLogs
| where TimeGenerated > ago(7d)
| where AuthenticationProtocol == "deviceCode"
| where ResultType == "0"
| project TimeGenerated, UserPrincipalName, IPAddress, DeviceDetail, AppDisplayName, LocationDetails
| join kind=inner (
    SigninLogs
    | where AuthenticationProtocol != "deviceCode" and ResultType == "0"
    | summarize NormalIPs = make_set(IPAddress) by UserPrincipalName
) on UserPrincipalName
| where IPAddress !in (NormalIPs)
// MITRE: T1528 Steal Application Access Token | FP: Legitimate device code logins from new devices

Azure Resource and Privilege Escalation Queries

Query 4: Privileged role added outside business hours or from new IP

AuditLogs
| where TimeGenerated > ago(7d)
| where OperationName in ("Add member to role","Add eligible member to role")
| extend InitiatedByIP = tostring(InitiatedBy.user.ipAddress)
| extend TargetUser = tostring(TargetResources[0].userPrincipalName)
| extend RoleName = tostring(TargetResources[1].displayName)
| extend Hour = datetime_part("Hour", TimeGenerated)
| where Hour !between (8 .. 18)  // Outside 8am-6pm local -- adjust to your timezone
   or RoleName has_any ("Global Administrator","Privileged Role Administrator","Security Administrator")
| project TimeGenerated, InitiatedBy, InitiatedByIP, TargetUser, RoleName
// MITRE: T1078.004 Cloud Accounts | FP: Legitimate after-hours admin actions

Query 5: Azure resource creation burst (potential cryptomining or data processing infrastructure)

AzureActivity
| where TimeGenerated > ago(1d)
| where OperationNameValue endswith "/write" and ActivityStatusValue == "Success"
| where ResourceProviderValue in ("MICROSOFT.COMPUTE","MICROSOFT.CONTAINERINSTANCE","MICROSOFT.BATCH")
| summarize
    ResourcesCreated = count(),
    ResourceTypes = make_set(ResourceProviderValue),
    Regions = make_set(ResourceGroup)
  by Caller, bin(TimeGenerated, 1h)
| where ResourcesCreated > 10
| project TimeGenerated, Caller, ResourcesCreated, ResourceTypes, Regions
// MITRE: T1578.002 Create Cloud Instance | FP: Auto-scaling, Infrastructure as Code deployments

Query 6: Service principal added to Owner or Contributor on high-value subscription

AzureActivity
| where TimeGenerated > ago(7d)
| where OperationNameValue == "MICROSOFT.AUTHORIZATION/ROLEASSIGNMENTS/WRITE"
| where ActivityStatusValue == "Success"
| extend Properties = parse_json(Properties)
| extend RoleDefinitionId = tostring(Properties.requestbody.properties.roleDefinitionId)
| extend PrincipalType = tostring(Properties.requestbody.properties.principalType)
| where RoleDefinitionId has_any ("8e3af657-a8ff-443c-a75c-2fe8c4bcb635","b24988ac-6180-42a0-ab88-20f7382dd24c")  // Owner, Contributor GUIDs
| where PrincipalType == "ServicePrincipal"
| project TimeGenerated, Caller, SubscriptionId, ResourceGroup, Properties
// MITRE: T1098.003 Additional Cloud Roles | FP: Legitimate automation deployment
Free daily briefing

Briefings like this, every morning before 9am.

Threat intel, active CVEs, and campaign alerts, distilled for practitioners. 50,000+ subscribers. No noise.

Microsoft 365 Behavioral Queries

Query 7: Inbox rule created and immediately followed by email forwarding

OfficeActivity
| where TimeGenerated > ago(7d)
| where Operation in ("New-InboxRule","Set-InboxRule","UpdateInboxRules")
| extend RuleParams = parse_json(Parameters)
| extend ForwardTo = tostring(RuleParams[?(@.Name == 'ForwardTo')].Value)
| extend DeleteMsg = tostring(RuleParams[?(@.Name == 'DeleteMessage')].Value)
| where isnotempty(ForwardTo) or DeleteMsg == "True"
| project TimeGenerated, UserId, Operation, ForwardTo, DeleteMsg, ClientIP
// MITRE: T1114.003 Email Forwarding Rule | FP: User-created legitimate forwarding rules

Query 8: Mass SharePoint file access in short time window

OfficeActivity
| where TimeGenerated > ago(7d)
| where RecordType == "SharePointFileOperation"
| where Operation in ("FileDownloaded","FileAccessed")
| summarize
    FileCount = count(),
    UniqueFiles = dcount(SourceFileName),
    Sites = dcount(Site_Url)
  by UserId, bin(TimeGenerated, 30m)
| where FileCount > 100 or UniqueFiles > 50
| project TimeGenerated, UserId, FileCount, UniqueFiles, Sites
// MITRE: T1530 Data from Cloud Storage Object | FP: Migration scripts, backup tools

Query 9: OAuth app granted high-privilege delegated permissions

AuditLogs
| where TimeGenerated > ago(7d)
| where OperationName in ("Consent to application","Add delegated permission grant")
| extend AppName = tostring(TargetResources[0].displayName)
| extend Scopes = tostring(TargetResources[0].modifiedProperties[?(@.displayName == 'DelegatedPermissionGrant.Scope')].newValue)
| where Scopes has_any ("Mail.ReadWrite","Files.ReadWrite.All","Directory.ReadWrite.All","offline_access")
| project TimeGenerated, InitiatedBy, AppName, Scopes
// MITRE: T1550.001 Use Alternate Authentication Material | FP: Legitimate OAuth integrations

On-Premises Windows Event Forwarding Queries

These queries assume Windows Security event logs are forwarded to Sentinel via the Log Analytics agent or Azure Monitor Agent.

Query 10: Kerberoasting detection (TGS request for service account SPN)

SecurityEvent
| where TimeGenerated > ago(7d)
| where EventID == 4769  // Kerberos service ticket request
| where AccountName !endswith "$"  // Exclude computer accounts
| extend ServiceName = tostring(TargetServerName)
| extend TicketEncryptionType = tostring(TicketEncryptionType)
| where TicketEncryptionType == "0x17"  // RC4 encryption -- weak, target of Kerberoasting
| summarize
    RequestCount = count(),
    Services = make_set(ServiceName)
  by Account, IpAddress, bin(TimeGenerated, 5m)
| where RequestCount > 5  // Multiple service ticket requests suggests enumeration
| project TimeGenerated, Account, IpAddress, RequestCount, Services
// MITRE: T1558.003 Kerberoasting | FP: Legitimate service connections with RC4 required

Query 11: DC Sync attack pattern (multiple DRSUAPI RPC calls from non-DC)

SecurityEvent
| where TimeGenerated > ago(7d)
| where EventID == 4662  // Operation performed on AD object
| where AccessMask == "0x100"  // Replication rights
| where Properties has "1131f6aa-9c07-11d1-f79f-00c04fc2dcd2"  // DS-Replication-Get-Changes GUID
| where SubjectUserName !endswith "$"  // Exclude computer accounts (DCs replicate legitimately)
| project TimeGenerated, SubjectUserName, SubjectLogonId, ObjectName, IpAddress
// MITRE: T1003.006 DCSync | FP: Backup products with replication rights, AD management tools

Query 12: New local admin account created on a domain-joined server

SecurityEvent
| where TimeGenerated > ago(7d)
| where EventID == 4720  // User account created
| join kind=inner (
    SecurityEvent
    | where EventID == 4732  // Member added to security-enabled local group
    | where TargetUserName == "Administrators"
) on Computer, $left.SubjectLogonId == $right.SubjectLogonId
| project TimeGenerated, Computer, SubjectUserName, TargetUserName, IpAddress
// MITRE: T1136.001 Create Local Account | FP: Legitimate local admin provisioning via GPO or automation

Operationalizing Hunting Queries in Sentinel

Convert successful hunting queries into scheduled analytics rules for continuous monitoring:

  1. In Sentinel, open Hunting > Queries
  2. Create new query, paste KQL, add MITRE ATT&CK mapping, entity mapping (Account, IP, Host), and description
  3. Run the query against the last 30 days to baseline the expected result volume
  4. For queries with consistent and actionable results, promote to Analytics rule: from the query, click 'Create detection rule'
  5. Set the query schedule (5-15 minutes for high-priority, hourly for lower-priority)
  6. Set the alert threshold (1 result = alert for high-confidence queries, higher threshold for noisy queries)
  7. Configure entity mapping to automatically extract entities (accounts, IPs, hosts) from results for correlation

For tuning false positives, use watchlists:

// Exclude known VPN exit IPs from impossible travel detection
let VPN_IPs = _GetWatchlist('known-vpn-ips') | project SearchKey;
SigninLogs
| where IPAddress !in (VPN_IPs)
| [rest of impossible travel query]

Sentinel watchlists allow tuning without modifying query logic -- add IP addresses, user accounts, or hostnames to a watchlist and reference it in queries to exclude known-safe entities.

The bottom line

These 12 queries cover the most behaviorally consistent attacker patterns across Entra ID, Azure, M365, and on-premises Windows environments in Sentinel. Run them as hunting queries first to tune out false positives, then promote to scheduled analytics rules. Use MITRE ATT&CK mappings in Sentinel to track coverage and identify which techniques in your threat model are not yet covered by hunting queries or detection rules.

Frequently asked questions

What log tables do I need to enable for these queries to work in Sentinel?

The queries in this guide require: SigninLogs and AADNonInteractiveUserSignInLogs (Entra ID sign-in logs -- enable via Entra ID diagnostic settings), AuditLogs (Entra ID audit -- same diagnostic settings), AzureActivity (Azure resource activity -- enable via subscription diagnostic settings), OfficeActivity (M365 -- enable via the Sentinel M365 connector), and SecurityEvent (Windows event logs -- requires Log Analytics agent or Azure Monitor Agent on Windows servers). Check your workspace Tables list to confirm which tables are populated before running queries.

How do I handle the Sentinel free 10 GB/day ingestion tier limit without truncating critical log sources?

Prioritize the high-fidelity log sources: Entra ID sign-in and audit logs, AzureActivity, OfficeActivity, and Security events from Tier 0 servers are the minimum for effective threat hunting. Windows Security events from all domain-joined workstations are the highest-volume source. To reduce workstation event volume, configure the Windows Security Event collection level in Sentinel to 'Common' rather than 'All Events' -- this captures the critical event IDs (4624, 4625, 4648, 4688, 4698, 4702, 4720, 4732, 4769) without collecting low-value events like object access noise.

How should I structure a threat hunting session using these queries?

Start with a hypothesis based on current threat intelligence or a recent incident type in your industry. Select the relevant queries (e.g., if the hypothesis is 'cloud credential theft via phishing', run the device code auth, impossible travel, and OAuth consent queries). Run each query over a 7-30 day window. Investigate any results that deviate from expected baselines. Bookmark suspicious findings in Sentinel. If a finding is confirmed as malicious, escalate to incident. If confirmed as benign, document the tuning action (add IP to watchlist, add user to exclusion group). End the session with a hunting log entry noting what was checked, what was found, and tuning changes made.

What is the difference between Sentinel hunting queries and analytics rules?

Hunting queries are manually executed -- a threat hunter runs them interactively as part of a proactive investigation. They have no automated alerting or scheduling. Analytics rules are automated -- they run on a schedule (as frequent as every 5 minutes), generate alerts when results exceed a threshold, create incidents, and integrate with playbooks for automated response. The workflow is: write a query as a hunting query first to validate it and tune false positives, then promote it to an analytics rule once it consistently produces actionable signal.

How do I use KQL to build a baseline and detect anomalies rather than fixed thresholds?

Use the KQL `percentile()` or `stdev()` aggregation functions over a historical window to establish a baseline, then compare the current period against it. Example pattern: calculate each user's average hourly sign-in count over the past 30 days with `summarize avg_logins=avg(LoginCount) by UserPrincipalName` in a `let` statement, then join the current hour's count against the baseline and flag users whose current count exceeds `avg_logins + (2 * stdev_logins)`. The `series_decompose_anomalies()` function provides built-in anomaly detection on time series data and is well-suited for metrics like authentication volume, data download size, or API call rate per identity.

How do I write a KQL query to detect password spray attacks in Entra ID sign-in logs?

Password spray produces many failed sign-ins with error code 50126 (invalid credentials) across multiple accounts from a single or small number of source IPs, often at a low rate per account to avoid lockout. KQL query for Microsoft Sentinel: `SigninLogs | where ResultType == '50126' | summarize FailedAttempts=count(), DistinctAccounts=dcount(UserPrincipalName) by IPAddress, bin(TimeGenerated, 1h) | where DistinctAccounts > 10 and FailedAttempts > 20 | order by DistinctAccounts desc`. A high ratio of distinct accounts to failed attempts from a single IP in a short window is the key spray indicator. Also check for error code 50034 (user does not exist) spikes from the same IP, which indicates username enumeration preceding the spray.

Sources & references

  1. Microsoft: Hunt for threats with Microsoft Sentinel
  2. Microsoft: KQL quick reference
  3. Microsoft Sentinel GitHub: Community hunting queries

Free resources

25
Free download

Critical CVE Reference Card 2025–2026

25 actively exploited vulnerabilities with CVSS scores, exploit status, and patch availability. Print it, pin it, share it with your SOC team.

No spam. Unsubscribe anytime.

Free download

Ransomware Incident Response Playbook

Step-by-step 24-hour IR checklist covering detection, containment, eradication, and recovery. Built for SOC teams, IR leads, and CISOs.

No spam. Unsubscribe anytime.

Free newsletter

Get threat intel before your inbox does.

50,000+ security professionals read Decryption Digest for early warnings on zero-days, ransomware, and nation-state campaigns. Free, daily, no spam.

Unsubscribe anytime. We never sell your data.

Eric Bang
Author

Founder & Cybersecurity Evangelist, Decryption Digest

Cybersecurity professional with expertise in threat intelligence, vulnerability research, and enterprise security. Covers zero-days, ransomware, and nation-state operations for 50,000+ security professionals every morning.

Black Hat Giveaway

Win a $2,495 Black Hat pass.

Full-access to Black Hat USA 2026 in Las Vegas. Subscribe free to enter.

Joins Decryption Digest daily briefing. Unsubscribe anytime.

Giveaway: Black Hat USA 2026 Full-Access Pass ($2,495 value)

Details →
Daily Briefing

Subscribe to enter the giveaway

Every subscriber is automatically entered. You also get daily threat intel every morning: zero-days, ransomware, and nation-state campaigns. Free. No spam.

Already subscribed? You're already entered.

Giveaway

Win a $2,495 Black Hat USA 2026 pass.