PowerShell event log queries are how we actually live in Windows logs day to day. The Event Viewer GUI is fine for clicking through a single host; Get-WinEvent with a properly built FilterHashtable is what makes it possible to ask "which user failed 50 logins in the last hour, on which DC, from which IP" without manually exporting anything. This post is the working baseline we hand to new admins on the team.
Key Takeaways
Get-WinEvent -FilterHashtableis dramatically faster thanGet-WinEvent | Where-Objectbecause it filters server-side on the EventLog channel rather than after retrieval.- The
Propertiesarray on each event is the structured payload. Positional access ($_.Properties[5].Value) is the standard pattern; positions differ per event ID and are documented per event. - Domain Controllers carry the highest-signal Windows security events; query them remotely with
-ComputerNameor fan out withInvoke-Command. - "No events found" raises a terminating error by default. Wrap with
-ErrorAction SilentlyContinueortry/catchto keep batches running. - Pair this with sensible log sizing and retention — otherwise the events you need are already overwritten by the time you ask.
Environment
- Windows 10/11 endpoints and Windows Server 2019/2022, including Active Directory Domain Controllers.
- Windows PowerShell 5.1 or PowerShell 7.4 — both ship the same
Get-WinEventcmdlet. - Administrative rights on each target. The Security log is unreadable without them.
- Advanced audit policy enabled (logon, process creation, account management, policy change) so the interesting event IDs are actually generated.
The Problem
The naive pattern Get-WinEvent -LogName Security | Where-Object Id -eq 4625 pulls every event off the channel and filters in PowerShell. On a busy DC with a few million security events that takes minutes and chews memory. FilterHashtable hands the filter to the Event Log service, which evaluates it natively and returns only the rows you asked for in seconds.
The other recurring problem is the Properties array. Event 4625 has 21 properties; the username sits in position 5, the source IP in position 19, the failure reason in position 9. There is no schema in the object — you have to look up the positions for each event ID. Once you have them, calculated properties give the output sensible column names.
The Solution
Step 1 — Discover the log you actually want
Most useful events live in three logs: Security, System, and Microsoft-Windows-PowerShell/Operational. List everything that has data to confirm which channels are actually being written:
Get-WinEvent -ListLog * |
Where-Object RecordCount -gt 0 |
Sort-Object RecordCount -Descending |
Select-Object -First 25 LogName, RecordCount, IsEnabled, LogMode
Channels reporting RecordCount = 0 are usually either disabled, scoped to a feature you do not have installed, or freshly cleared.
Step 2 — Build the filter on the server side
FilterHashtable accepts LogName, ID, Level, StartTime, EndTime, ProviderName, and a few more keys. Multiple values in any key become an OR; multiple keys combine with AND:
# Failed logons in the last hour
Get-WinEvent -FilterHashtable @{
LogName = 'Security'
Id = 4625
StartTime = (Get-Date).AddHours(-1)
} -ErrorAction SilentlyContinue
# Successful AND failed logons together
Get-WinEvent -FilterHashtable @{
LogName = 'Security'
Id = 4624, 4625
StartTime = (Get-Date).AddMinutes(-15)
} -ErrorAction SilentlyContinue
# Errors and warnings from System log, last 24 hours
Get-WinEvent -FilterHashtable @{
LogName = 'System'
Level = 2, 3 # Error, Warning
StartTime = (Get-Date).AddDays(-1)
} -ErrorAction SilentlyContinue
Always use -ErrorAction SilentlyContinue when the filter may return nothing — otherwise the cmdlet throws a terminating error that you then have to catch.
Step 3 — Project the properties you care about
Each event has a Properties array indexed by position. The mapping for 4625 (failed logon) is documented by Microsoft; the practical positions are:
Get-WinEvent -FilterHashtable @{
LogName = 'Security'; Id = 4625; StartTime = (Get-Date).AddHours(-1)
} -ErrorAction SilentlyContinue |
Select-Object TimeCreated,
@{Name='User'; Expression={ $_.Properties[5].Value }},
@{Name='Domain'; Expression={ $_.Properties[6].Value }},
@{Name='Reason'; Expression={ $_.Properties[8].Value }},
@{Name='SourceIP'; Expression={ $_.Properties[19].Value }},
@{Name='WorkstationName'; Expression={ $_.Properties[13].Value }}
For other event IDs, the simplest way to find the right positions is to grab one sample event and inspect $event.Properties | ForEach-Object { $_.Value } alongside $event.Message.
Step 4 — Detect simple brute-force patterns
Once you can pull failed logons with user and source IP, grouping turns them into a triage table:
Get-WinEvent -FilterHashtable @{
LogName = 'Security'; Id = 4625; StartTime = (Get-Date).AddHours(-6)
} -ErrorAction SilentlyContinue |
Select-Object @{Name='User'; Expression={ $_.Properties[5].Value }},
@{Name='SourceIP'; Expression={ $_.Properties[19].Value }} |
Group-Object User, SourceIP |
Where-Object Count -gt 10 |
Sort-Object Count -Descending
Real brute-force detection lives in a SIEM, not in an interactive PowerShell session — but on a single DC this query is enough to confirm whether something is actively going wrong.
Step 5 — Query multiple Domain Controllers at once
Domain logons land on whichever DC the client happened to talk to. Aggregate across all of them with Invoke-Command:
$dcs = (Get-ADDomainController -Filter *).HostName
Invoke-Command -ComputerName $dcs -ScriptBlock {
Get-WinEvent -FilterHashtable @{
LogName = 'Security'
Id = 4625
StartTime = (Get-Date).AddHours(-1)
} -ErrorAction SilentlyContinue |
Select-Object @{Name='DC'; Expression={ $env:COMPUTERNAME }},
TimeCreated,
@{Name='User'; Expression={ $_.Properties[5].Value }},
@{Name='SourceIP'; Expression={ $_.Properties[19].Value }}
} | Sort-Object TimeCreated -Descending
This fans out the query, then aggregates locally. For domains with more than a handful of DCs, consider scheduling the collection rather than running it ad-hoc.
Step 6 — Export the result for an analyst
Once you have a useful row set, pipe it to Export-Csv for sharing or follow-up analysis. The CSV-export guide linked below covers the encoding flags that keep Excel happy:
... |
Export-Csv -Path .\failed_logons.csv -NoTypeInformation -Encoding UTF8BOM
Frequently Asked Questions
Why is Get-WinEvent -FilterHashtable so much faster than Where-Object?
FilterHashtable is translated to an XPath query and pushed down into the Event Log service. The service evaluates it on the channel directly and returns only matching records. Where-Object only filters after the cmdlet has already pulled every row across the pipeline.
What is the difference between Get-EventLog and Get-WinEvent?
Get-EventLog targets the legacy classic event logs (System, Security, Application). Get-WinEvent targets both classic logs and the modern EventLog 2.0 channels (Microsoft-Windows-*, PowerShell/Operational, Sysmon). Use Get-WinEvent for new code; Get-EventLog is deprecated and absent from PowerShell 7.
Why does my filter return zero events when Event Viewer shows them?
The two most common causes are case-sensitive provider names and the wrong log channel. Confirm with Get-WinEvent -ListLog * and then with Get-WinEvent -ListProvider *. Time-zone mismatches also bite — StartTime is local, and your script may be evaluating UTC.
Can I query event logs from offline .evtx files?
Yes. Get-WinEvent -Path .\Security.evtx reads an exported log file directly. Combined with FilterHashtable's Path key it scales to hundreds of files for retrospective analysis.
How do I avoid getting throttled when querying a busy DC?
Use tight StartTime and EndTime windows, narrow Id filters, and set -MaxEvents when you only need a sample. For continuous collection, push to Windows Event Forwarding instead of polling.
Conclusion
Event logs only tell you anything useful when you can ask them sharp questions. Get-WinEvent -FilterHashtable is the right primitive, the Properties array is the structured payload underneath the human-readable message, and calculated properties turn the result into a table you can paste into a ticket. Spend ten minutes learning the property positions for the four or five event IDs you actually care about and you will outpace the GUI for anything beyond a single click-through.
Related Posts
- PowerShell Quick Guide: Managing Event Log Sizes and Retention — make sure the events still exist when you need to query them.
- Essential Windows Event IDs for Security Monitoring — the catalogue of event IDs worth pinning to a cheat sheet.
- PowerShell Quick Guide: Exporting Data to CSV Files — turn your filtered events into a shareable CSV.
Reference for the cmdlet and its filter syntax: Get-WinEvent on Microsoft Learn.
0 comments:
Post a Comment