PowerShell

PowerShell – Everything you wanted to know about Event Logs and then some

If you feel this title is very familiar to you it's because I have stolen the title from Kevin Marquette. I'm in awe of his posts that take you thru topic from beginning till the end. No splitting, no hiding anything, everything on a plate, in a single post. That's why I've decided to write a post that will take you on a trip on how to work with Event Logs, something that is an internal part of Windows Administration. If you've never worked with Events and you're in IT, you most likely should make an effort to find out what it is and how you can eat it. After you read this pretty lengthy blog post you may also be interested in The only PowerShell Command you will ever need to find out who did what in Active Directory. It massively simplifies gathering events from Active Directory, but not only that. It will be worth your time – I promise!

As any respected administrator should be aware (I hope) that anytime something happens in Windows, it's written to Event Log (with exceptions). The user is created, windows update is installed, time is synchronized, service is restarted. Anything and everything can be found in Event Logs. Of course, not everything is enabled by default, but we will talk about it a bit later. There are four default Event Log types. Those are:

Application – shows events related to software installed on a machine.
Security – contains events related to the security of a computer. It's very important Event Log and usually hardest to work with permission wise
Setup – holds setup events related to KB installations, performance events, and events that occur during installation
System – shows events that are related to the system. Restart of system, restart of services, system files, stuff like that

And a special kind of Event Log

Forwarded Events – is a special place to store event logs that are forwarded from other computers in the network. You have to explicitly set it up but it comes as a very useful way to have a central place for logs you verify often

But that's just what you get by default. There are many, many more Event Logs. Every time you install some feature or enable some option in Windows, it usually comes with its own set of logs. Sometimes it writes events directly to one of those four default Event Logs, but often it creates very own custom ones for additional, extended information.

Event Viewer - What's that?

Your first and usually last tool to help you work with Events is Event Viewer. It's a tool that Microsoft bundles as part of Windows since it's early beginnings and it hasn't changed much over the years. It's simple but at the same time a very powerful tool.

As you can see in the screenshot above, there are those five event log types I talked before, but also a bunch of other types created by applications and services that are installed in the system. Now, some of those event logs are empty, some contain useless information that you will never have to care about, but you should be aware of their existence.

Event Log Schema

Each event consists of multiple standard fields. Those fields are as follows:

Log Name – is the name of Event Log you want to view. Those are, among others, Application, Security, System and so on.
Source – Is a name that allows you to distinguish the source of events. Usually, it will be an application name or service that created an event.
Event ID – as the name suggests it's an ID of an Event. Each Event ID tells a different story. Combined with Log Name it's one of the most important information. Just by typing Event ID <number> in google will give you lots of possible descriptions on the potential problem you're having and hopefully a solution to it.
Level – There are 6 levels available Verbose, Informational, Warning, Error, Critical and LogAlways. This is event criticality. Often when diagnosing problems you can filter logs only to Critical, Error level to get instant information about issues on workstation/server.
User – This field usually contains a user that generated an event. Usually, it will be SYSTEM or N/A (Not available).
Logged – Time the event was logged on
Task Category – Each event source can have its category. Some events have it, some don't.
Keywords – There are 9 keywords in use. Most important are: AuditFailure, AuditSuccess
Computer – computer name that reported the event.

But they are not the only ones. Each event also consists of a message and a lot of other fields that contain data. It's up to developers to choose how they want to build their events and what properties they fill. This makes it a bit hard to automate because each event type can have an entirely different structure (to some degree).

Working with Windows Events in PowerShell

Microsoft offers multiple commands that allow Administrators to work with Event Logs. You can read, write and create event logs. For this article, I'm going to focus only on reading part of it. For this article, I will focus on the two most important commands from my perspective. The two commands that are provided with the system are: Get-EventLog and Get-WinEvent. Get-EventLog has been around for ages and is still available on modern systems. Get-WinEvent is technically replacement of it. While some people still use Get-EventLog it's slowly being phased out. It already is unable to report proper log sizes for event logs that are bigger than 4GB. You can read about this problem on my other blog post at Get-EventLog shows wrong maximum size of event logs. It can't work with most of the modern Event Logs created by applications either. You should make an effort to learn using Get-WinEvent or as I will try to show you start using my version (wrapper) called Get-Events which is available after you install PSEventViewer module. Let's not rush it thou and have a small tour over both offerings.

Get-Command *Event* -CommandType Cmdlet | Where-Object { $_.Source -ne 'AWSPowerShell' -and $_.Source -ne 'Hyper-V' }

