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:
And a special kind of Event Log
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.
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.
Each event consists of multiple standard fields. Those fields are as follows:
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).
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-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-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.
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.
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.
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.
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).
Actually, Get-WinEvent offers three complicated ways to ask for data. Those are:
All three methods offer some pros and cons. FilterHashtable is most commonly used because it's predictable and fairly easy to build.
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.
In comparison that's how Get-WinEvent output looks like.
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.
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.
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.
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.
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.
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:
$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.
$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.
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.
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-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.
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?
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 errors. All 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.