Security Azure

KQL Threat Hunting Playbooks: MITRE ATT&CK Mapping, UEBA, and Hunting Notebooks

Threat hunting is not “run more queries until something looks weird.” It is a disciplined loop: form a hypothesis grounded in a specific adversary technique, write a query that would falsify or confirm it, triage the residual, and either close the hunt or promote it into a durable detection. Microsoft Sentinel and Microsoft Defender XDR give you the substrate — KQL (Kusto Query Language) over a data lake of security telemetry, the Hunts experience, UEBA enrichment, livestream, and Jupyter notebooks via MSTICPy — but none of that is a program on its own. The value comes from how you wire the substrate into a repeatable playbook that a rotating SOC team can run next quarter and get the same rigour.

The premise that runs through everything below: every hunt starts as a written hypothesis tied to a MITRE ATT&CK technique ID, every successful hunt ends as a tracked artifact (a bookmark, a tuned analytics rule, or a watchlist), and nothing is one-off. If you cannot re-run it next quarter without re-deriving it from scratch, it was a demo, not a playbook. This article is the hunting program I stand up for teams that are drowning in alert queue and want to get ahead of the adversary rather than perpetually behind the last incident — the methodology, the ATT&CK plumbing, the tables you actually query, the KQL patterns that surface the five or six behaviours that matter, the UEBA that does baselining for you, the notebooks that turn a query into an investigation, and the operational machinery that stops good hunts from evaporating.

Read the prose once for the why; then keep the tables open. This is a reference you return to at 02:00 mid-hunt, so the technique-to-table mappings, the event-ID lists, the ResultType codes, the KQL operator cheat-sheets and the tier comparisons are all laid out as scannable tables. A hunter who memorises which table carries which signal, and which KQL primitive surfaces which behaviour, is three moves ahead of one who greps documentation while the adversary dwells.

What problem this solves

Detections answer questions you already knew to ask. They are precise, tuned, and — by construction — blind to anything you did not anticipate. The gap between “what my rules catch” and “what an adversary can do” is the dwell time window, and it is where breaches live: the 2024-era industry medians still put attacker dwell in the days-to-weeks range for many intrusions, most of that spent doing things no alert fired on. Threat hunting is the deliberate, human-led practice of closing that gap — proactively looking for the techniques your automated detections miss, before they become an incident.

Without a hunting program, three failure modes recur. First, coverage theatre: a green MITRE ATT&CK heatmap the CISO shows the board, where “covered” means “a rule exists” — never mind that half the rules are disabled, noisy, or have never fired usefully. Second, one-shot brilliance: a senior analyst finds something real on a Friday, fixes it, and the knowledge lives in their head; the same technique walks back in six months and nobody remembers the query. Third, tool-driven flailing: analysts run ad-hoc KQL with no hypothesis, chasing whatever looks odd, producing no measurable coverage and no reproducible artifact — motion without progress.

Who hits this: every SOC running Sentinel and/or Defender XDR at scale. It bites hardest on teams with large, high-volume telemetry (endpoint, sign-in, and cloud-audit logs measured in hundreds of GB/day) where the interesting signal is a needle in noise, on teams under E5/A5 licensing who have the data but not the practice, and on any org whose board metric is “techniques covered” rather than “techniques covered and validated in the last quarter.” The fix is not a product purchase — it is a methodology, a set of KQL patterns, and the operational discipline to promote what works and retire what doesn’t.

To frame the field before the deep dive, here is every hunt type this article equips you to run, the ATT&CK ground it covers, and the primary substrate it runs on:

Hunt class What you are looking for Primary ATT&CK tactics Primary tables Substrate
Rare / low-prevalence Processes, binaries, or connections seen on very few hosts Execution, Persistence, Defense Evasion DeviceProcessEvents, DeviceImageLoadEvents Defender XDR / Sentinel
Beaconing / C2 Regular, machine-like outbound intervals Command and Control, Exfiltration DeviceNetworkEvents, CommonSecurityLog Defender XDR / Sentinel
Lateral movement Unusual host-to-host auth, RDP/SMB/WinRM spread Lateral Movement, Discovery SecurityEvent, DeviceLogonEvents, IdentityLogonEvents Sentinel / Defender for Identity
Credential access Kerberoasting, DCSync, LSASS access, spraying Credential Access SecurityEvent, SigninLogs, DeviceEvents Sentinel / Defender XDR
Identity / ATO Anomalous sign-ins, new geo/ASN, token theft Initial Access, Persistence, Defense Evasion SigninLogs, AADNonInteractiveUserSignInLogs, BehaviorAnalytics Sentinel UEBA
Cloud / SaaS abuse Consent grants, forwarding rules, mass download Persistence, Collection, Exfiltration OfficeActivity, CloudAppEvents, AuditLogs Sentinel / Defender for Cloud Apps
Insider / peer-outlier Legitimate access that is anomalous for the peer group Collection, Exfiltration BehaviorAnalytics, UserPeerAnalytics Sentinel UEBA

Learning objectives

By the end of this article you can:

Prerequisites & where this fits

You should already be comfortable with KQL fundamentals — where, summarize, join, project, extend, mv-expand, and time-bucketing with bin() and make-series. If those are shaky, read KQL for Azure Monitor and Log Analytics: From Joins to Time-Series, Without Blowing the Budget and KQL That Earns Its Keep: summarize, join, make-series and Time-Bucketing Azure Logs first; this article assumes them and spends its budget on the hunting layer above. You should know how a Log Analytics workspace is structured (tables, retention, RBAC) — see How to Create a Log Analytics Workspace — because a Sentinel workspace is a Log Analytics workspace with the Sentinel solution enabled.

You should understand, at least in outline, the ATT&CK matrix (tactics as columns, techniques as cells) and the Microsoft security portfolio: Sentinel (cloud SIEM/SOAR on Log Analytics), Defender XDR (the unified XDR portal spanning endpoint, identity, email, and cloud-app signals via advanced hunting), Defender for Identity (on-prem AD/Entra Connect signal), and Defender for Cloud Apps (SaaS/CASB). This article sits at the top of the detection-engineering track: hunting is where new detections are born before they graduate into the standing rule set. It pairs with Operationalizing Entra ID Protection (which supplies risk signals you hunt around), Governing OAuth Consent and Application Permissions in Entra ID (the consent-grant attack surface), Taming Shadow IT and Risky SaaS, and Engineering Incident Response: Runbooks, Tabletop Exercises, and Cloud Forensics (where a confirmed hunt hands off). For infrastructure-as-code onboarding of the SIEM itself, see Terraform Module: Azure Microsoft Sentinel.

A quick map of who owns which layer during a hunt, so escalation is fast:

Layer What lives here Who usually owns it Hunt classes it feeds
Endpoint (MDE) Process, file, registry, network, image-load events Endpoint / EDR team Rare-process, beaconing, credential access
Identity (Entra + MDI) Sign-ins, directory changes, on-prem auth IAM / identity team ATO, lateral movement, credential access
Email + collab (MDO) Mail flow, URL clicks, attachments Messaging / SecOps Phishing, initial access, collection
Cloud apps (MDCA) SaaS audit, OAuth grants, file activity Cloud security Consent abuse, exfiltration, insider
SIEM (Sentinel) Correlated logs, UEBA, watchlists, rules SOC / detection engineering All — the correlation plane
Notebooks (MSTICPy) Ad-hoc investigation, enrichment, ML Threat-hunt / IR team Deep-dive on any confirmed lead

Core concepts

Six mental models make every later query obvious.

A hunt is a hypothesis test, not a search. The unit of work is not “a query” but “a claim about adversary behaviour that data can refute.” “Show me all PowerShell” is a search; “If an operator moved laterally with a stolen credential in the last 14 days, then I will see runas /netonly-style NewCredentials logons (type 9) from a workstation to a server that this account has never touched” is a hypothesis. The difference is falsifiability: a hypothesis names the table, the signal, and the condition under which you would close the hunt as negative. That discipline is what lets you measure coverage and avoid infinite exploratory drift.

The status code names the platform’s view, not the truth. Sentinel and Defender surface signal, not verdict. A high InvestigationPriority, a UEBA “first time from country,” a rare binary — these are leads, not confirmations. Every hunt produces a residual that a human must adjudicate as benign or malicious. The skill is shrinking the residual to something a person can read in one screen, then deciding whether what remains is explainable.

Sentinel and Defender XDR are two front-ends over overlapping data. Sentinel’s tables (SecurityEvent, SigninLogs, OfficeActivity, CommonSecurityLog, plus UEBA) live in a Log Analytics workspace and you query them in the Logs/Hunting blade. Defender XDR’s advanced hunting tables (Device*, Email*, Identity*, Cloud*, Alert*, UrlClickEvents) live in the Defender data store with 30-day retention and no separate ingestion charge. Since the unified SOC platform (Sentinel onboarded into the Defender portal, GA 2024), advanced hunting in the Defender portal can query both sets in one query — you can join a DeviceProcessEvents row to a SigninLogs row. Know which store a table lives in; it drives retention, cost, and where you write the query.

Telemetry is a pyramid of pain. David Bianco’s Pyramid of Pain ranks indicators by how much they cost the adversary to change: hash values and IPs are trivial to rotate (bottom), while TTPs (tools, tactics, techniques, procedures) are expensive (top). Good hunts target the top of the pyramid — behaviours like “process spawned from Office with an encoded command line,” not “this specific SHA256” — because behaviour-based hunts survive the adversary swapping infrastructure. Every KQL pattern below is deliberately behavioural.

UEBA does the baselining you would otherwise script. “Is this normal for this user/host?” is the recurring hunting question, and computing per-entity baselines by hand is expensive. Sentinel’s UEBA engine ingests SigninLogs, AuditLogs, SecurityEvent, and more, and writes enriched, baselined signal to queryable tables (BehaviorAnalytics chief among them), folding dozens of behavioural features into one InvestigationPriority score. Hunting against pre-baselined data is dramatically faster than rolling your own series_decompose for every entity.

Coverage is the program metric. A hunt’s worth to the program is the ATT&CK ground it retires. Tagging every hunt and rule with tactics and technique IDs turns a pile of queries into a measurable map — but only if you measure effective coverage (a rule that fired and was triaged) and not intended coverage (a rule that merely exists). The two diverge fast and the gap is where breaches hide.

The vocabulary in one table

Pin the moving parts before the deep sections; the glossary repeats these for lookup:

Term One-line definition Where it lives Why it matters
Hypothesis A falsifiable claim about adversary behaviour Your hunt record The unit of a real hunt
KQL Kusto Query Language — read-only analytics over logs Sentinel + Defender The one language for all hunting
Hunt (experience) Sentinel object: hypothesis + queries + bookmarks + status Sentinel Hunting blade Persists a hunt as a tracked artifact
Bookmark A saved query-result row with notes + entity mappings HuntingBookmark table The unit of evidence
Livestream Near-real-time test of a hunting query against live data Sentinel Hunting Validate a query before it’s a rule
BehaviorAnalytics UEBA output: per-activity enrichment + priority score Log Analytics The fastest anomaly signal
InvestigationPriority 0–10 UEBA score folding many behavioural features BehaviorAnalytics Rank leads in one number
MSTICPy Microsoft’s open-source Python hunting library Jupyter notebook Turns a query into an investigation
Analytics rule A saved query that runs on a schedule and raises incidents Sentinel The promotion target for a proven hunt
Summary rule Scheduled aggregation of high-volume data into a compact table Sentinel Hunt cheaply at scale
ATT&CK technique A specific adversary behaviour with a T-ID MITRE matrix The anchor that makes hunts measurable

The hunting methodologies: hypothesis-driven and PEAK

A methodology is what stops hunting from being a talented individual’s hobby. Three related frameworks give the program its shape.

The Sqrrl hunting loop

The original, still the clearest: create a hypothesis → investigate via tools and techniques → uncover new patterns and TTPs → inform and enrich automated analytics, then loop. The last arrow is the one teams drop — a hunt that finds something must feed back into detections, threat intel, and the next hypothesis, or the loop is broken and you re-hunt the same ground forever.

PEAK: Prepare, Execute, Act with Knowledge

PEAK (the framework popularised by Splunk’s SURGe team) organises each hunt into three phases and recognises three types of hunt. The phases:

The three PEAK hunt types are the practical menu — pick the one that fits your data and maturity:

PEAK hunt type You use it when… Method Output Example
Hypothesis-driven You have a specific technique in mind Test a falsifiable claim with targeted KQL Confirm/refute + a detection candidate “Kerberoasting via RC4 tickets (T1558.003)”
Baseline (exploratory data analysis) You don’t yet know what normal looks like Profile a data set, find the shape, flag outliers A baseline + anomalies worth chasing “Profile all outbound by process; what stands out?”
Model-Assisted (M-ATH) Volume/complexity defeats eyeballing ML/statistics (clustering, anomaly scoring) surface candidates A model + scored leads “Cluster failed-logon patterns with autocluster()

The Hunting Maturity Model

Bianco’s Hunting Maturity Model (HMM) tells you where your program stands and what to build next:

Level Name Analytics Hunting capability
HMM0 Initial Relies on automated alerting only No hunting; alerts drive everything
HMM1 Minimal Incorporates threat-intel indicators Searches for known IOCs
HMM2 Procedural Follows others’ hunt procedures Runs published hunts, little novelty
HMM3 Innovative Creates new hunt procedures Develops its own hypotheses and analytics
HMM4 Leading Automates successful hunts into detections Continuously converts hunts to automation

The goal is HMM3→HMM4: a team that invents hunts and automates the ones that work. The tell that you’re stuck at HMM1/2 is a backlog full of “search for these IOCs from the latest report” and no home-grown, behaviour-based hypotheses.

Writing a hypothesis that’s actually testable

A hunt hypothesis has three parts: the technique, the observable, and the falsifiable claim. Write it before you touch KQL.

Hypothesis (T1098.001 — Additional Cloud Credentials): “An attacker who phished an Entra admin would add a credential or federated trust to an app/service principal to persist. If that happened in the last 14 days, then I will see Add service principal credentials, certificate/secret additions, or Add federated identity credential in AuditLogs from an unusual actor or IP. I close this hunt negative if every such event maps to a known change ticket.”

That sentence dictates the query — you are testing a claim, not exploring:

// Hunt: T1098.001 — credentials/secrets/federation added to app registrations & SPs
let lookback = 14d;
AuditLogs
| where TimeGenerated > ago(lookback)
| where OperationName in (
    "Add service principal credentials",
    "Update application - Certificates and secrets management",
    "Add application key",
    "Add federated identity credential")
| where Result == "success"
| extend Actor   = tostring(InitiatedBy.user.userPrincipalName)
| extend ActorIp = tostring(InitiatedBy.user.ipAddress)
| extend TargetApp = tostring(TargetResources[0].displayName)
| where isnotempty(Actor)                 // exclude system / sync-driven changes
| project TimeGenerated, Actor, ActorIp, TargetApp, OperationName, Result
| sort by TimeGenerated desc

Two habits make this reusable instead of throwaway. Parameterize the volatile bits with let at the top (lookback, allow-lists, thresholds) so the next hunter tunes one block, not the whole query. Project a stable schema (Actor, ActorIp, TargetApp) so notebook enrichment, bookmarking, and entity-mapping always find the same columns. A playbook is a contract on its output shape as much as its logic. The properties of a hypothesis worth running:

Property Good hypothesis Bad hypothesis
ATT&CK-anchored Names a technique ID (T1558.003) “Look for weird auth”
Falsifiable States what a negative result looks like Can never be “done”
Data-grounded You already collect the table it needs Needs telemetry you don’t have
Scoped in time Bounded lookback (14d, 30d) “All time” — unbounded cost
Environment-aware Reflects your real exposure/crown jewels Generic, ignores your assets
Single-claim Tests one behaviour Bundles five unrelated ideas

Mapping hunts to MITRE ATT&CK and tracking coverage

Coverage is the metric that turns hunting from activity into a program. ATT&CK Enterprise organises adversary behaviour into 14 tactics (the “why” — the adversary’s goal) each containing techniques and sub-techniques (the “how”). Every saved hunting query and analytics rule in Sentinel carries tactics and technique IDs as first-class metadata; populate them deliberately and the platform draws you a map.

The 14 Enterprise tactics, in kill-chain order, with the tables where you most often hunt each:

Tactic (ID) Adversary goal Primary hunt tables
Reconnaissance (TA0043) Gather info to plan the attack SigninLogs, external TI
Resource Development (TA0042) Build/buy capabilities External TI, AuditLogs
Initial Access (TA0001) Get a foothold SigninLogs, EmailEvents, OfficeActivity
Execution (TA0002) Run adversary code DeviceProcessEvents, SecurityEvent (4688)
Persistence (TA0003) Keep the foothold AuditLogs, DeviceRegistryEvents, OfficeActivity
Privilege Escalation (TA0004) Gain higher permissions SecurityEvent (4672/4728), AuditLogs
Defense Evasion (TA0005) Avoid detection SecurityEvent (1102), DeviceProcessEvents
Credential Access (TA0006) Steal credentials SecurityEvent (4769/4662), DeviceEvents, SigninLogs
Discovery (TA0007) Learn the environment DeviceProcessEvents, SecurityEvent (4661)
Lateral Movement (TA0008) Move through the network SecurityEvent (4624), DeviceLogonEvents, IdentityLogonEvents
Collection (TA0009) Gather target data OfficeActivity, CloudAppEvents
Command and Control (TA0011) Communicate with implants DeviceNetworkEvents, CommonSecurityLog
Exfiltration (TA0010) Steal the data DeviceNetworkEvents, OfficeActivity, CloudAppEvents
Impact (TA0040) Destroy/disrupt/ransom SecurityEvent, DeviceFileEvents, AuditLogs

A pragmatic starter set of high-value techniques, the KQL signal that reveals each, and the table it lives in — this is a hunt backlog in table form:

Technique ATT&CK ID KQL signal Table
Password spraying T1110.003 Many accounts, few attempts/account, one IP; ResultType == 50126 SigninLogs
Kerberoasting T1558.003 EventID 4769, TicketEncryptionType == "0x17" (RC4), many SPNs SecurityEvent
DCSync T1003.006 EventID 4662 with DS-Replication GUID, non-DC account SecurityEvent
LSASS access T1003.001 Process opening lsass.exe handle DeviceEvents
Additional cloud creds T1098.001 Add service principal credentials AuditLogs
Illicit consent grant T1528 Consent to application to risky/new app AuditLogs
Email forwarding rule T1114.003 New-InboxRule with ForwardTo/RedirectTo external OfficeActivity
Beaconing C2 T1071.001 Low coefficient-of-variation inter-arrival times DeviceNetworkEvents
RDP lateral movement T1021.001 EventID 4624 LogonType 10, new src→dst pair SecurityEvent
Cloud storage exfil T1567.002 Large FileUploaded to unsanctioned app CloudAppEvents
Audit-log cleared T1070.001 EventID 1102 SecurityEvent
Rare service install T1543.003 EventID 7045, low prevalence SecurityEvent

Technique tags as structured metadata

In a scheduled analytics rule (and in saved hunting queries), the ATT&CK mapping is structured fields, not a comment. Bicep for a rule that tags the technique correctly:

resource credAddRule 'Microsoft.SecurityInsights/alertRules@2024-03-01' = {
  scope: workspace
  name: guid('hunt-T1098-cred-add')
  kind: 'Scheduled'
  properties: {
    displayName: 'Credential added to service principal (T1098.001)'
    severity: 'Medium'
    enabled: true
    query: loadTextContent('queries/t1098-sp-cred-add.kql')
    queryFrequency: 'PT1H'
    queryPeriod: 'P14D'
    triggerOperator: 'GreaterThan'
    triggerThreshold: 0
    tactics: [ 'Persistence', 'PrivilegeEscalation' ]
    techniques: [ 'T1098' ]
    subTechniques: [ 'T1098.001' ]
    entityMappings: [
      {
        entityType: 'Account'
        fieldMappings: [ { identifier: 'FullName', columnName: 'Actor' } ]
      }
    ]
  }
}

tactics, techniques, and subTechniques are the fields the MITRE ATT&CK coverage blade reads. subTechniques arrived later than techniques and is honoured on recent alertRules API versions (2023-02-01 and newer) — pin a current @version or the property is silently dropped and your sub-technique granularity disappears.

The metadata fields that drive coverage, and what each does:

Field Type Purpose Gotcha
tactics array of tactic names Shades the matrix columns Use ATT&CK names, not IDs, here
techniques array of T-IDs Shades technique cells T1098, not 1098
subTechniques array of T.sub-IDs Sub-technique granularity Needs API ≥ 2023-02-01
entityMappings array Binds columns to entities for investigation Wrong identifier breaks the graph
severity enum Incident priority Not coverage, but drives triage
queryPeriod / queryFrequency ISO8601 duration Lookback vs cadence queryPeriod ≥ queryFrequency

Intended vs effective coverage

Open Sentinel → MITRE ATT&CK to see the matrix shaded by your active rules and hunting queries. Treat the gaps as a backlog — but treat the green cells with suspicion, because the blade measures intent to detect, not efficacy. A green cell backed by a noisy, never-tuned, or disabled rule is worse than an empty one: it lies to the board. Pull coverage programmatically to track it over time rather than eyeballing the heatmap:

# Export technique coverage from active analytics rules
az rest --method get \
  --url "https://management.azure.com/subscriptions/$SUB/resourceGroups/$RG/providers/Microsoft.OperationalInsights/workspaces/$WS/providers/Microsoft.SecurityInsights/alertRules?api-version=2024-03-01" \
  --query "value[?properties.enabled].properties.{name:displayName, techniques:techniques, tactics:tactics}" -o table

Then join intended coverage (a rule is tagged with the technique) against effective coverage (that rule actually fired and someone triaged it) so the two move together:

// Effective coverage: which tagged techniques actually produced alerts in 90 days?
SecurityAlert
| where TimeGenerated > ago(90d)
| mv-expand Technique = todynamic(Techniques)
| extend Technique = tostring(Technique)
| where Technique matches regex @"^T\d{4}"
| summarize Fires = count(), Rules = dcount(AlertName), LastFired = max(TimeGenerated)
    by Technique
| sort by Fires asc     // techniques at the top: tagged but silent — your amber list

The two coverage states you must not conflate:

State Definition How you measure it What it tells the board
Intended A rule/hunt is tagged with the technique Matrix blade / alertRules API “We meant to detect this”
Effective The rule fired and was triaged in the window SecurityAlert fire counts + incident closure “We actually detect this, validated”
Amber (the gap) Tagged but zero useful fires in 90d Effective join shows Fires = 0 “Assume this is a blind spot”

The tables you hunt in

Hunting is table-literate work: knowing which table carries which signal is most of the speed. Here is the data-source-to-tactic map — the tables you will live in, what they carry, where they live, and the tactics they light up:

Table Carries Store Tactics it lights up
SecurityEvent Windows Security event log (via AMA/MMA) Log Analytics Execution, Cred Access, Lateral Movement, Priv Esc
SigninLogs Interactive Entra sign-ins Log Analytics Initial Access, Credential Access
AADNonInteractiveUserSignInLogs Token-refresh / non-interactive sign-ins Log Analytics Persistence, Defense Evasion
AuditLogs Entra directory changes Log Analytics Persistence, Priv Esc
OfficeActivity Exchange/SharePoint/OneDrive/Entra audit Log Analytics Collection, Exfiltration, Persistence
DeviceProcessEvents Process creation (MDE) Defender store Execution, Discovery, Defense Evasion
DeviceNetworkEvents Network connections (MDE) Defender store C2, Exfiltration
DeviceLogonEvents Endpoint logons (MDE) Defender store Lateral Movement
IdentityLogonEvents On-prem/hybrid auth (MDI) Defender store Lateral Movement, Credential Access
EmailEvents Mail flow (MDO) Defender store Initial Access, Collection
CloudAppEvents SaaS audit (MDCA) Defender store Exfiltration, Persistence
BehaviorAnalytics UEBA enrichment + priority Log Analytics All (as anomaly overlay)

Because the same conceptual signal often has two table names (Sentinel vs Defender store), keep this Rosetta stone handy — it saves you rewriting a query when you move between portals:

Signal Sentinel table (Log Analytics) Defender advanced-hunting table
Entra interactive sign-in SigninLogs AADSignInEventsBeta (preview)
Windows process creation SecurityEvent (EventID 4688) DeviceProcessEvents
Endpoint logon SecurityEvent (EventID 4624) DeviceLogonEvents
On-prem AD auth SecurityEvent (DCs) IdentityLogonEvents
Email delivery EmailEvents (if connected) EmailEvents
Cloud-app audit OfficeActivity / McasShadowItReporting CloudAppEvents
Alerts SecurityAlert AlertInfo + AlertEvidence

SecurityEvent: the Windows security log

SecurityEvent is the richest single table for endpoint and domain hunting — the Windows Security event log forwarded by the Azure Monitor Agent (AMA) via a Data Collection Rule. The event IDs you hunt by:

EventID Meaning Hunt use
4624 Successful logon Lateral movement (by LogonType), new src→dst pairs
4625 Failed logon Spraying/brute force (by count, IP)
4648 Logon with explicit credentials runas, credential reuse
4672 Special privileges assigned Privileged logon tracking
4688 Process creation (+ command line if audited) Execution, LOLBins, encoded commands
4720 User account created Persistence
4728 / 4732 / 4756 Member added to security-enabled group Priv esc (e.g. Domain Admins)
4768 / 4769 Kerberos TGT / service ticket requested Kerberoasting, golden/silver ticket
4771 Kerberos pre-auth failed AS-REP roasting, brute force
4776 NTLM credential validation Pass-the-hash, spraying
4662 Operation on an AD object DCSync (with replication GUID)
4661 Handle to an object requested Discovery / SAMR enumeration
5140 / 5145 Network share accessed (detailed) Lateral movement, collection
7045 Service installed Persistence, rare-service hunts
1102 Audit log cleared Defense evasion
4104 PowerShell script block logged Obfuscated execution (separate channel)

Logon types on 4624/4625 are the single most useful discriminator in lateral-movement hunts — memorise them:

LogonType Name Means Hunt relevance
2 Interactive At the console Physical/RDP console
3 Network SMB, file share, WinRM Lateral movement, share access
4 Batch Scheduled task Persistence execution
5 Service Service start Service-account abuse
7 Unlock Workstation unlock Low value
8 NetworkCleartext Creds sent in cleartext IIS basic auth, risky
9 NewCredentials runas /netonly Lateral movement with alt creds
10 RemoteInteractive RDP RDP lateral movement
11 CachedInteractive Cached domain creds Offline logon

SigninLogs: identity is the new perimeter

SigninLogs carries interactive Entra sign-ins; AADNonInteractiveUserSignInLogs carries token-refresh and background auth (often the larger and more interesting volume for token-theft hunts). Key columns:

Column Carries Hunt use
UserPrincipalName The account Entity pivot
AppDisplayName Target app Which app is being abused
IPAddress / Location Source IP + geo Impossible travel, new geo
ResultType Numeric outcome code Success/failure classification
ClientAppUsed Legacy vs modern auth Legacy-auth (no MFA) hunts
AuthenticationRequirement SFA vs MFA MFA-bypass hunts
ConditionalAccessStatus CA outcome Policy-gap hunts
RiskLevelDuringSignIn / RiskState Entra ID Protection risk Risk-correlated hunts
DeviceDetail OS/browser/compliance Unmanaged-device hunts
AuthenticationDetails Per-factor result MFA-fatigue / failed-factor hunts

The ResultType codes you actually branch on:

ResultType Meaning Hunt relevance
0 Success Baseline; the “it worked” set
50126 Invalid username or password Spraying / brute force
50053 Account locked (smart lockout) Aggressive brute force
50055 Password expired Low value
50057 Account disabled Attempts on dead accounts = recon
50074 Strong auth (MFA) required MFA challenged
50076 MFA required by CA Policy enforced
50079 MFA registration required New-device / new-user
50158 External security challenge not satisfied CA / external auth
53003 Blocked by Conditional Access Policy blocked the attempt
65001 Consent granted Illicit consent correlation
530032 Blocked by security defaults Legacy auth blocked

DeviceProcessEvents: endpoint execution

DeviceProcessEvents (MDE advanced hunting) is where execution, discovery, and defense-evasion hunts live. The columns that carry signal:

Column Carries Hunt use
FileName / FolderPath The process image + path LOLBins from odd paths
SHA256 / SHA1 / MD5 Hashes Prevalence, IOC match
ProcessCommandLine Full command line Encoded commands, flags
ProcessId PID Process-tree correlation
AccountName / AccountDomain Runs-as identity Whose context
InitiatingProcessFileName Parent process Office→cmd, unusual parents
InitiatingProcessCommandLine Parent command line Full attack chain
InitiatingProcessParentFileName Grandparent Three generations of the tree
DeviceName / DeviceId The host Prevalence, entity pivot

The wider Defender-store table families, so you know where to reach:

Family Tables Signal
Device DeviceProcessEvents, DeviceNetworkEvents, DeviceFileEvents, DeviceRegistryEvents, DeviceLogonEvents, DeviceImageLoadEvents, DeviceEvents, DeviceInfo, DeviceNetworkInfo Endpoint (MDE)
Email EmailEvents, EmailAttachmentInfo, EmailUrlInfo, EmailPostDeliveryEvents, UrlClickEvents Mail + collab (MDO)
Identity IdentityLogonEvents, IdentityQueryEvents, IdentityDirectoryEvents On-prem/hybrid identity (MDI)
Cloud CloudAppEvents SaaS audit (MDCA)
Alerts AlertInfo, AlertEvidence Cross-product alerts + evidence

OfficeActivity: the collaboration audit

OfficeActivity unifies the Microsoft 365 unified audit log (Exchange, SharePoint, OneDrive, and some Entra events) in Sentinel. The operations that matter for hunting:

Operation Workload Technique it reveals
New-InboxRule / Set-InboxRule / UpdateInboxRules Exchange T1114.003 forwarding-rule exfil
Add-MailboxPermission Exchange T1098 mailbox delegation
MailItemsAccessed Exchange T1114 mail collection (throttled signal)
Send / SendAs / SendOnBehalf Exchange Impersonation, BEC
FileDownloaded / FileSyncDownloadedFull SharePoint/OneDrive Mass-download exfil
FileUploaded SharePoint/OneDrive Staging / exfil to cloud
SharingSet / AnonymousLinkCreated SharePoint/OneDrive Data exposure, exfil
Add member to role Entra (via O365) Privilege escalation
Update StsRefreshTokenValidFrom Timestamp Entra Token revocation / session reset abuse
Consent to application Entra T1528 illicit consent

KQL hunting query patterns

Hunters live in a different subset of KQL than detection authors: detections want precision, hunts want to surface the weird. First, the operator cheat-sheet — the primitives that do most of the exploratory work:

Operator / function What it does Hunt use
make-series Aggregate into an evenly spaced time series Precondition for all time-series stats
series_decompose_anomalies() Seasonal+trend decomposition, flag residual outliers Per-entity anomaly vs its own baseline
series_outliers() Tukey-test point-anomaly score Cheap point outliers, no seasonality
series_periods_detect() Find dominant periods in a series Beaconing periodicity
evaluate autocluster() Smallest set of common patterns “What’s the dominant shape; what’s outside it”
evaluate basket() Frequent-itemset mining Co-occurring attribute clusters
prev() / next() (with serialize) Row-relative access Inter-arrival deltas, sessionization
dcount() / make_set() Cardinality / distinct collection Prevalence, stack counting
arg_max() / arg_min() Latest/earliest row per key Newest identity state
mv-expand / mv-apply Fan out / process dynamic arrays Parse Parameters, multi-value fields
join kind=inner/leftouter/leftanti Correlate/enrich/subtract Entity pivots, “never-seen-before”
externaldata / _GetWatchlist() Pull reference data Allow-lists, TI, known-good