CommandType     Name                                               Version    Source
-----------     ----                                               -------    ------
Cmdlet          Clear-EventLog                                     3.1.0.0    Microsoft.PowerShell.Management
Cmdlet          Get-Event                                          3.1.0.0    Microsoft.PowerShell.Utility
Cmdlet          Get-EventLog                                       3.1.0.0    Microsoft.PowerShell.Management
Cmdlet          Get-EventSubscriber                                3.1.0.0    Microsoft.PowerShell.Utility
Cmdlet          Get-WinEvent                                       3.0.0.0    Microsoft.PowerShell.Diagnostics
Cmdlet          Limit-EventLog                                     3.1.0.0    Microsoft.PowerShell.Management
Cmdlet          New-Event                                          3.1.0.0    Microsoft.PowerShell.Utility
Cmdlet          New-EventLog                                       3.1.0.0    Microsoft.PowerShell.Management
Cmdlet          New-WinEvent                                       3.0.0.0    Microsoft.PowerShell.Diagnostics
Cmdlet          Register-CimIndicationEvent                        1.0.0.0    CimCmdlets
Cmdlet          Register-EngineEvent                               3.1.0.0    Microsoft.PowerShell.Utility
Cmdlet          Register-ObjectEvent                               3.1.0.0    Microsoft.PowerShell.Utility
Cmdlet          Register-WmiEvent                                  3.1.0.0    Microsoft.PowerShell.Management
Cmdlet          Remove-Event                                       3.1.0.0    Microsoft.PowerShell.Utility
Cmdlet          Remove-EventLog                                    3.1.0.0    Microsoft.PowerShell.Management
Cmdlet          Show-EventLog                                      3.1.0.0    Microsoft.PowerShell.Management
Cmdlet          Unregister-Event                                   3.1.0.0    Microsoft.PowerShell.Utility
Cmdlet          Wait-Event                                         3.1.0.0    Microsoft.PowerShell.Utility
Cmdlet          Write-EventLog                                     3.1.0.0    Microsoft.PowerShell.Management
Get a list of available Event Logs - Get-WinEvent

Get-WinEvent has many good features. One of which is an easy way to get all available Event Logs.

Get-WinEvent -ListLog *

Of course, it also allows you to ask remote machines for the same type of data.

Get-WinEvent -ListLog * -ComputerName AD1

We can also provide credentials if the computer requires different ones then the ones we are currently logged with. Using Get-Credential will ask us to provide name and password we want to connect with.

$Credentials = Get-Credentials
Get-WinEvent -ListLog * -ComputerName AD1 -Credentials $Credentials
Get a list of available Event Logs - Get-EventLog

Get-EventLog also provides a similar way to get Event Logs list.

Get-EventLog -List

It also provides a way to do it with Credentials and on remote computers. But there's a big difference what they can show.

Same computer, a different command to list Event Logs available. The difference is close to 480 Event Logs that are missing from Get-EventLog command. It's essentially able to work only with Classic Event Logs.

Get few events from Application Log using Get-WinEvent or Get-EventLog

Both commands can be used to list event logs from. Below I'm listing five events from Application log using both commands. They work largely the same in that matter.

Get-WinEvent vs Get-EventLog on Event Log Details

There's one more useful feature of Get-WinEvent -ListLog over Get-EventLog -List. Get-EventLog provides a list and nothing more above what you see in the screenshot above. Get-WinEvent, on the other hand, delivers a lot more information.

In comparison, this is what is reported by Get-WinEvent. As you can notice below ListLog is string parameter that takes wildcards and allows you to limit output to Event Logs you need. Get-EventLog only uses switch parameter and outputs all logs, including much less information.

As you can see above Get-WinEvent ListLog is able to deliver full information about log size, last access time, last write time, the oldest record, records in total and many many more. This command can be used to effectively deliver information about all logs stored on the system.  You would typically use it to verify current and maximum log size, log mode (circular, backup) and file location.

Get event log with Get-CimInstance or Get-WmiObject

For full disclosure, there's also a way to work with Event Logs using WMI objects. But it doesn't offer all functionalities and has similar problems as Get-EventLog in some areas. If you would like to explore that route following is the output of WMI Class Win32_NTEventLogFile.

$WMI = Get-CimInstance -ClassName 'Win32_NTEventlogfile'
$WMI |Format-Table -AutoSize

And if we check details there's a lot more information available for each event log.

Get-CimInstance is a new, modern way to query WMI classes. If you want to use an old approach, be my guest

$WMI = Get-WMIObject -ClassName 'Win32_NTEventlogfile'
$WMI |Format-Table -AutoSize

As I've mentioned earlier, data returned by WMI shouldn't be trusted. My other blog post explains this.

Get-WinEvent vs Get-EventLog on Event Log Details

If you've been reading everything above you may be thinking now that why do I mention Get-EventLog at all and why people use it still when Get-WinEvent is superior in comparison. To understand where they are coming from let's take a look at the syntax of both commands. As a side note, I'm using Mathias Jessen regex to clean up output for visibility reasons.

Get-EventLog Synatx

PS C:\Users\pklys> (Get-Command -Name 'Get-EventLog' -Syntax) -replace '\]? \[*(?=-|<C)',"`r`n "

Get-EventLog
 -LogName] <string>
 -InstanceId] <long[]>
 -ComputerName <string[]>
 -Newest <int>
 -After <datetime>
 -Before <datetime>
 -UserName <string[]>
 -Index <int[]>
 -EntryType <string[]>
 -Source <string[]>
 -Message <string>
 -AsBaseObject
 <CommonParameters>]

Get-EventLog
 -ComputerName <string[]>
 -List
 -AsString
 <CommonParameters>]

Get-WinEvent Syntax

PS C:\Users\pklys> (Get-Command -Name 'Get-WinEvent' -Syntax) -replace '\]? \[*(?=-|<C)',"`r`n "

Get-WinEvent
 -LogName] <string[]>
 -MaxEvents <long>
 -ComputerName <string>
 -Credential <pscredential>
 -FilterXPath <string>
 -Force
 -Oldest
 <CommonParameters>]

Get-WinEvent
 -ListLog] <string[]>
 -ComputerName <string>
 -Credential <pscredential>
 -Force
 <CommonParameters>]

Get-WinEvent
 -ListProvider] <string[]>
 -ComputerName <string>
 -Credential <pscredential>
 <CommonParameters>]

