Advanced KQL: Mastering mv-expand.
Agent Foskett had found the evidence.
Unfortunately, it was all crammed into a single field.
One alert contained multiple devices, users, IP addresses and evidence records. Reading it as one dynamic object was painful. Then one operator transformed the investigation.
mv-expand turned the hidden collection into individual rows that could be filtered, searched, joined and understood.

Lesson overview
Learn how to use mv-expand to expand arrays and multi-value fields in Microsoft Defender XDR telemetry into investigation-ready rows.
Why mv-expand matters
mv-expand makes each value easier to search, filter and correlate.The mv-expand workflow
Step 1 — Expand a simple array
- 1
- 2
- 3
- 4
- 5
- 6
let SuspiciousUsers = dynamic(["alice@contoso.com", "bob@contoso.com", "charlie@contoso.com"]); print Users = SuspiciousUsers | mv-expand Users | project User = tostring(Users)
Step 2 — Review alert evidence as rows
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
AlertEvidence
| where Timestamp > ago(7d)
| where isnotempty(AccountName) or isnotempty(DeviceName) or isnotempty(RemoteIP)
| project Timestamp,
AlertId,
EntityType,
EvidenceRole,
AccountName,
DeviceName,
RemoteIP
| order by Timestamp descStep 3 — Expand authentication details
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
SigninLogs
| where TimeGenerated > ago(7d)
| where isnotempty(AuthenticationDetails)
| mv-expand AuthenticationDetails
| extend AuthStep = tostring(AuthenticationDetails.authenticationStepResultDetail),
AuthMethod = tostring(AuthenticationDetails.authenticationMethod),
AuthRequirement = tostring(AuthenticationDetails.authenticationStepRequirement)
| project TimeGenerated,
UserPrincipalName,
IPAddress,
Location,
AuthMethod,
AuthRequirement,
AuthStep
| order by TimeGenerated descStep 4 — Combine parse_json() with mv-expand
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
let TimeFrame = 7d;
CloudAppEvents
| where Timestamp > ago(TimeFrame)
| where isnotempty(RawEventData)
| extend ParsedEvent = parse_json(RawEventData)
| mv-expand ParsedEvent
| project Timestamp,
AccountDisplayName,
ActionType,
Application,
ParsedEventStep 5 — Expand, then summarise
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
SigninLogs
| where TimeGenerated > ago(14d)
| where isnotempty(AuthenticationDetails)
| mv-expand AuthenticationDetails
| extend AuthMethod = tostring(AuthenticationDetails.authenticationMethod)
| summarize AuthMethods = make_set(AuthMethod, 20),
SignInCount = count(),
FirstSeen = min(TimeGenerated),
LastSeen = max(TimeGenerated)
by UserPrincipalName
| order by SignInCount descStep 6 — Join expanded evidence with alert context
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
let RecentAlerts =
AlertInfo
| where Timestamp > ago(7d)
| project AlertId, Title, Severity, Category;
AlertEvidence
| where Timestamp > ago(7d)
| where isnotempty(AccountName)
| join kind=inner RecentAlerts on AlertId
| project Timestamp,
Title,
Severity,
Category,
AccountName,
DeviceName,
RemoteIP,
EvidenceRole
| order by Timestamp descStep 7 — Build a reusable mv-expand pattern
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
let TimeFrame = 7d;
let ExpandedAuth =
SigninLogs
| where TimeGenerated > ago(TimeFrame)
| where isnotempty(AuthenticationDetails)
| mv-expand AuthenticationDetails
| extend AuthMethod = tostring(AuthenticationDetails.authenticationMethod),
AuthResult = tostring(AuthenticationDetails.authenticationStepResultDetail)
| project TimeGenerated, UserPrincipalName, IPAddress, Location, AuthMethod, AuthResult;
ExpandedAuth
| where AuthMethod has_any ("SMS", "Phone", "Authenticator")
| summarize Methods = make_set(AuthMethod, 20),
Results = make_set(AuthResult, 20),
IPAddresses = make_set(IPAddress, 50),
Locations = make_set(Location, 20)
by UserPrincipalName
| order by UserPrincipalName ascCommon mv-expand mistakes
Investigation checklist
Related Agent Foskett Academy lessons
Coming next
Final thought
Advanced KQL Mastering mv-expand
Agent Foskett Academy Lesson 123 teaches defenders how to use mv-expand in KQL to expand arrays, dynamic fields, authentication details, alert evidence and Microsoft Defender XDR telemetry into searchable investigation rows.
Learn mv-expand for Microsoft Defender XDR hunting
mv-expand helps security analysts investigate dynamic arrays, multi-value fields, JSON data and nested evidence collections across Microsoft Defender XDR and Microsoft Sentinel.
KQL mv-expand investigation tutorial
This lesson explains how to combine mv-expand with parse_json(), joins, summarise patterns and reusable let statements to build advanced Microsoft security hunting queries.
