Building Bulletproof Sentinel Queries: The Universal EventData XML Parser Pattern

Microsoft Sentinel · KQL Deep Dive

Stop fragile index-based XML parsing and switch to a universal, future-proof EventData parser that works across all Windows event types in Microsoft Sentinel.

If you’ve ever worked with the Event table in Microsoft Sentinel or Azure Monitor Logs, you already know the pain:

  • EventData is XML and every event type uses a different schema.
  • Most examples use fragile array indexes like Data[7]['#text'].
  • This post gives you a universal KQL pattern that dynamically parses all <Data> fields.
  • No index guessing, no hardcoding, and it works across multiple Event IDs.

In this post, I’m sharing a universal EventData parser — a single KQL pattern that can extract all fields from the EventData XML dynamically, without relying on indexes.

This approach is already used deep inside Microsoft’s own threat-hunting content, but it’s rarely explained clearly. So I broke it down, cleaned it up, and turned it into a reusable query that you can run anywhere.

Claim: This is one of the most versatile EventData parsers you will ever use in Microsoft Sentinel.

Why the Usual Way of Parsing EventData Fails

Traditionally, analysts parse XML like this:

extend ThreatName = parse_xml(EventData).DataItem.EventData.Data[7]['#text']

Problems with this approach:

  • Fragile – If Microsoft adds or reorders <Data> fields, everything breaks.
  • Event-specific – Works only for one Event ID or one schema.
  • Hard to maintain – Counting indexes destroys readability.
  • Not scalable – You must rewrite the parser for each event type.

We clearly needed a better way.

Understanding the EventData XML Structure

A typical EventData XML looks like this:

<DataItem>
  <EventData>
    <Data Name="SubjectUserName">CONTOSO\Admin</Data>
    <Data Name="TargetUserName">Server01$</Data>
    <Data Name="IpAddress">10.0.0.4</Data>
    <Data Name="WorkstationName">WS-01</Data>
  </EventData>
</DataItem>

Important points:

  • Every <Data> node has two useful components:
Component Example Meaning
@Name "IpAddress" Attribute that becomes a column name.
#text "10.0.0.4" The field value for that column.

These map perfectly to column name and column value. If we extract each <Data> node, expand them into individual rows, and then pivot them back into columns… we get a clean, flattened table.

🔥 The Universal EventData XML Parser (KQL)

Here is the complete, production-quality version:

// Universal EventData XML parser for the Event table
Event
| where TimeGenerated >= ago(24h)
// Parse the EventData XML (DataItem is a wrapper node)
| extend ParsedEventData = parse_xml(EventData).DataItem.EventData.Data
// Expand each <Data> element into its own row
| mv-expand ParsedEventData
// Extract Name and value from each Data node
| extend
    FieldName  = tostring(ParsedEventData['@Name']),
    FieldValue = tostring(ParsedEventData['#text'])
// Pivot: one column per unique FieldName
| evaluate pivot(FieldName, any(FieldValue), TimeGenerated, Computer, EventID)

This query:

  • Extracts every field inside EventData.
  • Automatically names columns using <Data Name="...">.
  • Works across all event IDs, not just one.
  • Requires zero editing when the XML structure changes.

This is the cleanest and most flexible approach to EventData parsing that I’ve found for Microsoft Sentinel.

Step-by-Step Explanation

1. Parse the XML

extend ParsedEventData = parse_xml(EventData).DataItem.EventData.Data
  • parse_xml() turns the raw XML string into a structured dynamic object.
  • We directly access the array of all <Data> elements.
  • No guessing, no indexes — just the full Data array.

2. Expand the Data fields

| mv-expand ParsedEventData

This creates one row per field in the EventData XML. Conceptually, you get something like:

TimeGenerated EventID ParsedEventData
2025-01-01T10:00:00Z 4624 <Data Name=”IpAddress”>10.0.0.4</Data>
2025-01-01T10:00:00Z 4624 <Data Name=”TargetUserName”>Server01$</Data>