Get-WinEvent
 -ProviderName] <string[]>
 -MaxEvents <long>
 -ComputerName <string>
 -Credential <pscredential>
 -FilterXPath <string>
 -Force
 -Oldest
 <CommonParameters>]

Get-WinEvent
 -Path] <string[]>
 -MaxEvents <long>
 -Credential <pscredential>
 -FilterXPath <string>
 -Oldest
 <CommonParameters>]

Get-WinEvent
 -FilterHashtable] <hashtable[]>
 -MaxEvents <long>
 -ComputerName <string>
 -Credential <pscredential>
 -Force
 -Oldest
 <CommonParameters>]

Get-WinEvent
 -FilterXml] <xml>
 -MaxEvents <long>
 -ComputerName <string>
 -Credential <pscredential>
 -Oldest
 <CommonParameters>]

As you can see above Get-EventLog while offering fewer choices, it does provide more parameters for a single query. Get-WinEvents has more options to choose from, but they are a bit limited (at first sight).

$DateAfter = (Get-Date).AddDays(-1)
$DateBefore = (Get-Date)
$EventLogTest = Get-EventLog -LogName Security -InstanceId 4625 -Before $DateBefore -After $DateAfter -Newest 5
$WinEventTest = Get-WinEvent -FilterHashtable @{ LogName = 'Security'; Id = 4625; StartTime = $DateAfter; EndTime = $DateBefore } -MaxEvents 5

As you can see above what Get-EventLog offers as a natural, intuitive way to get events we want by providing named parameters for common scenarios. Get-WinEvent requires a bit more complicated syntax, but it gives far more options to it if you know what you're doing. It's more error prone but at the same time very powerful. At this point, I should mention that while on first look InstanceID looks like EventID and even acts like EventID it is NOT EventID. So while in the example above I've used it this way it may not give you expected results every time.

InstanceID is not EventID, but can be

The InstanceId property uniquely identifies an event entry for a configured event source. The InstanceId for an event log entry represents the full 32-bit resource identifier for the event in the message resource file for the event source. The EventID property equals the InstanceId with the top two bits masked off. Two event log entries from the same source can have matching EventID values, but have different InstanceId values due to differences in the top two bits of the resource identifier. If the application wrote the event entry using one of the WriteEntry methods, the InstanceId property matches the optional eventId parameter. If the application wrote the event using WriteEvent, the InstanceId property matches the resource identifier specified in the InstanceId of the instance parameter. If the application wrote the event using the Win32 API ReportEvent, the InstanceId property matches the resource identifier specified in the dwEventID parameter.

See the difference? Some EventID are the same as InstanceID, but some are not even close. That means you can't reliably trust in Get-EventLog return data if you use InstanceID (unless you are sure what you ask for).

Three ways to get data from Get-WinEvent

Actually, Get-WinEvent offers three complicated ways to ask for data. Those are:

FilterHashtable – Specifies a query in hash table format to select events from one or more event logs. The query contains a hash table with one or more key-value pairs.
FilterXPath – Specifies an XPath query that this cmdlet select events from one or more logs.
FilterXML – Specifies a structured XML query that this cmdlet selects events from one or more event logs.

All three methods offer some pros and cons. FilterHashtable is most commonly used because it's predictable and fairly easy to build.

Get-WinEvent - FilterHashTable How To

Below you can see multiple keys that you can define in a HashTable for Get-WinEvent.

LogName=<String[]>
ProviderName=<String[]>
Path=<String[]>
Keywords=<Long[]>
ID=<Int32[]>
Level=<Int32[]>
StartTime=<DateTime>
EndTime=<DataTime>
UserID=<SID>
Data=<String[]>
*=<String[]>

As you can see FilterHashTable has multiple options to filter on. It has LogName, ProviderName (a name for Source), Path (FilePath for scanning offline EVTX files), Keywords, ID, Level, UserID and Data and Dates. While all that is available it's not so easy to master. As you can notice Level and Keywords hashtable keys require a number, not a value you usually see in Event Viewer. Same for UserSID which is not something you will have on hand easily.

$FilterHashTable = @{
    LogName   = 'Security'
    ProviderName= 'Microsoft-Windows-Security-Auditing' 
    #Path = <String[]>
    #Keywords = <Long[]>
    ID        = 4625
    #Level = <Int32[]>
    StartTime = (Get-Date).AddDays(-1)
    EndTime   = Get-Date
    #UserID = <SID>
    #Data = <String[]>
}

Get-WinEvent -FilterHashtable $FilterHashTable -MaxEvents 5

Get-EventLog -LogName 'Security' -Source 'Microsoft-Windows-Security-Auditing' -Newest 5

Above you can see both Get-WinEvent and Get-EventLog in comparison. It doesn't look like an even fight to me. Get-EventLog is more comfortable providing the ability to ask for Events without checking any documentation, but at the same time, Get-WinEvent has more options to choose from. Get-EventLog has EntryType which is another name for Keywords. Get-WinEvent has Levels (as seen below), and Get-EventLog does not. This allows you to narrow results if needed. Again Get-EventLog EntryTypes comes in a readable form. You don't need to know long numbers that you have to provide to Get-WinEvent. Of course, you can define hashtables as I did below and use those data to simplify your input, but you need to remember about it.

$Keywords = @{
    AuditFailure     = 4503599627370496
    AuditSuccess     = 9007199254740992
    CorrelationHint2 = 18014398509481984
    EventLogClassic  = 36028797018963968
    Sqm              = 2251799813685248
    WdiDiagnostic    = 1125899906842624
    WdiContext       = 562949953421312
    ResponseTime     = 281474976710656
    None             = 0
}
$Levels = @{
    Verbose       = 5
    Informational = 4
    Warning       = 3
    Error         = 2
    Critical      = 1
    LogAlways     = 0
}

