How to Monitor Security Events with Windows Event Log on Windows Server 2025
The Windows Security event log is the closest thing Windows Server has to a native intrusion detection feed. Every successful and failed logon, every process launched, every account created, and every scheduled task registered generates a structured event that security teams can query, correlate, and alert on. Windows Server 2025 ships with an improved event infrastructure — larger default log sizes, extended audit subcategories, and tighter integration with Azure Monitor — but the fundamentals have not changed: you need to know which event IDs matter, how to query them efficiently with PowerShell, and how to forward them to a centralised security information and event management (SIEM) platform before an attacker can clear them. This tutorial covers all three areas in depth.
Prerequisites
- Windows Server 2025 with Advanced Audit Policy configured (see the audit policy section of this series)
- PowerShell 5.1 or later with the
Microsoft.PowerShell.Diagnosticsmodule (built-in) - Local Administrator or Event Log Readers group membership
- The Security event log size increased to at least 1 GB — the default 20 MB retains only minutes of events on a busy server
# Increase Security log size to 1 GB and set retention to overwrite as needed
$logName = 'Security'
wevtutil set-log $logName /maxsize:1073741824 /retention:false
wevtutil get-log $logName | Select-String 'maxSize|retention'
Step 1: Key Security Event IDs to Know
Understanding which event IDs carry security signal is the foundation of effective monitoring. The following table covers the most operationally important IDs:
- 4624 — Successful account logon. Logon Type 2 = interactive, Type 3 = network, Type 10 = remote interactive (RDP).
- 4625 — Failed logon attempt. High-frequency 4625 from a single source IP indicates brute force.
- 4648 — Logon using explicit credentials (RunAs, WMI with alternate credentials). Common in lateral movement.
- 4688 — New process created. Requires “Process Creation” audit subcategory and ideally command-line logging.
- 4698 — Scheduled task created. Persistence mechanism frequently used by malware.
- 4720 — User account created. Any unexpected creation outside your provisioning window is suspicious.
- 4722 — User account enabled. Attackers re-enable disabled accounts to use as backdoors.
- 4768 — Kerberos TGT requested. Useful for detecting Kerberoasting pre-authentication anomalies.
- 4776 — NTLM authentication attempt. High volume may indicate Pass-the-Hash or credential stuffing.
Step 2: Query Security Events with Get-WinEvent
Get-WinEvent with a FilterHashtable is far faster than Get-EventLog because it leverages the ETW binary log index. Always use it for Security log queries:
# Query failed logons in the last 24 hours
$filter = @{
LogName = 'Security'
Id = 4625
StartTime = (Get-Date).AddHours(-24)
}
Get-WinEvent -FilterHashtable $filter -ErrorAction SilentlyContinue |
ForEach-Object {
$xml = [xml]$_.ToXml()
$ns = New-Object System.Xml.XmlNamespaceManager($xml.NameTable)
$ns.AddNamespace('e', 'http://schemas.microsoft.com/win/2004/08/events/event')
[PSCustomObject]@{
TimeCreated = $_.TimeCreated
TargetAccount = $xml.SelectSingleNode('//e:Data[@Name="TargetUserName"]', $ns).'#text'
SubjectAccount = $xml.SelectSingleNode('//e:Data[@Name="SubjectUserName"]', $ns).'#text'
WorkstationName = $xml.SelectSingleNode('//e:Data[@Name="WorkstationName"]', $ns).'#text'
IpAddress = $xml.SelectSingleNode('//e:Data[@Name="IpAddress"]', $ns).'#text'
FailureReason = $xml.SelectSingleNode('//e:Data[@Name="FailureReason"]', $ns).'#text'
LogonType = $xml.SelectSingleNode('//e:Data[@Name="LogonType"]', $ns).'#text'
}
} | Sort-Object TimeCreated -Descending | Format-Table -AutoSize
Step 3: Filter with XPath for High-Performance Queries
For even tighter filtering — particularly when querying remote machines or forwarded event subscriptions — use XPath queries. XPath allows you to filter on individual event data fields at the provider level, returning only matching records from disk:
# XPath query: failed logons with LogonType = 3 (network) from a specific IP
$xpathQuery = @"
*[System[EventID=4625] and
EventData[Data[@Name='LogonType']='3'] and
EventData[Data[@Name='IpAddress']='192.168.1.200']]
"@
Get-WinEvent -LogName Security -FilterXPath $xpathQuery -ErrorAction SilentlyContinue |
Select-Object TimeCreated, Message | Format-List
# XPath query: process creations containing 'powershell' in the command line
$xpathPs = @"
*[System[EventID=4688] and
EventData[Data[@Name='CommandLine'][contains(., 'powershell')]]]
"@
Get-WinEvent -LogName Security -FilterXPath $xpathPs -ErrorAction SilentlyContinue |
ForEach-Object {
$xml = [xml]$_.ToXml()
$ns = New-Object System.Xml.XmlNamespaceManager($xml.NameTable)
$ns.AddNamespace('e', 'http://schemas.microsoft.com/win/2004/08/events/event')
[PSCustomObject]@{
Time = $_.TimeCreated
Process = $xml.SelectSingleNode('//e:Data[@Name="NewProcessName"]', $ns).'#text'
CommandLine = $xml.SelectSingleNode('//e:Data[@Name="CommandLine"]', $ns).'#text'
Subject = $xml.SelectSingleNode('//e:Data[@Name="SubjectUserName"]', $ns).'#text'
}
}
Step 4: Detect Brute-Force Patterns by Correlating Events
A single failed logon (4625) is noise. A cluster of failures followed by a success (4624) from the same source within a short window is a brute-force hit. PowerShell can perform this correlation directly against the local log:
# Detect IPs with 5+ failed logons in the last hour
$cutoff = (Get-Date).AddHours(-1)
$failures = @{}
Get-WinEvent -FilterHashtable @{ LogName='Security'; Id=4625; StartTime=$cutoff } `
-ErrorAction SilentlyContinue |
ForEach-Object {
$xml = [xml]$_.ToXml()
$ns = New-Object System.Xml.XmlNamespaceManager($xml.NameTable)
$ns.AddNamespace('e', 'http://schemas.microsoft.com/win/2004/08/events/event')
$ip = $xml.SelectSingleNode('//e:Data[@Name="IpAddress"]', $ns).'#text'
if ($ip -and $ip -ne '-') {
$failures[$ip] = ($failures[$ip] ?? 0) + 1
}
}
$suspects = $failures.GetEnumerator() | Where-Object Value -ge 5 | Sort-Object Value -Descending
if ($suspects) {
Write-Warning "Potential brute-force sources detected:"
$suspects | Format-Table @{L='SourceIP';E={$_.Key}}, @{L='FailureCount';E={$_.Value}} -AutoSize
# Check whether any suspect IP also produced a successful logon
foreach ($suspect in $suspects) {
$xSuccessCheck = "*[System[EventID=4624] and EventData[Data[@Name='IpAddress']='$($suspect.Key)']]"
$successHit = Get-WinEvent -LogName Security -FilterXPath $xSuccessCheck `
-MaxEvents 1 -ErrorAction SilentlyContinue
if ($successHit) {
Write-Host "ALERT: $($suspect.Key) had $($suspect.Value) failures AND a successful logon!" `
-ForegroundColor Red
}
}
} else {
Write-Host "No brute-force patterns detected in the last hour." -ForegroundColor Green
}
Step 5: Create Custom Views in Event Viewer
Custom views in Event Viewer let tier-1 analysts filter for specific event patterns without writing PowerShell. Save the view definition as an XML file and deploy it to all analyst workstations via GPO file preference:
# Save this XML as C:WindowsSystem32winevtViewsSecurityAlerts.xml
$viewXml = @'
Security Alerts — Logon Anomalies
Failed logons, explicit credential use, new accounts
*[System[(EventID=4625 or EventID=4648 or EventID=4720 or
EventID=4722 or EventID=4698)]]
'@
$viewPath = 'C:WindowsSystem32winevtViewsSecurityAlerts.xml'
$viewXml | Out-File -FilePath $viewPath -Encoding UTF8
Write-Host "Custom view saved. Restart Event Viewer to load it."
Step 6: Forward Events to a SIEM via Windows Event Forwarding
Windows Event Forwarding (WEF) uses a push or pull subscription model to centralise events from multiple servers to a Windows Event Collector (WEC). From there, a SIEM agent (Splunk Universal Forwarder, Azure Monitor Agent, or Elastic Agent) reads the forwarded events log.
# On the collector server — enable the WEC service
wecutil qc /q
# Create a subscription XML file describing what events to collect
$subscriptionXml = @'
SecurityAlerts
SourceInitiated
Collect security-relevant events from all member servers
true
http://schemas.microsoft.com/wbem/wsman/1/windows/EventLog
MinLatency
<![CDATA[
*[System[(EventID=4624 or EventID=4625 or EventID=4648 or
EventID=4688 or EventID=4698 or EventID=4720 or
EventID=4722 or EventID=4768 or EventID=4776)]]
]]>
http
RenderedText
ForwardedEvents
O:NSG:NSD:(A;;GA;;;DC)
'@
$subscriptionXml | Out-File 'C:WEFSecurityAlerts.xml' -Encoding UTF8
wecutil cs 'C:WEFSecurityAlerts.xml'
wecutil gs SecurityAlerts # Verify subscription status
Step 7: Reduce High-Volume Event Noise
On a busy domain controller, event IDs 4624 and 4776 can generate tens of thousands of events per hour, drowning out genuine alerts. Apply these strategies to reduce noise without losing visibility:
# Strategy 1: Exclude machine accounts from logon event collection (via XPath in subscription)
# Machine accounts end with '$' — filter them out in the subscription query:
#
# *[EventData[Data[@Name='TargetUserName'][ends-with(., '$')]]]
#
# Strategy 2: Suppress batch logon events (Type 4 = batch, Type 5 = service)
# Add to XPath Select:
# and not(EventData/Data[@Name='LogonType']='4')
# and not(EventData/Data[@Name='LogonType']='5')
# Strategy 3: Set per-source event throttling on the WEF collector
wecutil ss SecurityAlerts /SubscriptionType:SourceInitiated /ConfigurationMode:MinBandwidth
# Strategy 4: On the SIEM side, aggregate repeated 4776 events by source computer
# using a 1-minute window and only alert when count > threshold
Write-Host "Noise reduction strategies documented — apply in subscription XPath and SIEM rules."
The Windows Security event log becomes genuinely useful only when it is retained long enough, queried intelligently, and forwarded to a platform that can correlate events across many servers simultaneously. Start by increasing log sizes and enabling the critical audit subcategories outlined earlier in this series. Deploy Windows Event Forwarding to a central collector immediately — even before you have a full SIEM — so you have a historical record to investigate when an incident occurs. Add the PowerShell-based brute-force detection script to a scheduled task running every 15 minutes, and page your on-call engineer on any hit. The combination of native Windows capabilities and a disciplined forwarding strategy gives you enterprise-grade visibility at minimal cost.