Using mv-expand in KQL.
The evidence was all there.
But it was packed into one field.
One event. Many values. Too many clues hidden in the same row.
Agent Foskett knew the investigation needed each value to stand on its own.
Lesson overview
Learn how to use mv-expand to split arrays and multi-value fields into individual rows, making Microsoft Defender XDR evidence easier to filter, summarise and investigate.
Why mv-expand matters
The mv-expand investigation workflow
Step 1 — Inspect the dynamic field
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
DeviceEvents
| where Timestamp > ago(24h)
| where isnotempty(AdditionalFields)
| project Timestamp,
DeviceName,
ActionType,
AdditionalFields
| take 20Step 2 — Expand a simple dynamic array
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
DeviceEvents
| where Timestamp > ago(24h)
| extend Details = parse_json(AdditionalFields)
| extend Indicators = Details.Indicators
| mv-expand Indicators
| project Timestamp,
DeviceName,
ActionType,
IndicatorsStep 3 — Extract properties after expanding
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
DeviceEvents
| where Timestamp > ago(24h)
| extend Details = parse_json(AdditionalFields)
| extend Indicators = Details.Indicators
| mv-expand Indicators
| extend IndicatorType = tostring(Indicators.Type)
| extend IndicatorValue = tostring(Indicators.Value)
| project Timestamp,
DeviceName,
ActionType,
IndicatorType,
IndicatorValue
| order by Timestamp descStep 4 — Expand email recipients
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
EmailEvents
| where Timestamp > ago(7d)
| where isnotempty(RecipientEmailAddress)
| summarize Recipients = make_set(RecipientEmailAddress, 100)
by NetworkMessageId,
SenderFromAddress,
Subject
| mv-expand Recipients
| extend Recipient = tostring(Recipients)
| project NetworkMessageId,
SenderFromAddress,
Recipient,
SubjectStep 5 — Expand indicators for IOC hunting
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
let IOCs = dynamic([
"contoso-login.example",
"198.51.100.25",
"malicious-file.exe"
]);
print Indicator = IOCs
| mv-expand Indicator
| extend Indicator = tostring(Indicator)
| join kind=inner (
DeviceNetworkEvents
| where Timestamp > ago(7d)
| project Timestamp,
DeviceName,
RemoteUrl,
RemoteIP
) on $left.Indicator == $right.RemoteUrl
| project Timestamp,
DeviceName,
Indicator,
RemoteUrl,
RemoteIPStep 6 — Summarise expanded values
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
DeviceEvents
| where Timestamp > ago(7d)
| extend Details = parse_json(AdditionalFields)
| mv-expand Indicator = Details.Indicators
| extend IndicatorValue = tostring(Indicator.Value)
| where isnotempty(IndicatorValue)
| summarize Events = count(),
Devices = make_set(DeviceName, 20),
Actions = make_set(ActionType, 20)
by IndicatorValue
| order by Events descmv-expand patterns to remember
Real-world investigation
Investigation checklist
Related Agent Foskett Academy lessons
Coming next
Final thought
Using mv-expand in KQL
Agent Foskett Academy Lesson 97 teaches defenders how to use mv-expand in KQL to split arrays and multi-value fields into individual rows during Microsoft Defender XDR investigations.
Learn mv-expand for Microsoft Defender XDR
The mv-expand operator helps Microsoft security analysts unpack dynamic data, expand indicators, separate email recipients and analyse hidden values in Defender XDR telemetry.
KQL mv-expand tutorial for threat hunting
This lesson explains how to inspect dynamic fields, parse JSON, expand arrays, extract properties and summarise expanded evidence during Microsoft Defender XDR threat hunting.