I can hear you now saying…Wait! Even if something is missing you can filter it out. Just use Where-Object. Indeed you could but if you check what is the output of Get-EventLog you can clearly see there is EntryType which is the same as Keywords for Get-WinEvent but Level is not given.

Get-EventLog Output

In comparison that's how Get-WinEvent output looks like.

Get-WinEvent Output

There's some more data on Get-WinEvent to choose from. There's one fair warning thou. You shouldn't use Where-Object to filter out data from Event Logs. Why not? Because for Where-Object to do its job both Get-WinEvent and Get-Eventlog has to first get all the data and then pass it to Where-Object which drops elements that we don't' want. While it may seem innocent it's actually a very heavy process. Imagine asking Event Logs for 2000 events that match where you're actually interested in 5 of them. You would first get all 2000 events and use Where-Object locally to apply the filter and get that 5 objects you want. You just wasted seconds, minutes if not hours doing that. Think of Event Logs like a Database. The better the query you build, the better results you get and in a shorter amount of time.

Write-Color 'Scanning Event Log with Get-EventLog' -Color Blue

$Time2 = Start-TimeLog
$Event2 = Get-EventLog -LogName 'Security' -Source 'Microsoft-Windows-Security-Auditing' -InstanceId 4625 -Before (Get-Date) -After ((Get-Date).AddDays(-1))| Where-Object { $_.Index -eq '4125545' }
Stop-TimeLog -Time $Time2

Write-Color 'Scanning Event Log with Get-WinEvent' -Color Green
$Time = Start-TimeLog
$FilterHashTable = @{
    LogName      = 'Security'
    ProviderName = 'Microsoft-Windows-Security-Auditing' 
    #Path = <String[]>
    #Keywords = <Long[]>
    ID           = 4625
    #Level = <Int32[]>
    StartTime    = (Get-Date).AddDays(-1)
    EndTime      = Get-Date
    #UserID = <SID>
    #Data = <String[]>
}

$Event1 = Get-WinEvent -FilterHashtable $FilterHashTable | Where-Object { $_.RecordID -eq '4125545'}
Stop-TimeLog -Time $Time

The results of this test surprised me if I am, to be honest. While I wanted to show you the difference you get when using Where-Object filtering and how it can affect speed I didn't expect a huge difference in what is essentially the same scan for the same data. The results are 10 seconds for Get-EventLog and 3 minutes 42 seconds for Get-WinEvent. Wow! Totally not what I expected but let's leave it for now, and I'll show you later what Get-WinEvent can do to mitigate that problem. Get-EventLog was faster because InstanceID is actually Indexed property, while EventID is not. And knowing that InstanceID is not exactly EventID (as stated earlier) it's possible that for different data requests Get-EventLog can give you wrong results. This is why to compare Oranges to Oranges we need to use Where-Object and explicitly ask for EventID. You need to remember thou that when you use Where-Object all logs that matched criteria are first downloaded to PowerShell, stored in memory and then filtered to get a single Record.

Write-Color 'Scanning Event Log with Get-EventLog' -Color Blue

$Time2 = Start-TimeLog
$Event2 = Get-EventLog -LogName 'Security' -Source 'Microsoft-Windows-Security-Auditing' -Before (Get-Date) -After ((Get-Date).AddDays(-1)) | Where-Object { $_.EventID -eq 4625 -and $_.Index -eq '4096742' }
Stop-TimeLog -Time $Time2

Write-Color 'Scanning Event Log with Get-WinEvent' -Color Green
$Time = Start-TimeLog
$FilterHashTable = @{
    LogName      = 'Security'
    ProviderName = 'Microsoft-Windows-Security-Auditing' 
    ID           = 4625
    StartTime    = (Get-Date).AddDays(-1)
    EndTime      = Get-Date
}

$Event1 = Get-WinEvent -FilterHashtable $FilterHashTable -Verbose | Where-Object { $_.RecordID -eq '4096742'}
Stop-TimeLog -Time $Time

Even with the modification of our query for Get-EventLog, it was still on top of Get-WinEvent with no real change to results. This changes a lot when we have to query a remote machine. As you can see below when we queried my Domain Controller Get-EventLog scanned all Security Logs, and after it got them, it dropped the ones that didn't match, losing all of them as there were 0 results. It took 26 minutes to make that scan. Get-WinEvent, on the other hand, took only 8 seconds to find out that there is actually no 4625 EventID in the database and terminated shortly after. This proves that using Where-Object to filter events is very time-consuming. It may be fast in some instances, but for others, it will take ages to get proper data.

Get-WinEvent - FilterXML

While FilterHashTable was useful it's just a helper for FilterXML. Let's have a quick look at what Verbose message shows for Get-WinEvent when you run it, using the same example with FilterHashTable as a parameter.

<QueryList>
    <Query Id="0" Path="security">
        <Select Path="security">*[(System/TimeCreated[@SystemTime>='2019-02-18T19:46:24.000Z' and @SystemTime<='2019-02-19T19:46:27.000Z']) and (System/EventID=4625)]</Select>
    </Query>
</QueryList>.

It actually creates XML Query behind the scenes and passing it to Get-WinEvent for us. As you can see in XML there are Dates From / To, and there is Event ID. If we use the same query as above we can simply pass it as a parameter to Get-WinEvent.

