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.Diagnostics module (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.