Two structural rules that separate a working hunt query from a broken one. First, make-series needs default=0 and a bounded from/to — without them, gaps in activity silently distort the series and every downstream statistic. Second, put the smaller, filtered set on the left of a join — Kusto loads the left side, so filter early and hard.

Rare-process hunting (stack counting / least-frequency of occurrence)

The oldest hunting technique and still one of the best: stack counting. Aggregate an attribute across the fleet and look at the tail — the processes, binaries, or paths that appear on very few hosts. Malware, by definition, has low prevalence.

// Rare-process hunt: binaries seen on <= 3 devices, outside standard paths (T1059/T1204)
let lookback = 7d;
let rareThreshold = 3;
DeviceProcessEvents
| where Timestamp > ago(lookback)
| where FolderPath !startswith @"C:\Program Files"
    and FolderPath !startswith @"C:\Windows\System32"
    and FolderPath !startswith @"C:\Windows\SysWOW64"
| summarize DeviceCount = dcount(DeviceId),
            FirstSeen   = min(Timestamp),
            SampleCmds  = make_set(ProcessCommandLine, 5),
            Paths       = make_set(FolderPath, 5)
    by FileName, SHA256
| where DeviceCount <= rareThreshold
| order by DeviceCount asc, FirstSeen desc

The tuning knobs and their trade-offs:

Knob Effect Lower it to… Raise it to…
rareThreshold (device count) How rare counts as “rare” Cut noise, miss slow-spreading tools Catch broader spread, more noise
lookback Baseline window Faster/cheaper, misses slow campaigns Stronger baseline, higher cost
Path exclusions Suppress known-good locations (more noise) Focus on user-writable/odd paths
Group key (FileName vs SHA256) Prevalence granularity Hash: catches renamed binaries Name: catches recompiled variants
AccountName filter Whose context Focus on privileged users Whole fleet

The refinement that makes stack counting pay off: don’t just count rarity, subtract your known-good. Maintain a baseline of approved binaries (from a golden image or a 30-day historical set) and use join kind=leftanti to keep only processes never seen in the baseline — a true “first observed in the environment” hunt.

Beaconing detection via inter-arrival time-series

Command-and-control channels beacon home on a regular cadence. Humans and normal apps generate irregular traffic; a beacon generates machine-regular intervals. The tell is a low coefficient of variation (standard deviation ÷ mean) of the inter-arrival times between a host and a destination.

// Beaconing hunt: regular outbound intervals per (device, remote IP) (T1071.001 / T1571)
let lookback = 3d;
let minConnections = 20;      // need enough events to judge regularity
let maxCoV = 0.15;            // <= 0.15 is suspiciously regular
DeviceNetworkEvents
| where Timestamp > ago(lookback)
| where ActionType == "ConnectionSuccess" and RemotePort in (80, 443, 8080, 53)
| where InitiatingProcessFileName !in~ ("chrome.exe","msedge.exe","firefox.exe","OneDrive.exe","teams.exe")
| project Timestamp, DeviceId, RemoteIP, InitiatingProcessFileName
| order by DeviceId, RemoteIP, Timestamp asc
| serialize
| extend PrevTime = prev(Timestamp),
         SameStream = (DeviceId == prev(DeviceId) and RemoteIP == prev(RemoteIP))
| where SameStream
| extend DeltaSec = datetime_diff('second', Timestamp, PrevTime)
| summarize ConnCount = count(),
            AvgIntervalSec = avg(DeltaSec),
            StdevSec = stdev(DeltaSec),
            Process = any(InitiatingProcessFileName)
    by DeviceId, RemoteIP
| where ConnCount >= minConnections and AvgIntervalSec > 0
| extend CoefficientOfVariation = round(StdevSec / AvgIntervalSec, 3)
| where CoefficientOfVariation <= maxCoV
| order by CoefficientOfVariation asc

An alternate approach uses make-series on connection counts per bin and series_periods_detect() to surface the dominant period directly — useful when beacons “jitter” but retain an underlying cadence. The parameters that govern a beaconing hunt and how to reason about each:

Parameter Meaning Too low Too high
minConnections Minimum events to judge regularity Statistically meaningless CoV Miss short-lived beacons
maxCoV Regularity threshold Miss jittered beacons Flood of benign polling
Process allow-list Exclude known chatty apps Drowns in browser/telemetry noise Might exclude a hijacked binary
RemotePort filter Which channels to inspect Narrow, misses odd ports All ports, more noise
Jitter handling Tolerate randomised intervals Miss modern beacons False positives from bursty apps

Lateral movement

Lateral movement shows as authentication to hosts an account has never touched, especially via network (type 3), RDP (type 10), or alternate credentials (type 9). The “new source→destination pair” pattern is the workhorse — build a baseline of seen pairs and hunt the residual:

// Lateral movement: new (source-account, destination-host) RDP/network pairs (T1021)
let lookback = 1d;
let baselineWindow = 14d;
let seen = SecurityEvent
    | where TimeGenerated between (ago(baselineWindow) .. ago(lookback))
    | where EventID == 4624 and LogonType in (3, 9, 10)
    | distinct Account, Computer;
SecurityEvent
| where TimeGenerated > ago(lookback)
| where EventID == 4624 and LogonType in (3, 9, 10)
| where Account !endswith "$"                    // exclude machine accounts
| summarize LogonCount = count(),
            LogonTypes = make_set(LogonType),
            SrcIPs = make_set(IpAddress, 5)
    by Account, Computer
| join kind=leftanti seen on Account, Computer    // keep only pairs never seen before
| order by LogonCount desc

The lateral-movement signals worth standing up, and where each lives:

Signal Technique Table + filter Note
New RDP source→dest T1021.001 SecurityEvent 4624 LogonType 10 Highest-fidelity for admins
SMB/admin-share access T1021.002 SecurityEvent 5140/5145 Watch ADMIN$, C$
WinRM/PSRemoting T1021.006 SecurityEvent 4624 type 3 + wsmprovhost.exe Pair with process events
Alt-credential logon T1550 SecurityEvent 4648 / 4624 type 9 runas /netonly pattern
Pass-the-hash T1550.002 SecurityEvent 4624 type 3, NTLM, no 4776 chain Hard; corroborate
Endpoint-to-endpoint (MDE) T1021 DeviceLogonEvents remote logons Cross-check with SecurityEvent

Credential access

Credential-access hunts are among the most valuable because they sit early in the kill chain. Kerberoasting (T1558.003) requests service tickets with weak RC4 encryption to crack offline:

// Kerberoasting: RC4 service tickets for many SPNs from one account (T1558.003)
let lookback = 1d;
SecurityEvent
| where TimeGenerated > ago(lookback)
| where EventID == 4769
| where TicketEncryptionType == "0x17"          // RC4-HMAC — weak, crackable
| where ServiceName !endswith "$"               // exclude machine-account services
| where TicketOptions == "0x40810000"
| summarize DistinctSPNs = dcount(ServiceName),
            RequestCount = count(),
            Services = make_set(ServiceName, 10)
    by Account, ClientAddress = tostring(IpAddress)
| where DistinctSPNs >= 5                        // one account, many SPNs = roasting
| order by DistinctSPNs desc

DCSync (T1003.006) abuses directory replication to pull password hashes — the tell is a non-domain-controller account requesting the replication access right:

// DCSync: replication rights requested by a non-DC account (T1003.006)
SecurityEvent
| where TimeGenerated > ago(1d)
| where EventID == 4662 and AccessMask == "0x100"
| where Properties has "1131f6ad-9c07-11d1-f79f-00c04fc2dcd2"   // DS-Replication-Get-Changes-All
     or Properties has "1131f6aa-9c07-11d1-f79f-00c04fc2dcd2"   // DS-Replication-Get-Changes
| where Account !endswith "$"                                    // legit DCs are machine accounts
| project TimeGenerated, Account, ObjectName, SubjectLogonId, Computer

The credential-access catalogue, so you can build the backlog:

Technique ID Signal Table
Password spraying T1110.003 Many accounts / few tries / one IP; ResultType 50126 SigninLogs
Kerberoasting T1558.003 4769 RC4, many SPNs SecurityEvent
AS-REP roasting T1558.004 4768 with no pre-auth SecurityEvent
DCSync T1003.006 4662 replication GUID, non-DC SecurityEvent
LSASS memory access T1003.001 Handle to lsass.exe DeviceEvents
NTDS.dit access T1003.003 Access to ntds.dit / ntdsutil DeviceProcessEvents
MFA fatigue T1621 Repeated 50074/50088, one approval SigninLogs
Token/cookie theft T1550.004 Non-interactive sign-in, new ASN, no prior device AADNonInteractiveUserSignInLogs

For LSASS access, MDE surfaces processes opening a handle to the credential store:

// LSASS access: non-system processes opening a handle to lsass.exe (T1003.001)
DeviceEvents
| where Timestamp > ago(1d)
| where ActionType == "OpenProcessApiCall"
| where FileName =~ "lsass.exe"
| where InitiatingProcessFileName !in~ ("MsMpEng.exe","csrss.exe","wininit.exe","services.exe")
| project Timestamp, DeviceName, InitiatingProcessFileName,
          InitiatingProcessCommandLine, InitiatingProcessAccountName
| order by Timestamp desc

Exfiltration

Exfil hunts split by channel: email, cloud storage, and raw network. The highest-fidelity cloud hunt is the auto-forwarding rule to an external address (T1114.003), because it is both a persistence and an exfil mechanism and almost never legitimate at scale:

// Exfil via mail forwarding rule to an external recipient (T1114.003)
let corpDomains = dynamic(["contoso.com","contoso.onmicrosoft.com"]);
OfficeActivity
| where TimeGenerated > ago(7d)
| where OfficeWorkload == "Exchange"
| where Operation in ("New-InboxRule","Set-InboxRule","UpdateInboxRules")
| mv-apply p = parse_json(Parameters) on (
    where p.Name in ("ForwardTo","RedirectTo","ForwardAsAttachmentTo")
    | project ForwardDest = tostring(p.Value)
  )
| where isnotempty(ForwardDest)
| extend ExternalDomain = tolower(extract(@"@([\w.\-]+)", 1, ForwardDest))
| where isnotempty(ExternalDomain) and ExternalDomain !in (corpDomains)
| project TimeGenerated, UserId, ClientIP, Operation, ForwardDest, ExternalDomain
| order by TimeGenerated desc

The exfiltration signals and their channels:

Channel Technique Signal Table
Mail forwarding rule T1114.003 New-InboxRule → external OfficeActivity
Mass mailbox access T1114 MailItemsAccessed burst OfficeActivity
Mass file download T1567 FileDownloaded/FileSyncDownloadedFull spike OfficeActivity
Anonymous share link T1567.002 AnonymousLinkCreated OfficeActivity
Unsanctioned cloud upload T1567.002 FileUploaded to risky app CloudAppEvents
Large outbound transfer T1048 High egress bytes to new dest DeviceNetworkEvents
DNS tunneling T1048.003 / T1071.004 High-entropy/long DNS queries DeviceNetworkEvents

UEBA: BehaviorAnalytics, anomalies, and entity pages

Sentinel’s UEBA (User and Entity Behavior Analytics) does the per-entity baselining for you and writes it to queryable tables. Enable it under Sentinel → Settings → Entity behavior (it needs the relevant data sources connected — SigninLogs, AuditLogs, SecurityEvent, AzureActivity, OfficeActivity). The tables you hunt against:

Table What it gives you
BehaviorAnalytics Per-activity enrichment with InvestigationPriority (0–10), “first time” flags, peer/blast-radius context
IdentityInfo Identity attributes: group membership, manager, account state, on-prem SID, department
UserPeerAnalytics Peer ranking — who this user behaves like, for peer-outlier hunts
UserAccessAnalytics Effective access / blast radius derived from role and group data

The single most useful field is InvestigationPriority. It folds dozens of behavioural signals into one score, so a high-signal account-takeover (ATO) or insider hunt is often just “rank activities by priority, filter to the unusual ones”:

// ATO / insider hunt: high-priority anomalous activity + identity context
BehaviorAnalytics
| where TimeGenerated > ago(7d)
| where InvestigationPriority > 5
| extend FirstTimeFromCountry = tobool(ActivityInsights.FirstTimeUserConnectedFromCountry)
| extend UncommonForUser      = tobool(ActivityInsights.ActionUncommonlyPerformedByUser)
| extend FirstTimeFromDevice  = tobool(ActivityInsights.FirstTimeUserLoggedOnToDevice)
| where FirstTimeFromCountry or UncommonForUser or FirstTimeFromDevice
| join kind=leftouter (
    IdentityInfo
    | summarize arg_max(TimeGenerated, *) by AccountUPN
    | project AccountUPN, GroupMembership, IsAccountEnabled, Manager, JobTitle
  ) on $left.UserPrincipalName == $right.AccountUPN
| project TimeGenerated, UserName, ActivityType, SourceIPAddress,
          InvestigationPriority, FirstTimeFromCountry, UncommonForUser,
          FirstTimeFromDevice, GroupMembership, Manager, JobTitle
| sort by InvestigationPriority desc

The ActivityInsights dynamic column is where the behavioural flags live — the ones you branch on most:

ActivityInsights flag Fires when… Hunt use
FirstTimeUserConnectedFromCountry User’s first sign-in from this country ATO / impossible travel
FirstTimeConnectionFromCountryObservedInTenant First time anyone connected from here Novel-geo campaign
ActionUncommonlyPerformedByUser This action is rare for this user Insider / peer-outlier
ActionUncommonlyPerformedInTenant Rare across the whole org Novel technique
FirstTimeUserLoggedOnToDevice New device for this user Lateral movement / ATO
FirstTimeUserAccessedResource New resource for this user Discovery / collection
UncommonHighVolumeOfActivities Volume spike vs baseline Exfil / automation

How to read InvestigationPriority, because a raw number needs a policy:

Score band Interpretation Typical action
0–2 Routine, well within baseline Ignore in bulk hunts
3–5 Mild deviation Sample; watch for clusters
6–8 Notable anomaly Triage the entity page
9–10 Strong multi-signal anomaly Bookmark; consider incident

For insider risk, invert the lens: instead of impossible travel, hunt for legitimate access that is anomalous for the peer group — a finance analyst suddenly enumerating HR shares. UserPeerAnalytics supplies the peer set; the deviation is the signal. And use the entity pages (Sentinel’s timeline view for a user/host/IP) to pivot: from any BehaviorAnalytics row, open the entity to see its full activity timeline, insights, and related alerts in one pane — the fastest way to adjudicate a lead as benign or worth an incident.

Hunting notebooks: Jupyter and MSTICPy

Notebooks are where a hunt stops being a single query and becomes an investigation. MSTICPy (Microsoft Threat Intelligence Center Python library) wraps Sentinel/Defender queries, entity enrichment (VirusTotal, GeoIP, threat intel), and visualization behind a clean API. Sentinel can launch notebooks on an Azure ML compute instance, or you run them locally against the same workspace.

The MSTICPy components you actually use:

Component Purpose
init_notebook() One-call bootstrap: loads config, query/TI providers, plotting
QueryProvider Connect to a data backend (MSSentinel, MDE, Kusto, Splunk, LocalData) and run KQL
TILookup Multi-provider threat-intel lookups (VirusTotal, AlienVault OTX, etc.)
GeoLiteLookup / IPStackLookup IP geolocation enrichment
Pivot Attach enrichment/query functions to entity types (Host, IpAddress, Account)
nbwidgets Interactive parameter widgets (time ranges, entity pickers)
nbdisplay Timelines, process trees, network graphs
IoCExtract / transform Pull IOCs (IPs, hashes, domains) out of free text; decode base64
MpConfigEdit GUI editor for msticpyconfig.yaml

Configuration lives in msticpyconfig.yaml (point to it with the MSTICPYCONFIG env var) so connection strings and API keys never land in cells:

# msticpyconfig.yaml
AzureSentinel:
  Workspaces:
    Default:
      WorkspaceId: "00000000-1111-2222-3333-444444444444"
      TenantId:    "55555555-6666-7777-8888-999999999999"
TIProviders:
  VirusTotal:
    Args:
      AuthKey:
        EnvironmentVar: "VT_AUTH"
    Primary: true
    Provider: "VirusTotal"

A parameterized hunt notebook then reads like this — parameterizing the entity and lookback is what lets one notebook serve every “investigate this user” hunt:

import msticpy as mp
mp.init_notebook(globals())                    # loads config, query providers, helpers

# --- parameters cell (tag this cell "parameters" for headless/Papermill runs) ---
target_user   = "j.doe@contoso.com"
lookback_days = 14

qry_prov = mp.QueryProvider("MSSentinel")
qry_prov.connect(workspace="Default")           # uses msticpyconfig.yaml

signins = qry_prov.exec_query(f"""
    SigninLogs
    | where TimeGenerated > ago({lookback_days}d)
    | where UserPrincipalName == '{target_user}'
    | project TimeGenerated, IPAddress, AppDisplayName, ResultType, Location
""")

# enrich distinct source IPs against threat intel, then plot a timeline
ti = mp.TILookup()
ti_results = ti.lookup_iocs(data=signins, ioc_col="IPAddress")
mp.nbdisplay.display_timeline(signins, source_columns=["AppDisplayName", "Location"])

When to reach for a notebook versus staying in the portal:

Situation Portal KQL MSTICPy notebook
Quick single-table query ✅ Fastest Overkill
Multi-source enrichment (TI + geo + WHOIS) ✖ Manual ✅ One pipeline
Iterative deep-dive on one entity ✖ Loses state ✅ Cells hold state
Reproducible, reviewed, versioned hunt ✖ Ad hoc ✅ Commit the .ipynb
Scheduled/headless run Use analytics rule ✅ Papermill on Azure ML
Statistical/ML modeling (M-ATH) ✖ Limited ✅ Full Python stack

The discipline that makes notebooks pay off: keep the parameter cell at the top and tag it (Papermill injects parameters into a tagged cell for headless runs), and commit the .ipynb to the same Git repo as your KQL. A hunting notebook is code — it gets reviewed and versioned like code, and Microsoft ships a large gallery of starter notebooks you fork rather than write from zero.

From hunt to detection: bookmarks, livestream, and promotion

A hunt that found something must leave a durable trace, or the next hunter repeats your work. The exits, in increasing permanence:

Exit What it is Persistence When to use
Bookmark A saved result row + notes + entity mappings HuntingBookmark table Evidence of a specific finding
Hunt record The Hunts object: hypothesis, queries, status, findings Sentinel Hunts Track the whole hunt lifecycle
Livestream Near-real-time run of a candidate query Ephemeral, notifies on hit Validate a query before it’s a rule
Analytics rule Scheduled/NRT query that raises incidents Standing detection A hunt that proved reliable
Defender custom detection Advanced-hunting query as a standing rule Standing detection Endpoint/identity/email hunts
Watchlist Externalised tuning data (VIPs, known-good) Watchlist Reference data queries share

Bookmarks capture an interesting query-result row with your notes and entity mappings — the unit of evidence. From a hunting-query result, select rows and “Add bookmark”; in the Hunts experience, bookmarks attach to the hunt itself and can be promoted to an incident when they warrant triage. They land in the HuntingBookmark table, so you can even hunt over your own bookmarks.

Livestream is the validation step people skip. Before you promote a candidate query to a rule, run it as a hunting livestream session: Sentinel executes it against incoming data in near-real-time and notifies you on each hit, so you see the real-world hit rate and false-positive shape over hours before committing it to a standing rule. It is the cheapest way to answer “will this rule be noisy?”

Promotion turns a validated hunt into a standing detection. In Sentinel that is a scheduled or NRT analytics rule (section examples above); in the Defender portal it is a custom detection rule built from an advanced-hunting query. The Sentinel rule types you promote into:

Rule type Cadence Best for
Scheduled Every 5 min – 14 days Most hunts; full KQL, aggregation, thresholds
NRT (Near-Real-Time) ~Every 1 min High-urgency single-event detections
Microsoft Security On source alert Re-emit/route Defender alerts as incidents
Anomaly Continuous (ML) Built-in behavioural baselines
Fusion Continuous (ML) Multi-stage attack correlation
Threat Intelligence Scheduled Match logs against TI indicators

Defender custom detections have their own cadence menu and a hard requirement: the query must return the right columns to bind evidence:

Defender detection frequency Runs Notes
Continuous (NRT) Near real-time Subset of tables; single-event style
Every 1 hour Hourly, looks back 2h Common default
Every 3 hours Looks back 6h Balance freshness/cost
Every 12 hours Looks back 24h Lower-urgency behaviours
Every 24 hours Looks back 30d window Slow campaigns

Not every hunt should become a rule. The promotion readiness checklist — a hunt graduates only when it clears every row:

Gate Question Pass criterion
Fidelity What’s the false-positive rate? Low + explainable (livestream-measured)
Actionability Is there a clear response? A named next step for the analyst
Entity mapping Can the incident be investigated? Entities mapped (Account/Host/IP)
ATT&CK tagging Is it tagged? Tactics + technique(s) set
Ownership Who tunes it? A named owner / team
Suppression Is known-good excluded? Watchlist or let allow-list wired in