$XML = @'
<QueryList>
    <Query Id="0" Path="security">
        <Select Path="security">*[(System/TimeCreated[@SystemTime>='2019-02-18T19:46:24.000Z' and @SystemTime<='2019-02-19T19:46:27.000Z']) and (System/EventID=4625)]</Select>
    </Query>
</QueryList>
'@

Get-WinEvent -FilterXML $XML

You will get exactly the same results as we did when using FilterHashTable. So why would you go and create a bit complicated XML instead of using reasonably comfortable to use HashTable? If you would use the same query, we did before where I was so surprised with the lousy performance of Get-WinEvent you would get the same results. But if our goal was to get single RecordID you should be pleasantly surprised. Let's see, and build a simple XML query with RecordID, something that is not possible with FilterHashTable.

<QueryList>
    <Query Id="0" Path="Security">
        <Select Path="Security"> *[System[EventRecordID=4096742]]
        </Select>
    </Query>
</QueryList>

As you can see in PowerShell code below I'm simply using here-strings to wrap that XML in a nicely formatted way and I pass it as a single parameter to Get-WinEvent.

$XML = @'
<QueryList>
    <Query Id="0" Path="Security">
        <Select Path="Security"> *[System[EventRecordID=4096742]]
        </Select>
    </Query>
</QueryList>
'@

$Time = Start-TimeLog
$Events = Get-WinEvent -FilterXML $XML
$Events
Stop-TimeLog -Time $Time 

Results? 100ms to get 1 RecordID we wanted. Exactly that record and nothing else. Fast right? So while Get-EventLog seemed to win with Get-WinEvent on the larger query, that we had to filter anyways, we just asked Get-WinEvent to provide one event, and it delivered it in 100ms. This is the real deal and full Power of what Get-WinEvent can offer. Flexibility. It allows you to build complicated queries asking for precise data and deliver it in no time.

At this point, I should mention that each Event is actually available as XML. What you see in Event Viewer is also accessible via an XML schema. As you can see above each event has lots of data stored in it. What is prettified in Event Viewer in form of the long message showing as below, is actually written as each field separately in XML.

See where I am getting at? You can build your XML query that includes that data you want to query for. Let's build an XML that looks for all failed logins on user called Test.

<QueryList>
    <Query Id="0" Path="Security">
        <Select Path="Security"> *[EventData[(Data[@Name='SubjectUserName'] = 'Test') or (Data[@Name='TargetUserName'] = 'Test')]]
        </Select>
    </Query>
</QueryList>

On a side note, when I first saw this I wasn't really happy I would need to create this, especially that it came as one line. It really hard to understand what is happening in that XML. But that's where VSCode helps with XML formatting and all.

$XML = @'
<QueryList>
    <Query Id="0" Path="Security">
        <Select Path="Security"> *[EventData[(Data[@Name='SubjectUserName'] = 'Test') or (Data[@Name='TargetUserName'] = 'Test')]]
        </Select>
    </Query>
</QueryList>
'@

$Time = Start-TimeLog
$Events = Get-WinEvent -FilterXML $XML
$Events.Count
Stop-TimeLog -Time $Time 

In just over 5 seconds I've found 131 events that match my criteria. Something that wouldn't be possible even with reasonably fast Get-EventLog. The only problem with FilterXML path is you need to create that XML. With simpler ones, it's not that hard, but more complicated ones can get tricky, especially with dates and multiple ID's involved.

Get-WinEvent - FilterXPath

Finally, we're at XPath. How it's different than 2 other options? It's just stripped down XML. I think it will be easiest if I will just show you how it looks?

$XPath = @'
*[EventData[(Data[@Name='SubjectUserName'] = 'Test') or (Data[@Name='TargetUserName'] = 'Test')]]
'@

$Events = Get-WinEvent -FilterXPath $XPath -LogName 'Security'

As you can see above all I had to do is leave the essential bits of XML. I also had to add parameter LogName because in stripped down version of XML we've also removed a log that is supposed to have that scan. Not a huge difference right? It delivers the same results as two other options.

Get-WinEvent filtering alternative in Get-EventLog

While Get-WinEvent doesn't have ability to define search in such degree it does allow you to filter things on Message using a wildcard.

Get-EventLog -LogName 'Security' -Message '*Test*' -Newest 2000

Using the above code you should get similar results like the one above with few exceptions. It will be slower (I actually had to limit output to 2000 records to make sure it finishes quick enough) and it won't be as direct. After all Test word could be anywhere else in Message.

Tips and Tricks of Get-WinEvent and Get-EventLog

We've established above there are few differences between the two commands but there are also other reasons why people prefer to use Get-EventLog. They are called ReplacementStrings. You see when you use either Get-WinEvent or Get-EventLog you get an event that has a lot of information stored but it stores it in Message property.

