Intermediate ⏱ 90 min πŸ“‹ 12 Steps

Build KQL Analytics Rules & Threat Detection

Master KQL query language for security analytics, build scheduled and near-real-time (NRT) detection rules, configure Fusion correlation, and implement MITRE ATT&CK-mapped detections for identity, endpoint, and cloud threats.

πŸ“‹ Overview

About This Lab

This lab deepens your Sentinel skills by building production-grade analytics rules using KQL. You will create detections for real-world attack scenarios including password spray, impossible travel, and MFA fatigue. Each detection is mapped to the MITRE ATT&CK framework and tuned with watchlists and threat intelligence to minimize false positives while maximizing coverage across identity, endpoint, and cloud attack surfaces.

🏒 Enterprise Use Case

A healthcare organization with 5,000 employees experiences credential stuffing, impossible travel, and suspicious mailbox access. Their Security Operations Center (SOC) team needs automated detections mapped to MITRE ATT&CK to identify and respond to these identity-based attacks in real time. Regulatory requirements (HIPAA, HITRUST) demand documented detection rationale and coverage mapping. The SOC must reduce mean time to detect (MTTD) from days to minutes while maintaining a manageable false positive rate across a 24/7 operation.

🎯 What You Will Learn

  1. Write advanced KQL queries with joins, time windows, and statistical functions
  2. Create scheduled analytics rules with proper entity mapping
  3. Deploy near-real-time (NRT) rules for critical detections
  4. Configure Fusion rules for multi-stage attack detection
  5. Map rules to MITRE ATT&CK framework tactics and techniques
  6. Tune alert thresholds to reduce false positives
  7. Build detections for impossible travel, password spray, and MFA fatigue
  8. Use watchlists and threat intelligence indicators in analytics rules

πŸ”‘ Why This Matters

Detection engineering is the backbone of SOC operations. Without well-tuned analytics rules, your SIEM is just expensive log storage. Every dollar spent on Microsoft Sentinel ingestion is wasted if the data flowing through it does not generate actionable alerts. Production-grade detections require KQL proficiency, deep understanding of attack patterns, proper entity mapping for incident correlation, and continuous tuning to adapt to your environment’s baseline. This lab equips you with the skills to build, deploy, and maintain a detection library that turns raw telemetry into high-fidelity security alerts.

βš™οΈ Prerequisites

  • Completed Lab 1 . Microsoft Sentinel deployed with the Microsoft Entra ID data connector configured and active (Sentinel Lab 01)
  • Microsoft Sentinel Contributor role . Required to create and manage analytics rules, watchlists, and threat intelligence indicators
  • Data flowing in SigninLogs and AuditLogs tables . Verify that sign-in and audit events are actively being ingested (at least 24 hours of data recommended)
  • Basic KQL knowledge . You should be comfortable with where, summarize, project, and extend operators
  • Microsoft Entra ID P1 or P2 license . Required for sign-in log availability and conditional access data
πŸ’‘ Pro Tip: Review the official documentation on creating custom analytics rules before starting: Create custom analytics rules to detect threats

Step 1 Β· Review Ingested Data Sources

Before writing detection rules, you need to verify that your data sources are healthy and ingesting data. Stale or missing data will cause analytics rules to silently fail. producing zero alerts and giving you a false sense of security.

Check Data Freshness

Navigate to Microsoft Sentinel β†’ Logs and run the following KQL query to check the freshness and volume of your ingested tables:

// Data Freshness Check: Are your log sources healthy and current?
// PURPOSE: Before writing detections, verify data is actively flowing
// WHY: Stale data = silent rule failures = false sense of security
union SigninLogs, AuditLogs       // Combine both identity log tables
| summarize
    EventCount = count(),         // Total events per table - should be >0
    LastEvent  = max(TimeGenerated),  // Most recent event - should be within ~15 min
    FirstEvent = min(TimeGenerated)   // Oldest event in the workspace
  by Type                         // Group by table name (SigninLogs vs AuditLogs)
| extend DataAge = now(). LastEvent  // How stale is the data? >30 min = investigate
| project Type, EventCount, FirstEvent, LastEvent, DataAge
| order by LastEvent desc
// Expected output: Two rows showing event counts and freshness per table
// ACTION: If DataAge > 30 min, check Data connectors blade for connector health

Verify Data Connector Health

  • Navigate to Data connectors in the Sentinel left menu
  • Select the Microsoft Entra ID connector
  • Verify that the status shows Connected (green indicator)
  • Check the Data received graph. you should see steady data points over the last 24 hours
  • If data is stale (DataAge > 30 minutes), investigate the connector configuration and permissions

Inspect Table Schemas

Explore the key columns you will use in your detection queries:

// Inspect Sign-in Log Schema: Explore key columns for detection queries
// PURPOSE: Understand the data available for each authentication event
// Run this to see real values and data formats before writing detections
SigninLogs
| take 1                          // Just one row - we’re exploring structure, not hunting
| project
    TimeGenerated,                // UTC timestamp of the sign-in attempt
    UserPrincipalName,            // Target account (e.g., user@contoso.com)
    IPAddress,                    // Source IP - key for threat intel and geo lookups
    Location,                     // String like "US" or "GB" derived from IP
    ResultType,                   // Error code: 0=success, 50126=bad password, 50053=locked
    ResultDescription,            // Human-readable description of ResultType
    AppDisplayName,               // App being accessed (Azure Portal, Teams, Exchange, etc.)
    ClientAppUsed,                // Protocol: Browser, Mobile App, IMAP, SMTP, etc.
    ConditionalAccessStatus,      // CA policy result: success, failure, notApplied
    AuthenticationRequirement,    // singleFactorAuthentication or multiFactorAuthentication
    MfaDetail                     // MFA method used and result (push, SMS, TOTP, etc.)
// Expected output: One row showing real values for each column
πŸ’‘ Pro Tip: Bookmark the Entra ID error code reference. Understanding ResultType codes like 50126 (invalid password), 50053 (locked account), and 50074 (MFA required) is critical for writing accurate detection queries.

Step 2 Β· Write a Password Spray Detection Query

Password spraying is an attack where an adversary tries a single common password (like Spring2026!) against many user accounts, then rotates to another password. Unlike brute force (many passwords against one account), password spray stays below per-account lockout thresholds while targeting a wide surface.

Password Spray vs Brute Force

  • Brute Force: Many passwords β†’ one account (triggers account lockout)
  • Password Spray: One password β†’ many accounts (evades per-account lockout)
  • Credential Stuffing: Known credential pairs from breaches β†’ many accounts

The detection pattern: look for a single IP address that fails authentication against multiple distinct users within a short time window.

KQL Detection Query

// Password Spray Detection
// MITRE ATT&CK: T1110.003. Brute Force: Password Spraying
SigninLogs
| where TimeGenerated > ago(1h)
| where ResultType == "50126"  // Invalid username or password
| summarize
    DistinctUsers  = dcount(UserPrincipalName),
    AttemptCount   = count(),
    TargetUsers    = make_set(UserPrincipalName, 50),
    TargetApps     = make_set(AppDisplayName, 10),
    FirstAttempt   = min(TimeGenerated),
    LastAttempt    = max(TimeGenerated)
  by IPAddress, bin(TimeGenerated, 1h)
| where DistinctUsers > 5
| extend AttackDuration = LastAttempt. FirstAttempt
| project
    IPAddress,
    DistinctUsers,
    AttemptCount,
    TargetUsers,
    TargetApps,
    FirstAttempt,
    LastAttempt,
    AttackDuration
| order by DistinctUsers desc

Query Breakdown

  • ResultType == "50126" . Filters for failed sign-ins due to invalid credentials
  • dcount(UserPrincipalName) . Counts distinct targeted users per source IP
  • make_set() . Collects the unique usernames into a list for investigation
  • bin(TimeGenerated, 1h) . Groups events into 1-hour windows
  • DistinctUsers > 5 . Threshold: alert when an IP targets more than 5 unique users
πŸ’‘ Pro Tip: Tune the DistinctUsers > 5 threshold based on your environment. In a large enterprise with shared VPN exit IPs, you may need to raise this to 10Β·15 to avoid false positives from legitimate users behind a single NAT IP.

Step 3 Β· Build an Impossible Travel Detection

Impossible travel occurs when a user authenticates from two geographically distant locations within a time window that makes physical travel impossible. For example, signing in from New York and then from Tokyo 30 minutes later implies credential compromise. no human can travel 10,800 km in that time.

KQL Detection Query

// Impossible Travel Detection
// MITRE ATT&CK: T1078. Valid Accounts
let timeWindow = 1d;
let maxTravelSpeedKmH = 900; // Max plausible speed (commercial flight)
SigninLogs
| where TimeGenerated > ago(timeWindow)
| where ResultType == 0  // Successful sign-ins only
| where isnotempty(LocationDetails)
| extend
    Latitude  = toreal(LocationDetails.geoCoordinates.latitude),
    Longitude = toreal(LocationDetails.geoCoordinates.longitude),
    City      = tostring(LocationDetails.city),
    State     = tostring(LocationDetails.state),
    Country   = tostring(LocationDetails.countryOrRegion)
| where isnotnull(Latitude) and isnotnull(Longitude)
| project
    TimeGenerated, UserPrincipalName, IPAddress,
    Latitude, Longitude, City, State, Country, AppDisplayName
| sort by UserPrincipalName asc, TimeGenerated asc
| serialize
| extend
    PrevTime      = prev(TimeGenerated, 1),
    PrevLat       = prev(Latitude, 1),
    PrevLon       = prev(Longitude, 1),
    PrevIP        = prev(IPAddress, 1),
    PrevCity      = prev(City, 1),
    PrevCountry   = prev(Country, 1),
    PrevUser      = prev(UserPrincipalName, 1)
| where UserPrincipalName == PrevUser  // Same user
| where IPAddress != PrevIP            // Different IPs
| extend
    TimeDiffMinutes = datetime_diff('minute', TimeGenerated, PrevTime),
    DistanceKm      = geo_distance_2points(Longitude, Latitude, PrevLon, PrevLat) / 1000
