Advanced Multi-Value Investigations with mv-apply
Some evidence does not arrive as one simple value in one simple column.
It might be stored inside an array, a dynamic field, a list of indicators, a collection of URLs, a set of entities or a nested telemetry object.
This is where mv-apply becomes useful. Instead of only expanding values into separate rows, defenders can apply filtering, projection and summarisation logic to each value while keeping the original investigation context.
In this Agent Foskett Academy lesson, you will learn how defenders use the KQL mv-apply operator to inspect arrays, nested values and multi-value telemetry inside Microsoft Defender XDR and Microsoft Sentinel investigations.
Lesson overview
Learn how mv-apply helps defenders inspect multi-value fields while preserving the surrounding investigation evidence.
Why mv-apply matters
This is useful when the field contains multiple items and the analyst wants to process those items carefully, instead of simply expanding everything and creating unnecessary noise.
Defenders can use mv-apply to inspect arrays, filter nested values, project important fields and summarise suspicious evidence while keeping the original event details available.
Investigation scenario
Some events contain multiple URLs, several extracted indicators and nested JSON values. A simple expansion may show too many rows too quickly.
The investigation needs a controlled way to look inside each multi-value field, filter what matters and keep the original user, device, sender and timestamp evidence attached to the result.
Step 1 — Use mv-apply with a dynamic list
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
SecurityAlert | where TimeGenerated > ago(7d) | extend Indicators = dynamic(["invoice-update.com", "login-check.net", "185.10.20.30"]) | mv-apply Indicator = Indicators on ( where Indicator contains "." | project Indicator ) | project TimeGenerated, AlertName, Severity, Indicator
Step 2 — Filter each value inside the mv-apply block
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
EmailEvents | where Timestamp > ago(30d) | extend CandidateDomains = dynamic(["contoso.com", "secure-login-check.com", "invoice-update.net"]) | mv-apply Domain = CandidateDomains on ( where Domain has_any ("login", "invoice", "secure") | project SuspiciousDomain = Domain ) | project Timestamp, SenderFromAddress, RecipientEmailAddress, Subject, SuspiciousDomain
Step 3 — Use mv-apply with parse_json()
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
SecurityAlert | where TimeGenerated > ago(7d) | extend EntitiesJson = parse_json(Entities) | mv-apply Entity = EntitiesJson on ( where tostring(Entity.Type) in ("account", "host", "ip") | project EntityType = tostring(Entity.Type), EntityValue = tostring(Entity.Name) ) | project TimeGenerated, AlertName, Severity, EntityType, EntityValue
What mv-apply does
This makes it especially useful when investigating dynamic telemetry, alert entities, arrays and lists that need more control than a simple mv-expand.
mv-expand vs mv-apply
mv-apply is better when you want to do more work on each value while it is being expanded, such as filtering, projecting or summarising inside a controlled subquery.
Step 4 — Summarise suspicious values after mv-apply
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
EmailEvents | where Timestamp > ago(30d) | extend CandidateWords = dynamic(["invoice", "password", "verify", "payment"]) | mv-apply Word = CandidateWords on ( where Subject contains Word | project MatchedWord = Word ) | summarize EmailCount = count(), Recipients = dcount(RecipientEmailAddress) by tostring(MatchedWord) | sort by EmailCount desc
Common investigation uses
Common mistakes
What you learned
Related Agent Foskett Academy lessons
Continue learning with Extracting Evidence with extract(), KQL Threat Hunting Guide and Microsoft Security.
Develop IT. Protect IT. GEMXIT PTY LTD | GEMXIT UK LTD