As you can see above there's a lot of data in Message property and each event type, each event can have different data stored in it. And it's stored as plain text. To parse it, every time you would want to do that it would take some heavy PowerShell skills (well maybe not really, but for the sake of this article lets pretend it is hard – I wouldn't do it myself). What Get-EventLog offers is ReplacementStrings.

$EventLogTest = Get-EventLog -LogName Security -InstanceId 4625 -Before $DateBefore -After $DateAfter -Newest 5
$EventLogTest[0].ReplacementStrings | Format-List *

As we can see above, all values that are stored in the message are now given to us on a silver platter. Of course, it's missing fork and knife because there are no property names next to it, but it's something right? Of course, Get-WinEvent also offers this functionality which is accessed in a bit different way

$WinEventTest = Get-WinEvent -FilterHashtable @{ LogName = 'Security'; Id = 4625; StartTime = $DateAfter; EndTime = $DateBefore } -MaxEvents 5
$WinEventTest[0] |Format-List *
$WinEventTest[0].Properties

Different tools, almost the same data. Still missing property names but it's something you can work with. For example, you could use the following solution to create PSCustomObject and have created your properties with the values you already have — just a bit of mix and match. Let's pick this random event from Application log. It has three properties that we care about.

$FilterHashTable = @{
    LogName   = 'Application'
    ID        = 1534
    StartTime = (Get-Date).AddHours(-1)
    EndTime   = Get-Date
}


$Events = Get-WinEvent -FilterHashtable $FilterHashTable | ForEach-Object {
    $Values = $_.Properties | ForEach-Object { $_.Value }
    
    # return a new object with the required information
    [PSCustomObject]@{
        Time      = $_.TimeCreated
        # index 0 contains the name of the update
        Event     = $Values[0]
        Component = $Values[1]
        Error     = $Values[2]
        User      = $_.UserId.Value
    }
}

$Events | Format-Table -AutoSize

Exactly same query can be done with Get-EventLog.

$EventLog = Get-EventLog -LogName 'Application' -After ((Get-Date).AddHours(-1)) -InstanceId 1534 | ForEach-Object {   
    # return a new object with the required information
    [PSCustomObject]@{
        Time      = $_.TimeGenerated
        # index 0 contains the name of the update
        Event     = $_.ReplacementStrings[0]
        Component = $_.ReplacementStrings[1]
        Error     = $_.ReplacementStrings[2]
        User      = $_.UserName
    }
}

$EventLog | Format-Table -AutoSize

As you can see, results are largely similar with exception of User property which is translated to the proper name. Get-WinEvent left us with SID. While not a big deal it's something we have to be aware of. Interesting enough if we time both solutions Get-WinEvent wins this one with a margin of five seconds.

Things to know when using Get-WinEvent or Get-EventLog

Both Get-EventLog and Get-WinEvent are still in use and they have their pros and cons specific to their use cases. But both suffer from a couple of issues. Those are:

Get-WinEvent is not able to query more than 23 EventIDs on one scan
$EventID = @(1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23)
$Test = Get-WinEvent -FilterHashtable @{ ID = $EventID; LogName = 'Application' }
$EventID = @(1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24)
$Test = Get-WinEvent -FilterHashtable @{ ID = $EventID; LogName = 'Application' }

As you can see above it returns right away saying that there were no events that matched your criteria. If you wouldn't know about this issue you could falsely assume there are indeed no events like that. You may wonder now why would you require to query more than 23 events at the same time? Well, it's much faster to ask once for 23 events than to ask 23 times for 1 event. From my own testing, it takes about the same time to query for one event id as it takes for 23 events with of course some added time to parse those events. Solution to this is to simply split into 2 chunks and ask for remaining EventID's another time.

Get-WinEvent is not able to query multiple computers at the same time. You can't pass an Array of ComputerNames to it and get all results back.
$FilterHashTable = @{
    LogName   = 'Application'
    ID        = 903
    StartTime = (Get-Date).AddDays(-1)
    EndTime   = Get-Date
}

$ComputerName = 'AD1', 'AD2'
$Event1 = foreach ($Computer in $ComputerName) {
    Get-WinEvent -FilterHashtable $FilterHashTable -ComputerName $Computer
}

Using the above PowerShell, we can ask multiple computers one by one. First AD1 will be scanned and then AD2. Add that to multiple IDs (over 23) and you would have to create a loop within a loop.

Get-WinEvent and Get-EventLog don't provide property names for data in Message property. They contain values, but the naming part is not there. You either have to build it yourself as shown in one of the examples above, or you have to go deep into XML returned by Get-WinEvent to get them. And trust me there are at least four different types of XML events that you can find in Event Logs. You need to know XML to get to that data if you want to have it in an automated way.
Get-EventLog is very slow to work with remote machines. Mainly because it has to rely on Where-Object to filter out complicated results since it doesn't allow you to ask for EventID directly in a query (instead of giving you InstanceID which is not the same). Most queries would take ages to process as everything that remotely looks like an Event will have to be downloaded first and then filtered right after. It means you may be getting 2000 events to get 5 of them.
PSEventViewer - What's that?

As I've mentioned at the beginning of this article, there's a 3rd option called PSEventViewer. At the time of writing this article, it contains three commands. Those are Get-Events, Get-EventsFilter and Get-EventsInformation. No, I've not written another parser of Event Logs. It's a wrapper over Get-WinEvent that provides many useful features that you would otherwise need to do on your own. It consolidates some tips and overcomes issues I've mentioned above.

Get-EventsFilter - How do I use it?

To be perfectly clear this function is not written by me but by cduff on Spiceworks. It was further extendd by Justin Grote and finally a bit by me. So credit for this goes to those guys! What Get-EventsFilter can do is create both XPath and XML filters for you. Let's go thru a few examples.

Get-EventsFilter -XPathOnly -NamedDataFilter @{ TargetUserName = 'Test'; SubjectUserName = 'Test' } -LogName 'Security'

<# XPATH
*[EventData[(Data[@Name='SubjectUserName'] = 'Test') or (Data[@Name='TargetUserName'] = 'Test')]]
#>

Get-EventsFilter -NamedDataFilter @{ TargetUserName = 'Test'; SubjectUserName = 'Test' } -LogName 'Security'

<# XML
<QueryList>
    <Query Id="0" Path="Security">
        <Select Path="Security">
                *[EventData[(Data[@Name='SubjectUserName'] = 'Test') or (Data[@Name='TargetUserName'] = 'Test')]]
        </Select>
    </Query>
</QueryList>
#>

Basically, you tell it what you need and it's able to generate XML and XPATH for you. Let's have a look at another one.

Get-EventsFilter -NamedDataFilter @{ TargetUserName = 'Test'; SubjectUserName = 'Test' } -ProviderName 'Microsoft-Windows-Security-Auditing' -LogName 'Security'

<# XML
<QueryList>
    <Query Id="0" Path="Security">
        <Select Path="Security">
                (*[System[Provider[@Name='Microsoft-Windows-Security-Auditing']]]) and (*[EventData[(Data[@Name='SubjectUserName'] = 'Test') or (Data[@Name='TargetUserName'] = 'Test')]])
        </Select>
    </Query>
</QueryList>
#>

As you can see above you're able to generate XML without having to deal with potential errors of playing by hand with XML. And another one. This time query is a bit more complicated as we asked for LogName, Providername, EventIDs, and NamedDataFilter.

Get-EventsFilter -NamedDataFilter @{ TargetUserName = 'Test'; SubjectUserName = 'Test' } `
    -ProviderName 'Microsoft-Windows-Security-Auditing' `
    -LogName 'Security' `
    -ID 1,2,3,4,5,6,7,8,9,10

<# XML
<QueryList>
    <Query Id="0" Path="Security">
        <Select Path="Security">
                ((*[System[(((((((((EventID=1) or (EventID=2)) or (EventID=3)) or (EventID=4)) or (EventID=5)) or (EventID=6)) or (EventID=7)) or (EventID=8)) or (EventID=9)) or (EventID=10)]]) and (*[System[Provider[@Name='Microsoft-Windows-Security-Auditing']]])) and (*[EventData[(Data[@Name='SubjectUserName'] = 'Test') or (Data[@Name='TargetUserName'] = 'Test')]])
        </Select>
    </Query>
</QueryList>
#>

Easy right? This gives you full control over Get-WinEvent and allows you to pick what you want when you want it. This should optimize the time it takes to get Windows Events for any query.

Get-EventsFilter -NamedDataFilter @{ TargetUserName = 'Test'; SubjectUserName = 'Test' } `
    -ProviderName 'Microsoft-Windows-Security-Auditing' `
    -LogName 'Security' `
    -ID 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 `
    -StartTime ((Get-Date).AddDays(-1)) `
    -EndTime (Get-Date)

<# XML
<QueryList>
    <Query Id="0" Path="Security">
        <Select Path="Security">
                ((((*[System[(((((((((EventID=1) or (EventID=2)) or (EventID=3)) or (EventID=4)) or (EventID=5)) or (EventID=6)) or (EventID=7)) or (EventID=8)) or (EventID=9)) or (EventID=10)]]) and (*[System[TimeCreated[timediff(@SystemTime) <= 86400001]]])) and (*[System[TimeCreated[timediff(@SystemTime) >= 1]]])) and (*[System[Provider[@Name='Microsoft-Windows-Security-Auditing']]])) and (*[EventData[(Data[@Name='SubjectUserName'] = 'Test') or (Data[@Name='TargetUserName'] = 'Test')]])
        </Select>
    </Query>
</QueryList>
#>

Isn't it cool? What's cooler is that this knowledge is also required for non-powershell tasks. You will need it if you want to create Subscriptions for Event Forwarding in Event Viewer. Sure it can generate some XML/XPATH automatically but if you want very specific information you will have to get it yourself. And that's where Get-EventFilter comes with help.

Get-EventsInformation - How do I use it?

Get-EventInformation is a little helper to give you details about Event Logs on remote computers. You can ask multiple computers for multiple logs and it will scan them in parallel. It means that you will be able to scan 10-20 machines in no time without having to wait for each machine to finish. You can also ask it to scan file logs at the same time.

Get-EventsInformation -Machine AD1, AD2 -LogName Security,Application | Format-Table -AutoSize

It's possible that I will most likely work on this command a bit more so it will cover more information in the future. I'll make sure to update this section when it comes to that.

Get-Events - What the fuss is all about?

I know what you're thinking. I shouldn't have used Get-Events name because it's close to Get-Event which is a command that's available in the system (but not working in same way Get-WinEvent and Get-EventLog does). I've chosen this name a while back, published it, used it in PSEventViewer and at some point, people told me Get-Event exists. I don't want to change it. It makes no sense now. You will have to get used to it or not use it at all. It took us a moment to get here and if you've read everything above this function solves it all. It's one in all tool that mostly works. But let's get thru its features, shall we?

Get-Events – it doesn't have EventID limit. If you give it multiple EventID's it will divide them into groups of events making sure the limit of Get-WinEvent is not reached and then it queries for all of them separately. It does that in parallel. You don't have to wait for one query to finish to go to another.
Get-Events – has built-in Get-EventsFilter. This means that you can create most filters in one command without ever using XML.

Get-Events – you can ask it for single RecordID and it will ask politely for that record without having to go thru Where-Object. As you can see below it generated proper XML query on the fly and delivered results in 200 ms. That was 100ms more because it actually tried to do it via RunSpaces in parallel. If there were more computers it would start all the computer scans at the same time.

Get-Events – finally it recovers both Property Name and Value for fields hidden in Message. It reads all XML types (that I am aware of) and extracts that information if possible (some XML events don't have names given). It provides those properties and their names as part of your standard object you would get from Get-WinEvent.

Get-Events – doesn't have Computer limits. You can pass an Array of computers to it, and it will scan all of them. It will do so in parallel so you won't have to wait. It will submit all your queries to all servers you want at the same time making sure the limit of 23 Events is not reached either. You can also match it with NamedDataFilter and NamedDataExcludeFilter. Which means you can ask for the certain property but only if other property doesn't have some data.

Get-Events – provides easy to use Keywords and Levels as well. No more you need to remember numbers for Keywords or Levels. Those get translated for you and inserted directly into XML query.

Feel free to experiment. I've made a lot of effort to iron out the bugs but if you find some cases where it doesn't work like you expect it to please let me know. There are a couple of other things you should know thou. By default, Get-Events hide errorsAll of them. When Get-WinEvent doesn't find anything it throws an error. Get-Events doesn't. Same for other errors. It hides them. If it can't reach some server for any reason it won't tell you that unless you explicitly tell it to. You can either use Verbose switch for that and you will see errors as part of that, or you can use ExtenededOutput switch. In such a case, Hashtable is returned with two properties: Output and Errors.

There is also special parameter available ExtendedInput which was created for PSWinReporting project. PSWinReporting is reporting platform built on top of PSEventViewer that provides a way to create reports from events to Email, Microsoft Teams, Slack and Discord. The new version of PSWinReporting is almost ready which has few tricks upon its sleeves.

        Server                                                    LogName         EventID                     Type
        ------                                                    -------         -------                     ----
        AD1                                                       Security        {5136, 5137, 5141, 5136...} Computer
        AD2                                                       Security        {5136, 5137, 5141, 5136...} Computer
        EVO1                                                      ForwardedEvents {5136, 5137, 5141, 5136...} Computer
        AD1.ad.evotec.xyz                                         Security        {5136, 5137, 5141, 5136...} Computer
        AD2.ad.evotec.xyz                                         Security        {5136, 5137, 5141, 5136...} Computer
        C:\MyEvents\Archive-Security-2018-08-21-23-49-19-424.evtx Security        {5136, 5137, 5141, 5136...} File
        C:\MyEvents\Archive-Security-2018-09-08-02-53-53-711.evtx Security        {5136, 5137, 5141, 5136...} File
        C:\MyEvents\Archive-Security-2018-09-14-22-13-07-710.evtx Security        {5136, 5137, 5141, 5136...} File
        C:\MyEvents\Archive-Security-2018-09-15-09-27-52-679.evtx Security        {5136, 5137, 5141, 5136...} File
        AD1                                                       System          104                         Computer
        AD2                                                       System          104                         Computer
        EVO1                                                      ForwardedEvents 104                         Computer
        AD1.ad.evotec.xyz                                         System          104                         Computer
        AD2.ad.evotec.xyz                                         System          104                         Computer
        C:\MyEvents\Archive-Security-2018-08-21-23-49-19-424.evtx System          104                         File
        C:\MyEvents\Archive-Security-2018-09-08-02-53-53-711.evtx System          104                         File
        C:\MyEvents\Archive-Security-2018-09-14-22-13-07-710.evtx System          104                         File
        C:\MyEvents\Archive-Security-2018-09-15-09-27-52-679.evtx System          104                         File

ExtenededInput parameter takes an Array of PSCustomObjects in a specific format allowing to pass everything in one go. Get-Events will take it up and process everything on one go. Whether it's a mix of servers, forwarders or file-based logs, it will scan it all. I don't expect it to be used because it was prepared solely for PSWinReporting, but I wanted you to know it's there. Best of all both PSEventViewer and PSWinReporting are FREE and OpenSource. I host them on GitHub with all my other projects. Finally, I've made every effort for this article to be as transparent as possible. If you find any errors, typos, things that are not clear or you see any mistakes in my thinking, please don't hesitate to contact me. If you have some feature requests or bugs specifically for PSEventViewer, please report them on GitHub. If you know some other tips and tricks that you would like to share and provide them to readers of this blog post I'll be happy to update this article with them.

This post was last modified on %s = human-readable time difference 12:09

Przemyslaw Klys

System Architect with over 14 years of experience in the IT field. Skilled, among others, in Active Directory, Microsoft Exchange and Office 365. Profoundly interested in PowerShell. Software geek.

Share
Published by
Przemyslaw Klys

Recent Posts

Upgrade Azure Active Directory Connect fails with unexpected error

Today, I made the decision to upgrade my test environment and update the version of…

1 month ago

Mastering Active Directory Hygiene: Automating Stale Computer Cleanup with CleanupMonster

Have you ever looked at your Active Directory and wondered, "Why do I still have…

2 months ago

Active Directory Replication Summary to your Email or Microsoft Teams

Active Directory replication is a critical process that ensures the consistent and up-to-date state of…

7 months ago

Syncing Global Address List (GAL) to personal contacts and between Office 365 tenants with PowerShell

Hey there! Today, I wanted to introduce you to one of the small but excellent…

11 months ago

Active Directory Health Check using Microsoft Entra Connect Health Service

Active Directory (AD) is crucial in managing identities and resources within an organization. Ensuring its…

1 year ago

Seamless HTML Report Creation: Harness the Power of Markdown with PSWriteHTML PowerShell Module

In today's digital age, the ability to create compelling and informative HTML reports and documents…

1 year ago