| where DistanceKm > 500              // More than 500 km apart
| where TimeDiffMinutes > 0           // Ensure chronological order
| extend RequiredSpeedKmH = (DistanceKm / TimeDiffMinutes) * 60
| where RequiredSpeedKmH > maxTravelSpeedKmH
| project
    UserPrincipalName,
    PreviousSignIn  = PrevTime,
    PreviousIP      = PrevIP,
    PreviousCity    = strcat(PrevCity, ", ", PrevCountry),
    CurrentSignIn   = TimeGenerated,
    CurrentIP       = IPAddress,
    CurrentCity     = strcat(City, ", ", Country),
    TimeDiffMinutes,
    DistanceKm      = round(DistanceKm, 0),
    RequiredSpeedKmH = round(RequiredSpeedKmH, 0)
| order by RequiredSpeedKmH desc

How It Works

  • geo_distance_2points() . Calculates the great-circle distance between two geographic coordinates in meters
  • prev() . Accesses the previous row’s value within a serialized result set
  • maxTravelSpeedKmH = 900 . Accounts for commercial aviation; anything faster is impossible travel
  • DistanceKm > 500 . Ignores short-distance moves that could be VPN or mobile network handoffs
  • Only successful sign-ins (ResultType == 0) are analyzed. failed attempts from distant IPs are less suspicious
⚠️ Important: VPN usage is the #1 source of false positives for impossible travel. Users connecting through VPN servers in different countries will trigger this rule. Consider maintaining a watchlist of corporate VPN egress IPs and filtering them out in your query. We will build this in Step 9.
πŸ’‘ Pro Tip: The serialize operator is essential here. Without it, the prev() function cannot access previous rows. This is a common pitfall when writing KQL queries that compare consecutive events for the same entity.

Step 4 Β· Detect MFA Fatigue Attacks

MFA fatigue (also called MFA bombing or push exhaustion) is an attack where an adversary who already has the user’s password repeatedly triggers MFA push notifications, hoping the user will approve one out of frustration or confusion. This technique was used in the 2022 Uber breach and the 2022 Cisco breach. both high-profile incidents where MFA fatigue led to full compromise.

Attack Pattern

  • Attacker obtains valid credentials (phishing, credential dump, password spray success)
  • Attacker initiates sign-in, triggering an MFA push notification to the user’s phone
  • User denies or ignores the prompt
  • Attacker repeats. sometimes dozens of times, often late at night
  • User eventually approves out of frustration, thinking it’s a system glitch

KQL Detection Query

// MFA Fatigue Attack Detection
// MITRE ATT&CK: T1621. Multi-Factor Authentication Request Generation
SigninLogs
| where TimeGenerated > ago(1h)
| where ResultType in ("50074", "50076", "500121")
  // 50074 = Strong auth required
  // 50076 = MFA required (user didn't pass)
  // 500121 = Authentication failed during MFA
| summarize
    MFAPrompts     = count(),
    DistinctIPs    = dcount(IPAddress),
    SourceIPs      = make_set(IPAddress, 10),
    DistinctApps   = dcount(AppDisplayName),
    TargetApps     = make_set(AppDisplayName, 10),
    FirstPrompt    = min(TimeGenerated),
    LastPrompt     = max(TimeGenerated)
  by UserPrincipalName, bin(TimeGenerated, 10m)
| where MFAPrompts > 5
| extend BombingDuration = LastPrompt. FirstPrompt
| project
    UserPrincipalName,
    MFAPrompts,
    DistinctIPs,
    SourceIPs,
    TargetApps,
    FirstPrompt,
    LastPrompt,
    BombingDuration
| order by MFAPrompts desc

Enhanced Detection: MFA Fatigue Followed by Success

This advanced query detects the worst-case scenario. an MFA fatigue attack followed by a successful authentication, indicating the user approved the malicious prompt:

// MFA Fatigue followed by successful sign-in (HIGH severity)
let MFAFatigueUsers =
    SigninLogs
    | where TimeGenerated > ago(1h)
    | where ResultType in ("50074", "50076", "500121")
    | summarize MFAPrompts = count() by UserPrincipalName, bin(TimeGenerated, 10m)
    | where MFAPrompts > 5
    | distinct UserPrincipalName;
SigninLogs
| where TimeGenerated > ago(1h)
| where ResultType == 0  // Successful sign-in
| where UserPrincipalName in (MFAFatigueUsers)
| project
    TimeGenerated,
    UserPrincipalName,
    IPAddress,
    AppDisplayName,
    Location = strcat(LocationDetails.city, ", ", LocationDetails.countryOrRegion),
    UserAgent = tostring(DeviceDetail.browser)
| order by TimeGenerated desc
πŸ’‘ Pro Tip: Microsoft now supports number matching in Authenticator push notifications, which largely mitigates MFA fatigue attacks. If your organization hasn’t enabled number matching yet, this detection becomes even more critical. Check your Entra ID authentication methods policy to verify.

