An attacker spent three weeks inside a corporate network. They created a backdoor user, installed a persistence service, and cleared the security log before leaving. Every single action generated a Windows Event — but nobody was watching.

TL;DR

  • Windows logs security-critical events natively — but default audit settings miss most of them
  • Around 15-20 Event IDs cover 90% of what you actually need for threat detection
  • PowerShell Get-WinEvent lets you filter, correlate, and export logs in seconds
  • Logon Type is the single most underused field in Event ID 4624/4625
  • You can export structured CSVs and drop them directly into the SOC Log Analyzer for automated threat detection

Why Windows Event Logs Are Your Most Reliable Forensic Source

Every Windows system — from a home laptop to a Server 2022 domain controller — keeps a detailed record of security-relevant events. Unlike network logs, which require external infrastructure, or EDR alerts, which require a commercial product, Event Logs are built-in and always on.

They’re also frequently ignored until something goes wrong.

This guide is for defenders, SOC analysts, and sysadmins who want to actually use those logs — not just know they exist.


Understanding the Log Architecture

Windows organizes logs into three classic categories plus several modern application-specific channels:

LogLocationContains
SecuritySecurityLogons, account changes, privilege use, audit policy
SystemSystemService installs, driver loads, system errors
ApplicationApplicationApplication-level events (varies by software)
PowerShellMicrosoft-Windows-PowerShell/OperationalScript execution, module loads
Task SchedulerMicrosoft-Windows-TaskScheduler/OperationalScheduled task creation and execution
SysmonMicrosoft-Windows-Sysmon/OperationalProcess creation, network connections, registry (requires Sysmon install)

The Security log is where the vast majority of relevant security events live. Everything else is secondary context.

Where logs are stored

C:\Windows\System32\winevt\Logs\
├── Security.evtx
├── System.evtx
├── Application.evtx
└── Microsoft-Windows-Sysmon%4Operational.evtx

These are .evtx files — a binary format you cannot read with a text editor. Use Event Viewer, PowerShell, or a tool like FullEventLogView to parse them.


Enable the Right Audit Policies First

Default audit settings on Windows 10, 11, and Server 2019/2022 log less than you’d expect. Before anything else, verify what’s actually being captured.

Terminal window
# Check current audit policy settings
auditpol /get /category:*

The most critical categories to enable:

Terminal window
:: Run cmd or PowerShell as Administrator
:: Using GUIDs — these work on all Windows languages (EN, FI, DE, etc.)
:: Logon events (4624, 4625)
auditpol /set /subcategory:"{0CCE9215-69AE-11D9-BED3-505054503030}" /success:enable /failure:enable
:: Credential Validation (4776, 4777)
auditpol /set /subcategory:"{0CCE923F-69AE-11D9-BED3-505054503030}" /success:enable /failure:enable
:: User Account Management (4720, 4722, 4725, 4738, 4740)
auditpol /set /subcategory:"{0CCE9235-69AE-11D9-BED3-505054503030}" /success:enable /failure:enable
:: Security Group Management (4728, 4732, 4756)
auditpol /set /subcategory:"{0CCE9237-69AE-11D9-BED3-505054503030}" /success:enable /failure:enable
:: Process Creation (4688)
auditpol /set /subcategory:"{0CCE922B-69AE-11D9-BED3-505054503030}" /success:enable /failure:enable
:: Audit Policy Change (4719)
auditpol /set /subcategory:"{0CCE922F-69AE-11D9-BED3-505054503030}" /success:enable /failure:enable
:: Sensitive Privilege Use (4672)
auditpol /set /subcategory:"{0CCE9228-69AE-11D9-BED3-505054503030}" /success:enable /failure:enable

Note: Subcategory names passed to auditpol are language-sensitive — they must match the OS language exactly. GUIDs are language-independent and work identically on English, Finnish, German, or any other Windows locale.

Additionally, enable process command line logging — without this, Event ID 4688 only shows the process name, not its arguments:

Terminal window
# Enable command line in process creation events (GPO path or registry)
Set-ItemProperty -Path "HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Policies\System\Audit" `
-Name "ProcessCreationIncludeCmdLine_Enabled" -Value 1 -Type DWord

Windows version note: Group Policy paths for Advanced Audit Policy are identical across Windows 10, 11, Server 2019, and Server 2022. Domain environments should configure these through GPO (Computer Configuration → Windows Settings → Security Settings → Advanced Audit Policy Configuration).


The Event IDs That Actually Matter

Authentication and Logons

These are your bread and butter for detecting unauthorized access, lateral movement, and brute force.

Event ID 4624 — Successful Logon

Generated every time a logon succeeds. The Logon Type field is critical:

Logon TypeMeaningWhy it matters
2Interactive (local console)User sat at the keyboard
3NetworkSMB, file share, WMI access
4BatchScheduled task ran
5ServiceA service started
7UnlockWorkstation unlocked
8NetworkCleartextCredentials sent in cleartext (legacy web forms, BasicAuth)
9NewCredentialsrunas /netonly — common attacker technique
10RemoteInteractiveRDP session — high priority to monitor
11CachedInteractiveOffline logon using cached credentials

A Logon Type 3 from an unusual source IP is lateral movement. Logon Type 10 is RDP — if you see this from an unexpected account or workstation, investigate.

Event ID 4625 — Failed Logon

The primary brute force indicator. The Sub Status Code tells you why the logon failed:

Sub StatusMeaning
0xC000006AWrong password — account exists
0xC0000064Username doesn’t exist
0xC000006DBad username or auth info
0xC0000234Account locked out
0xC0000072Account disabled
0xC000015BAccount not granted logon type

Multiple 0xC000006A failures = password spraying or brute force. Multiple 0xC0000064 failures = username enumeration.

Event ID 4648 — Logon with Explicit Credentials

Generated when a process uses credentials other than the current user’s — think runas, net use, or Pass-the-Hash tooling. Attackers regularly trigger this during lateral movement.

Event ID 4672 — Special Privileges Assigned to New Logon

Generated when an administrator-equivalent account logs on. The privileges listed include SeDebugPrivilege, SeTcbPrivilege, and similar. High-privilege logons outside business hours are worth investigating.


Account Management

Event ID 4720 — User Account Created

Any new account creation. In an environment without a provisioning system, this should be rare outside of your normal IT workflow.

Event ID 4728 / 4732 / 4756 — Member Added to Security Group

  • 4728: Added to a global security group
  • 4732: Added to a local security group
  • 4756: Added to a universal security group

Attackers add themselves to Administrators, Remote Desktop Users, or Domain Admins after gaining access. Alert on any addition to privileged groups.

Event ID 4740 — Account Locked Out

Useful in bulk — a single lockout is noise, but 50 lockouts across 20 accounts in two minutes is a spraying attack in progress.


Process Execution

Event ID 4688 — New Process Created

Every process launch. Without command line logging enabled, you only see the executable path. With it enabled, you see the full command — which is the difference between seeing powershell.exe and seeing powershell.exe -enc JABjAGwAaQBlAG4AdA....

Watch for:

  • powershell.exe, cmd.exe, wscript.exe, cscript.exe launched from unusual parent processes (e.g., word.exe spawning cmd.exe)
  • Base64-encoded arguments (-enc, -EncodedCommand)
  • Execution from %TEMP%, %APPDATA%, or user-writable directories
  • Living-off-the-land binaries: certutil.exe, regsvr32.exe, mshta.exe, wmic.exe

Persistence

Event ID 4698 — Scheduled Task Created

One of the most common persistence mechanisms. The event includes the task name, XML definition, and creating user. A scheduled task created by a non-admin account or pointing to a temp directory is a red flag.

Event ID 7045 — New Service Installed (System log)

Recorded in the System log, not Security. Malware frequently installs itself as a service for persistence. The event includes the service name, binary path, and startup type.


Covering Tracks

Event ID 1102 — Audit Log Cleared (Security log)

The attacker cleared the Security log. This event itself is written to the now-empty log — it’s one of the few self-referential events Windows generates.

Event ID 104 — System Log Cleared (System log)

Same as 1102 but for the System log.

Any log clearing event should generate an immediate alert in a production environment.


PowerShell

Event ID 4104 — Script Block Logging

Captures the full text of executed PowerShell scripts, including dynamically generated code. This is where you’ll see deobfuscated payloads — PowerShell deobfuscates before logging.

Enable script block logging via GPO:

Computer Configuration → Administrative Templates → Windows Components → Windows PowerShell
→ Turn on PowerShell Script Block Logging: Enabled

Or via registry:

Terminal window
New-Item -Path "HKLM:\SOFTWARE\Policies\Microsoft\Windows\PowerShell\ScriptBlockLogging" -Force
Set-ItemProperty -Path "HKLM:\SOFTWARE\Policies\Microsoft\Windows\PowerShell\ScriptBlockLogging" `
-Name "EnableScriptBlockLogging" -Value 1

Reading Logs with PowerShell

Event Viewer is useful for ad-hoc investigation, but for any real analysis you want PowerShell’s Get-WinEvent.

Basic filtering

Terminal window
# All failed logons in the last 24 hours
Get-WinEvent -FilterHashtable @{
LogName = 'Security'
Id = 4625
StartTime = (Get-Date).AddHours(-24)
} | Select-Object TimeCreated, Message

Extract specific fields from events

Events store structured data in Properties — each index maps to a specific field. This is faster than parsing the Message string.