Perfect input for the next step: extracting key/value pairs and pivoting.

3. Extract the key/value pairs

| extend
    FieldName = tostring(ParsedEventData['@Name']),
    FieldValue = tostring(ParsedEventData['#text'])

Now we have a clean list of column names and values in two simple fields: FieldName and FieldValue.

4. Convert rows into columns

| evaluate pivot(FieldName, any(FieldValue), TimeGenerated, Computer, EventID)

This transforms your row-wise key/value list into a familiar, analyst-friendly table:

TimeGenerated Computer EventID IpAddress TargetUserName SubjectUserName
2025-01-01T10:00:00Z SERVER01 4624 10.0.0.4 Server01$ CONTOSO\Admin

Exactly what analysts always wanted: a flat, self-explanatory schema.

Why This Parser Matters (and Why It’s Powerful)

1. No More Indexing or Hardcoding

Fields are dynamically extracted based on XML attributes. You never again need to write:

Data[7]['#text']

2. Works for All Event IDs

You can use the same pattern across:

  • 4624 logons
  • 4688 process creation
  • 7045 service installation
  • Sysmon events
  • Custom Windows events
  • AMA / DCR-based logs
  • …and more
One parser to rule them all. You no longer maintain separate parsers for each Event ID or XML schema.

3. Ideal for SOC Analysts, Detection Engineers & Hunters

  • Build detections faster.
  • Write cleaner, index-free KQL.
  • Investigate events more efficiently.
  • Extract full context without knowing the XML beforehand.

4. Future-proof

Even if Microsoft changes ordering or adds new fields, your parser continues working because the logic is based on field names, not positions.

Optional: Wrap Into a Reusable Function

You can publish this once as a function and call it anywhere inside Microsoft Sentinel:

let parse_eventdata = (input:(TimeGenerated:datetime, Computer:string, EventID:int, EventData:string)) {
    input
    | extend ParsedEventData = parse_xml(EventData).DataItem.EventData.Data
    | mv-expand ParsedEventData
    | extend
        FieldName  = tostring(ParsedEventData['@Name']),
        FieldValue = tostring(ParsedEventData['#text'])
    | evaluate pivot(FieldName, any(FieldValue), TimeGenerated, Computer, EventID)
};
// Usage
parse_eventdata(Event | where TimeGenerated >= ago(1d))

Result: Beautiful, reusable, and scalable. Add it to your hunting workbook, use it in analytics rules, or plug it into ad-hoc investigations.

Performance Tips

As with any XML parsing in KQL, a few best practices go a long way:

  • Always use a time filter (e.g., ago(24h) or smaller).
  • Filter by EventLog or EventID before parsing where possible.
  • Avoid parsing months of raw EventData at once unless absolutely required.
  • Consider transforming only the events you truly need (e.g., specific Event IDs).

How Unique Is This Method?

This pattern does appear inside advanced Microsoft content (for example, deep-dive threat hunting guidance) and in a few high-level community posts. However, it is:

  • Rarely documented clearly.
  • Almost never explained step-by-step.
  • Not widely known among SOC analysts.

Most blogs still teach index-based parsing. By publishing this post, you’re giving the community a:

  • Universal,
  • Reusable,
  • Production-ready,
  • Clear

solution for EventData parsing that people can plug directly into their Sentinel environments.

Final Thoughts

This universal parser is one of the most powerful ways to work with Windows Event Logs in Microsoft Sentinel. It simplifies analysis, enhances detection engineering, and eliminates the brittle, outdated practice of index-based XML parsing.

If you work with Sentinel long enough, this quickly becomes one of your core KQL tools — something you reuse across detections, hunting queries, and incident investigations.

Next step: Drop this parser into your Sentinel environment, run it on your most common Event IDs, and see how much cleaner your investigations become.

Leave a comment