Step 5 Β· Create a Scheduled Analytics Rule (Password Spray)

Now that you have a validated KQL query, it’s time to operationalize it as a Sentinel analytics rule. This transforms your query from an ad-hoc investigation tool into an automated detection that continuously runs, generates alerts, and creates incidents for SOC review.

Navigate to Analytics

  • In Microsoft Sentinel, navigate to Analytics in the left menu
  • Click + Create β†’ Scheduled query rule

General Tab

  • Name: Password Spray Attack Detected
  • Description: Detects when a single IP address fails authentication against more than 5 distinct user accounts within a 1-hour window, indicating a password spray attack.
  • Severity: High
  • MITRE ATT&CK: Select Credential Access β†’ T1110.003. Brute Force: Password Spraying
  • Status: Enabled

Set Rule Logic Tab

Paste the password spray KQL query from Step 2 into the Rule query field:

// Password Spray Detection - Analytics Rule Query
// PURPOSE: Detect when a single IP tries credentials against many users
// WHY: Password spray evades per-account lockout by spreading attempts across accounts
// MITRE ATT&CK: T1110.003 - Brute Force: Password Spraying
SigninLogs
| where ResultType == "50126"                  // 50126 = invalid username or password
| summarize
    DistinctUsers  = dcount(UserPrincipalName), // Count unique targeted accounts per IP
    AttemptCount   = count(),                   // Total failed attempts from this IP
    TargetUsers    = make_set(UserPrincipalName, 50), // List of targeted user accounts
    TargetApps     = make_set(AppDisplayName, 10),    // Apps being sprayed (Portal, Exchange, etc.)
    FirstAttempt   = min(TimeGenerated),        // Attack start time
    LastAttempt    = max(TimeGenerated)          // Attack end time
  by IPAddress, bin(TimeGenerated, 1h)          // Group by source IP per 1-hour window
| where DistinctUsers > 5                       // Threshold: >5 unique users = spray pattern
// Expected output: Attacker IPs with targeted user lists and attempt counts
// Entity mapping: IP β†’ IPAddress, Account β†’ TargetUsers
  • Run query every: 1 hour
  • Lookup data from the last: 1 hour
  • Alert threshold: Generate alert when number of query results is greater than 0

Entity Mapping

Entity mapping connects your query results to Sentinel entities, enabling incident correlation and investigation pivot points:

  • Entity 1 . IP: Map Address β†’ IPAddress
  • Entity 2 . Account: Map FullName β†’ TargetUsers

Incident Settings Tab

  • Incident creation: Enabled
  • Alert grouping: Group alerts triggered by this rule into a single incident if: Alerts have matching entities
  • Group by: IP
  • Re-open closed matching incidents: Disabled (for lab; enable in production)

Automated Response Tab

Leave this blank for now. we will configure automation playbooks in a later lab. Click Review + create, then Create.

πŸ’‘ Pro Tip: After creating the rule, click Test with current data on the rule details page to validate that the query runs without errors and to see if it would have generated alerts on historical data. This is the fastest way to catch configuration mistakes before the rule goes live.

Step 6 Β· Create a Near-Real-Time (NRT) Rule

Near-real-time (NRT) analytics rules run every minute with a 1-minute lookback, providing detection latency of approximately 1Β·2 minutes. Use NRT rules for your most critical detections where every second of delay increases blast radius.

When to Use NRT vs Scheduled

Feature NRT Rule Scheduled Rule
Run frequencyEvery ~1 minute5 min Β· 14 days
Lookback windowLast ~1 minuteConfigurable (5 min Β· 14 days)
Detection latency~1Β·2 minutesDepends on schedule
KQL limitationsSingle table, no joinsFull KQL supported
Best forCritical, simple detectionsComplex, correlation-heavy rules

Create NRT Rule for MFA Fatigue

  • Navigate to Analytics β†’ + Create β†’ NRT query rule
  • Name: MFA Fatigue Attack Detected (NRT)
  • Severity: High
  • MITRE ATT&CK: Credential Access β†’ T1621. Multi-Factor Authentication Request Generation

NRT Rule Query

NRT rules operate on a single table with no joins. Use a simplified version of the MFA fatigue query:

// NRT Rule: MFA Fatigue Detection (runs every ~1 minute)
// PURPOSE: Near-real-time detection of MFA bombing attacks
// WHY: MFA fatigue is time-sensitive - every minute of delay = risk of user approving
// MITRE ATT&CK: T1621 - Multi-Factor Authentication Request Generation
// NRT CONSTRAINT: Single table only, no joins allowed
SigninLogs
| where ResultType in ("50074", "50076", "500121")
  // 50074 = Strong auth required (MFA challenge issued)
  // 50076 = User did not pass MFA (denied or timed out)
  // 500121 = Authentication failed during MFA challenge
| summarize
    MFAPrompts  = count(),                      // Total MFA prompts per user in this minute
    SourceIPs   = make_set(IPAddress, 10),       // IPs triggering the prompts
    TargetApps  = make_set(AppDisplayName, 10)   // Apps being targeted
  by UserPrincipalName                           // Group by victim user
