Some links on getting the most recent defragmentation time of a Windows volume
Posted by jpluimers on 2026/02/25
This worked on the built-in Windows PowerShell to get the recommendation status:
$volume = Get-WmiObject -Class Win32_Volume -Filter "DriveLetter = 'C:'"
$analysis = $volume.DefragAnalysis()
$analysis.DefragAnalysis
$analysis.DefragRecommended
Without elevation token, $analysis.DefragAnalysis will be empty and $analysis.DefragRecommended will return False, but elevated it will return the analysis data and $analysis.DefragRecommended will return False or True depending on the analysis result.,
And this gets the most recent defragmentation action from the event-log:
$defragEvents = Get-WinEvent -FilterHashtable @{LogName = 'Application'; ID = '258'; ProviderName='Microsoft-Windows-Defrag'} -MaxEvents 1
This is way faster than leaving out the -MaxEvents and works fine because if no event can be found, you will get this error:
Get-WinEvent : No events were found that match the specified selection criteria.
Now my goal was to figure out the most recent log entry that shows the real defragmentation of a specific drive from the event-log.
This turned out to be quite a bit more complex than I hoped for, hence this blog post.
In the end, the below notes, queries, links and Microsoft documentation got me this:
aaaaa___________________________________________________________a
The quest notes
Since the quest was quite long especially because of link rot and references that have moved to new places, but not being properly redirected to the new place.
- The optimum filter syntaxes for these queries are because of historic reasons (both Windows Manage Instrumentation – usually called WMI – and the Windows Event Log predate .NET and PowerShell a lot) and take quite a bit of getting used to. That pays off in flexibility and speed.
Get-WinEventhas ways of filtering that vastly differ in usage and speed:| Where-Object-FilterHashtable(the most easy to read one, as it uses hash tables from PowerShell@hash-table object, but gettingData=to work on complex XML a tad more difficult, see further below)-FilterXPath(is easier to understand for old schoolers like me that have used XPath quite a bit, but only a subset of XPath 1.o is supported)-FilterXML(the most flexible way, and compatible with the XML query syntax of the Windows built-in Event Viewer tool using a query formatted as XML; you can even use the XPath 1.0 subset here)
| Where-Objectis orders of magnitude slower than the other three, and of those,-FilterHashtableseems to be slower as well. The basic differences between the filter mechanisms are explained in anchor “Example 16: Filter event log results” of theGet-WinEventlink below. More elaborate filters canGet-WmiObjecthas different options too, but because of the smaller data sets, absolute performance does not vary that much:| Where-Object- -Filter
- The
| Format-List -Property *and| Get-Memberare golden for inspection of and spelunking into data structures you don’t yet are familiar with:$events[0] | Format-List - Property *$events[0] | Get-Member
Optimize-Volumedoes not return any output, so it is kind of useless for inspection
Queries
I could not archive the numerous queries as Google Search results cannot be archived any more and the Duck Duck Go queries don’t return the results I used.
- [Wayback/Archive] powershell get time since last defragmentation at DuckDuckGo
- [Wayback/Archive] defrag C: from powershell at DuckDuckGo
- [Wayback/Archive] powershell “get-winevent” filter at DuckDuckGo
- “win-event” get specific “eventdata” – Google Search
- [Wayback/Archive] “win-event” get specific “eventdata” at DuckDuckGo
- [Wayback/Archive] Posts matching ‘[get-winevent] eventdata’ – Stack Overflow
- [Wayback/Archive] powershell optimise get-winevent at DuckDuckGo
- [Wayback/Archive] event viewer querylist query select eventdata at DuckDuckGo
- [Wayback/Archive] XPath indexing starts at one at DuckDuckGo
Links
Note that some of the links below get their quoting in the source code wrong. For example
$drive = Get-WmiObject -Class Win32_Volume -Filter "DriveLetter = `e:'"
instead of
$drive = Get-WmiObject -Class Win32_Volume -Filter "DriveLetter = 'e:'"
Usually this is caused by encoding errors adding up because of historic migrations and nobody bothering enough to fully check out the results.
General links
- [Wayback/Archive] windows server 2008 r2 – Find last run defrag WMI or Reg key – Server Fault (thanks [Wayback/Archive] Clayton for the Event ID 258 in the Application event log plus
-FilterHashtabletips and [Wayback/Archive] Nixphoe for theDefragAnalysistip).
Run a query for Event ID 258 in the Application event logPS F:\ps1> get-winevent -filterHashTable @{Logname = 'Application'; ID = '258';} | select-object -first 1 -prop machinename,timecreated,message | fl…
$driveLetter = Get-WmiObject -Class Win32_Volume -Filter "DriveLetter = 'C:'" $driveDefrag = $driveLetter.DefragAnalysis() $driveDefrag.DefragAnalysis$report.DefragRecommendedwill display if the OS thinks it needs a defrag and$report.DefragAnalysishas a lot of additional useful information as well. - [Wayback/Archive] Use PowerShell to Determine Fragmentation of Your Drive – Scripting Blog [archived] had encoding issues, but also taught me about Invoke-WmiMethod:
…
PS C:\> $drive = Get-WmiObject -Class Win32_Volume -Filter “DriveLetter = ‘e:'”PS C:\> $report = $drive.DefragAnalysis()PS C:\> $report.DefragAnalysis…
To use the Invoke-WmiMethod cmdlet, I first use the Get-WmiObject cmdlet to retrieve an instance of the Win32_Volume WMI class that is associated with my drive E. Next, I call the Invoke-WmiMethod to pass the input object that is stored in the $drive variable, and I use the name parameter to call the DefragAnalysis method. This method requires no arguments. The command and its associated output are shown here.PS C:\> $drive = Get-WmiObject -Class Win32_Volume -Filter “DriveLetter = ‘e:'”PS C:\> Invoke-WmiMethod -InputObject $drive -Name DefragAnalysis - [Wayback/Archive] Optimize and Defrag Drives in Windows 10 – Page 13 – | Tutorials
- [Wayback/Archive] Dymax I/O software – Improvement in Backup Throughput I/O? | Page 2 | Windows 11 Forum showing this script which does not work on my machine:
Get-WinEvent -FilterHashtable @{LogName='Application';ProviderName='Microsoft-Windows-Defrag'} |% {$x=[xml]$_.ToXml(); If($_.Id -eq 258 -Or $_.Id -eq 264) {If($x.Event.EventData.Data[1] -ne $z) {$z=$x.Event.EventData.Data[1];$y='~'} else{$y=''}; $_ |select @{l='TimeCreated';e={$_.TimeCreated}}, @{l='Level';e={"$($_.Level) $($_.LevelDisplayName)"}}, Id, @{l='~';e={$y}}, @{l='Volume';e={$z}}, @{l='Hex';e={"$([String]$x.Event.EventData.Binary[10])$([String]$x.Event.EventData.Binary[11])$([String]$x.Event.EventData.Binary[8])$([String]$x.Event.EventData.Binary[9])"}}, @{l='Data';e={$x.Event.EventData.Data[0]}}, @{l='Message';e={$x.Event.EventData.Data[2]}} }} |? {$_.Hex -eq '02AD'} |ogv -Title 'Retrim' - [Wayback/Archive] What powershell scripts are you using | Windows 11 Forum has the same script as above.
- [Wayback/Archive] PowerShell/Get-WinEventData.ps1 at master · RamblingCookieMonster/PowerShell · GitHub has this RAW source code file
[Wayback/Archive] raw.githubusercontent.com/RamblingCookieMonster/PowerShell/refs/heads/master/Get-WinEventData.ps1
That in turns is based on the now disappeared blog post [Wayback/Archive] PowerShell Get-WinEvent XML Madness: Getting details from event logs – Goatee PFE – Site Home – TechNet Blogs by early PowerShell guru [Wayback/Archive] GoateePFE (Ashley McGlone) · GitHub. The final location of the blog post before it got removed was [Wayback/Archive] PowerShell Get-WinEvent XML Madness: Getting details from event logs – GoateePFE – Archived with this interesting bit
The ugly: All of the juicy nuggets of event data in the message body are stored in XML. And nearly every combination of event ID and provider has a unique event schema for storing the data we want. Neo’s MSDN blog post gets us most of the way there. AskDS and Hey Scripting Guy show how we can use the GUI to help write the XML filter syntax. Now my head is spinning. This is the farthest point from intuitive. Don’t even get me started on XPATH.
Note: In all fairness to the product this data structure is necessary. All events have a few common properties like provider, ID number, date/time, source, etc. But in order to capture the unique details of each event we needed a way to store a variable number of properties. So the design is good, just a bit complicated to script.I tracked down this blog post and the mentioned blog posts and put the most recent locations here (the archived links work). This blog-post:
- This [Wayback/Archive] PowerShell Get-WinEvent XML Madness: Getting details from event logs – GoateePFE – Archived, but due to link rot disappeared so I reconstructed it at PowerShell Get-WinEvent XML Madness: Getting details from event logs – Ashley McGlone – disappeared from blogs.technet.microsoft.com
- [Wayback/Archive] Forensics: Automating Active Directory Account Lockout Search with PowerShell (an example of deep XML filtering of event logs across multiple servers in parallel) | Microsoft Learn, which the prior blog post above refers to, however the current version lost the images due to link rot, so I reconstructed the full blog post at Forensics: Automating Active Directory Account Lockout Search with PowerShell (an example of deep XML filtering of event logs across multiple servers in parallel) – where the images disappeared from blogs.technet.microsoft.com
Mentioned blog posts are from the Microsoft documentation, so I quote from them in the Microsoft documentation section further below. There are the links though:
- [Wayback/Archive] How to filter event log more efficiently. – neo
- [Wayback/Archive] Advanced XML filtering in the Windows Event Viewer | Microsoft Learn
- [Wayback/Archive] Use Custom Views from Windows Event Viewer in PowerShell – Scripting Blog [archived]
The mentioned limitations section fails to mention that “
starts-with” and “contains” are not supported in Windows Event Viewer and it’s API wrappers. Oh, and did I already mention I hate link rot? - [Wayback/Archive] Event Viewer XML Query Unnamed EventData – Programming & Development – Spiceworks Community
- [Wayback/Archive] How to select for content contains substring in Windows Event Viewer using XPath 1.0? – Stack Overflow showing how to embed XPath in the XML Query Syntax (thanks [Wayback/Archive] Mark Moore and [Wayback/Archive] Martin Honnen) but confirms that it is an XPath 1.0 subset:
Q
…
<QueryList> <Query Id="0" Path="System"> <Select Path="System">*[EventData[Data[@Name="updateTitle"]]]</Select> </Query> </QueryList>…
Edit the filter to change the Select element to
<Select Path="System">*[EventData[contains(Data, "Intelligence Update")]]</Select>, click OK twice and you should get this error message:“The specified query is invalid”
…
it definitely seems WEV does not accept a valid XPath 1.0 query (e.g.
"*[EventData[Data[contains(., "Intelligence Update")]]]), so the implications is that WEV does not support any of the XPath 1.0 functions except the three mentioned in the restrictions.…
C
Thanks @martin-honnen. It looks like the XPath implementation doesn’t support
contains()even though they don’t document that limitation. Although it says “The specified query is invalid” and doesn’t work, I’ve accepted your answer. Maybe someday someone at Microsoft will respond with a better solution. - [Wayback/Archive] Cover page | xpath | W3C standards and drafts | W3C
- [Wayback/Archive] windows – How to search string in Event Viewer XML Query? – Super User (thanks [Wayback/Archive] SchoolforDesign, [Wayback/Archive] w32sh and [Wayback/Archive] DavidPostill):
C
I think you may need to use PowerShell for that. stackoverflow.com/questions/8671194/…
A
I want to search all Events which contain LIKE 192.168.
Unfortunately I don’t think that is directly possible, because:Windows Event Log supports a subset of XPath 1.0. There are limitations to what functions work in the query. For instance, you can use the “position”, “Band”, and “timediff” functions within the query but other functions like “starts-with” and “contains” are not currently supported.However, as w32sh pointed out in a comment, it is possible with PowerShell. See this Stack Overflow question: Using XPath starts-with or contains functions to search Windows event logsNote that almost at the same time as finding this SuperUser answer, I also had found another source mentioned in it. It’s referenced both above fromGet-WinEventData.ps1and below in the “Microsoft documentation” section. - [Wayback/Archive] xml – Using XPath starts-with or contains functions to search Windows event logs – Stack Overflow (thanks [Wayback/Archive] Keith Walton, [Wayback/Archive] Kirill Polishchuk and [Wayback/Archive] Richard Sandoz)
Q
I want to do a partial match:<QueryList> <Query Id="0" Path="Application"> <Select Path="Application">*[EventData[Data and (Data[starts-with(.,"Session")])]]</Select> </Query> </QueryList>Event log gives me the error:The specified query is invalidDo I have the syntax wrong?A
Windows Event Log supports a subset of XPath 1.0. It has only three functions:position,Band,timediff.Reference: https://learn.microsoft.com/en-us/windows/desktop/WES/consuming-events#xpath-10-limitationsA
If you don’t mind two passes, you can always use a powershell script to re-filter the data as its-whereoperator supports-like,-match, and-contains:nv.ps1$Query = @" <QueryList> <Query Id="0" Path="System"> <Select Path="System"> *[System[(EventID=20001)]] </Select> </Query> </QueryList> "@ $events = Get-WinEvent -FilterXml $Query ForEach ($Event in $Events) { # Convert the event to XML $eventXML = [xml]$Event.ToXml() Add-Member -InputObject $Event -MemberType NoteProperty -Force -Name DriverVersion -Value $eventXML.Event.UserData.InstallDeviceID.DriverVersion Add-Member -InputObject $Event -MemberType NoteProperty -Force -Name DriverDescription -Value $eventXML.Event.UserData.InstallDeviceID.DriverDescription Add-Member -InputObject $Event -MemberType NoteProperty -Force -Name Data -Value $eventXML.Event.EventData.Data } $Events | Select TimeCreated, Id, DriverDescription, DriverVersion, ProviderName, @{Name="MessageData";Expression={$_.Message + $_.Data}} | Where {$_.DriverDescription -match "NVIDIA GeForce GTX*"} | Out-GridView pauseA cmd to launch it (nv.cmd):powershell.exe -executionpolicy bypass "& '.\nv.ps1'" - [Wayback/Archive] powershell – Optimize Get-WinEvent to run through entries faster – Stack Overflow (thanks [Wayback/Archive] Salve, [Wayback/Archive] Theo and [Wayback/Archive] TheMadTechnician)
A
Almost forgot.. You can limit the events to be from the last 7 days by extending the filter:$filter = @{ LogName ='Security' ProviderName ='Microsoft-Windows-Security-Auditing' ID = 4624 StartTime = (Get-Date).AddDays(-7).Date }A (formatted for readability)
So after some heavy googling I’ve optimized the script to run in about 3min instead of 6 hours… Apparently the-FilterHashtableis known to be extremely slow, instead I’m using-FilterXPath.Another benefit to that is the param-LogName(that is not available with-FilterHashtable) that cuts down the time the most since instead of filtering the entire log I’m only looking into the specific log I’m interested in.Here is the final code:$startTime = (Get-Date) $XPath = "*[EventData[Data[@Name='TargetUserName'] and (Data='User1' or Data='User2' or Data='User3')] and System[TimeCreated[timediff(@SystemTime) <= 86400000]]]" $entries = foreach ($entry in (Get-WinEvent -LogName 'Security' -FilterXPath $XPath -ComputerName 'localhost')){ $eventXml = ([xml]$entry.ToXml()).Event $userName = ($eventXml.EventData.Data | Where-Object { $_.Name -eq 'TargetUserName' }).'#text' $computer = ($eventXml.EventData.Data | Where-Object { $_.Name -eq 'WorkstationName' }).'#text' [PSCustomObject]@{ Time = [DateTime]$eventXml.System.TimeCreated.SystemTime UserName = $userName Computer = $computer } } $filetime = Get-Date -Format "yyyyMMddHHmm" $entries | Export-Csv -Path "C:\Temp\UsedAccounts$filetime.csv" -UseCulture -NoTypeInformation $endTime = (Get-Date) 'Duration: {0:hh}h {0:mm}min {0:ss}sec' -f ($endTime-$startTime)A
Theo was faster than me, but in addition to his suggestions I would recommend using-FilterXmlIf you change to using XML for your filter instead of a hashtable you can definitely improve your filter options, including specifying how recent your results are.$filter = @' <QueryList> <Query Id="0" Path="Security"> <Select Path="Security">*[System[TimeCreated[@SystemTime>='2021-09-15T17:39:00.000Z']]]</Select> </Query> </QueryList> '@ $entries = Get-WinEvent -FilterXml $filter…
Past that I would really consider just running a regex match against the message of the event rather than converting everything to XML. Admittedly, the bulk of your time is just getting the events into PowerShell, but processing them is going to take time too, and just running a regex match against the message rather than converting to XML and parsing that takes (on my dev box at least) 60x longer to convert to XML and parse that.
…
$filtered=ForEach($entry in $entries){ if($entry.Message -match "(?ms)Target Subject:.+? Account Name:\s+(user1|user2|user3)"){ [pscustomobject]@{ TimeCreated=$entry.TimeCreated User=$Matches[1] Computer=$env:COMPUTERNAME } } } $filetime = Get-Date -Format "ddMMyyyyHHmm" $filtered | Out-File "C:\Temp\UsedAccounts$filetime.txt" - [Wayback/Archive] Need faster Get-WinEvent execution : PowerShell
You should be adding a ‘StartTime’ key/value in the FilterHashtable. The way you’re doing it now collects all past matching security events then filters them.Get-WinEvent -FilterHashtable @{ LogName='Security' Id=5157,5152,5031,5150,5151,5155,5159,5153 StartTime = [datetime]::Now.AddHours(-8) }…
Add the hashtable keys which
Get-WinEvent -FilterHashtablerecognises to tab complete when writing the hashtable. - [Wayback/Archive] c# – Get EventData from Get-WinEvent from File – How to group by EventData.Data? – Stack Overflow (thanks [Wayback/Archive] surfmuggle and [Wayback/Archive] mike crowley)
A
Events have a toxml method which makes this possible:$event.toxml()You could continue to work with the XML directly, or convert it to a PS Object, which is what I’ve done ([xml]$event.toxml()). You can swap out your event data values as appropriate.$filter = @{ ID = 1000, 1003 startTime = [datetime]"1/1/2023" path = "C:\tmp\SavedEvents.evtx" } $Events = Get-WinEvent -FilterHashtable $filter $CustomEventObjects = foreach ($event in $events) { $xmlevent = [xml]$event.toxml() [pscustomobject]@{ TimeCreated = $event.TimeCreated RequestPath = ($xmlevent.Event.EventData.Data | Where-Object name -eq RequestPath).'#text' ActionName = ($xmlevent.Event.EventData.Data | Where-Object name -eq ActionName).'#text' ActionId = ($xmlevent.Event.EventData.Data | Where-Object name -eq ActionId).'#text' } } #possible outputs, but you can take it from here: $CustomEventObjects | Group-Object RequestPath $CustomEventObjects | Group-Object ActionName $CustomEventObjects | Group-Object ActionId - [Wayback/Archive] Powershell -get-eventdata – Stack Overflow reminded me there are limitation parameters
-Newestand-MaxEvent(thanks [Wayback/Archive] TessellatingHeckler, but please next time get your casing consistent):
A
…
# Get the most recent event 4776 $event = Get-EventLog -LogName Security -InstanceId 4776 -newest 1…
$event = Get-WinEvent -FilterHashtable @{id=4776} -LogName Security -maxevent 1 $event.ToXML()…
- [Wayback/Archive] powershell – get-winevent: add parts of extended data to csv-columns – Stack Overflow taught me about the much under-documented
EventPropertySelector(thanks [Wayback/Archive] Peter Core and [Wayback/Archive] Mathias R. Jessen), but I think it has the same XPath 1.0 limitations as the rest of the Windows Event Log suffers from. Anyway:
Any ideas how to write them (only this fields, not the additional ones) to a csv file with headernames the same as the selected data-items?Plenty!Here’s the safest (although probably least obvious) – use anEventPropertySelector!Get-WinEvent -FilterHashtable @{Path="c:\temp\raw_data\SavedSecurity.evtx";} |Where {($_.id -eq "4624" -and $_.properties[8].value -in 10)} |%{ $SelectorStrings = [string[]]@( 'Event/EventData/Data[@Name="TargetUserName"]', 'Event/EventData/Data[@Name="TargetDomainName"]', 'Event/EventData/Data[@Name="TargetLogonId"]', 'Event/EventData/Data[@Name="LogonType"]', 'Event/EventData/Data[@Name="WorkstationName"]', 'Event/EventData/Data[@Name="IpAddress"]', 'Event/EventData/Data[@Name="IpPort"]' ) $PropertySelector = [System.Diagnostics.Eventing.Reader.EventLogPropertySelector]::new($SelectorStrings) $UserName,$Domain,$LogonId,$LogonType,$ComputerName,$IPAddres,$Port = $_.GetPropertyValues($PropertySelector) New-Object psobject -Property @{ Message = $_.Message UserName = $UserName Domain = $Domain LogonId = $LogonId LogonType = $LogonType ComputerName = $ComputerName IPAddres = $IPAddres Port = $Port TimeCreated = $_.TimeCreated } }In the above code snippet, we use XPath location selectors to grab the relevantDatanodes from the Event’s XML structure. If any of them don’t exist, the corresponding variable will simply be an empty string - [Wayback/Archive] xslt – Why do indexes in XPath start with 1 and not 0? – Stack Overflow (thanks [Wayback/Archive] Edward Tanguay and [Wayback/Archive] Andrew) referencing [Wayback/Archive] Mukul Gandhi: One-based indexes in XPath quoting Michael Howard Kay (developer of Saxon XSLT and XQuery processing software, and editor of the XSLT 2.0 and 3.0 language specifications for XML transformation)
…1-based logic was the right choice for XPath and XSLT…because the language was designed for users, not for programmers, and users still have this old-fashioned habit of referring to the first chapter in a book as Chapter One…
then followed by Andrew’s answer:
To answer this question, we must examine the history of some technologies.
RSS XML XSLT and XPath History
Version 0.9 of RSS was originally released as RDF Site Summary in 1999 by a couple of guys at Netscape for Netscape’s my.netscape.com portal. Later that year, it was renamed to RSS (Rich Site Summary) with the v0.91 update. Development of the project changed hands several times, but RSS version 1.0 was released by December of 2000. With the v1.0 update, RSS included support for XML.
During 2002 v2.0 was released in September as RSS (Really Simple Syndication) and began to evolve into a major internet technology. In it’s early history, RSS feeds (and the XML data they contained) were read by humans in the raw format. Blogs and other news sources used RSS feeds and XML to output continuously updated information. Since XML was being read by mere mortals (non-programmers), XPath and XSLT also needed to be easily understandable, so that these mere mortals would not be overwhelmed by complexity when interacting with it. That is why XPath mimics the style of URIs, which is something that end-users were already familiar with. One of the concessions made for the purpose of readability by users, was to use old-fashioned numbering techniques i.e. 1-based indexes instead of 0-based indexes. That is the same concession that you mentioned with VBScript, and it was made for similar reasons.
Although RSS feeds and XML were made to be readable for most people, RSS readers were developed to provide a more pleasant interface for humans to read RSS feeds. Now, raw RSS and XML data are read almost exclusively with some sort of reader or graphical interface. XML is still in frequent (perhaps permanent) use across the web, but it is masked by fancy graphical user interfaces to provide a better experience for end users.
*The term, “mere mortals,” refers to humans who are not programers
The last main bullets finally shows how to query EventData.Data mimicking contains: a two-stage solution doing the contains part in PowerShell itself.
Microsoft documentation links
I tried to keep these in lexicographic order.
- [Wayback/Archive] Discovering objects, properties, and methods – PowerShell | Microsoft Learn
- [Wayback/Archive] EventLogPropertySelector Class (System.Diagnostics.Eventing.Reader) | Microsoft Learn
Contains an array of strings that represent XPath queries for elements in the XML representation of an event, which is based on the Event Schema. The queries in this object are used to extract values from the event.
- [Wayback/Archive] Format-List (Microsoft.PowerShell.Utility) – PowerShell | Microsoft Learn
- [Wayback/Archive] about_Assignment_Operators – PowerShell | Microsoft Learn showing the
@type assignment - [Wayback/Archive] about_Hash_Tables – PowerShell | Microsoft Learn explaining the objects underlying the
@type assignment - [Wayback/Archive] Get-Command (Microsoft.PowerShell.Core) – PowerShell | Microsoft Learn
- [Wayback/Archive] Get-EventLog (Microsoft.PowerShell.Management) – PowerShell | Microsoft Learn has a limitation parameter
-Newest
…
-Newest
Begins with the newest events and gets the specified number of events. The number of events is required, for example-Newest 100. Specifies the maximum number of events that are returned.…
- [Wayback/Archive] Get-Member (Microsoft.PowerShell.Utility) – PowerShell | Microsoft Learn
- [Wayback/Archive] Get-WinEvent (Microsoft.PowerShell.Diagnostics) – PowerShell | Microsoft Learn
- [Wayback/Archive] Get-WinEvent (Microsoft.PowerShell.Diagnostics) – PowerShell | Microsoft Learn: Example 16: Filter event log results
- [Wayback/Archive] Get-WinEvent (Microsoft.PowerShell.Diagnostics) – PowerShell | Microsoft Learn: -FilterHashTable
- [Wayback/Archive] Creating Get-WinEvent queries with FilterHashtable – PowerShell | Microsoft Learn
When you work with large event logs, it’s not efficient to send objects down the pipeline to aWhere-Objectcommand. Prior to PowerShell 6, theGet-EventLogcmdlet was another option to get log data. For example, the following commands are inefficient to filter the Microsoft-Windows-Defrag logs:Get-EventLog -LogName Application | Where-Object Source -Match defrag Get-WinEvent -LogName Application | Where-Object { $_.ProviderName -match 'defrag' }The following command uses a hash table that improves the performance:Get-WinEvent -FilterHashtable @{ LogName='Application' ProviderName='*defrag' }…
The following table displays the key names, data types, and whether wildcard characters are accepted for a data value.
Key name Value data type Accepts wildcard characters? LogName <String[]>Yes ProviderName <String[]>Yes Path <String[]>No Keywords <Long[]>No ID <Int32[]>No Level <Int32[]>No StartTime <DateTime>No EndTime <DateTime>No UserID <SID>No Data <String[]>No <named-data><String[]>No The<named-data>key represents a named event data field. For example, the Perflib event 1008 can contain the following event data:<EventData> <Data Name="Service">BITS</Data> <Data Name="Library">C:\Windows\System32\bitsperf.dll</Data> <Data Name="Win32Error">2</Data> </EventData>[Wayback/Archive] Use FilterHashTable to Filter Event Log with PowerShell – Scripting Blog [archived]
The valid
Get-WinEventkey/value pairs are as follows:- LogName=
<String[]> - ProviderName=
<String[]> - Path=
<String[]> - Keywords=
<Long[]> - ID=
<Int32[]> - Level=
<Int32[]> - StartTime=
<DateTime> - EndTime=
<DateTime> - UserID=
<SID> - Data=
<String[]> <named-data>=<String[]>- SuppressHashFilter=
<Hashtable>
Note that most of these parameters are arrays. See one of the examples above (with
Id=#,#,#,…) - [Wayback/Archive] Creating Get-WinEvent queries with FilterHashtable – PowerShell | Microsoft Learn
- [Wayback/Archive] Get-WinEvent (Microsoft.PowerShell.Diagnostics) – PowerShell | Microsoft Learn: -FilterXML
- [Wayback/Archive] Get-WinEvent (Microsoft.PowerShell.Diagnostics) – PowerShell | Microsoft Learn: -FilterXPath
And. note this limiting parameter:
-MaxEvents
Specifies the maximum number of events that are returned. Enter an integer such as 100. The default is to return all the events in the logs or files.
- [Wayback/Archive] Get-WmiObject (Microsoft.PowerShell.Management) – PowerShell | Microsoft Learn
- [Wayback/Archive] Invoke-WmiMethod (Microsoft.PowerShell.Management) – PowerShell | Microsoft Learn
This cmdlet does not generate any output.
- [Wayback/Archive] Optimize-Volume (Storage) | Microsoft Learn
- [Wayback/Archive] Select-Object (Microsoft.PowerShell.Utility) – PowerShell | Microsoft Learn
- [Wayback/Archive] Where-Object (Microsoft.PowerShell.Core) – PowerShell | Microsoft Learn
- [Wayback/Archive] Win32_Volume class (Windows) | Microsoft Learn
- [Wayback/Archive] Consuming Events (Windows Event Log) – Win32 apps | Microsoft Learn
…
Windows Event Log supports a subset of XPath 1.0. For details on the limitations, see XPath 1.0 limitations.The following examples show simple XPath expressions.…
XPath 1.0 limitations
Windows Event Log supports a subset of XPath 1.0. The primary restriction is that only XML elements that represent events can be selected by an event selector. An XPath query that does not select an event is not valid. All valid selector paths start with * or “Event”. All location paths operate on the event nodes and are composed of a series of steps. Each step is a structure of three parts: the axis, node test, and predicate. For more information about these parts and about XPath 1.0, see XML Path Language (XPath). Windows Event Log places the following restrictions on the expression:
- Axis: Only the Child (default) and Attribute (and its shorthand “
@“) axis are supported. - Node Tests: Only node names and NCName tests are supported. The “
*” character, which selects any character, is supported. - Predicates: Any valid XPath expression is acceptable if the location paths conform to the following restrictions:
- Standard operators
OR,AND,=,!=,<=,<,>=,>, and parentheses are supported. - Generating a string value for a node name is not supported.
- Evaluation in reverse order is not supported.
- Node sets are not supported.
- Namespace scoping is not supported.
- Namespace, processing, and comment nodes are not supported.
- Context size is not supported.
- Variable bindings are not supported.
- The
positionfunction, and its shorthand array reference, is supported (on leaf nodes only). - The
Bandfunction is supported. The function performs a bitwise AND for two integer number arguments. If the result of the bitwise AND is nonzero, the function evaluates to true; otherwise, the function evaluates to false. - The
timedifffunction is supported. The function computes the difference between the second argument and the first argument. One of the arguments must be a literal number. The arguments must use FILETIME representation. The result is the number of milliseconds between the two times. The result is positive if the second argument represents a later time; otherwise, it is negative. When the second argument is not provided, the current system time is used.
- Standard operators
…
It fails to mention that
containsandstarts-withare not supported. - Axis: Only the Child (default) and Attribute (and its shorthand “
- [Wayback/Archive] How to filter event log more efficiently. – neo shows how to post-process the EventData.Data as XML. Roughly this is (their code formatting was wrong, so I need to check this out – I already corrected some of the quoting going ”“ instead of
"):
PS C:\WINDOWS\system32> $query = @" <QueryList> <Query Id="0" Path="Microsoft-Windows-GroupPolicy/Operational"> <Select Path="Microsoft-Windows-GroupPolicy/Operational">*[System[(Level=1 or Level=2 or Level=3)]]</Select> </Query> </QueryList> "@
…
PS C:\WINDOWS\system32> $file = New-Item -Name log1053.txt -Path c:\temp -Force -type file PS C:\WINDOWS\system32> Get-WinEvent -FilterXml $query | %{ $evt = $_.toxml(); # Cast here with [ xml ] if($evt.Event.EventData.Data | ?{$_.'#text' -eq 1053 -and $_.name -eq "errorcode"}) { $_ | fl * | Out-File $file -Append } }
- [Wayback/Archive] Advanced XML filtering in the Windows Event Viewer | Microsoft Learn which after some event header queries, starts explaining EventData queries:
Perhaps you want to monitor two users – test5 and test9 – for any security events. Inside the search query, we can use the Boolean OR operator to include users that have the name test5 or test9.The query below searches for any security events that include test5 or test9.<QueryList> <Query Id="0"> <Select Path="Security"> *[EventData[Data[@Name='SubjectUserName'] and (Data='test5' or Data=’test9’)]] </Select> </Query> </QueryList>,
Now let’s say we are only interested in a specific Event ID involving either of these users. We can incorporate an AND Boolean to filter on the System data.The query below looks for 4663 events for user test5 or test9.<QueryList> <Query Id="0"> <Select Path="Security"> *[EventData[Data[@Name='SubjectUserName'] and (Data='test5' or Data='test9')]] and *[System[(EventID='4663')]] </Select> </Query> </QueryList>, with this one finally getting me to understand the EventData.Data “contains” filtering
Say you wanted to filter on events involving test5 but were unsure if it would be in SubjectUserName, TargetUserName, or somewhere else. You don’t need to specify the specific name that the data can be in, but just search that some data in <EventData> contains test5.
The query below looks for events that any data in <EventData> equals test5.
<QueryList> <Query Id="0"> <Select Path="Security"> *[EventData[Data and (Data='test5')]] </Select> </Query> </QueryList>, using multiple “OR” queriesThe example below will pull 4663 events from the security event log and 1704 events from the application event log.
<QueryList> <Query Id="0"> <Select Path="Security">*[System[(EventID='4663')]]</Select> <Select Path="Application">*[System[(EventID='1704')]]</Select> </Query> </QueryList>and then briefly mentions
XPath 1.0 Limitations:
Windows Event Log supports a subset of XPath 1.0. There are limitations to what functions work in the query. For instance, you can use the “position”, “Band”, and “timediff” functions within the query but other functions like “starts-with” and “contains” are not currently supported.
The problem with the above
EventDataXML queries is that they do not cover unnamedDatafields. I need to check that out, but it might be this trick:*[EventData[Data and (Data='test5')]] - [Wayback/Archive] Use Custom Views from Windows Event Viewer in PowerShell – Scripting Blog [archived] shows that you can save these XML queries in the Event Viewer for future use and that you can load the XML from file during PowerShell querying:
Get-WinEvent -FilterXml ([xml](Get-Content C:\fso\Past24CustomView.xml)) - _______________________________________________________________________________________________________________________________________________
Filtering on the EventData bit
This is the XML of one eventlog entry:
<?xml version="1.0"?> <Event xmlns="http://schemas.microsoft.com/win/2004/08/events/event"> <System> <Provider Name="Microsoft-Windows-Defrag"/> <EventID Qualifiers="0">258</EventID> <Version>0</Version> <Level>0</Level> <Task>0</Task> <Opcode>0</Opcode> <Keywords>0x80000000000000</Keywords> <TimeCreated SystemTime="2025-09-10T08:58:33.1052311Z"/> <EventRecordID>141094</EventRecordID> <Correlation/> <Execution ProcessID="0" ThreadID="0"/> <Channel>Application</Channel> <Computer>DESKTOP-i7-3770</Computer> <Security/> </System> <EventData> <Data>defragmentation</Data> <Data>(C:)</Data> <Binary>0000000034020000FD0100000000000022B651A2296BEDD85E9C8D030000000000000000</Binary> </EventData> </Event>
I want to filter on the first and second EventData.Data elements which contain the defragmentation type and drive. So a filter like:
Event.System.Provider Name="Microsoft-Windows-Defrag"Event.System.EventID="258"Event.EventData.Data[1]="defragmentation"Event.EventData.Data[2]="(#:)"where # is the drive letter
In this notation, Event.System.TimeCreated is the defragmentation timestamp.
Oh, and this takes into account that XPath array indices are one-based, see my blog-post last week titled “Programming languages where the default array starting index is one“.
Queries that return results with “Information” level (Level=4 or Level=0) and EventId=258 (the EventId for Microsoft Windows Defrag events):
-
- All
Microsoft-Windows-Defragevents:
<QueryList> <Query Id="0" Path="Application"> <Select Path="Application"> *[System[Provider[@Name='Microsoft-Windows-Defrag'] and (Level=4 or Level=0) and (EventID=258)]] and *[EventData[Data]] </Select> </Query> </QueryList> Microsoft-Windows-Defragevents having one or moreEventData.Datawith any node(s) having the value “defragmentation”
<QueryList> <Query Id="0" Path="Application"> <Select Path="Application"> *[System[Provider[@Name='Microsoft-Windows-Defrag'] and (Level=4 or Level=0) and (EventID=258)]] and *[EventData[Data='defragmentation']] </Select> </Query> </QueryList>Microsoft-Windows-Defragevents having the firstEventData.Datanode having the value “defragmentation” (yes, XPath indexing starts at one – see the referenced links about one based languages)
<QueryList> <Query Id="0" Path="Application"> <Select Path="Application"> *[System[Provider[@Name='Microsoft-Windows-Defrag'] and (Level=4 or Level=0) and (EventID=258)]] and *[EventData[Data[1]='defragmentation']] </Select> </Query> </QueryList>Note: this is the place where you can see that XPath array indices are one-based.
Microsoft-Windows-Defragevents having the firstEventData.Datanode having the value “defragmentation” and the second secondEventData.Datanode having the value “(C:)” which allows filtering on the XML side with a specific drive letter:
<QueryList> <Query Id="0" Path="Application"> <Select Path="Application"> *[System[Provider[@Name='Microsoft-Windows-Defrag'] and (Level=4 or Level=0) and (EventID=258)]] and *[EventData[Data[1]='defragmentation']] and *[EventData[Data[2]='(C:)']] </Select> </Query> </QueryList>
- All
With that XML query syntax using XPath queries, I finally could assemble a PowerShell fragment that does what I want (adapting it to a parameterised function is as an exercise to the student <g>) using the -FilterXML parameter of the Get-WinEvent cmdlet:
--jeroen






Leave a comment