Skip to content

Sentinel & Identity Threat Hunting Queries

Production hunting queries for Microsoft Sentinel querying SigninLogs, AADNonInteractiveUserSignInLogs, AuditLogs, and OfficeActivity.


  • 🎯 What it looks for: Sequences of successful authentication events (ResultType == 0) for the same user originating from two distinct geographic countries within a physical travel time window of under 120 minutes.
  • πŸ’‘ Why it matters: Indicates stolen primary session cookies (PRT) or access tokens being replayed through proxy networks or attacker infrastructure located overseas while the legitimate user is actively working from their home country.
let Lookback = 24h;
let ThresholdHours = 2h;
SigninLogs
| where TimeGenerated > ago(Lookback)
| where ResultType == 0 // Successful sign-in only
| where isnotempty(LocationDetails.countryOrRegion)
| project TimeGenerated, UserPrincipalName, IPAddress,
Country = tostring(LocationDetails.countryOrRegion),
City = tostring(LocationDetails.city)
| sort by UserPrincipalName, TimeGenerated asc
| serialize
| extend PrevTime = prev(TimeGenerated), PrevCountry = prev(Country), PrevUser = prev(UserPrincipalName), PrevIP = prev(IPAddress)
| where UserPrincipalName == PrevUser and Country != PrevCountry
| extend TimeDelta = datetime_diff('minute', TimeGenerated, PrevTime)
| where TimeDelta <= 120 // Less than 2 hours apart
| project TimeGenerated, UserPrincipalName, PreviousCountry = PrevCountry, CurrentCountry = Country, TimeDeltaMinutes = TimeDelta, PreviousIP = PrevIP, CurrentIP = IPAddress

🚿 2. Password Spray Detection (Horizontal Brute Force)

Section titled β€œπŸšΏ 2. Password Spray Detection (Horizontal Brute Force)”
  • 🎯 What it looks for: A single external source IP address attempting failed authentications (ResultType 50126 or 50053) across 10 or more distinct corporate user accounts within a 6-hour rolling window.
  • πŸ’‘ Why it matters: Adversaries use password spraying (testing a single weak password like Summer2026! against thousands of corporate accounts) to avoid triggering per-user account lockout thresholds. Spotting the spray at the source IP allows instant blocking in Conditional Access or network firewalls.
let Lookback = 6h;
let FailureThreshold = 10; // 10 distinct users targeted
SigninLogs
| where TimeGenerated > ago(Lookback)
| where ResultType in (50126, 50053, 50056) // Invalid password, account locked
| summarize
FailedAttempts = count(),
TargetedAccounts = dcount(UserPrincipalName),
AccountList = make_set(UserPrincipalName, 20),
StartTime = min(TimeGenerated),
EndTime = max(TimeGenerated)
by IPAddress, AppDisplayName
| where TargetedAccounts >= FailureThreshold
| sort by TargetedAccounts desc

πŸ“¬ 3. Suspicious Mailbox Forwarding / Redirect Rules (BEC Indicator)

Section titled β€œπŸ“¬ 3. Suspicious Mailbox Forwarding / Redirect Rules (BEC Indicator)”
  • 🎯 What it looks for: Exchange Online audit telemetry (OfficeActivity) recording the creation of new inbox forwarding rules (ForwardTo, RedirectTo, ForwardAsAttachmentTo) routing messages to external domain addresses (.com, .xyz, webmail providers).
  • πŸ’‘ Why it matters: The hallmark of Business Email Compromise (BEC). Once inside an executive or finance mailbox, attackers create stealth redirect rules to intercept billing threads, wire transfer approvals, and invoice PDF attachments while moving incoming alerts to the RSS or Deleted Items folders.
let Lookback = 7d;
OfficeActivity
| where TimeGenerated > ago(Lookback)
| where RecordType == "ExchangeItem" or Operation in~ ("New-InboxRule", "Set-InboxRule", "UpdateInboxRules")
| extend RuleDetail = parse_json(Parameters)
| mv-expand RuleDetail
| where RuleDetail.Name in~ ("ForwardTo", "RedirectTo", "ForwardAsAttachmentTo")
or RuleDetail.Value has_any ("@", ".com", ".net", ".xyz")
| project TimeGenerated, UserId, ClientIP, Operation,
RuleParam = RuleDetail.Name,
ForwardDestination = RuleDetail.Value
| sort by TimeGenerated desc

πŸ”‘ 4. Service Principal Credential / Secret Addition (Token Theft & Persistence)

Section titled β€œπŸ”‘ 4. Service Principal Credential / Secret Addition (Token Theft & Persistence)”
  • 🎯 What it looks for: Audit log events recording the generation of new application certificates or client secrets (Add service principal credentials) assigned to existing Microsoft Entra enterprise applications or App Registrations.
  • πŸ’‘ Why it matters: High-level persistence that bypasses user credential rotation. If an attacker gains temporary Global Administrator or Application Administrator rights, they generate an API key for a trusted Service Principal with pre-existing Microsoft Graph permissions, allowing permanent API access even after the administrator account is reset.
let Lookback = 14d;
AuditLogs
| where TimeGenerated > ago(Lookback)
| where LoggedByService =~ "Core Directory"
| where OperationName in~ ("Add service principal credentials", "Update application - Certificates and secrets management")
| extend Initiator = tostring(InitiatedBy.user.userPrincipalName)
| extend TargetApp = tostring(TargetResources[0].displayName)
| extend TargetKeyType = tostring(TargetResources[0].modifiedProperties[0].displayName)
| project TimeGenerated, OperationName, Initiator, TargetApp, TargetKeyType, Result, TargetResources
| sort by TimeGenerated desc

πŸ›‘οΈ 5. Sudden Privileged Role Activation (PIM Abuse)

Section titled β€œπŸ›‘οΈ 5. Sudden Privileged Role Activation (PIM Abuse)”
  • 🎯 What it looks for: Entra Privileged Identity Management (PIM) audit events showing sudden elevation, role assignment modifications, or persistent memberships granted for Tier-0 roles (Global Administrator, Privileged Role Administrator, Security Administrator).
  • πŸ’‘ Why it matters: Attackers who compromise user credentials with eligible PIM assignments immediately activate the role to alter security settings, disable Conditional Access, or exfiltrate sensitive cloud data. Detecting the activation in real-time allows security teams to verify whether the elevation was business-sanctioned or malicious.
AuditLogs
| where TimeGenerated > ago(7d)
| where LoggedByService =~ "PIM" or OperationName has "Add member to role"
| where TargetResources[0].modifiedProperties has_any ("Global Administrator", "Privileged Role Administrator", "Security Administrator")
| extend Actor = tostring(InitiatedBy.user.userPrincipalName)
| extend RoleGranted = tostring(TargetResources[0].modifiedProperties[1].newValue)
| extend TargetUser = tostring(TargetResources[2].displayName)
| project TimeGenerated, OperationName, Actor, RoleGranted, TargetUser, ResultDescription
| sort by TimeGenerated desc