← script library

Audit Failed Logons (Event 4625) with PowerShell

Security & AuditingAugust 17, 2026

Parses security event 4625 over a lookback window and groups the failures twice: by target account and by source address or workstation. One account failing everywhere is a stale saved credential; one source failing against many accounts is something else entirely, and the two views are what tell them apart.

powershell
# Audit Failed Logons
# Single use case: summarize failed logon attempts (event 4625) of the last N hours
#-----------------------------------------------------------------

function Write-Log {
    param($Message)
    Write-Host "<WRITE-LOG = `"*$Message*`">"
}

# --- Parameters (replace via ServerEngine API parameters if needed) ---
$LookbackHours = 24
$WarnThreshold = 20   # warn when one account/source exceeds this many failures

$since = (Get-Date).AddHours(-[int]$LookbackHours)
$events = Get-WinEvent -FilterHashtable @{ LogName = "Security"; Id = 4625; StartTime = $since } -ErrorAction SilentlyContinue

if (-not $events) {
    Write-Log "No failed logons (4625) in the last $LookbackHours hours."
    return
}

Write-Log "Failed logons in the last ${LookbackHours}h: $(@($events).Count)"

$parsed = foreach ($e in $events) {
    $xml = [xml]$e.ToXml()
    $data = @{}
    foreach ($d in $xml.Event.EventData.Data) { $data[$d.Name] = $d.'#text' }
    [pscustomobject]@{
        Account = "$($data['TargetDomainName'])\$($data['TargetUserName'])"
        Source  = if ($data['IpAddress'] -and $data['IpAddress'] -ne "-") { $data['IpAddress'] } else { $data['WorkstationName'] }
    }
}

Write-Log "================ By Account (top 10) ================"
$parsed | Group-Object Account | Sort-Object Count -Descending | Select-Object -First 10 | ForEach-Object {
    $flag = if ($_.Count -ge [int]$WarnThreshold) { "WARNING: " } else { "" }
    Write-Log "$flag x$($_.Count.ToString().PadLeft(5))  $($_.Name)"
}

Write-Log "================ By Source (top 10) ================"
$parsed | Group-Object Source | Sort-Object Count -Descending | Select-Object -First 10 | ForEach-Object {
    $flag = if ($_.Count -ge [int]$WarnThreshold) { "WARNING: " } else { "" }
    Write-Log "$flag x$($_.Count.ToString().PadLeft(5))  $($_.Name)"
}

Write-Log "Failed logon audit complete. Sources above $WarnThreshold attempts may indicate brute force."

Run it across your fleet

This script runs as-is on a single host. Paste it into ServerEngine to schedule it, run it on a whole server group in parallel, and keep the credentials out of the file — see the scripts documentation and the credential store.

Ready when you are.

Try ServerEngine free for 7 days.