Build a proactive threat hunting program in Microsoft Sentinel. craft KQL hunting queries mapped to MITRE ATT&CK techniques, leverage bookmarks and livestream, create coverage workbooks, and establish a systematic hunting cadence.
In this advanced lab you will build threat hunting queries mapped to MITRE ATT&CK techniques across Initial Access, Persistence, and Lateral Movement tactics. You will use Sentinel bookmarks to capture evidence, livestream queries for real-time detection, promote hunting results to analytics rules, and build a workbook that visualizes your ATT&CK coverage. closing gaps and strengthening your organization’s security posture.
A government agency with 10,000 users has been targeted by APT groups conducting low-and-slow intrusions. Their SOC operates reactively. investigating alerts only after they fire. A recent audit revealed that only 30% of MITRE ATT&CK techniques are covered by existing analytics rules, leaving critical gaps in Initial Access, Execution, Persistence, Privilege Escalation, and Lateral Movement. Leadership has mandated a systematic threat hunting program to reduce mean dwell time from 21 days to under 48 hours.
The median dwell time for advanced threat actors remains 10·21 days globally, with some APTs persisting for months undetected. Reactive detection alone is insufficient. attackers actively evade analytics rules. Proactive threat hunting closes blind spots by hypothesis-driven investigation of telemetry that has not triggered alerts. Organizations with mature hunting programs detect breaches 60% faster and reduce breach costs by an average of $1.1M (IBM Cost of a Data Breach 2024). MITRE ATT&CK provides the common language to measure and systematically expand your coverage.
SigninLogs, AuditLogs, and SecurityEvent data ingested for the queries to return meaningful results.
MITRE ATT&CK is a globally accessible knowledge base of adversary tactics and techniques based on real-world observations. Microsoft Sentinel natively integrates ATT&CK by allowing you to tag analytics rules, hunting queries, and incidents with specific technique IDs.
Before hunting, you need a baseline of your current detection posture. Use this KQL query against the Sentinel API metadata to enumerate which ATT&CK techniques have active coverage:
// Enumerate analytics rules and their MITRE ATT&CK technique mappings
// PURPOSE: Audit which ATT&CK techniques are covered by active analytics rules
// WHY: Gaps in technique coverage = blind spots attackers can exploit undetected
let ActiveRules = _GetWatchlist('AnalyticsRuleTemplates')
| where Status == "Enabled"; // Only active (not disabled) rules
//
// Alternative approach: Query SecurityAlert table for technique tags
// This works even without the watchlist - uses historical alert data
SecurityAlert
| where TimeGenerated > ago(30d) // Last 30 days of alert history
| extend Techniques = extract_all(@"(T\d{4}(\.\d{3})?)", tostring(ExtendedProperties))
// Regex extracts MITRE technique IDs: T1078, T1110.003, etc.
// Matches both techniques (T####) and sub-techniques (T####.###)
| mv-expand Technique = Techniques // Expand arrays - one row per technique per alert
| summarize
RuleCount = dcount(AlertName), // How many distinct rules map to this technique
Rules = make_set(AlertName) // Which rules cover this technique
by tostring(Technique) // Group by MITRE technique ID
| sort by RuleCount desc
// Expected output: Ranked list of techniques by detection coverage
// Techniques at the top = well covered; missing techniques = priority gaps
// ACTION: Cross-reference with MITRE ATT&CK Navigator to visualize coverageNavigate to Microsoft Sentinel → Hunting → + New Query. Create the following hunting queries for Initial Access techniques.
Hunt for compromised valid accounts by detecting sign-ins from unusual locations, impossible travel, or sign-ins to rarely-accessed applications:
// T1078 - Valid Accounts: Detect sign-ins from new countries not seen in past 14 days
// PURPOSE: Hunt for compromised accounts being used from unfamiliar geolocations
// WHY: Attackers often operate from different countries than the legitimate user
// MITRE ATT&CK: T1078 - Valid Accounts (Initial Access)
//
// Step 1: Build a baseline of known countries per user over the past 14 days
let LookbackPeriod = 14d; // Baseline window - defines "normal" behavior
let HuntingWindow = 1d; // Hunt window - look for anomalies in the last 24 hours
let KnownCountries = SigninLogs
| where TimeGenerated between (ago(LookbackPeriod) .. ago(HuntingWindow))
| where ResultType == 0 // Only successful sign-ins for baseline
| summarize by UserPrincipalName, Country = LocationDetails.countryOrRegion;
//
// Step 2: Find recent sign-ins from countries NOT in the user’s baseline
SigninLogs
| where TimeGenerated > ago(HuntingWindow) // Only the last 24 hours
| where ResultType == 0 // Only successful sign-ins (actual access)
| extend Country = tostring(LocationDetails.countryOrRegion)
| join kind=leftanti KnownCountries // leftanti = keep rows with NO match
on UserPrincipalName, Country // Match on user + country combination
//
// Step 3: Summarize anomalous sign-ins per user+country
| summarize
FirstSeen = min(TimeGenerated), // When the new-country sign-in started
LastSeen = max(TimeGenerated), // Most recent sign-in from new country
SigninCount = count(), // How many sign-ins from this new country
Apps = make_set(AppDisplayName, 5), // Which apps were accessed
IPs = make_set(IPAddress, 5) // Source IPs from the new country
by UserPrincipalName, Country
| where SigninCount >= 1 // Any new-country sign-in is suspicious
| sort by SigninCount desc
// Expected output: Users signing in from countries they’ve never used before
// ACTION: Verify with user; if unauthorized → disable account, revoke sessions
// FALSE POSITIVES: Business travelers, newly deployed VPN in a new regionCorrelate email delivery events with risky sign-in behavior within a short time window:
// T1566 - Phishing: Correlate suspicious email delivery with risky sign-in behavior
// PURPOSE: Detect phishing emails that led to actual account compromise
// WHY: Email alone is noisy; correlating with risky sign-ins finds SUCCESSFUL phishing
// MITRE ATT&CK: T1566 - Phishing (Initial Access)
let TimeWindow = 2h; // Max time between email delivery and suspicious sign-in
EmailEvents
| where TimeGenerated > ago(1d) // Last 24 hours of email data
| where ThreatTypes has "Phish" or DeliveryAction == "Delivered" // Phishing or delivered emails
| extend RecipientUPN = tolower(RecipientEmailAddress) // Normalize for case-insensitive join
| join kind=inner ( // inner join = only matching pairs
SigninLogs
| where TimeGenerated > ago(1d)
| where RiskLevelDuringSignIn in ("high", "medium") // Only risky sign-ins
| extend SigninUPN = tolower(UserPrincipalName) // Normalize to match email recipient
| project SigninTime = TimeGenerated, SigninUPN, IPAddress,
RiskLevel = RiskLevelDuringSignIn, AppDisplayName
) on $left.RecipientUPN == $right.SigninUPN // Join on user identity
| where SigninTime between (TimeGenerated .. (TimeGenerated + TimeWindow))
// Sign-in must occur AFTER email delivery, within the 2-hour window
| project
EmailTime = TimeGenerated, SigninTime,
User = RecipientUPN, Subject, SenderFromAddress,
IPAddress, RiskLevel, AppDisplayName
// Expected output: Users who received phishing email AND had a risky sign-in within 2h
// ACTION: HIGH PRIORITY - likely successful phishing; disable account, investigate email contentHunt-T[ID]-[ShortDescription]. This makes it easy to filter and correlate in the Hunting blade and in workbook visualizations.
Hunt for new accounts created outside of standard provisioning workflows, especially accounts created by non-admin users or service principals:
// T1136 - Create Account: Detect unauthorized account creation outside business hours
// PURPOSE: Hunt for rogue accounts created outside standard provisioning workflows
// WHY: Attackers create backdoor accounts for persistence during off-hours to avoid detection
// MITRE ATT&CK: T1136 - Create Account (Persistence)
let ProvisioningHoursStart = 8; // 8 AM - start of normal business/provisioning window
let ProvisioningHoursEnd = 18; // 6 PM - end of normal business/provisioning window
AuditLogs
| where TimeGenerated > ago(7d) // Look back 7 days
| where OperationName in ("Add user", "Add member to group", "Add service principal")
// These operations create identities that could be used for persistence
| extend
InitiatedBy = tostring(InitiatedBy.user.userPrincipalName), // Human initiator
InitiatedByApp = tostring(InitiatedBy.app.displayName), // App/automation initiator
TargetUser = tostring(TargetResources[0].displayName), // New account display name
TargetUPN = tostring(TargetResources[0].userPrincipalName), // New account UPN
HourOfDay = hourofday(TimeGenerated), // Hour (0-23) of creation
DayOfWeek = dayofweek(TimeGenerated) // Day of week (0=Sun)
| where HourOfDay !between (ProvisioningHoursStart .. ProvisioningHoursEnd)
or DayOfWeek in (6d, 0d) // Flag weekend creation (Sat=6d, Sun=0d)
| project
TimeGenerated, OperationName,
InitiatedBy, InitiatedByApp,
TargetUser, TargetUPN,
HourOfDay, DayOfWeek
| sort by TimeGenerated desc
// Expected output: Accounts created outside business hours or on weekends
// ACTION: Verify with HR/IT provisioning team; unknown creators = investigate immediately
// FALSE POSITIVES: Automated provisioning systems, global teams in different time zonesHunt for unexpected role assignments, especially high-privilege roles like Global Administrator or Security Administrator:
// T1098 - Account Manipulation: Detect sensitive Entra ID role assignments
// PURPOSE: Hunt for unexpected privilege escalation via high-impact role grants
// WHY: Attackers assign themselves admin roles to maintain persistent elevated access
// MITRE ATT&CK: T1098 - Account Manipulation (Persistence / Privilege Escalation)
let SensitiveRoles = dynamic([
"Global Administrator", "Security Administrator",
"Exchange Administrator", "SharePoint Administrator",
"Privileged Role Administrator", "Application Administrator",
"Cloud Application Administrator", "User Administrator"
]); // High-privilege roles that grant broad access - any unexpected assignment is critical
AuditLogs
| where TimeGenerated > ago(7d) // Last 7 days of audit activity
| where OperationName == "Add member to role" // Role assignment operation
| extend
TargetUser = tostring(TargetResources[0].userPrincipalName), // Who received the role
RoleAssigned = tostring(TargetResources[0].modifiedProperties[1].newValue), // Which role
InitiatedBy = coalesce(
tostring(InitiatedBy.user.userPrincipalName), // Human who assigned the role
tostring(InitiatedBy.app.displayName)) // Or app/automation that did it
| where RoleAssigned has_any (SensitiveRoles) // Only high-privilege role assignments
| project
TimeGenerated, InitiatedBy, TargetUser,
RoleAssigned, OperationName,
CorrelationId // Use to correlate with other audit events
| sort by TimeGenerated desc
// Expected output: Recent sensitive role assignments with who-assigned-what-to-whom
// ACTION: Cross-reference with PIM activations and change management tickets
// Unexpected assignments = investigate immediately for privilege escalation attackHunt for lateral movement through remote desktop or SSH by detecting connections between unusual host pairs:
// T1021 - Remote Services: Detect NEW RDP connections between hosts
// PURPOSE: Find lateral movement via RDP to host pairs never seen in the baseline
// WHY: Attackers use RDP to move between compromised systems - new pairs = anomalous
// MITRE ATT&CK: T1021 - Remote Services (Lateral Movement)
let Baseline = 14d; // Baseline period: what’s "normal" for your environment
let HuntWindow = 1d; // Hunt window: look for anomalies in the last 24 hours
//
// Step 1: Build baseline of known RDP source→target pairs from the past 14 days
let KnownPairs = SecurityEvent
| where TimeGenerated between (ago(Baseline) .. ago(HuntWindow))
| where EventID == 4624 // Windows logon success event
| where LogonType == 10 // LogonType 10 = RemoteInteractive (RDP)
| summarize by SourceIP = IpAddress, TargetHost = Computer;
//
// Step 2: Find recent RDP connections that are NOT in the baseline
SecurityEvent
| where TimeGenerated > ago(HuntWindow) // Only the last 24 hours
| where EventID == 4624 // Successful logon
| where LogonType == 10 // RDP connections only
| extend SourceIP = IpAddress, TargetHost = Computer
| join kind=leftanti KnownPairs on SourceIP, TargetHost // Remove known pairs
| summarize
FirstSeen = min(TimeGenerated), // When this new RDP pair first appeared
LastSeen = max(TimeGenerated), // Most recent connection
ConnectionCount = count(), // How many times this new pair connected
Accounts = make_set(TargetUserName, 5) // Which accounts were used
by SourceIP, TargetHost
| sort by ConnectionCount desc
// Expected output: Newly observed RDP connections not seen in the prior 14 days
// ACTION: High ConnectionCount from unexpected sources = likely lateral movement
// FALSE POSITIVES: New admin workstations, IT help desk remote sessionsHunt for token replay attacks by detecting sign-ins where the same token or session is used from different IP addresses:
// T1550 - Alternate Auth Material: Detect token replay from multiple IPs
// PURPOSE: Hunt for stolen tokens/cookies being reused from different network locations
// WHY: If the same session (CorrelationId) appears from multiple IPs, the token was stolen
// MITRE ATT&CK: T1550 - Use Alternate Authentication Material (Lateral Movement)
SigninLogs
| where TimeGenerated > ago(1d) // Last 24 hours
| where ResultType == 0 // Only successful authentications
| where TokenIssuerType == "AzureAD" // Entra ID-issued tokens (not federated/on-prem)
| summarize
IPCount = dcount(IPAddress), // Unique IPs using the same session
IPs = make_set(IPAddress, 10), // List the IPs for investigation
Countries = make_set(LocationDetails.countryOrRegion, 10), // Geographic spread
Apps = make_set(AppDisplayName, 5), // Which apps the token accessed
FirstSeen = min(TimeGenerated), // Session start
LastSeen = max(TimeGenerated) // Most recent use
by UserPrincipalName, CorrelationId // Group by user + session correlation ID
| where IPCount > 1 // Same session from >1 IP = token replay
| extend TimeDelta = LastSeen. FirstSeen // Time span of the session
| where TimeDelta < 30m // Within 30 min = not a natural IP change
| sort by IPCount desc
// Expected output: Sessions where the same token was used from different IPs
// ACTION: High confidence token theft - revoke sessions, force re-authentication
// FALSE POSITIVES: VPN/proxy users may show multiple IPs; check named locationsWhen a hunting query returns interesting results, bookmarks let you save specific rows as evidence. Bookmarks are persisted in Sentinel and can be promoted to incidents.
T1078, InitialAccess, Hunt-2026-Q1Hunt-2026-Q1-W3), the MITRE technique ID, and a severity rating. This makes it trivial to generate hunt reports and track patterns over time.
Livestream provides a near-real-time feed of hunting query results directly within the Sentinel portal. It is ideal for active hunting sessions where you want to observe an adversary’s actions as they happen.
// Livestream: Monitor sensitive Entra ID role assignments in real time
// PURPOSE: Get instant visibility into privilege escalation as it happens
// WHY: Role assignment attacks are fast - by the time a scheduled rule fires, damage is done
// MITRE ATT&CK: T1098 - Account Manipulation
// NOTE: Livestream queries run continuously and show results as they appear
AuditLogs
| where OperationName == "Add member to role" // Only role assignment operations
| extend
TargetUser = tostring(TargetResources[0].userPrincipalName), // Who received the role
RoleAssigned = tostring(TargetResources[0].modifiedProperties[1].newValue), // Which role
InitiatedBy = coalesce(
tostring(InitiatedBy.user.userPrincipalName), // Human initiator
tostring(InitiatedBy.app.displayName)) // Or app/automation initiator
| project TimeGenerated, InitiatedBy, TargetUser, RoleAssigned
// Expected output: Real-time feed of role assignments as they occur
// ACTION: Bookmark any unexpected assignments immediately for investigationWhen a hunting query consistently identifies true positive activity, promote it to a scheduled analytics rule so Sentinel detects the behavior automatically going forward.
// Promoted analytics rule: Detect sign-ins from new (never-before-seen) countries
// Originally a hunting query (T1078), now automated as a scheduled detection
// Schedule: Run every 1h, lookback 1d - caught during hunting, now runs automatically
// MITRE ATT&CK: T1078 - Valid Accounts (Initial Access)
let LookbackPeriod = 14d; // Baseline: 14 days of historical country data per user
let HuntingWindow = 1h; // Detection window: check last hour for new countries
//
// Build per-user country baseline from the past 14 days
let KnownCountries = SigninLogs
| where TimeGenerated between (ago(LookbackPeriod) .. ago(HuntingWindow))
| where ResultType == 0 // Only successful sign-ins for baseline
| summarize by UserPrincipalName, Country = tostring(LocationDetails.countryOrRegion);
//
// Find sign-ins from countries NOT in each user’s baseline
SigninLogs
| where TimeGenerated > ago(HuntingWindow) // Last hour only (matches run frequency)
| where ResultType == 0 // Successful sign-ins = actual access granted
| extend Country = tostring(LocationDetails.countryOrRegion)
| join kind=leftanti KnownCountries on UserPrincipalName, Country // Exclude known countries
| summarize
SigninCount = count(), // Sign-ins from the new country
IPs = make_set(IPAddress, 5) // Source IPs for investigation
by UserPrincipalName, Country
| where SigninCount >= 1 // Any new-country sign-in triggers the alert
// Entity mapping: Account → UserPrincipalName, IP → IPsCreate a custom workbook that visualizes your MITRE ATT&CK hunting coverage, tracks hunt cycles, and shows coverage trends over time.
</>){
"version": "Notebook/1.0",
"items": [
{
"type": 1,
"content": {
"json": "# ?? MITRE ATT&CK Hunting Coverage Dashboard\n\nThis workbook tracks threat hunting activity mapped to MITRE ATT&CK.\n\n---"
}
},
{
"type": 3,
"content": {
"version": "KqlItem/1.0",
"query": "HuntingBookmark\n| where TimeGenerated > ago(90d)\n| extend Technique = tostring(Tags)\n| summarize BookmarkCount = count() by Technique\n| sort by BookmarkCount desc",
"size": 1,
"title": "Bookmarks by MITRE Technique",
"queryType": 0,
"visualization": "barchart"
}
},
{
"type": 3,
"content": {
"version": "KqlItem/1.0",
"query": "HuntingBookmark\n| where TimeGenerated > ago(90d)\n| summarize Bookmarks = count() by bin(TimeGenerated, 7d)\n| render timechart",
"size": 1,
"title": "Hunting Activity Over Time (Weekly)",
"queryType": 0,
"visualization": "timechart"
}
},
{
"type": 3,
"content": {
"version": "KqlItem/1.0",
"query": "SecurityAlert\n| where TimeGenerated > ago(30d)\n| extend Techniques = extract_all(@\"(T\\d{4})\", tostring(ExtendedProperties))\n| mv-expand Technique = Techniques\n| summarize AlertCount = count() by tostring(Technique)\n| sort by AlertCount desc\n| take 20",
"size": 1,
"title": "Top 20 ATT&CK Techniques Triggering Alerts",
"queryType": 0,
"visualization": "barchart"
}
},
{
"type": 3,
"content": {
"version": "KqlItem/1.0",
"query": "let AllTechniques = datatable(Tactic:string, TechniqueId:string) [\n 'InitialAccess', 'T1078',\n 'InitialAccess', 'T1566',\n 'Persistence', 'T1136',\n 'Persistence', 'T1098',\n 'LateralMovement', 'T1021',\n 'LateralMovement', 'T1550'\n];\nlet Covered = SecurityAlert\n| where TimeGenerated > ago(30d)\n| extend Techniques = extract_all(@\"(T\\d{4})\", tostring(ExtendedProperties))\n| mv-expand Technique = Techniques\n| distinct tostring(Technique);\nAllTechniques\n| extend IsCovered = iff(TechniqueId in (Covered), 'Covered', 'Gap')\n| summarize count() by Tactic, IsCovered",
"size": 1,
"title": "Coverage Gaps by Tactic",
"queryType": 0,
"visualization": "barchart"
}
}
],
"styleSettings": {},
"$schema": "https://github.com/Microsoft/Application-Insights-Workbooks/blob/master/schema/workbook.json"
}HuntingBookmark table combined with a static ATT&CK technique list to calculate the percentage.
A one-time hunt has limited value. To achieve the government agency’s goal of reducing dwell time from 21 days to under 48 hours, you need a structured, recurring program.
| Cadence | Activity | Owner |
|---|---|---|
| Weekly | Execute all active hunting queries, review bookmark queue, update workbook | Threat Hunter |
| Bi-weekly | Develop 2·3 new hunting queries based on threat intelligence reports | Threat Hunter + CTI Analyst |
| Monthly | Review coverage workbook, promote successful hunts to analytics rules, retire low-value queries | SOC Lead |
| Quarterly | Compare ATT&CK coverage to baseline, publish hunt report to leadership, set priorities for next quarter | CISO / SOC Manager |
Every hunt should start with a documented hypothesis. Use this format:
Hunt ID: HUNT-2026-Q1-007
Hypothesis: APT actors are using stolen OAuth tokens to access
mailboxes from infrastructure outside our known IP ranges.
MITRE Technique: T1550.001. Application Access Token
Data Sources: SigninLogs, OfficeActivity, CloudAppEvents
KQL Query: Hunt-T1550-OAuthTokenReuse
Timeframe: Last 14 days
Expected Result: Identify sessions using tokens from IPs not in
our named location list.
Outcome: [ ] True Positive → Incident
[ ] Benign True Positive → Allowlist
[ ] No Findings → Close hunt
[ ] Needs Tuning → Refine query
Analyst: J. Smith
Date: 2026-03-05Hunting queries run against data already ingested in your Log Analytics workspace. There is no additional cost for running hunting queries, bookmarks, or livestream beyond your existing workspace ingestion charges. Workbooks are free. Analytics rules are included in the Sentinel pricing tier.
| Resource | Description |
|---|---|
| Threat hunting in Microsoft Sentinel | Overview of proactive threat hunting capabilities |
| Hunting bookmarks in Microsoft Sentinel | Save and annotate hunting query results for investigation |
| Livestream in Microsoft Sentinel | Monitor hunting queries in real time with livestream sessions |
| Jupyter Notebooks in Microsoft Sentinel | Advanced hunting and investigation with notebooks |
| MITRE ATT&CK Framework | Knowledge base of adversary tactics, techniques, and procedures |
| MITRE ATT&CK coverage for Microsoft Sentinel | Map your detections to the MITRE ATT&CK framework |
| Hunting with the REST API | Automate hunting queries programmatically |