Terminal window
# Failed logons with IP, username, and failure reason
Get-WinEvent -FilterHashtable @{LogName='Security'; Id=4625} |
ForEach-Object {
[PSCustomObject]@{
Time = $_.TimeCreated
Username = $_.Properties[5].Value # TargetUserName
Domain = $_.Properties[6].Value # TargetDomainName
LogonType = $_.Properties[10].Value # LogonType
IP = $_.Properties[19].Value # IpAddress
Port = $_.Properties[20].Value # IpPort
SubStatus = '0x{0:X}' -f [int]$_.Properties[9].Value # SubStatus
}
}

Detect brute force: IPs with many failures

Terminal window
$since = (Get-Date).AddHours(-1)
Get-WinEvent -FilterHashtable @{LogName='Security'; Id=4625; StartTime=$since} |
ForEach-Object { $_.Properties[19].Value } | # IpAddress
Where-Object { $_ -match '\d+\.\d+\.\d+\.\d+' } |
Group-Object |
Where-Object { $_.Count -ge 10 } |
Sort-Object Count -Descending |
Select-Object Name, Count |
Format-Table -AutoSize

Detect username spraying: single IP hitting many accounts

Terminal window
$since = (Get-Date).AddHours(-1)
Get-WinEvent -FilterHashtable @{LogName='Security'; Id=4625; StartTime=$since} |
ForEach-Object {
[PSCustomObject]@{
IP = $_.Properties[19].Value
User = $_.Properties[5].Value
}
} |
Group-Object IP |
ForEach-Object {
[PSCustomObject]@{
IP = $_.Name
UniqueUsers = ($_.Group.User | Sort-Object -Unique).Count
TotalFails = $_.Count
}
} |
Where-Object { $_.UniqueUsers -ge 5 } |
Sort-Object UniqueUsers -Descending

Detect RDP sessions from unusual accounts

Terminal window
# Successful RDP logons (LogonType 10)
Get-WinEvent -FilterHashtable @{LogName='Security'; Id=4624} |
Where-Object { $_.Properties[8].Value -eq 10 } | # LogonType = RemoteInteractive
ForEach-Object {
[PSCustomObject]@{
Time = $_.TimeCreated
Username = $_.Properties[5].Value
IP = $_.Properties[18].Value
}
} |
Format-Table -AutoSize

Find new services installed in the last 7 days

Terminal window
Get-WinEvent -FilterHashtable @{
LogName = 'System'
Id = 7045
StartTime = (Get-Date).AddDays(-7)
} |
ForEach-Object {
[PSCustomObject]@{
Time = $_.TimeCreated
ServiceName = $_.Properties[0].Value
ImagePath = $_.Properties[1].Value
StartType = $_.Properties[3].Value
RunAs = $_.Properties[4].Value
}
} |
Format-Table -AutoSize

Automated Alerting: Scheduled Script

You don’t need a SIEM to get basic alerting. This script runs on schedule, checks for threats, and sends an email if anything looks suspicious.