The tuning data that keeps promoted rules quiet belongs in watchlists, not hardcoded let blocks scattered across queries — externalise VIP accounts, sanctioned admin IPs, known-good service principals, and terminated employees so you update one list, not twenty rules:

// Reference a watchlist of sanctioned admin source IPs to suppress known-good
let SanctionedAdminIPs = _GetWatchlist('AdminJumpHosts') | project SearchKey;
SigninLogs
| where TimeGenerated > ago(1d)
| where AppDisplayName == "Azure Active Directory PowerShell"
| where IPAddress !in (SanctionedAdminIPs)      // hunt the residual
| project TimeGenerated, UserPrincipalName, IPAddress, ResultType
# Create the watchlist from a CSV so it lives in source control and deploys with the workspace
az sentinel watchlist create \
  --resource-group rg-sec-sentinel --workspace-name law-sentinel-prod \
  --watchlist-alias AdminJumpHosts --display-name "Sanctioned admin jump hosts" \
  --provider "SOC" --source "admin_jump_hosts.csv" --items-search-key "SearchKey"

Hunting at scale: summary rules, table tiers, and ADX

The high-value hunts — beaconing, rare-process, DNS analysis — run over the highest-volume tables (network, DNS, proxy), which are exactly the tables you cannot afford to keep in the premium Analytics tier at full retention. Scaling a hunting program is a data-economics problem as much as a KQL problem. Log Analytics/Sentinel gives you three table tiers to place data in:

Tier Ingestion cost Interactive retention KQL support Alerts Use for
Analytics Full price 30 days free, up to 730 Full KQL, joins, functions ✅ Rules + hunts Security-critical, correlated tables
Basic ~Reduced (fraction of Analytics) Up to 30 days Limited (single-table, subset) Limited High-volume, occasionally queried
Auxiliary Lowest 30 days interactive + long-term Limited; summary rules OK Summary-rule driven Verbose logs, compliance retention

Beyond interactive retention, data rolls into long-term retention (up to 12 years total) where you reach it via search jobs and restore — cheap to store, slow to query, right for “we need it if there’s a breach” logs.

Summary rules are the scale primitive: a scheduled KQL aggregation that reads a high-volume (often Basic/Auxiliary) table and writes a compact result to a custom _CL table you then hunt and alert on cheaply. You keep the raw firehose in a cheap tier and hunt the pre-aggregated summary:

// Summary-rule body: hourly per-(device, remote IP) connection profile from raw network logs.
// Output goes to a custom table (e.g. NetworkBeaconSummary_CL) you hunt cheaply.
DeviceNetworkEvents
| where ActionType == "ConnectionSuccess"
| summarize ConnCount = count(),
            DistinctPorts = dcount(RemotePort),
            BytesOut = sum(coalesce(toint(column_ifexists("SentBytes", 0)), 0))
    by DeviceId, RemoteIP, InitiatingProcessFileName, bin(TimeGenerated, 1h)

Azure Data Explorer (ADX) is the other half of scale. For very long retention or truly massive volumes, land data in an ADX cluster and query it from Sentinel with the cross-service adx() operator, or join Sentinel and ADX data in one query — you get ADX’s cost/perf for cold data while keeping one hunting surface:

// Cross-service query: join hot Sentinel sign-ins to cold auth history in ADX
let ColdAuth = adx('https://seccluster.eastus.kusto.windows.net/secdb').AuthArchive;
SigninLogs
| where TimeGenerated > ago(1d) and ResultType == 0
| join kind=leftouter (
    ColdAuth | where Timestamp > ago(365d) | summarize KnownIPs = make_set(IPAddress) by UserPrincipalName
  ) on UserPrincipalName
| where IPAddress !in (KnownIPs)     // sign-in from an IP unseen in a year of history

The scale mechanisms compared, so you place data deliberately:

Mechanism What it does Cost profile Query experience Best for
Analytics tier Full interactive analytics Highest ingest Full KQL, instant Correlated, alertable security tables
Basic tier Cheap high-volume storage Low ingest, pay-per-query Limited KQL Firewall/proxy/verbose logs
Auxiliary tier Cheapest, long retention Lowest ingest Limited + summary rules Compliance + occasional hunt
Summary rules Pre-aggregate raw → compact table Query-time savings Full KQL on the summary Beaconing/prevalence at scale
ADX cross-query Query external Kusto from Sentinel ADX cluster cost Full KQL via adx() Multi-year retention, huge volume
Long-term retention Archive within Log Analytics Cheap storage Search jobs / restore “Need it only in a breach” logs

Architecture at a glance

Picture the hunting program as four planes stacked from raw telemetry at the bottom to durable detections at the top, with the hunter working across all four.

The data plane at the base is the telemetry lake: Windows security logs land in SecurityEvent via the Azure Monitor Agent and a Data Collection Rule; Entra sign-ins and directory changes flow into SigninLogs, AADNonInteractiveUserSignInLogs, and AuditLogs; Microsoft 365 audit lands in OfficeActivity; and the Defender store holds the Device*, Email*, Identity*, and Cloud* families at 30-day retention. High-volume tables (network, DNS, proxy) sit in the Basic or Auxiliary tiers or in an ADX cluster, with summary rules distilling them into compact _CL tables. Everything is reachable from one KQL surface — the unified advanced-hunting experience in the Defender portal spans both the Log Analytics and Defender stores.

The enrichment plane sits just above: UEBA reads the sign-in, audit, and security tables and writes baselined signal to BehaviorAnalytics, IdentityInfo, UserPeerAnalytics, and UserAccessAnalytics, folding behavioural features into a single InvestigationPriority. Watchlists supply known-good reference data, and threat-intel indicators arrive through the TI connector. This is the plane that answers “is this normal for this entity?” without the hunter scripting a baseline.

The analysis plane is where the hunter operates: portal KQL for quick tests, hunting queries saved with ATT&CK tags, livestream for near-real-time validation, and MSTICPy notebooks on Azure ML compute for multi-source deep dives. A hypothesis enters here as text, becomes parameterized KQL, and produces a residual the hunter adjudicates. Bookmarks capture evidence; the Hunts object tracks the hypothesis, linked queries, and status.

The detection plane at the top is where validated hunts become permanent: scheduled and NRT analytics rules in Sentinel, custom detection rules in Defender, each tagged with tactics and techniques so the MITRE ATT&CK coverage matrix shades honestly. The feedback arrow — from a promoted detection back to the next hypothesis and to refreshed baselines — is what closes the Sqrrl loop. Read the whole picture left to right and bottom to top: telemetry is enriched into baselined signal, the hunter tests a hypothesis against it, evidence is captured, and what proves reliable is promoted into a standing detection whose coverage is measured as effective, not merely intended.

Real-world scenario

Meridian Assurance, a global insurer, ran a Sentinel workspace ingesting ~220 GB/day across endpoint, sign-in, and Microsoft 365 audit, with ~140 analytics rules tagged across 60+ ATT&CK techniques. The MITRE matrix the CISO showed the board was reassuringly green. Then the quarterly purple-team exercise walked an OAuth consent-grant attack (T1528) — a malicious multi-tenant app that phished a finance manager into granting Mail.Read and Files.Read.All — straight through undetected, and used it to read the CFO’s mailbox for nine days before the red team self-reported.

The post-exercise review found the real defect was not a missing rule but a broken measurement. Coverage was tracked as “a rule exists.” Several “covered” techniques were backed by rules disabled months earlier during a noise-reduction sprint, and nobody had updated the matrix. T1528 had a tagged rule; it had been switched off in the cleanup because it fired on legitimate admin consents, and the amber never surfaced because the board metric only counted tags.

The hunt team fixed the measurement first. A scheduled MSTICPy notebook (Papermill, nightly on an Azure ML compute) pulled every rule via the alertRules API, joined enabled state and 90-day fire counts from SecurityAlert, and emitted a coverage report distinguishing intended from effective coverage. Techniques with a rule but zero useful fires in 90 days were flagged amber, not green. The amber list — 23 techniques — became the hunt backlog.

For T1528 specifically they ran a hypothesis-driven hunt: “If an attacker phished a user into consenting to a malicious app, I will see a Consent to application event in AuditLogs for an app that is newly registered, multi-tenant, and requests high-value Graph scopes, from a user who is not an admin.” The query joined AuditLogs consent events to app metadata and filtered to first-seen apps requesting Mail.Read, Mail.ReadWrite, Files.Read.All, or full_access_as_app. It surfaced the purple-team app immediately — and two real risky grants nobody had noticed: a browser extension with offline_access and a defunct vendor integration still holding Sites.FullControl.All.

They validated the query on livestream for 48 hours (three benign hits, all sanctioned admin consents, which they added to a SanctionedConsentApps watchlist), then promoted it to a scheduled analytics rule tagged Persistence / T1528, with the watchlist suppressing known-good. They also wired the consent surface reduction from Governing OAuth Consent and Application Permissions in Entra ID — admin-consent workflow plus restricted user consent — so the attack surface shrank while the detection went live.

Six weeks later the board metric changed from “techniques covered” to “techniques covered and validated in the last quarter.” The matrix had fewer green cells — 44, down from a claimed 60+ — but every green cell was backed by a rule that had fired and been triaged at least once. The incident-to-hunt timeline told the lesson in order:

Phase What happened Effect The lesson
Purple team T1528 consent grant walks through 9-day undetected mailbox read Green ≠ detecting
Review Found rule existed but was disabled Coverage was theatre Measure effective, not intended
Measurement fix Nightly notebook: intended vs effective 23 amber techniques surfaced Automation makes coverage honest
Hypothesis hunt Consent-grant KQL over AuditLogs Found purple app + 2 real grants Behaviour beats IOC
Validation 48h livestream, 3 benign hits Tuned suppression before promotion Livestream sizes the noise
Promotion Scheduled rule + watchlist + ATT&CK tag T1528 truly covered The hunt’s exit is a detection
Board metric “Covered and validated last quarter” Fewer green cells, all honest The only number worth reporting

Advantages and disadvantages

Structured, ATT&CK-anchored hunting on Sentinel/Defender both finds what detections miss and risks turning into an expensive, noisy time sink if run without discipline. Weigh it honestly:

Advantages (why this approach wins) Disadvantages (where it bites)
Finds behaviour your standing detections never anticipated — closes the dwell-time gap Requires skilled analysts; a bad hunt produces false confidence, not coverage
ATT&CK tagging turns hunts into a measurable coverage map The matrix measures intent; a green cell can be a lie without effective-coverage tracking
UEBA does per-entity baselining for you — anomalies in one field UEBA needs the right sources connected and a warm-up period; empty BehaviorAnalytics misleads
One KQL surface (unified advanced hunting) spans endpoint, identity, email, cloud High-volume tables are expensive at Analytics tier; naive queries scan TB and cost money
Notebooks make hunts reproducible, reviewable, versioned code Notebook infra (Azure ML compute, MSTICPy config, secrets) is real setup overhead
Livestream sizes a rule’s noise before you promote it Skipping validation ships noisy rules that erode analyst trust in the whole program
Successful hunts graduate into standing detections — the loop compounds Without a cadence + peer review, hunts stay one-shot and knowledge evaporates
Summary rules + Auxiliary tier + ADX make scale affordable Data-tier placement is a design decision; wrong tier = either blind or broke

The approach is right for any SOC past HMM1 that has the telemetry and wants to convert it into proactive coverage. It bites hardest on teams that hunt without a hypothesis (motion without progress), track coverage without measuring efficacy (theatre), or hunt high-volume tables at Analytics tier without summary rules (a surprising bill). Every disadvantage is manageable — but only if you know it exists, which is the point of the methodology sections above.

Hands-on lab

Stand up the hunting workflow end to end on an existing Sentinel workspace: run a hypothesis hunt, bookmark a finding, tag it to ATT&CK, promote it to a rule via IaC, and build a summary rule for scale. Run in Cloud Shell (Bash) unless noted. This assumes a Sentinel-enabled Log Analytics workspace with SigninLogs and AuditLogs flowing; if you don’t have one, How to Create a Log Analytics Workspace covers the setup and the Sentinel solution is a one-click add.

Step 1 — Set variables and confirm the workspace.

RG=rg-sec-sentinel
WS=law-sentinel-prod
SUB=$(az account show --query id -o tsv)
az monitor log-analytics workspace show -g $RG -n $WS --query "{name:name, retention:retentionInDays}" -o table

Expected: the workspace row with its retention. If it errors, fix the names before continuing.

Step 2 — Run a hypothesis hunt (spraying, T1110.003) and confirm it returns a usable shape. Paste into Logs (portal) or run headless:

SigninLogs
| where TimeGenerated > ago(1d) and ResultType == 50126
| summarize TargetedAccounts = dcount(UserPrincipalName), Attempts = count()
    by IPAddress, AppDisplayName, bin(TimeGenerated, 1h)
| where TargetedAccounts >= 10 and Attempts >= 15
| order by TargetedAccounts desc

Expected on a real tenant: zero-to-few rows. Zero is a valid negative result — the hunt is falsifiable. A row with high TargetedAccounts from one IP is a spraying lead.

Step 3 — Verify UEBA is populating (needed for the ATO hunt).

BehaviorAnalytics | where TimeGenerated > ago(1d) | summarize Rows = count()

Expected: a non-zero count. Zero after 24h+ means UEBA is off or its source tables aren’t connected — enable it under Settings → Entity behavior and wait for the baseline to warm.

Step 4 — Create a bookmark from a finding (portal). In Hunting → Queries, run the Step 2 query, select an interesting row, choose Add bookmark, map the IPAddress column to the IP entity and UserPrincipalName to Account, add a note (“T1110.003 spraying candidate”), and save. Confirm it landed:

HuntingBookmark
| where TimeGenerated > ago(1d)
| project TimeGenerated, BookmarkName, Tags, CreatedBy, QueryResultRow

Expected: your bookmark row.

Step 5 — Promote the validated query to a scheduled analytics rule (Bicep). Save this as spray-rule.bicep and the KQL as queries/t1110-spray.kql, then deploy:

param workspaceName string
resource ws 'Microsoft.OperationalInsights/workspaces@2023-09-01' existing = { name: workspaceName }
resource sprayRule 'Microsoft.SecurityInsights/alertRules@2024-03-01' = {
  scope: ws
  name: guid('hunt-T1110-spray')
  kind: 'Scheduled'
  properties: {
    displayName: 'Password spraying — many accounts, one IP (T1110.003)'
    severity: 'Medium'
    enabled: true
    query: loadTextContent('queries/t1110-spray.kql')
    queryFrequency: 'PT1H'
    queryPeriod: 'P1D'
    triggerOperator: 'GreaterThan'
    triggerThreshold: 0
    tactics: [ 'CredentialAccess' ]
    techniques: [ 'T1110' ]
    subTechniques: [ 'T1110.003' ]
    entityMappings: [
      { entityType: 'IP', fieldMappings: [ { identifier: 'Address', columnName: 'IPAddress' } ] }
    ]
  }
}
az deployment group create -g $RG --template-file spray-rule.bicep \
  --parameters workspaceName=$WS -o table

Expected: a successful deployment; the rule appears under Analytics → Active rules tagged to T1110.003.

Step 6 — Confirm the technique now shows in coverage.

az rest --method get \
  --url "https://management.azure.com/subscriptions/$SUB/resourceGroups/$RG/providers/Microsoft.OperationalInsights/workspaces/$WS/providers/Microsoft.SecurityInsights/alertRules?api-version=2024-03-01" \
  --query "value[?properties.enabled].properties.techniques" -o tsv | sort -u

Expected: T1110 appears in the list — intended coverage is now real and IaC-tracked.

Step 7 — Create a summary rule for scale (portal or ARM). In Sentinel → Summary rules → Create, point it at a high-volume source with the aggregation from the scale section, set an hourly bin, and target a new NetworkBeaconSummary_CL table. Confirm it produces rows after the first run window.

Validation checklist. You ran a falsifiable hunt, confirmed UEBA is live, captured evidence as a bookmark with entity mappings, promoted the query to an ATT&CK-tagged rule as code, verified the coverage moved, and built a summary rule for scale. What each step proved:

Step What you did What it proves
2 Ran the spraying hypothesis Hunts are falsifiable; a negative is a result
3 Checked BehaviorAnalytics UEBA readiness gates ATO hunts
4 Bookmarked with entity mappings Findings become durable evidence
5 Promoted via Bicep Detections are code, reviewed and versioned
6 Verified coverage Intended coverage is real and tracked
7 Built a summary rule Scale is a data-tier design choice

Teardown. Remove only what the lab created (leave the workspace):

az deployment group delete -g $RG -n spray-rule 2>/dev/null
# In the portal: delete the analytics rule, the bookmark, and the summary rule if they were test-only.

Cost note. Running these queries is pennies (query cost on Analytics tier is included; Basic/Auxiliary queries bill per-GB scanned). The scheduled rule adds negligible cost. The only real spend is ingestion, which you already pay — the lab adds no new data.

Common mistakes & troubleshooting

The failure modes that waste the most hunting time, as a scannable playbook first, then the reasoning:

# Symptom Root cause Confirm (exact query / path) Fix
1 series_decompose_anomalies returns garbage / mismatched rows Not mv-expand-ing all returned series in lockstep Compare column lengths after decompose mv-expand anomalies, score, baseline together
2 Anomaly query misses obvious spikes make-series without default=0 / bounded range Inspect the series for gaps Add default=0 ... from ago(N) to now()
3 BehaviorAnalytics empty after enabling UEBA Source tables not connected or still warming BehaviorAnalytics | count = 0 after 24h Connect SigninLogs/SecurityEvent; wait for baseline
4 Beaconing hunt floods with browser noise No process/port allow-list Top results are chrome.exe/msedge.exe Exclude known chatty processes; tune maxCoV
5 Rare-process hunt returns thousands of rows Threshold too high; no path exclusions Result count in the thousands Lower rareThreshold; exclude standard paths; leftanti baseline
6 Kerberoasting hunt finds nothing on a real DC 4769 not audited / RC4 disabled SecurityEvent | where EventID==4769 | count = 0 Enable Kerberos audit; note AES-only tenants
7 subTechniques silently missing on a deployed rule Old alertRules API version Rule JSON lacks subTechniques Pin API ≥ 2023-02-01
8 ATT&CK matrix green but breach walked through Tracking intended, not effective, coverage SecurityAlert fire count = 0 for the technique Add effective-coverage join; flag amber
9 Cross-portal query fails: “table not found” Table lives in the other store Query the wrong portal Use unified advanced hunting; check the Rosetta table
10 Promoted rule is extremely noisy Skipped livestream validation Incident queue floods post-promotion Validate on livestream first; wire a watchlist suppression
11 Notebook auth fails / secrets in cells No msticpyconfig.yaml / env-var secrets init_notebook errors on connect Point MSTICPYCONFIG; use EnvironmentVar for keys
12 High-volume hunt is slow and expensive Querying raw firehose at Analytics tier Query scans TB; long runtime Summary rule → compact table; move raw to Basic/Auxiliary
13 OfficeActivity forwarding hunt misses rules Parameters not parsed / wrong operation names mv-apply returns nothing Parse Parameters JSON; include all three rule ops
14 Entity mapping wrong; incident graph empty Wrong identifier for the entity type Investigation graph has no nodes Use correct identifier (Account=FullName, IP=Address)

The entries that bite hardest, expanded:

1. Mismatched mv-expand after decomposition. series_decompose_anomalies() returns three series (anomalies, score, baseline). If you mv-expand only one, the others don’t flatten in lockstep and rows misalign. Confirm: the flattened score doesn’t correspond to its TimeGenerated. Fix: expand all of them plus the axis together — mv-expand TimeGenerated to typeof(datetime), series to typeof(long), anomalies to typeof(int), score to typeof(double).

8. Intended vs effective coverage. The single most expensive hunting-program mistake. The matrix shades on tags, so a disabled or never-firing rule still paints a cell green and the board believes it’s covered. Confirm: run the SecurityAlert fire-count join from the ATT&CK section; techniques with zero 90-day fires are your blind spots. Fix: track effective coverage nightly and treat “tagged but silent” as amber, not green.

10. Promoting without livestream. A query that looks clean on a day of historical data can be noisy against live traffic (a nightly batch job, a monitoring probe). Confirm: the incident queue floods right after you enable the rule. Fix: run the candidate as a livestream session for 24–48h first, size the false positives, and add a watchlist-based suppression before you promote.

12. Hunting the firehose at premium tier. Beaconing/DNS/proxy hunts over raw Analytics-tier tables scan terabytes and cost real money per run. Confirm: query runtime in tens of seconds and a scan volume in TB. Fix: pre-aggregate with a summary rule into a compact _CL table, keep the raw data in the Auxiliary tier, and hunt the summary; reach for ADX for multi-year retention.

Best practices

Security notes

Hunting is a privileged activity over sensitive data, so the hunting platform itself is an attack surface. Grant hunters Microsoft Sentinel Responder or Reader (and Log Analytics Reader) rather than Contributor — reading and bookmarking needs no write to the workspace; only rule authors need Microsoft Sentinel Contributor. Never hand hunters subscription Owner “to make it easier.” Notebook compute (Azure ML) should run under a managed identity with least-privilege access to the workspace, and MSTICPy secrets (VirusTotal keys, etc.) belong in environment variables or Key Vault, never in cells or committed config.

