Investigate identity-based attacks using Defender XDR's unified incident view, correlate identity signals with endpoint and email evidence, contain and remediate compromised accounts, create custom detection rules, and build identity incident response playbooks.
When identity-based attacks trigger Defender XDR incidents, the unified portal correlates identity signals from Entra ID Protection with endpoint, email, and cloud app data. This lab teaches you to investigate identity attacks end-to-end: from the initial risk detection through lateral movement to data exfiltration, using the unified incident timeline, attack story, and advanced hunting.
A user account is flagged for suspicious activity: leaked credentials detected offline, followed by a sign-in from an anonymous IP, then bulk email forwarding rules created, and SharePoint files downloaded. The SOC needs to investigate this multi-stage attack using Defender XDR''s unified incident view, determine the blast radius, contain the compromise, and remediate.
Identity attacks rarely stop at credentials. Attackers pivot from compromised identities to email access, data theft, and lateral movement. Investigating each signal in isolation misses the full picture. Defender XDR''s unified incident correlates all these signals automatically, reducing investigation time from hours to minutes.
User.ReadWrite.All and Directory.ReadWrite.All scopes for containment actionsThe Attack Story provides a visual, chronological timeline of exactly how the attack unfolded across workloads. Understanding the sequence - initial credential compromise, then pivoting to email, then data exfiltration - is critical for determining blast radius and prioritising remediation actions. Before relying solely on the portal view, use KQL to detect the initial access vector, especially brute force and password spray attacks that may precede the incident.
// WHAT: Detect brute force and password spray attacks via failed sign-in patterns
// WHY: Password spray attacks try a few common passwords against many accounts,
// staying under per-account lockout thresholds. Brute force attacks try many
// passwords against a single account. Both are common initial access vectors.
// TABLE: AADSignInEventsBeta - Entra ID sign-in logs
// KEY FIELDS:
// ErrorCode 50126 = "Invalid username or password" (failed credential validation)
// ErrorCode 50053 = "Account is locked" (lockout threshold reached)
// DETECTION LOGIC:
// Password spray: single IP failing against 10+ distinct accounts in 1 hour
// Brute force: single account receiving 20+ failed attempts in 1 hour
// OUTPUT: Source IPs performing spray attacks with targeted account count
// --- Password Spray Detection ---
AADSignInEventsBeta
| where Timestamp > ago(24h)
| where ErrorCode in (50126, 50053)
| summarize TargetedAccounts = dcount(AccountUpn),
FailedAttempts = count(),
AccountsSampled = make_set(AccountUpn, 5)
by IPAddress, bin(Timestamp, 1h)
| where TargetedAccounts >= 10
| order by TargetedAccounts desc// WHAT: Detect brute force attacks - many failed attempts against a single account
// WHY: Unlike spray attacks, brute force concentrates on one user with many
// password guesses. Successful brute force is often followed by immediate
// mailbox access and data exfiltration.
// DETECTION: 20+ failed sign-ins to a single account within 1 hour
// CORRELATION: Join with successful sign-ins to detect "brute force then success" pattern
let bruteForceUsers =
AADSignInEventsBeta
| where Timestamp > ago(24h)
| where ErrorCode in (50126, 50053)
| summarize FailedAttempts = count(), SourceIPs = make_set(IPAddress, 5)
by AccountUpn, bin(Timestamp, 1h)
| where FailedAttempts >= 20;
// Find successful sign-ins by brute-forced accounts (compromise indicator)
AADSignInEventsBeta
| where Timestamp > ago(24h)
| where ErrorCode == 0 // successful sign-in
| where AccountUpn in (bruteForceUsers | project AccountUpn)
| project Timestamp, AccountUpn, IPAddress, City, Country,
Application, RiskLevelDuringSignIn
| order by Timestamp descAfter initial credential compromise, attackers typically pivot across workloads: accessing email to set up forwarding, downloading files from SharePoint/OneDrive, consenting to malicious OAuth apps, or moving laterally to other accounts. Use KQL to trace this lateral movement and determine the full blast radius of the identity compromise.
// WHAT: Detect lateral movement indicators after identity compromise
// WHY: After compromising one account, attackers often:
// 1. Access other users' mailboxes (delegate access)
// 2. Download files across multiple SharePoint sites
// 3. Grant themselves admin roles or consent to OAuth apps
// 4. Use the compromised account to phish other internal users
// TABLES: CloudAppEvents (Microsoft 365 operations), AADSignInEventsBeta
// HOW: Unions suspicious post-compromise activities for a specific user
// OUTPUT: Timeline of high-risk activities performed by the compromised user
let compromisedUser = "user@contoso.com";
let compromiseTime = datetime(2026-03-01T00:00:00Z); // set to incident start time
// Track mailbox rule creation, file downloads, admin role changes, and app consent
CloudAppEvents
| where Timestamp between (compromiseTime .. (compromiseTime + 72h))
| where AccountObjectId in (
AADSignInEventsBeta
| where AccountUpn =~ compromisedUser
| distinct AccountObjectId
)
| where ActionType in (
"New-InboxRule", // email forwarding setup
"Set-Mailbox", // mailbox delegation changes
"FileDownloaded", // bulk file download from SharePoint/OneDrive
"FileAccessed", // file access in SharePoint
"Add member to role.", // admin role assignment
"Consent to application." // OAuth app consent
)
| project Timestamp, ActionType, Application,
AccountDisplayName = AccountId,
Details = RawEventData
| order by Timestamp asc// WHAT: Detect phishing emails sent FROM the compromised account to internal users
// WHY: Attackers frequently use a compromised internal account to send phishing
// emails to other employees. Internal emails bypass external email security
// filters and have higher trust from recipients.
// TABLE: EmailEvents - Exchange Online email flow data
// OUTPUT: All emails sent by the compromised account post-compromise
let compromisedUser = "user@contoso.com";
let compromiseTime = datetime(2026-03-01T00:00:00Z);
EmailEvents
| where Timestamp between (compromiseTime .. (compromiseTime + 72h))
| where SenderFromAddress =~ compromisedUser
| project Timestamp, Subject, RecipientEmailAddress,
SenderIPAddress, DeliveryAction, AuthenticationDetails
| order by Timestamp ascAdvanced Hunting is your most powerful investigation tool. These KQL queries go beyond what the incident view shows, enabling you to search for specific attack patterns, discover related compromise across other accounts, and build a complete evidence timeline for your investigation report.
// WHAT: Unified timeline of ALL activities by a compromised user across Microsoft 365
// WHY: After credential compromise, attackers pivot across services (email, files, cloud apps).
// This query unions four key tables to build a complete attack timeline.
// TABLES:
// DeviceLogonEvents - endpoint sign-in activity (local & network logons)
// EmailEvents - email send/receive activity in Exchange Online
// CloudAppEvents - SaaS and cloud app operations (SharePoint, Teams, etc.)
// AADSignInEventsBeta - Entra ID sign-in events with risk and CA policy results
// OUTPUT: Chronological timeline of all user activities across all workloads
let compromisedUser = "user@contoso.com";
union DeviceLogonEvents, EmailEvents, CloudAppEvents, AADSignInEventsBeta
| where Timestamp > ago(7d)
| where AccountUpn =~ compromisedUser or SenderFromAddress =~ compromisedUser
| project Timestamp, ActionType, Application, IPAddress, DeviceName
| order by Timestamp asc// WHAT: Detect inbox rules created by users who had a high-risk sign-in
// WHY: A classic BEC/ATO attack pattern - attacker compromises credentials, then creates
// mail forwarding rules to exfiltrate email to an external account silently.
// Correlating "New-InboxRule" with high-risk sign-ins surfaces this attack chain.
// TABLE: CloudAppEvents - captures all Microsoft 365 cloud app operations
// KEY FIELDS:
// ActionType = "New-InboxRule" - inbox rule creation event in Exchange Online
// AccountObjectId - correlates with AADSignInEventsBeta to find risky users
// RawEventData - contains the full rule details (forwarding address, conditions)
// OUTPUT: Inbox rules created by users who also had high-risk sign-ins
CloudAppEvents
| where Timestamp > ago(7d)
| where ActionType == "New-InboxRule"
| where AccountObjectId in (
AADSignInEventsBeta
| where RiskLevelDuringSignIn == "high"
| distinct AccountObjectId
)
| project Timestamp, AccountDisplayName, ActionType, RawEventDataInvestigateUser(upn:string). During future investigations you can simply call InvestigateUser("user@contoso.com") instead of rewriting the full query each time.Containment is the most time-sensitive phase of identity incident response. Every minute a compromised account remains active, the attacker can exfiltrate more data, send more phishing emails, or escalate privileges. The three-step containment sequence is: disable the account (stop new sign-ins), revoke all sessions (invalidate active tokens), then confirm the block is effective.
# WHAT: Immediately contain a compromised user account via Graph PowerShell
# WHY: Disabling the account blocks all new sign-ins; revoking sessions
# invalidates ALL existing access tokens and refresh tokens, forcing
# re-authentication (which will be blocked by the disabled account)
# PERMISSIONS REQUIRED: User.ReadWrite.All
$userId = "compromised-user-id"
# Step 1: Disable the account - prevents any new sign-ins
Update-MgUser -UserId $userId -AccountEnabled:$false
# Step 2: Revoke all active sessions and refresh tokens
# This forces sign-out from all devices and apps within ~1 hour
# (or immediately with Continuous Access Evaluation enabled)
Revoke-MgUserSignInSession -UserId $userId
Write-Host "User disabled and sessions revoked."# WHAT: Full containment script - disable account, revoke sessions, reset password
# WHY: Combines all containment actions into a single script for rapid response.
# Generates a cryptographically random password so the attacker's stolen
# credentials are immediately invalidated.
# PERMISSIONS REQUIRED: User.ReadWrite.All, UserAuthenticationMethod.ReadWrite.All
# IMPORTANT: Run this IMMEDIATELY upon confirming compromise.
param(
[Parameter(Mandatory)]
[string]$CompromisedUpn
)
# Connect with required scopes
Connect-MgGraph -Scopes "User.ReadWrite.All","UserAuthenticationMethod.ReadWrite.All" -NoWelcome
$user = Get-MgUser -Filter "userPrincipalName eq '$CompromisedUpn'" -Property Id,DisplayName,AccountEnabled
if (-not $user) { Write-Error "User not found: $CompromisedUpn"; return }
Write-Host "[1/4] Disabling account: $($user.DisplayName)" -ForegroundColor Yellow
Update-MgUser -UserId $user.Id -AccountEnabled:$false
Write-Host "[2/4] Revoking all sessions and refresh tokens" -ForegroundColor Yellow
Revoke-MgUserSignInSession -UserId $user.Id
Write-Host "[3/4] Resetting password to random value" -ForegroundColor Yellow
# Generate 32-char random password with special characters
$newPassword = -join ((65..90) + (97..122) + (48..57) + (33,35,36,37,38,42) |
Get-Random -Count 32 | ForEach-Object { [char]$_ })
$passwordProfile = @{
Password = $newPassword
ForceChangePasswordNextSignIn = $true
}
Update-MgUser -UserId $user.Id -PasswordProfile $passwordProfile
Write-Host "[4/4] Containment complete. Recording timestamp." -ForegroundColor Green
$timestamp = Get-Date -Format "yyyy-MM-dd HH:mm:ss UTC" -AsUTC
Write-Host "Containment completed at: $timestamp"
Write-Host "Account: $CompromisedUpn"
Write-Host "Status: Disabled, sessions revoked, password reset"
Write-Host "Next: Proceed to remediation (remove inbox rules, revoke OAuth apps)" -ForegroundColor CyanRemediation goes beyond containment: you must undo everything the attacker configured during the compromise. Attackers establish persistence through inbox rules (forwarding email to external addresses), OAuth app consent (maintaining access even after password reset), MFA method registration (adding their own phone number), and email delegates. Each persistence mechanism must be identified and removed before re-enabling the account.
// WHAT: Detect all post-compromise persistence mechanisms for remediation
// WHY: Before re-enabling the account, you must remove EVERY persistence
// mechanism the attacker established. This query finds them all.
// TABLES: CloudAppEvents - Microsoft 365 cloud operations
// HOW: Searches for inbox rules, mail forwarding, OAuth consent, role changes,
// and MFA registration events during the compromise window
// OUTPUT: All persistence-related actions by the compromised account
let compromisedUser = "user@contoso.com";
let compromiseStart = datetime(2026-03-01T00:00:00Z);
let compromiseEnd = datetime(2026-03-04T00:00:00Z);
CloudAppEvents
| where Timestamp between (compromiseStart .. compromiseEnd)
| where AccountId =~ compromisedUser
| where ActionType in (
"New-InboxRule", // inbox rule (forwarding)
"Set-InboxRule", // modified inbox rule
"Enable-InboxRule", // re-enabled inbox rule
"Set-Mailbox", // mailbox forwarding config
"Add-MailboxPermission", // delegate access added
"Consent to application.", // OAuth app consent
"Add member to role.", // admin role granted
"Update user.", // user profile changes (MFA methods)
"Set-MailboxJunkEmailConfiguration" // junk email rule changes
)
| project Timestamp, ActionType, Application,
Details = RawEventData
| order by Timestamp asc# WHAT: Remove all inbox rules and revoke OAuth apps for a compromised user
# WHY: Inbox rules forwarding to external addresses and OAuth apps are the two
# most common persistence mechanisms. Both must be removed before
# re-enabling the account.
# PERMISSIONS REQUIRED: Mail.ReadWrite, Application.ReadWrite.All
# MODULES REQUIRED: ExchangeOnlineManagement, Microsoft.Graph
param(
[Parameter(Mandatory)]
[string]$CompromisedUpn
)
# --- Remove suspicious inbox rules ---
Connect-ExchangeOnline -UserPrincipalName $CompromisedUpn -ShowBanner:$false
$rules = Get-InboxRule -Mailbox $CompromisedUpn
foreach ($rule in $rules) {
# Flag rules that forward or redirect to external addresses
if ($rule.ForwardTo -or $rule.ForwardAsAttachmentTo -or $rule.RedirectTo) {
Write-Host "Removing suspicious rule: $($rule.Name)" -ForegroundColor Red
Write-Host " ForwardTo: $($rule.ForwardTo)"
Write-Host " RedirectTo: $($rule.RedirectTo)"
Remove-InboxRule -Identity $rule.Identity -Mailbox $CompromisedUpn -Confirm:$false
}
}
# --- Disable mailbox forwarding ---
Set-Mailbox -Identity $CompromisedUpn -ForwardingSmtpAddress $null -ForwardingAddress $null
Write-Host "Mailbox forwarding disabled." -ForegroundColor Green
# --- Revoke OAuth app consents ---
$user = Get-MgUser -Filter "userPrincipalName eq '$CompromisedUpn'"
$appConsents = Get-MgUserOAuth2PermissionGrant -UserId $user.Id
foreach ($consent in $appConsents) {
# Review each consent - remove any granted during the compromise window
Write-Host "OAuth consent: ClientId=$($consent.ClientId), Scope=$($consent.Scope)"
# Uncomment to remove: Remove-MgOAuth2PermissionGrant -OAuth2PermissionGrantId $consent.Id
}
Write-Host "Review complete. Uncomment removal lines after verifying which consents are malicious."Thorough documentation transforms a single investigation into institutional knowledge. Every identity incident should produce a structured report that can be used for lessons learned, executive briefings, regulatory compliance, and tuning detection rules. Use the KQL query below to build a dashboard tile tracking identity attack patterns over time.
// WHAT: Identity attack pattern dashboard - trending attack types over 90 days
// WHY: Provides strategic visibility into which identity attack techniques are
// most prevalent in your environment. Use this to prioritise:
// - Detection rule development (focus on the most common attack types)
// - User training (if phishing dominates, invest in awareness)
// - Control improvements (if brute force dominates, tighten lockout policies)
// TABLE: AADSignInEventsBeta
// KEY FIELDS:
// RiskEventTypes_V2 - JSON array of risk detection types that fired
// RiskLevelDuringSignIn - overall risk level for the sign-in
// OUTPUT: Weekly counts of each attack type for trend analysis
AADSignInEventsBeta
| where Timestamp > ago(90d)
| where RiskLevelDuringSignIn in ("medium", "high")
| mv-expand RiskType = parse_json(RiskEventTypes_V2)
| extend AttackType = tostring(RiskType)
| where isnotempty(AttackType)
| summarize DetectionCount = count(), UniqueUsers = dcount(AccountUpn)
by AttackType, Week = startofweek(Timestamp)
| order by Week desc, DetectionCount desc// WHAT: SOC investigation metrics - mean time to detect, contain, and remediate
// WHY: Tracks SOC performance for identity incidents. These metrics are often
// required for compliance reporting (SOC 2, ISO 27001) and demonstrate
// security program maturity to leadership.
// TABLE: AlertInfo, AlertEvidence (Defender XDR incident data)
// OUTPUT: Average investigation lifecycle times for identity incidents
AlertInfo
| where Timestamp > ago(90d)
| where ServiceSource == "Microsoft Entra ID Protection"
| join kind=inner (AlertEvidence | where EntityType == "User") on AlertId
| summarize
TotalAlerts = count(),
AvgSeverity = avg(case(Severity == "High", 3, Severity == "Medium", 2, 1)),
UniqueUsersImpacted = dcount(AccountObjectId)
by bin(Timestamp, 1w)
| order by Timestamp desc| Resource | Description |
|---|---|
| Defender XDR Incidents | Unified incident investigation across all Defender workloads |
| Advanced Hunting Overview | KQL-based threat hunting across identity, endpoint, email, and cloud data |
| Investigate Risk | How to investigate risky users and risky sign-ins in Identity Protection |
| Respond to First Incident | Step-by-step incident response guidance in Defender XDR |
| Compromised Account Playbook | Microsoft IR playbook for compromised identities and malicious apps |
| MITRE ATT&CK: Credential Access | Reference for mapping identity attacks to ATT&CK techniques |