Get-EventLog has been deprecated since PowerShell 6 and only sees the three legacy logs (Application, System, Security). Real Windows troubleshooting in 2026 means Get-WinEvent โ the modern cmdlet that talks to every log channel (300+ on a typical Windows Server), supports XPath, supports server-side filtering via FilterHashtable, and does not load the entire log into memory before filtering.
This guide is the practical reference: the right way to filter, how to chase a security incident, how to find the hard-to-spot kernel events, and the patterns that matter when a single Windows Server has 50 million events in its Security log. Free PDF cheat sheet at the end.
Table of Contents
Why Get-WinEvent (not Get-EventLog)
Three reasons. First, Get-EventLog only sees Application, Security, System and a handful of older logs โ modern logs (the entire Microsoft-Windows-... hierarchy) are invisible to it. Second, Get-WinEvent filters on the server side via FilterHashtable or XPath โ the result of Get-EventLog | Where-Object is "load all 5 million events into memory, then filter", which is unusable on a busy Windows Server. Third, Get-WinEvent is faster on every measurable axis even for the legacy logs.
If you still see Get-EventLog in old scripts, replace it. The conversion is mechanical.
List available log channels
# All registered log providers (300+ on a typical server)
Get-WinEvent -ListLog * | Where-Object RecordCount -gt 0 |
Sort-Object RecordCount -Descending |
Select-Object LogName, RecordCount, FileSize, LastWriteTime |
Select-Object -First 20
# Find a specific provider
Get-WinEvent -ListProvider *defender* | Format-Table Name
Basic queries
# Last 10 events from System log
Get-WinEvent -LogName System -MaxEvents 10
# All events of one type
Get-WinEvent -LogName System -MaxEvents 100 |
Where-Object Id -eq 6005 # event log service started
# Pretty-print a single event
$e = Get-WinEvent -LogName System -MaxEvents 1
$e | Format-List TimeCreated, Id, LevelDisplayName, ProviderName, Message
Note: Where-Object Id -eq 6005 is filter after retrieval. For anything bigger than a few hundred events, switch to FilterHashtable.
FilterHashtable โ the production filter
This is the most important section. FilterHashtable pushes the filter into the Windows event subsystem, so only matching events ever leave the kernel. On a 50-million-event Security log, this is the difference between 2 seconds and "give up after an hour".
# Failed logons in the last 24 hours
Get-WinEvent -FilterHashtable @{
LogName = 'Security'
Id = 4625 # Logon failure
StartTime = (Get-Date).AddHours(-24)
}
# Multiple IDs
Get-WinEvent -FilterHashtable @{
LogName = 'Security'
Id = 4624,4625,4634 # logon, failure, logoff
StartTime = (Get-Date).AddDays(-1)
}
# By provider
Get-WinEvent -FilterHashtable @{
ProviderName = 'Microsoft-Windows-Defender'
Level = 1,2,3 # Critical, Error, Warning
StartTime = (Get-Date).AddDays(-7)
}
# By user (note: needs SID, not name)
Get-WinEvent -FilterHashtable @{
LogName = 'Security'
Id = 4624
Data = (New-Object System.Security.Principal.NTAccount("DOMAIN\jsmith")).Translate([System.Security.Principal.SecurityIdentifier]).Value
}
Allowed keys in the hashtable: LogName, ProviderName, Path, Keywords, Id, Level, StartTime, EndTime, UserID, Data.
XPath filters
For filters that FilterHashtable cannot express (specific EventData fields, complex predicates), use XPath via -FilterXPath. The syntax is identical to what Event Viewer's Custom View dialog generates.
# Logon events for a specific account name
Get-WinEvent -LogName Security -FilterXPath "
*[System[EventID=4624]]
and *[EventData[Data[@Name='TargetUserName']='jsmith']]
"
# All audit failures from a specific IP
Get-WinEvent -LogName Security -FilterXPath "
*[System[EventID=4625]]
and *[EventData[Data[@Name='IpAddress']='10.20.30.40']]
"
Generate the XPath the easy way: in Event Viewer, build a Custom View, click "Edit Filter โ XML โ Edit query manually", and copy the inner Select Path="..." body.
Hunting security events
The IDs you actually look at most:
| ID | Meaning |
|---|---|
| 4624 | Successful logon |
| 4625 | Failed logon |
| 4634 / 4647 | Logoff |
| 4672 | Special privileges assigned (admin login) |
| 4720 / 4722 / 4725 / 4726 | Account create / enable / disable / delete |
| 4732 / 4733 | Member added / removed from local group |
| 4738 | User account changed |
| 4768 / 4769 / 4771 | Kerberos TGT / service ticket / pre-auth fail |
| 4776 | NTLM authentication attempt |
| 1102 | Audit log cleared (suspicious!) |
# Top 10 source IPs of failed logons in the last 24h
Get-WinEvent -FilterHashtable @{ LogName='Security'; Id=4625; StartTime=(Get-Date).AddHours(-24) } |
ForEach-Object {
$ip = ($_.Properties[19]).Value
if ($ip) { [PSCustomObject]@{ IP = $ip } }
} |
Group-Object IP | Sort-Object Count -Descending |
Select-Object -First 10 Name, Count
Remote queries
# Single remote computer
Get-WinEvent -ComputerName web01 -LogName System -MaxEvents 20
# Multiple computers in parallel (PS 7+)
'web01','web02','web03' | ForEach-Object -Parallel {
Get-WinEvent -ComputerName $_ -FilterHashtable @{
LogName = 'Security'; Id = 4625
StartTime = (Get-Date).AddHours(-1)
} | Select-Object @{n='Server';e={$using:_}}, TimeCreated, Id, Message
} -ThrottleLimit 10
Remote queries require permission on the target's Event Log Readers group (or admin) and TCP/135 + dynamic RPC ports open inbound. WinRM-based remoting is more firewall-friendly: Invoke-Command -ComputerName web01 -ScriptBlock { Get-WinEvent ... }.
Export and rotation
# Export to .evtx (binary, can be re-opened in Event Viewer)
wevtutil epl Security C:\Backup\Security-2026-04-17.evtx
# Export to CSV
Get-WinEvent -FilterHashtable @{ LogName='Security'; Id=4624; StartTime=(Get-Date).AddDays(-7) } |
Select-Object TimeCreated, Id, @{n='User';e={$_.Properties[5].Value}}, Message |
Export-Csv .\logons-7d.csv -NoTypeInformation
# Clear a log (requires admin; audit logged as 1102)
wevtutil cl Security
Performance tips
- Always use
FilterHashtableor-FilterXPath. NeverGet-WinEvent ... | Where-Objecton busy logs. - Use
-MaxEventsfor ad-hoc spelunking โ saves RAM. - For incident response, copy the
.evtxfile off the host withwevtutil epland analyse offline. - Querying remote Security logs is slower than local โ use
-Pathagainst an exported file when possible. - Stream large result sets through the pipeline; don't
$x = Get-WinEvent ...when there are millions of events.
Cheat sheet
Every filter, every important security event ID on a single PDF: Get-WinEvent Cheat Sheet.
FAQ
Why does my filter return nothing for a recent event?
The most common cause: the event source is logging to a different channel than you assume. Check with Get-WinEvent -ListLog * | Where-Object RecordCount -gt 0 and look for the right LogName. Microsoft channels are deeply nested.
How do I read the EventData fields?
$event.Properties is an array; the position depends on the event ID. For 4624, position 5 is the target user, position 19 is the IP address. The Microsoft docs for each event ID list the field order.
Can I subscribe to events in real time?
Yes โ use Register-WinEvent for cmdlet-based subscriptions, or wevtutil sub for system-level event forwarding to a collector.
What permissions do I need to read the Security log?
Local administrator OR membership in the built-in Event Log Readers group, plus SeAuditPrivilege for some advanced reads.
Why are some event channels missing entries?
The channel may be disabled. Check with Get-WinEvent -ListLog <channel> | Format-List and look at IsEnabled. Enable with wevtutil sl <channel> /e:true.
How do I correlate events across multiple servers?
Either pull them all to one place with -ComputerName and merge by TimeCreated, or use Windows Event Forwarding (WEF) with a collector. WEF is the production answer for fleets.
Get-WinEvent is slow against the Security log โ what now?
Almost always means you are filtering with Where-Object instead of FilterHashtable. Move every condition you can into the hashtable and the query usually becomes 100x+ faster.