| where MFAPrompts > 3                           // >3 in ~1 min = aggressive MFA bombing
// Expected output: Users experiencing MFA fatigue with source IPs
// ACTION: Contact user immediately; if they approved, disable account & revoke sessions
  • Entity mapping: Account β†’ UserPrincipalName ; IP β†’ SourceIPs
  • Click Review + create, then Create
⚠️ Important: NRT rules consume more Sentinel compute resources than scheduled rules. Use them sparingly. only for your highest-priority detections. Microsoft recommends keeping the total number of NRT rules under 50 per workspace. See NRT analytics rules documentation for current limits.

Step 7 Β· Configure Fusion Rules

Fusion is Sentinel’s built-in machine learning correlation engine that automatically detects multi-stage attacks by correlating low-fidelity alerts from multiple data sources into high-fidelity incidents. Instead of triggering on individual suspicious events, Fusion identifies attack chains. for example, a suspicious sign-in followed by privilege escalation followed by data exfiltration.

Enable Fusion

  • Navigate to Analytics in the Sentinel left menu
  • Click the Rule templates tab
  • Filter by Rule type = Fusion
  • Select Advanced Multistage Attack Detection
  • Click Create rule (or verify it is already enabled)
  • Review the Fusion rule configuration. it is largely pre-configured by Microsoft

How Fusion Correlates Across Data Sources

Fusion analyzes signals across these connected data sources:

  • Microsoft Entra ID . Suspicious sign-in patterns, impossible travel, unfamiliar locations
  • Microsoft Defender for Cloud Apps . Mass file downloads, suspicious inbox rules
  • Microsoft Defender for Endpoint . Malware execution, lateral movement indicators
  • Scheduled analytics rules . Your custom detections (like the ones we just built) can feed into Fusion
  • Palo Alto, AWS, GCP . Network and cloud infrastructure signals (if connectors are active)

Example Fusion Scenarios

  • Ransomware: Suspicious sign-in β†’ credential access β†’ privilege escalation β†’ data encryption attempt
  • Data exfiltration: Impossible travel β†’ anomalous Office 365 activity β†’ mass file download
  • Crypto mining: Suspicious sign-in β†’ Azure resource creation β†’ compute-intensive activity

Review Built-in Fusion Detections

// Review Fusion-generated incidents
// PURPOSE: Check which multi-stage attack patterns Fusion has detected
// WHY: Fusion correlates low-fidelity alerts across data sources into high-fidelity incidents
// These represent multi-step attacks (e.g., suspicious sign-in β†’ privilege escalation β†’ exfil)
SecurityIncident
| where ProviderName == "Azure Sentinel Fusion Engine"  // Only Fusion-correlated incidents
| summarize count() by Title                             // Group by attack scenario type
| order by count_ desc
// Expected output: Incident titles with counts, e.g.:
//   "Suspicious sign-in followed by anomalous Office 365 activity" - 3
//   "Impossible travel followed by suspicious inbox rule" - 1
// No results? Enable more data connectors to give Fusion more signals to correlate
πŸ’‘ Pro Tip: Fusion works best when you have multiple data connectors enabled. The more diverse your data sources, the richer the correlation opportunities. Check Fusion detection documentation for the full list of supported detection scenarios and data source requirements.

Step 8 Β· Import Threat Intelligence Indicators

Threat intelligence indicators (IOCs. Indicators of Compromise) let you match your log data against known malicious infrastructure. When a user signs in from a known-bad IP or accesses a known-malicious domain, Sentinel can automatically generate an alert.

Navigate to Threat Intelligence

  • In Microsoft Sentinel, navigate to Threat intelligence in the left menu
  • Click + Add new to manually add an indicator
  • For lab purposes, add sample IOCs:
    • Type: IPv4 address . 203.0.113.50 (RFC 5737 documentation range)
    • Threat type: malicious-activity
    • Confidence: 80
    • Valid until: Set to 90 days from today
    • Description: Known C2 infrastructure from threat feed
  • Repeat for a domain: evil-example.com

Query Threat Intelligence Indicators

// View active threat intelligence indicators in your workspace
// PURPOSE: Inventory your current TI data - know what IOCs you’re matching against
// WHY: Stale or expired indicators create blind spots in threat detection
ThreatIntelligenceIndicator
| where Active == true                          // Only currently active indicators
| where ExpirationDateTime > now()              // Filter out expired IOCs
| summarize count() by IndicatorType = tostring(parse_json(tostring(AdditionalFields)).indicatorType)
| order by count_ desc
// Expected output: Counts grouped by type (ipv4-addr, domain-name, url, file, etc.)
// ACTION: If count is 0, connect a TAXII feed or manually add IOCs in Step 8

Match Sign-ins Against Threat Intel