The queries themselves can leak: a bookmark or notebook output can contain live credentials, PII, or the exact detection logic an insider could evade. Treat hunt artifacts as sensitive, restrict who can read HuntingBookmark and exported notebooks, and audit access to the workspace. The RBAC and hardening reference:

Concern Control Why
Hunter access Sentinel Responder/Reader, not Contributor Read + bookmark needs no workspace write
Rule authoring Sentinel Contributor, scoped Only rule authors change detections
Notebook compute Managed identity, least privilege No standing secrets on the box
TI/API keys Env var or Key Vault Keys out of cells and Git
Bookmark/notebook data Restrict read; audit access Outputs contain creds/PII/logic
Watchlist contents Treat VIP/admin lists as sensitive They map your crown jewels
Detection logic Version control + review An insider who reads it can evade it
Cross-tenant apps Correlate consent hunts with governance T1528 is the modern initial-access path

Pair the hunting program with the identity controls that shrink the attack surface it hunts: risk-based Conditional Access from Operationalizing Entra ID Protection, consent hardening from Governing OAuth Consent and Application Permissions in Entra ID, and privileged-access controls from Privileged Identity Management and PAM Architecture. A hunt that finds a technique whose surface you could have removed is a hint to remove the surface, not just detect it.

Cost & sizing

The bill for a hunting program is dominated by data ingestion and retention, not by running queries. Sentinel charges a per-GB analysis fee on top of Log Analytics ingestion (with commitment tiers that discount at 100/200/300+ GB/day), while Defender XDR advanced-hunting data carries no separate ingestion charge and is retained 30 days. The cost drivers, ranked:

Driver What inflates it How to control it
Analytics-tier ingestion High-volume verbose logs at premium price Move firewall/proxy/DNS to Basic/Auxiliary
Interactive retention Long retention on every table Retain only correlated tables long; archive the rest
Query scan (Basic/Aux) Ad-hoc scans over huge tables Summary rules; scan compact _CL tables
ADX cluster Under-utilised cold-storage cluster Right-size; autoscale; reserved capacity
Notebook compute Idle Azure ML instances Auto-shutdown; small SKUs; schedule runs
Redundant ingestion Same signal from two connectors De-duplicate connectors; prefer the free path

The tier economics that make scale affordable (relative cost, not exact regional pricing — always price your own region):

Data placement Relative ingest cost Relative query cost Retention Use for
Analytics tier 1.0× (baseline) Included 30d free → 2y Alertable, correlated security tables
Basic tier ~0.2–0.3× Per-GB scanned Up to 30d + long-term High-volume, occasionally queried
Auxiliary tier ~0.05–0.1× Per-GB scanned 30d + up to 12y Verbose logs, compliance retention
Summary output (_CL) Analytics rate on tiny volume Included Your choice Pre-aggregated hunt targets
ADX cluster Cluster + storage Included in cluster Years Massive volume, multi-year
Defender store No extra ingest Included 30d fixed Endpoint/identity/email/cloud hunts

Sizing guidance: a mid-size SOC ingesting ~200 GB/day typically buys a 100 GB/day commitment tier and pushes verbose network/proxy/DNS into Auxiliary, cutting the effective per-GB cost of the noisy 60% of volume by roughly 5–10×. In INR terms, moving 60 GB/day of proxy logs from Analytics (~₹190–210/GB region-dependent) to Auxiliary (a small fraction) is the difference between a five-figure and a low-four-figure daily line item — the single highest-leverage cost decision in a hunting program. Notebook compute is negligible if you auto-shutdown; leaving an Azure ML instance running 24×7 is a common surprise on the bill.

Interview & exam questions

Q1. What makes a threat hunt different from running a detection? A detection is an automated, precise rule for a known pattern; a hunt is a human-led, hypothesis-driven search for behaviour your detections don’t yet catch, aimed at closing the dwell-time gap. A hunt’s output is knowledge — a refuted hypothesis or a new detection — not just an alert.

Q2. State a good hunt hypothesis and explain why it’s testable. “If an attacker Kerberoasted, I’ll see 4769 events with RC4 (0x17) tickets for many SPNs from one account in the last day.” It’s testable because it names the table (SecurityEvent), the exact signal (EventID 4769, TicketEncryptionType 0x17), the condition (many SPNs, one account), and an implicit negative (no such pattern → close the hunt).

Q3. What is the difference between intended and effective ATT&CK coverage? Intended coverage means a rule is tagged with a technique; effective coverage means that rule actually fired and was triaged in a window. The MITRE matrix shades on intent, so a disabled or never-firing rule paints a cell green — you must join SecurityAlert fire counts to measure efficacy.

Q4. Why must you mv-expand all outputs of series_decompose_anomalies together? The function returns three parallel series (anomalies, score, baseline) aligned to the time axis. Expanding only one leaves the others as arrays, so rows misalign and the score no longer corresponds to its timestamp. Expand the axis and all three in one mv-expand.

Q5. How would you hunt for beaconing in KQL? Compute inter-arrival times between a host and each remote destination (sort, then prev() under serialize), aggregate per stream, and flag streams with a low coefficient of variation (stdev ÷ mean) of the intervals — machine-regular timing. Alternatively use make-series plus series_periods_detect() to find a dominant period.

Q6. Which tables carry lateral-movement signal, and what discriminates it? SecurityEvent 4624 (LogonType 3 network, 9 NewCredentials, 10 RDP), DeviceLogonEvents, and IdentityLogonEvents. The discriminator is a new source-account→destination-host pair — build a baseline of seen pairs and leftanti-join to keep only the unseen.

Q7. What does UEBA’s InvestigationPriority represent and how do you use it? It’s a 0–10 score folding many behavioural features (first-time-from-country, uncommon-for-user, etc.) into one number in BehaviorAnalytics. You rank activities by it and filter to the high band, then pivot to the entity page — it collapses a multi-signal ATO/insider hunt into a sort.

Q8. How do you promote a hunt to a detection responsibly? Validate the candidate on livestream for 24–48h to size false positives, confirm it clears the readiness checklist (fidelity, actionability, entity mapping, ATT&CK tag, ownership, suppression), then deploy it as an analytics rule in IaC with a watchlist suppressing known-good.

Q9. When would you use a summary rule vs ADX? Summary rules pre-aggregate a high-volume table into a compact _CL table on a schedule so you hunt cheaply within Sentinel; ADX is for very large volumes or multi-year retention queried cross-service via adx(). Summary rules reduce query cost within Log Analytics; ADX offloads storage and compute entirely.

Q10. Why is MSTICPy config in msticpyconfig.yaml rather than in cells? So connection strings and API keys are never hardcoded or committed — the file references secrets via EnvironmentVar, and MSTICPYCONFIG points to it. It also makes notebooks portable and reviewable as code.

Q11. A rule you deployed shows no subTechniques in its JSON. Why? The alertRules API version is too old; subTechniques is honoured only on 2023-02-01 and newer. Pin a current API version and redeploy.

Q12. How do you detect an illicit consent grant (T1528)? Hunt AuditLogs for Consent to application events where the target app is newly registered, multi-tenant, requests high-value Graph scopes (Mail.Read, Files.Read.All, full_access_as_app), and the granting user is not an admin — then suppress sanctioned apps via a watchlist. Maps to SC-200 identity-threat content.

These map most directly to SC-200 (Security Operations Analyst) — hunting, KQL, Sentinel, and Defender XDR are core exam domains — and touch SC-100 (designing a SOC) and AZ-500 (monitoring).

Quick check

  1. What three parts must a hunt hypothesis contain?
  2. Which SigninLogs ResultType indicates invalid-credential failures you’d use for a spraying hunt?
  3. What statistical property of inter-arrival times flags beaconing?
  4. What is the difference between intended and effective ATT&CK coverage?
  5. Which validation step sizes a candidate rule’s noise before promotion?

Answers

  1. The technique (an ATT&CK T-ID), the observable (the table and signal), and the falsifiable claim (the condition, including what a negative result looks like).
  2. 50126 — “invalid username or password.” Many accounts with 50126 from one IP in a short window is the spraying signature.
  3. A low coefficient of variation (standard deviation ÷ mean) of the intervals — machine-regular, low-variance timing between connections to the same destination.
  4. Intended coverage means a rule is tagged with the technique; effective coverage means the rule actually fired and was triaged in the measurement window. Only effective coverage reflects real detection.
  5. Hunting livestream — running the candidate query against live incoming data for 24–48h to observe the real hit rate and false-positive shape before promoting it to a standing rule.

Glossary

Term Definition
Threat hunting Proactive, human-led, hypothesis-driven search for adversary behaviour that automated detections miss
KQL Kusto Query Language — the read-only analytics language for Sentinel and Defender advanced hunting
MITRE ATT&CK A knowledge base of adversary tactics (goals) and techniques (methods), each with an ID
Tactic The adversary’s objective in a phase (e.g. Lateral Movement, TA0008)
Technique / sub-technique A specific way to achieve a tactic (e.g. T1021.001 RDP), with a T-ID
PEAK Prepare, Execute, Act with Knowledge — a hunting framework with three hunt types
Hunting Maturity Model (HMM) A 0–4 scale rating a program from alert-only to hunt-automating
UEBA User and Entity Behavior Analytics — Sentinel’s per-entity behavioural baselining engine
BehaviorAnalytics The UEBA output table with per-activity enrichment and InvestigationPriority
InvestigationPriority A 0–10 UEBA score folding many behavioural features into one number
Bookmark A saved hunting-query result row with notes and entity mappings; the unit of evidence
Livestream Near-real-time execution of a hunting query against live data for validation
Analytics rule A saved Sentinel query that runs on a schedule/NRT and raises incidents
Summary rule A scheduled KQL aggregation that distils high-volume data into a compact table
MSTICPy Microsoft’s open-source Python library for hunting, enrichment, and visualization in notebooks
Stack counting Aggregating an attribute across the fleet and inspecting the low-prevalence tail
Beaconing Regular, machine-timed outbound communication characteristic of C2
Watchlist Externalised reference data (VIPs, known-good, TI) queried via _GetWatchlist
ADX Azure Data Explorer — a Kusto engine for massive-volume/long-retention data, queried via adx()
Pyramid of Pain A ranking of indicators by how costly they are for an adversary to change; TTPs sit at the top

Next steps

KQLthreat-huntingMITRE-ATTACKUEBAMicrosoft-SentinelDefender-XDRMSTICPyJupyter
Need this built for real?

Vinod is a Senior Cloud Architect (22+ yrs) — available for Azure / AWS / GCP architecture, landing zones, and migrations.

Work with me

Comments

Keep Reading