Most SOC teams spend their days responding to alerts. Alerts that fire, get triaged, get closed, get reopened. The cycle is exhausting — and it misses the point. The adversaries worth worrying about are the ones your alerts never catch.
Threat hunting inverts the model. Instead of waiting for the SIEM to raise its hand, a hunter starts with a hypothesis — a belief about how an attacker might be operating inside your environment — and goes looking for evidence. This article is about doing that in Microsoft Sentinel, with real queries you can run today.
1. Why Threat Hunting Matters
Detection engineering covers known TTPs. Threat hunting covers the unknown-knowns: techniques you haven’t written rules for yet, attackers who’ve already bypassed your perimeter, and lateral movement that looks like noise.
The numbers bear this out. Industry research consistently shows mean dwell time — how long adversaries sit undetected — measured in weeks. Alerts don’t catch them. Hunters do.
Three categories of hunting value for cloud SOC practitioners:
- Hypothesis-driven hunting: You suspect credential harvesting is occurring; you look for it proactively.
- Anomaly-based hunting: Baselines diverge — logins at 3am from a new country — you investigate before an alert would fire.
- Intel-driven hunting: A threat feed publishes new IOCs; you hunt your environment against them immediately.
Hunting is not about replacing detections. It’s about discovering the gaps in your detection coverage — then closing them.
2. Sentinel’s Hunting Capabilities
Microsoft Sentinel ships with a dedicated hunting experience that most teams underuse. Here’s what matters:
Hunting Queries Gallery
Sentinel’s built-in gallery contains 200+ community-contributed KQL queries mapped to MITRE ATT&CK techniques. Use these as starting points, not endpoints. Every environment is different — tune aggressively.
Bookmarks
When a hunting query surfaces interesting results, bookmark the relevant rows. Bookmarks persist across sessions and can be promoted to incidents. This creates a traceable record of your investigation chain — critical for post-hunt reporting.
Livestream
Run a query, pin it as a Livestream, and Sentinel will re-execute it every 30 seconds. Useful when you’re actively hunting and want near-real-time feedback without spinning up a full alert rule.
Notebooks
For complex hunts — especially ML-assisted ones — Sentinel integrates with Azure ML notebooks. Bring your own Python, run statistical baselines, visualise anomaly clusters. The Jupyter environment connects directly to your Log Analytics workspace.
Watchlists
Watchlists let you bring external context into your KQL. Examples: high-value asset lists, known-admin IP ranges, terminated employee accounts. Joining watchlists to security events is one of the most powerful and underused capabilities in Sentinel.
3. Practical Hunting Queries
The following KQL queries are production-grade starting points. Each targets a specific TTP category. Adjust table names, time ranges, and thresholds for your environment.
Hunt 1: Impossible Travel / Credential Abuse
Detects the same account authenticating from geographically distant locations within a short window — a classic indicator of credential compromise or token theft.
// Impossible Travel Detection
let timeWindow = 2h;
SigninLogs
| where TimeGenerated > ago(24h)
| where ResultType == "0" // Successful sign-ins only
| project TimeGenerated, UserPrincipalName,
IPAddress, Location,
lat = toreal(LocationDetails.geoCoordinates.latitude),
lon = toreal(LocationDetails.geoCoordinates.longitude)
| join kind=inner (
SigninLogs
| where TimeGenerated > ago(24h)
| where ResultType == "0"
| project TimeGenerated2=TimeGenerated,
UserPrincipalName,
IPAddress2=IPAddress, Location2=Location,
lat2=toreal(LocationDetails.geoCoordinates.latitude),
lon2=toreal(LocationDetails.geoCoordinates.longitude)
) on UserPrincipalName
| where abs(TimeGenerated - TimeGenerated2) < timeWindow
| where IPAddress != IPAddress2
// Haversine approximation for distance
| extend DistKm = 111.2 * sqrt(
pow(lat - lat2, 2) +
pow((lon - lon2) * cos(radians((lat + lat2) / 2)), 2))
| where DistKm > 500
| project UserPrincipalName, TimeGenerated, Location,
TimeGenerated2, Location2, DistKm
| order by DistKm desc
Hunt 2: Dormant Account Reactivation
Accounts that haven’t authenticated in 90+ days suddenly becoming active is a high-fidelity hunting signal — often indicates compromised credentials or insider threat.
// Dormant Account Reactivation Hunt
let dormantThreshold = 90d;
let recentWindow = 7d;
let dormantAccounts = SigninLogs
| where TimeGenerated between (
ago(dormantThreshold + 30d) .. ago(dormantThreshold))
| where ResultType == "0"
| summarize LastSeen = max(TimeGenerated)
by UserPrincipalName
| where LastSeen < ago(dormantThreshold);
SigninLogs
| where TimeGenerated > ago(recentWindow)
| where ResultType == "0"
| join kind=inner dormantAccounts on UserPrincipalName
| project UserPrincipalName, TimeGenerated,
IPAddress, Location, LastSeen,
DormantDays = datetime_diff('day',
TimeGenerated, LastSeen)
| order by DormantDays desc
Hunt 3: Privilege Escalation via Role Assignment
Detecting new Azure RBAC Owner or Privileged Role Administrator assignments — especially outside business hours or from unfamiliar principals.
// Privilege Escalation: High-Priv Role Assignment
AuditLogs
| where TimeGenerated > ago(30d)
| where OperationName has "Add member to role"
| extend Role = tostring(
TargetResources[0].modifiedProperties[1]
.newValue)
| extend Actor = tostring(
InitiatedBy.user.userPrincipalName)
| extend Target = tostring(
TargetResources[0].userPrincipalName)
| where Role has_any (
"Owner",
"Global Administrator",
"Privileged Role Administrator",
"User Access Administrator")
| project TimeGenerated, Actor, Target,
Role, CorrelationId
| order by TimeGenerated desc
Hunt 4: Exfil Candidate — Anomalous SharePoint Downloads
Identifies users downloading significantly more data than their personal 30-day baseline — a data exfiltration precursor signal.
// SharePoint Exfil: Baseline vs Spike
let baselineWindow = 30d;
let huntWindow = 1d;
let userBaseline = OfficeActivity
| where TimeGenerated between (
ago(baselineWindow) .. ago(huntWindow))
| where Operation =~ "FileDownloaded"
| summarize BaselineAvgDownloads =
count() / 30.0
by UserId;
OfficeActivity
| where TimeGenerated > ago(huntWindow)
| where Operation =~ "FileDownloaded"
| summarize TodayCount = count() by UserId
| join kind=inner userBaseline on UserId
| extend Multiplier = TodayCount / max_of(
BaselineAvgDownloads, 1.0)
| where Multiplier > 5
| project UserId, TodayCount,
BaselineAvgDownloads, Multiplier
| order by Multiplier desc
4. Real Example: Enterprise Threat Hunt
Scenario: Supply Chain Compromise via Partner Tenant
A financial services firm noticed unusual external collaboration activity in their M365 audit logs. The initial alert was low-severity — a guest account accessing SharePoint. A routine triage would have closed it. A hunter didn’t.
Phase 1 — Hypothesis Formation
The hunter’s hypothesis: if this guest account is compromised, the attacker will have used it to access high-value document libraries and potentially invite additional guests to extend their foothold.
Phase 2 — Initial Pivots
// Phase 2: Guest Account Activity Pivot
let suspectUPN = "external.user@partnerdomain.com";
union
(SigninLogs | where UserPrincipalName =~ suspectUPN),
(OfficeActivity | where UserId =~ suspectUPN),
(AuditLogs
| where InitiatedBy.user.userPrincipalName
=~ suspectUPN)
| project TimeGenerated, Type,
OperationName, IPAddress,
Location, Details=tostring(pack_all())
| order by TimeGenerated asc
Result: 3 distinct IP addresses across 2 countries. The guest account had authenticated from both the partner’s known corporate IP range and a residential IP in a different geography on the same day.
Phase 3 — Lateral Discovery
// Phase 3: Guest-Initiated Invitations
AuditLogs
| where TimeGenerated > ago(14d)
| where InitiatedBy.user.userPrincipalName
=~ "external.user@partnerdomain.com"
| where OperationName has_any (
"Invite external user",
"Add member to role",
"Update application")
| project TimeGenerated, OperationName,
TargetResources, CorrelationId
Result: The guest had invited two additional external addresses within 48 hours of their first anomalous login. Classic pivot-and-persist pattern.
Phase 4 — Containment
The hunt findings were escalated as a P1 incident. The guest account was disabled, the new external invitees were reviewed (one had already accepted), and partner tenant contacts were alerted. The entire hunt took 4 hours from hypothesis to containment.
Post-Hunt Outcome: 3 New Detection Rules
- Guest account impossible travel rule (threshold: 500km in under 2 hours)
- Guest-initiated external invitation within 72 hours of first login
- Multi-IP authentication for B2B guest accounts with cross-session geolocation mismatch
The hunt found what no existing alert would have caught. That’s the entire point. Every successful hunt should close at least one detection gap.
5. Building a Repeatable Hunting Practice
Ad-hoc hunting is better than nothing. Systematic hunting compounds over time. Here’s the minimal viable structure:
- Document every hunt in a shared notebook — hypothesis, queries, pivots, findings
- Maintain a hunt backlog: new threat intel, customer environment changes, MITRE ATT&CK coverage gaps
- Run dedicated hunt sprints — at minimum, 2 focused hunting sessions per month
- Gate each hunt on a written hypothesis. No hypothesis, no hunt.
- Every hunt should output either a new detection rule or a watchlist update
The teams that do this well treat hunting as a forcing function for detection engineering. The hunt uncovers the blind spot; the detection closes it; the coverage improves.
Published by SunExplains — Cloud SOC Practitioner Series | Surya, CISSP · SC-100 · AZ-500 · PCNSE
Leave a Reply