// Match sign-in IPs against threat intelligence indicators
// PURPOSE: Detect users authenticating from known-malicious IP addresses
// WHY: A sign-in from a C2 server IP = high-confidence compromise indicator
// MITRE ATT&CK: T1071 - Application Layer Protocol (C2 communication)
//
// Step 1: Build a list of known-bad IPs from your TI feed
let ThreatIPs =
    ThreatIntelligenceIndicator
    | where Active == true              // Only active indicators
    | where isnotempty(NetworkIP)        // Must have an IP address value
    | distinct NetworkIP;               // Deduplicate for efficient matching
//
// Step 2: Find sign-ins originating from those malicious IPs
SigninLogs
| where TimeGenerated > ago(24h)        // Last 24 hours of sign-in data
| where IPAddress in (ThreatIPs)        // Match against TI indicator list
| project
    TimeGenerated,
    UserPrincipalName,                  // Potentially compromised account
    IPAddress,                          // The malicious IP that matched
    AppDisplayName,                     // What the user/attacker accessed
    ResultType,                         // 0 = success (worst case), non-zero = blocked
    Location = strcat(LocationDetails.city, ", ", LocationDetails.countryOrRegion)
| order by TimeGenerated desc
// Expected output: Sign-ins from IPs matching your threat intelligence
// ACTION: ResultType=0 β†’ CRITICAL: account compromised, disable immediately
// ACTION: ResultType≠0 → blocked attempt, monitor for persistence

Connect a TAXII Feed (Optional)

For production environments, connect automated threat intelligence feeds using the TAXII protocol:

  • Navigate to Data connectors β†’ search for Threat Intelligence Β· TAXII
  • Configure with a TAXII server URL, collection ID, and API key from your TI provider
  • Popular providers: AlienVault OTX, Anomali STAXX, MISP (open source)
πŸ’‘ Pro Tip: Enable the built-in Threat Intelligence matching analytics rules in the Analytics rule templates. These automatically correlate your TI indicators against all connected log sources. no custom KQL required. See Understand threat intelligence in Sentinel.

Step 9 Β· Create Watchlists for Allowlisting

Watchlists are reusable reference tables in Sentinel that you can join into your KQL queries. They are ideal for maintaining lists of VIP users, known-good IPs, service accounts, or any enrichment data that your analytics rules need to reference.

Create a VIP Users Watchlist

  • Navigate to Watchlist in the Sentinel left menu
  • Click + Add new
  • Name: VIPUsers
  • Alias: VIPUsers
  • Description: High-value user accounts requiring enhanced monitoring
  • Source: Upload a CSV file

CSV Format Example

UserPrincipalName,DisplayName,Department,RiskLevel
ceo@contoso.com,Jane Smith,Executive,Critical
cfo@contoso.com,John Doe,Finance,Critical
ciso@contoso.com,Alex Johnson,Security,Critical
vp-engineering@contoso.com,Sarah Lee,Engineering,High
admin-global@contoso.com,IT Admin,IT,Critical
  • SearchKey: Select UserPrincipalName as the primary lookup column
  • Click Review + create, then Create

Query the Watchlist

// View all entries in the VIPUsers watchlist
// PURPOSE: Verify watchlist contents before using it in analytics rules
// WHY: Incorrect watchlist data causes missed detections or false positives
_GetWatchlist('VIPUsers')                    // Built-in function to query a Sentinel watchlist
| project UserPrincipalName, DisplayName, Department, RiskLevel
// Expected output: Table of VIP users as defined in your uploaded CSV
// These users receive enhanced monitoring with lower alert thresholds

Use Watchlist in Analytics Rules

Enhance your password spray detection to flag VIP-targeted attacks at higher severity:

// Password spray targeting VIP users (escalated severity)
// PURPOSE: Detect password spray attempts specifically against high-value accounts
// WHY: VIPs (C-suite, admins) have elevated access - compromise = maximum blast radius
// Use a LOWER threshold (>3 vs >5) because any attack on VIPs warrants investigation
let VIPUsers = _GetWatchlist('VIPUsers') | project UserPrincipalName; // Load VIP list
SigninLogs
| where ResultType == "50126"                        // 50126 = invalid password attempt
| where UserPrincipalName in (VIPUsers)              // Only VIP accounts
| summarize
    AttemptCount = count(),                          // Failed attempts against this VIP
    SourceIPs    = make_set(IPAddress, 10),           // Attacker source IPs
    FirstAttempt = min(TimeGenerated),               // Attack window start
    LastAttempt  = max(TimeGenerated)                 // Attack window end
  by UserPrincipalName, bin(TimeGenerated, 1h)       // Group per VIP per hour
| where AttemptCount > 3                             // Lower threshold for VIPs (3 vs 5)
| project
    UserPrincipalName,
    AttemptCount,
    SourceIPs,
    FirstAttempt,
    LastAttempt
// Expected output: VIP accounts under credential attack with source IPs
// ACTION: Immediately alert CISO; consider proactive password reset for targeted VIPs

Create a Corporate VPN Allowlist

Reduce impossible travel false positives by maintaining a list of known corporate VPN egress IPs:

// Impossible travel with VPN allowlist - reduce false positives
// PURPOSE: Exclude known corporate VPN egress IPs from impossible travel detection
// WHY: Users on VPN appear to sign in from the VPN server’s location, not their own
// This causes false impossible travel alerts when users switch between VPN and direct
let VPNIPs = _GetWatchlist('CorporateVPNIPs') | project IPAddress; // Load VPN IP allowlist
SigninLogs
| where ResultType == 0                      // Only successful sign-ins (same as travel query)
| where IPAddress !in (VPNIPs)               // Exclude known VPN egress IPs
// ... rest of impossible travel query       // Continue with geo-distance calculation
πŸ’‘ Pro Tip: Watchlists support up to 10 million rows and can be updated via the API or CSV upload. For dynamic data like threat intelligence or cloud IP ranges, consider automating watchlist updates using a Logic App that pulls fresh data on a schedule. See Watchlists documentation.

Step 10 Β· Map Detections to MITRE ATT&CK

The MITRE ATT&CK framework provides a standardized taxonomy of adversary tactics and techniques. Mapping your detections to ATT&CK gives your SOC team a structured way to measure detection coverage, identify gaps, and communicate threat posture to leadership.

Review Coverage in Sentinel

  • Navigate to Microsoft Sentinel β†’ MITRE ATT&CK (under Threat management)
  • Review the heat map showing which tactics and techniques you have active detections for
  • Darker cells indicate more rules mapped to that technique
  • Identify gaps. particularly in Initial Access, Persistence, and Lateral Movement

Common MITRE Mappings for Identity Attacks

Detection Tactic Technique ID
Password SprayCredential AccessBrute Force: Password SprayingT1110.003
Impossible TravelInitial AccessValid AccountsT1078
MFA FatigueCredential AccessMFA Request GenerationT1621
Privilege EscalationPrivilege EscalationAccount ManipulationT1098
Suspicious Inbox RuleCollectionEmail CollectionT1114
OAuth App ConsentPersistenceApplication Access TokenT1550.001
Token TheftCredential AccessSteal Application Access TokenT1528

Query Your Coverage Programmatically

// Review analytics rules and their MITRE ATT&CK mappings
// PURPOSE: Programmatically identify which rules are tagged with MITRE techniques
// WHY: Helps validate that all rules have proper ATT&CK classification
SentinelAudit
| where SentinelResourceType == "Analytic Rule"  // Only analytics rule changes
| where Description has "MITRE" or Description has "ATT&CK"  // Filter for ATT&CK-tagged rules
| project RuleName = SentinelResourceName, Description
| distinct RuleName, Description              // Deduplicate
// Expected output: List of rule names with their MITRE-tagged descriptions
// ACTION: Cross-reference with the ATT&CK blade to find untagged rules
πŸ’‘ Pro Tip: Use the MITRE ATT&CK Navigator to create a visual coverage map of your detections. Export your Sentinel rule mappings and overlay them on the ATT&CK matrix to identify priority gaps. Executive stakeholders respond well to visual heat maps showing detection coverage improvements over time.

Step 11 Β· Tune and Test Your Rules

Creating detection rules is only half the job. Production-grade detections require continuous tuning to balance detection coverage (catching real attacks) against false positive rate (not drowning the SOC in noise). A rule that generates 200 alerts per day will be ignored by analysts. effectively making it useless.

Review Rule Health

  • Navigate to Analytics β†’ select each rule β†’ check the Last run column
  • Rules showing Failed status require immediate attention. usually a KQL syntax issue or missing table
  • Rules showing Success but with zero results may need threshold adjustments

Monitor Alert Volume

// Alert volume by rule over the last 7 days
// PURPOSE: Identify noisy rules and tune thresholds for optimal SOC workload
// WHY: Rules generating >20 alerts/day overwhelm analysts and get ignored
// Target: 1-10 meaningful alerts per rule per day
SecurityAlert
| where TimeGenerated > ago(7d)               // Look back one full week for trends
| where ProviderName == "Azure Sentinel"      // Only Sentinel-generated alerts (not Defender)
| summarize
    AlertCount     = count(),                 // Total alerts from this rule in 7 days
    DistinctDays   = dcount(bin(TimeGenerated, 1d))  // How many days it fired on
  by AlertName                                // Group by rule name
| extend AvgAlertsPerDay = round(AlertCount * 1.0 / DistinctDays, 1)  // Daily average
| order by AlertCount desc
// Expected output: Table of rules ranked by total alerts
// ACTION: AvgAlertsPerDay > 20? β†’ raise thresholds, add watchlist exclusions
// ACTION: AvgAlertsPerDay = 0 for 7+ days? β†’ lower thresholds or verify data source

Threshold Tuning Guidelines

  • Too many alerts (> 20/day)? Raise the threshold, add exclusions (watchlists), or narrow the scope
  • Zero alerts for 7+ days? Lower the threshold or check that the data source is active
  • Target: Each rule should generate 1Β·10 meaningful alerts per day in a mid-size environment
  • Iterate: Review false positive feedback from SOC analysts weekly and adjust accordingly

Suppression Settings

