When working with Windows event logs, especially the Security log, there might be instances where you need to extract specific information from events.
But you’ll find out, it’s not as easy as you’d anticipate.
Recently, I needed to query Windows events from the Security event log for user logon events (Event ID 4624 to be specific). But not all events, just events matching a specific username syntax of domain_name/username
.
Join me on a journey of Windows event logs, XML and XPath as we parse Windows event logs with PowerShell. To demonstrate, let’s walk through an example.
Query the Event Log with Get-WinEvent
You first need to pull at least one Windows event. Since I’m working with user logon events, I’ll pull just one event as an example with event ID 4624.
$eventRecord = Get-WinEvent -MaxEvents 1 -FilterHashtable @{LogName='Security';ID=4624}
Done! You now have an System.Diagnostics.Eventing.Reader.EventLogRecord object.
Convert the Event Record to XML
Now, this tutorial would be pretty short if I just needed to filter on basic properties like Id or TimeCreated but unfortunately, I need to filter information from an event’s Message field. Get-WinEvent doesn’t create a friendly object for us to query. Instead, you must build your own.
To do so, you must first convert the record to XML using the ToXml()
method.
The ToXml()
method converts the entire event object to an XML string.
$xmlEventRecord = $eventRecord.ToXml()
Unless you’re a sadomasochist and prefer to use regex, you need to get this into a structured format to query elements inside of it. Lucky for us, you can easily cast XML strings to Xml.Document types using the [xml]
type accelerator.
$xmlEventRecord = [xml]$eventRecord.ToXml()
Parse the XML to Extract Domain and Username
The XML format of the event contains various elements, and we need to navigate through these to find TargetUserName
and TargetDomainName
. We will use XPath queries to achieve this.
💡 Windows event logs don’t have full XPath support. They use a subset of XML 1.0 that is severely limited in the functions you can use.
On every XmlDocument
object, you have a SelectSingleNode()
method that allows you to pass an XPath query to to find elements in the document but you need to come up with the XPath query first.
When building XPath queries, you must first figure out the “path” to the entity you’re querying or at least the node that holds the value you’re after. In this case, the elements we’re after are in EventEventData
stored as a Data
node with Name
attributes with values of TargetUserName
and TargetDomainName
that we’re after.
<Event xmlns='http://schemas.microsoft.com/win/2004/08/events/event'>
<System>
<Provider Name='Microsoft-Windows-Security-Auditing' Guid='{54849625-5478-4994-a5ba-3e3b0328c30d}'/>
<EventID>4624</EventID>
<Version>2</Version>
<Level>0</Level>
<Task>12544</Task>
<Opcode>0</Opcode>
<Keywords>0x8020000000000000</Keywords>
<TimeCreated SystemTime='2024-06-07T17:23:16.5812423Z'/>
<EventRecordID>260494</EventRecordID>
<Correlation/>
<Execution ProcessID='768' ThreadID='1708'/>
<Channel>Security</Channel>
<Computer>ATA-Devo-SRV-01.atadevo.local</Computer>
<Security/>
</System>
<EventData>
<Data Name='SubjectUserSid'>S-1-5-18</Data>
<Data Name='SubjectUserName'>ATA-DEVO-SRV-01$</Data>
<Data Name='SubjectDomainName'>ATADEVO</Data>
<Data Name='SubjectLogonId'>0x3e7</Data>
<Data Name='TargetUserSid'>S-1-5-21-3909178112-971454023-1868345993-500</Data>
<Data Name='TargetUserName'>atadevoadmin</Data>
<Data Name='TargetDomainName'>ATADEVO</Data>
<Data Name='TargetLogonId'>0x546f80</Data>
<Data Name='LogonType'>7</Data>
<Data Name='LogonProcessName'>Negotiat</Data>
<Data Name='AuthenticationPackageName'>Negotiate</Data>
<Data Name='WorkstationName'>ATA-DEVO-SRV-01</Data>
<Data Name='LogonGuid'> { 97292dbd-b376-1168-64b5-b750cafa6348 }</Data>
<Data Name='TransmittedServices'>-</Data>
<Data Name='LmPackageName'>-</Data>
<Data Name='KeyLength'>0</Data>
<Data Name='ProcessId'>0x300</Data>
<Data Name='ProcessName'>C:WindowsSystem32lsass.exe</Data>
<Data Name='IpAddress'>-</Data>
<Data Name='IpPort'>-</Data>
<Data Name='ImpersonationLevel'>%%1833</Data>
<Data Name='RestrictedAdminMode'>-</Data>
<Data Name='TargetOutboundUserName'>-</Data>
<Data Name='TargetOutboundDomainName'>-</Data>
<Data Name='VirtualAccount'>%%1843</Data>
<Data Name='TargetLinkedLogonId'>0x0</Data>
<Data Name='ElevatedToken'>%%1842</Data>
</EventData>
</Event>
If you don’t work with XPath too often, you’re first inclination would probably be to do something like this:
$xmlEventRecord.SelectSingleNode("//Data[@Name='TargetUserName']")
The XPath query is sound. It’s looking for all Data
nodes with an attribute of Name
and a value of TargetUserName
. But, it won’t work. Why? This XML use a namespace.
xmlns='http://schemas.microsoft.com/win/2004/08/events/event
XML namespaces are used for providing uniquely named elements and attributes in an XML document. An XML instance may contain element or attribute names from more than one XML vocabulary. If each vocabulary is given a namespace, the ambiguity between identically named elements or attributes can be resolved.
To query an XML document that uses a namespace, you must first define an XmlNamespaceManager and then add that namespace to it.
$eventNamespace = New-Object System.Xml.XmlNamespaceManager($xmlEventRecord.NameTable)
$eventNamespace.AddNamespace("evt", "http://schemas.microsoft.com/win/2004/08/events/event")
Once you have the namespace defined, you must then use that namespace in the SelectSingleNode()
query.
$xmlEventRecord.SelectSingleNode("//evt:Data[@Name='TargetUserName']", $eventNamespace)
At this point, you should be able to extract the XML node.
Finding the Text Value
The SelectSingleNode()
method will not return a simple string that you’ll need. Instead, it returns an System.Xml.XmlElement
object which you’ll need to extract the node value from. To do that, you can use your familiar PowerShell dot notation to reference the InnerText
property.
$xmlEventRecord.SelectSingleNode("//evt:Data[@Name='TargetUserName']", $eventNamespace).InnerText
Once you know how to find the text value, you’re back into familiar PowerShell territory. At this point, you just need to extract both of the strings (TargetUserName
and TargetDomainName
) in this case and concatenate them together.
$targetUserName = $xmlEventRecord.SelectSingleNode("//evt:Data[@Name='TargetUserName']", $eventNamespace).InnerText
$targetDomainName = $xmlEventRecord.SelectSingleNode("//evt:Data[@Name='TargetDomainName']", $eventNamespace).InnerText
$domainUser = "$targetDomainName$targetUserName"
Finally, combine the extracted TargetUserName
and TargetDomainName
to form the DomainNameUsername format.
And you’re done!
Conclusion and Script
If you’re building any PowerShell scripts querying Windows event logs, chances are you’re going to have to especially if you want to query the Message
field.
Here’s the complete script if you want to copy and paste.
# Step 1: Query the Security log for Event ID 4624
$eventRecord = Get-WinEvent -MaxEvents 1 -FilterHashtable @{LogName='Security';ID=4624}
# Step 2: Convert the event record to XML
$xmlEventRecord = [xml]$eventRecord.ToXml()
# Step 3: Define the namespace manager
$eventNamespace = New-Object System.Xml.XmlNamespaceManager($xmlEventRecord.NameTable)
$eventNamespace.AddNamespace("evt", "<http://schemas.microsoft.com/win/2004/08/events/event>")
# Step 4: Extract TargetUserName and TargetDomainName using XPath
$targetUserName = $xmlEventRecord.SelectSingleNode("//evt:Data[@Name='TargetUserName']", $eventNamespace).InnerText
$targetDomainName = $xmlEventRecord.SelectSingleNode("//evt:Data[@Name='TargetDomainName']", $eventNamespace).InnerText
# Step 5: Combine DomainName and UserName
$domainUser = "$targetDomainName\$targetUserName"
$domainUser