PowerShell event log management has two halves that are usually written about separately and shouldn't be: querying the logs efficiently, and making sure the events you need are still on disk when you go looking. The Event Viewer GUI is fine for clicking through a single host; Get-WinEvent with a properly built FilterHashtable is what lets you ask "which user failed 50 logins in the last hour, on which DC, from which IP" without exporting anything by hand. But none of that helps if the default 20 MB Security log rolled over three days before you asked. This guide covers both: the querying patterns I use day to day, and the sizing and retention configuration that keeps the answers available.
Key Takeaways
- PowerShell event log management is querying and retention together — efficient
Get-WinEventfilters are useless if the events have already been overwritten. Get-WinEvent -FilterHashtableis dramatically faster thanGet-WinEvent | Where-Objectbecause it filters server-side on the channel rather than after retrieval.- The
Propertiesarray on each event is the structured payload; positional access like$_.Properties[5].Valueis the standard pattern, and positions differ per event ID. - The default 20 MB Security log on a busy Domain Controller can roll over in hours. Plan for 1–4 GB on DCs and at least 256 MB on workstations.
- Configure size and retention with
wevtutil sl, roll it out with Group Policy, and forward critical logs to a central collector — local sizing is a fallback, not the strategy.
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, and every retention setting unwritable, 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 query 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. And even a fast query is worthless if the data is gone: the Windows event log defaults were last sensibly sized when servers had 4 GB of RAM, and a modern DC writes thousands of 4624 and 4634 events per minute. A 20 MB log holds a few hours of that before circular overwrite erases the trail your incident response process assumes exists.
So the two problems are linked. Part 1 below fixes the querying — server-side filters, the Properties array, multi-DC fan-out. Part 2 fixes the retention — per-channel sizing, retention modes, and rollout — so the events survive long enough to query.
Querying Logs Efficiently with Get-WinEvent and FilterHashtable
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 disabled, scoped to a feature you do not have installed, or freshly cleared.
Build the filter on the server side
FilterHashtable accepts LogName, ID, Level, StartTime, EndTime, ProviderName, and more. 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
# 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 "No events found" raises a terminating error that you then have to catch, which breaks batch loops.
Project the properties you care about
Each event has a Properties array indexed by position. There is no schema in the object — you have to know the positions for each event ID. For 4625 (failed logon), the username is position 5, the source IP position 19, the failure reason position 8:
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 positions is to grab one sample event and inspect $event.Properties | ForEach-Object { $_.Value } alongside $event.Message. The catalogue of which IDs are worth pinning down lives in essential Windows Event IDs for security monitoring.
Turn failed logons into a brute-force triage table
Once you can pull failed logons with user and source IP, grouping turns them into a triage table — and fanning the same query across every Domain Controller aggregates logons that landed on whichever DC the client happened to reach:
$dcs = (Get-ADDomainController -Filter *).HostName
Invoke-Command -ComputerName $dcs -ScriptBlock {
Get-WinEvent -FilterHashtable @{
LogName = 'Security'; Id = 4625; StartTime = (Get-Date).AddHours(-6)
} -ErrorAction SilentlyContinue |
Select-Object @{N='DC';E={$env:COMPUTERNAME}},
@{N='User';E={ $_.Properties[5].Value }},
@{N='SourceIP';E={ $_.Properties[19].Value }}
} | Group-Object User, SourceIP | Where-Object Count -gt 10 | Sort-Object Count -Descending
Real brute-force detection belongs in a SIEM, not an interactive session, but on a handful of DCs this confirms or rules out an active password attack in seconds. Fanning out with Invoke-Command is covered in depth in building a PowerShell security audit pipeline.
Sizing and Retention — Keeping the Events Long Enough to Query
Inspect the current state, then size per channel
Before changing anything, see what each channel is sized to and how full it is:
Get-WinEvent -ListLog * |
Where-Object RecordCount -gt 0 |
Select-Object LogName,
@{N='MaxMB'; E={ [math]::Round($_.MaximumSizeInBytes/1MB,1) }},
@{N='PercentFull';E={ [math]::Round(($_.FileSize/$_.MaximumSizeInBytes)*100,1) }},
LogMode, IsEnabled |
Sort-Object PercentFull -Descending |
Format-Table -AutoSize
Anything above 80% on a workload you care about is a candidate for resizing; above 95% it is already losing events. As starting points I use: DC Security 4 GB, DC Directory Service 1 GB, member server Security 1 GB, workstation Security 256 MB, PowerShell/Operational 1 GB where script block logging is on, Sysmon/Operational 4 GB. Measure rollover on a representative box for a week and adjust.
Resize and change retention with wevtutil
wevtutil works against any channel, classic or modern, and is the only option for the Microsoft-Windows-* channels. Three retention modes matter: Circular (default, overwrites oldest), AutoBackup (rolls a full log into an archive and starts fresh), and Retain (refuses new events when full — rarely what you want):
# Set Security log to 4 GB, circular overwrite (default mode)
wevtutil sl Security /ms:4294967296
# Switch retention to AutoBackup (archive full log, start a fresh one)
wevtutil sl Security /ms:4294967296 /rt:false /ab:true
# Set Sysmon to 4 GB
wevtutil sl 'Microsoft-Windows-Sysmon/Operational' /ms:4294967296
/rt:false with /ab:true produces AutoBackup. /rt:true alone produces Retain, which makes Windows refuse new events when the log fills — operationally painful, so reserve it for a compliance requirement that explicitly forbids overwrite.
Roll out via Group Policy and watch for runaway logs
For more than a handful of machines, configure the settings under Computer Configuration → Administrative Templates → Windows Components → Event Log Service. Each classic log (Application, Security, Setup, System) has a "maximum log file size (KB)" and a "Retain old events" setting. Microsoft-Windows-* channels are not covered there — push wevtutil via Group Policy Preferences at boot, or via Intune/DSC. Then run a scheduled fullness check so a chatty driver or broken audit policy does not silently fill a channel:
$threshold = 80
Get-WinEvent -ListLog * |
Where-Object { $_.RecordCount -gt 0 -and $_.MaximumSizeInBytes -gt 0 } |
ForEach-Object {
$percent = ($_.FileSize / $_.MaximumSizeInBytes) * 100
if ($percent -ge $threshold) {
[PSCustomObject]@{
Host = $env:COMPUTERNAME; LogName = $_.LogName
PercentFull = [math]::Round($percent,1)
MaxMB = [math]::Round($_.MaximumSizeInBytes/1MB,1)
}
}
}
Schedule it every 15 minutes and pipe the output into your existing alerting. Before any destructive change, export the channel first — wevtutil epl Security "C:\LogArchive\Security_$(Get-Date -Format 'yyyyMMdd_HHmm').evtx" — since the exported .evtx opens in Event Viewer and is queryable with Get-WinEvent -Path. For long-term audit, forward critical logs to a central collector with Windows Event Forwarding or a SIEM; local sizing only covers the gap before forwarding.
Frequently Asked Questions
Why is Get-WinEvent -FilterHashtable so much faster than Where-Object?
FilterHashtable is translated to an XPath query and pushed into the Event Log service, which evaluates it on the channel 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 logs only. 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 wrong provider names and the wrong log channel. Confirm with Get-WinEvent -ListLog * and Get-WinEvent -ListProvider *. Time-zone mismatches also bite — StartTime is local, and your script may be reasoning in UTC.
What is the default size of the Windows Security log?
20 MB on modern Windows. On a Domain Controller that holds only a few hours of authentication events, which is why DC Security logs need sizing up to the gigabyte range.
Should I use Circular, AutoBackup, or Retain mode?
Circular for workstations and member servers, AutoBackup for Domain Controllers and audited servers, and Retain only when a compliance requirement explicitly forbids overwrite — and expect operational pain when those logs fill.
Is forwarding logs better than local retention?
For incident response and long-term audit, yes. Windows Event Forwarding (free, built-in) or a SIEM agent ships events to a central collector you can query and back up independently. Local retention remains useful for the gap before forwarding and for offline analysis.
Conclusion
Event logs only help when you can both ask them sharp questions and trust the answers are complete. Get-WinEvent -FilterHashtable is the right query primitive, the Properties array is the structured payload under the human-readable message, and calculated properties turn the result into a table you can paste into a ticket. None of that matters if the channel rolled over yesterday, so size the logs for the audit window your IR process needs, deploy it by Group Policy, and forward the critical channels off-box. Get both halves right and the events are there, and findable, when you need them.
Related Posts
- Essential Windows Event IDs for Security Monitoring — which events to retain and query in the first place.
- PowerShell Script Block Logging with Event ID 4104 — one of the highest-value channels to size up and query.
- Windows Event Forwarding Setup for Centralised Security Logs — the central-collector strategy that local retention backs up.
Reference for the cmdlet and its filter syntax: Get-WinEvent on Microsoft Learn, and for the retention utility, wevtutil on Microsoft Learn.
Editorial note: posts on this blog are drafted with AI assistance and then reviewed, edited, and tested against a real environment before publishing. Commands, output, and screenshots come from systems I actually ran the work on.
0 comments:
Post a Comment