C:\Scripts\WinLogAlert.ps1
# Schedule with Task Scheduler every 15 minutes
param(
[string]$SmtpServer = "smtp.yourdomain.com",
[string]$To = "soc@yourdomain.com",
[string]$From = "alerts@yourdomain.com"
)
$since = (Get-Date).AddMinutes(-15)
$threshold = 10 # failed logons per IP to trigger alert
$alerts = @()
# --- Brute force check ---
$failures = Get-WinEvent -FilterHashtable @{LogName='Security'; Id=4625; StartTime=$since} -EA SilentlyContinue
if ($failures) {
$bruteIPs = $failures |
ForEach-Object { $_.Properties[19].Value } |
Where-Object { $_ -match '\d+\.\d+\.\d+\.\d+' } |
Group-Object |
Where-Object { $_.Count -ge $threshold }
foreach ($ip in $bruteIPs) {
$alerts += "BRUTE FORCE: $($ip.Name)$($ip.Count) failed logons in 15 min"
}
}
# --- Log cleared check ---
$cleared = Get-WinEvent -FilterHashtable @{LogName='Security'; Id=1102; StartTime=$since} -EA SilentlyContinue
if ($cleared) { $alerts += "LOG CLEARED: Security event log was cleared at $($cleared[0].TimeCreated)" }
# --- New service check ---
$newSvc = Get-WinEvent -FilterHashtable @{LogName='System'; Id=7045; StartTime=$since} -EA SilentlyContinue
foreach ($svc in $newSvc) {
$alerts += "NEW SERVICE: '$($svc.Properties[0].Value)' → $($svc.Properties[1].Value)"
}
# --- Send if anything found ---
if ($alerts.Count -gt 0) {
$body = "Windows Security Alerts — $(Get-Date -f 'yyyy-MM-dd HH:mm')`n`n" + ($alerts -join "`n")
Send-MailMessage -SmtpServer $SmtpServer -To $To -From $From `
-Subject "⚠️ Windows Security Alert: $($alerts.Count) finding(s)" -Body $body
}

Register it as a scheduled task:

Terminal window
$action = New-ScheduledTaskAction -Execute "powershell.exe" `
-Argument "-NonInteractive -File C:\Scripts\WinLogAlert.ps1"
$trigger = New-ScheduledTaskTrigger -RepetitionInterval (New-TimeSpan -Minutes 15) -Once -At (Get-Date)
Register-ScheduledTask -TaskName "WinLogSecurityAlert" -Action $action -Trigger $trigger `
-RunLevel Highest -User "SYSTEM"

Export to CSV for Tool-Based Analysis

If you want to analyze logs in a dedicated tool — including dropping them into the SOC Log Analyzer — export with properly named columns that the tool can automatically detect.

Failed logon export (brute force / spraying detection)

Terminal window
Get-WinEvent -FilterHashtable @{LogName='Security'; Id=4625} |
ForEach-Object {
[PSCustomObject]@{
timestamp = $_.TimeCreated.ToString("o") # ISO 8601
username = $_.Properties[5].Value
ip = $_.Properties[19].Value
status = "failure"
logon_type= $_.Properties[10].Value
substatus = '0x{0:X}' -f [int]$_.Properties[9].Value
}
} |
Export-Csv -Path ".\failed_logons.csv" -NoTypeInformation -Encoding UTF8

This exports columns named timestamp, username, ip, and status — exactly the column names the analyzer looks for. Drop the file in and it will automatically detect brute force patterns and username spraying.

Network logon export (lateral movement detection)

Terminal window
Get-WinEvent -FilterHashtable @{LogName='Security'; Id=4624} |
Where-Object { $_.Properties[8].Value -in @(3, 10) } | # Network or RDP
ForEach-Object {
[PSCustomObject]@{
timestamp = $_.TimeCreated.ToString("o")
username = $_.Properties[5].Value
ip = $_.Properties[18].Value
status = "success"
logon_type = $_.Properties[8].Value
}
} |
Export-Csv -Path ".\network_logons.csv" -NoTypeInformation -Encoding UTF8

Windows Version Differences

FeatureWindows 10Windows 11Server 2019Server 2022
Advanced Audit Policy
PowerShell Script Block Logging
Default Security log size20 MB20 MB20 MB20 MB
Sysmon support
Windows Event Forwarding
Entra ID join event loggingPartialPartial
Enhanced phishing protection events✓ (22H2+)

Increase log retention size — the default 20 MB fills up fast on active systems:

Terminal window
# Set Security log to 512 MB, overwrite oldest when full
wevtutil sl Security /ms:524288000 /rt:false
wevtutil sl System /ms:104857600 /rt:false

Or via PowerShell:

Terminal window
$log = Get-WinEvent -ListLog Security
$log.MaximumSizeInBytes = 512MB
$log.SaveChanges()

Quick Reference: Event ID Cheat Sheet

Event IDLogMeaningPriority
4624SecuritySuccessful logonMedium — filter by Logon Type
4625SecurityFailed logonHigh — watch for volume
4648SecurityLogon with explicit credentialsHigh
4672SecurityAdmin privilege logonMedium
4720SecurityUser account createdHigh
4728/4732/4756SecurityUser added to security groupHigh
4740SecurityAccount locked outMedium
4688SecurityNew process createdMedium — enable cmdline
4698SecurityScheduled task createdHigh
4702SecurityScheduled task updatedMedium
1102SecuritySecurity log clearedCritical
4719SecurityAudit policy changedCritical
7045SystemNew service installedHigh
104SystemSystem log clearedCritical
4104PowerShellScript block executedHigh — requires enabling

What You Can Do Today

  1. Run auditpol /get /category:* and check whether Logon and Account Management auditing is enabled with success and failure
  2. Increase log sizes — 20 MB will be overwritten within hours on a busy server
  3. Enable command line logging for Event ID 4688 — it’s off by default and dramatically improves visibility
  4. Enable PowerShell Script Block Logging — required to catch PowerShell-based attacks
  5. Test your visibility — create a test user, add them to a group, then verify the events appear in Security log
  6. Export a sample and analyze it — run the failed logon export above, drop it into the SOC Log Analyzer, and see what it finds
  7. Monitor the Event Log service itself — attackers can crash the Windows Event Log service (EventLogCrasher, patched but variants exist), making your SIEM blind. Alert on unexpected stops of the EventLog service via System log or WMI monitoring
  8. Use Chainsaw for rapid IR — if you’re responding to an incident and need to tear through large .evtx files fast, Chainsaw parses them with Sigma rules and is significantly faster than PowerShell for bulk analysis


Sources