Suppression prevents the same rule from generating duplicate alerts within a specified time window:

  • Navigate to Analytics β†’ select a rule β†’ Edit β†’ Set rule logic tab
  • Scroll to Results simulation and then Suppression
  • Enable suppression and set the window (e.g., 5 hours for the password spray rule)
  • This ensures a single sustained attack generates one alert instead of multiple hourly alerts

Alert Grouping Strategies

  • Group by entity (recommended): All alerts with the same IP or account are grouped into one incident
  • Group by time window: All alerts from the same rule within a time window become one incident
  • No grouping: Every alert creates a separate incident (useful for critical detections)
πŸ’‘ Pro Tip: Establish a detection tuning review cycle. weekly for the first month after deploying new rules, then monthly for stable rules. Track false positive rates and mean time to triage for each rule. If a rule’s false positive rate exceeds 80%, either rewrite the KQL logic or disable it until you can properly tune it.

Step 12 Β· Review & Document Detection Library

A detection library is a living catalog of all your analytics rules, their purpose, tuning history, and operational status. Maintaining this document is critical for SOC knowledge transfer, audit compliance, and detection lifecycle management.

Export Rules as ARM Templates

Export your analytics rules as ARM templates for version control and deployment across environments:

  • Navigate to Analytics β†’ select a rule β†’ click Export
  • Select ARM template format
  • Save the JSON file to your Git repository
  • Alternatively, use the Sentinel API to export all rules programmatically:
# Export all analytics rules using Azure CLI
# PURPOSE: Back up your detection library for version control and cross-environment deployment
# --output json : Full JSON export including KQL queries, entity mappings, and MITRE tags
# Pipe to a file in your Git repo for infrastructure-as-code management
az sentinel alert-rule list \
  --resource-group rg-sentinel-lab \
  --workspace-name law-sentinel-lab \
  --output json > sentinel-rules-export.json
# Expected output: JSON file containing all analytics rules with full configuration
# ACTION: Commit to Git and use CI/CD to deploy across dev/staging/prod workspaces

Document Detection Rationale

For each analytics rule, document the following:

  • Rule name and unique identifier
  • MITRE ATT&CK mapping (tactic, technique, sub-technique)
  • Data source(s) and required tables
  • Detection logic summary . plain-English description of what the rule detects
  • Threshold values and tuning rationale
  • Known false positive scenarios and mitigations
  • Response playbook reference . what should the SOC analyst do when this fires?
  • Change history . when was it last tuned and why?

Build a Detection Catalog

Use a standardized template for your detection catalog. Here’s a summary of what we built in this lab:

# Rule Name Type MITRE Severity
1Password Spray Attack DetectedScheduledT1110.003High
2Impossible Travel DetectedScheduledT1078Medium
3MFA Fatigue Attack Detected (NRT)NRTT1621High
4MFA Fatigue Followed by SuccessScheduledT1621Critical
5Threat Intel IP MatchScheduledT1071High
6VIP Account Password SprayScheduledT1110.003Critical

What You Accomplished

Congratulations! In this lab you have:

  • Verified data source health and freshness with KQL
  • Written production-grade KQL detections for password spray, impossible travel, and MFA fatigue
  • Created a scheduled analytics rule with proper entity mapping and MITRE ATT&CK classification
  • Deployed a near-real-time (NRT) rule for critical MFA fatigue detection
  • Configured Fusion for multi-stage attack correlation
  • Imported threat intelligence indicators and matched them against sign-in data
  • Built watchlists for VIP users and VPN allowlisting
  • Mapped all detections to the MITRE ATT&CK framework
  • Tuned alert thresholds, suppression, and grouping strategies
  • Documented your detection library with export and cataloging workflows

πŸš€ Next Steps

  • Next Lab: Automate Incident Response with Playbooks
  • Build automation playbooks (Logic Apps) to auto-enrich and auto-respond to incidents
  • Create workbooks for detection coverage dashboards and SOC metrics
  • Implement hunting queries for proactive threat hunting exercises
  • Explore User and Entity Behavior Analytics (UEBA) for anomaly-based detections
πŸ’‘ Pro Tip: Store your detection rules in a Git repository and use CI/CD pipelines (GitHub Actions or Azure DevOps) to deploy rules across dev, staging, and production Sentinel workspaces. Infrastructure-as-code for detection engineering ensures consistency, auditability, and rollback capability.

πŸ“š Documentation Resources

ResourceDescription
KQL quick referenceComplete reference for Kusto Query Language operators, functions, and syntax
Analytics rules best practicesCreate custom scheduled query rules to detect threats in Sentinel
Near-real-time (NRT) analytics rulesDeploy and manage NRT detection rules
Fusion detectionAdvanced multistage attack detection powered by machine learning
Threat intelligence in SentinelImport, manage, and use threat intelligence indicators
WatchlistsCreate and manage reusable reference data in Sentinel
MITRE ATT&CK frameworkAdversary tactics, techniques, and procedures knowledge base
Entra ID authentication error codesComplete list of ResultType codes for sign-in log analysis
← Previous Lab Next Lab β†’