PowerShell Script to Report Failed Logins from Event Log

PowerShell Script to Report Failed Logins from Event Log

PowerShell Tips Editor 5 min read

Every Windows environment generates a continuous stream of security events — and failed logon attempts hide inside that noise. A solid powershell failed login report event log workflow gives you daily visibility into brute-force attempts, locked-out accounts, and misconfigured service credentials without buying a SIEM. PowerShell’s Get-WinEvent can query Event ID 4625, enrich records with usernames and source IPs, and export a polished HTML report in a single script you can schedule in minutes.

Understanding Event ID 4625 and Logon Types

Event ID 4625 is logged in the Windows Security channel every time an account fails to authenticate. Each event carries a Logon Type field that tells you how the logon was attempted:

  • Type 2 — Interactive (keyboard at the console)
  • Type 3 — Network (SMB, mapped drives)
  • Type 10 — RemoteInteractive (RDP)
  • Type 8 — NetworkCleartext (often a legacy app sending plain-text credentials)

Correlating the logon type with the source IP and timestamp is how you distinguish a mistyped password from a credential-stuffing attack. You need to run the script as a local Administrator or with SeSecurityPrivilege to read the Security log.

Using Get-WinEvent with FilterHashtable for Performance

Get-WinEvent -FilterHashtable pushes the filter down to the Event Log engine, which is dramatically faster than piping all events through Where-Object. Always prefer it for large logs.

$filter = @{
    LogName   = 'Security'
    Id        = 4625
    StartTime = (Get-Date).AddDays(-1)
}

$rawEvents = Get-WinEvent -FilterHashtable $filter -ErrorAction Stop
Write-Host "Found $($rawEvents.Count) failed logon events in the last 24 hours."

Set StartTime to however far back your retention window allows. On a busy domain controller you may want to narrow to the last few hours to keep the report manageable.

Extracting Username, IP Address and Timestamp from XML

The structured data inside each event lives in XML, not the rendered message text. Casting the event to XML and addressing named data values is reliable across all Windows versions.

$parsed = foreach ($event in $rawEvents) {
    $xml   = [xml]$event.ToXml()
    $data  = $xml.Event.EventData.Data

    [PSCustomObject]@{
        TimeCreated  = $event.TimeCreated
        TargetUser   = ($data | Where-Object Name -eq 'TargetUserName').'#text'
        Domain       = ($data | Where-Object Name -eq 'TargetDomainName').'#text'
        LogonType    = ($data | Where-Object Name -eq 'LogonType').'#text'
        SourceIP     = ($data | Where-Object Name -eq 'IpAddress').'#text'
        WorkStation  = ($data | Where-Object Name -eq 'WorkstationName').'#text'
        FailureReason= ($data | Where-Object Name -eq 'SubStatus').'#text'
    }
}

Filter out machine accounts (usernames ending in $) and the catch-all - IP entries before grouping.

Grouping Results and Identifying Repeat Offenders

Repeat failures from the same source IP or against the same account are the most actionable signals. Group your parsed objects and sort by count descending.

# Top accounts by failure count
$topAccounts = $parsed |
    Where-Object { $_.TargetUser -notmatch '\$$' -and $_.TargetUser -ne '-' } |
    Group-Object TargetUser |
    Sort-Object Count -Descending |
    Select-Object -First 20 Name, Count

# Top source IPs
$topIPs = $parsed |
    Where-Object { $_.SourceIP -match '\d{1,3}\.' } |
    Group-Object SourceIP |
    Sort-Object Count -Descending |
    Select-Object -First 20 Name, Count

$topAccounts | Format-Table -AutoSize
$topIPs      | Format-Table -AutoSize

Exporting to CSV and HTML Report

For automated distribution, export both a raw CSV for ticketing systems and a styled HTML report for management dashboards.

$date      = Get-Date -Format 'yyyy-MM-dd'
$csvPath   = "C:\Reports\FailedLogins-$date.csv"
$htmlPath  = "C:\Reports\FailedLogins-$date.html"

# CSV — full detail
$parsed | Export-Csv -Path $csvPath -NoTypeInformation -Encoding UTF8

# HTML — summary table
$htmlBody = $topAccounts |
    ConvertTo-Html -Property Name, Count `
        -Head "<style>body{font-family:sans-serif}table{border-collapse:collapse}td,th{border:1px solid #ccc;padding:6px}</style>" `
        -PreContent "<h2>Failed Logins — $date</h2>" |
    Out-String

$htmlBody | Out-File -FilePath $htmlPath -Encoding UTF8
Write-Host "Reports saved: $csvPath  |  $htmlPath"

Scheduling the Script as a Daily Task

Register a scheduled task that runs the script every morning before business hours. Use a service account that has Log on as batch job and local Administrator rights.

$action  = New-ScheduledTaskAction -Execute 'pwsh.exe' `
               -Argument '-NonInteractive -File "C:\Scripts\FailedLoginReport.ps1"'
$trigger = New-ScheduledTaskTrigger -Daily -At '06:00'
$settings= New-ScheduledTaskSettingsSet -ExecutionTimeLimit (New-TimeSpan -Hours 1)

Register-ScheduledTask `
    -TaskName   'DailyFailedLoginReport' `
    -Action     $action `
    -Trigger    $trigger `
    -Settings   $settings `
    -RunLevel   Highest `
    -Description 'Generates failed login HTML/CSV report from Security event log'

Common Errors and Fixes

Access denied when querying the Security log without elevation. The Security event log requires Administrator rights. Run the script elevated or prefix with Start-Process pwsh -Verb RunAs. In a scheduled task, set Run Level to Highest privileges.

Get-WinEvent returns nothing because log retention is too short. Check the Security log’s maximum size in Event Viewer (Properties → Maximum log size). A default 20 MB cap on a busy server can roll over in hours. Increase to at least 512 MB or configure Windows Event Forwarding to a central collector before the events age out.

Related Cmdlets / See Also

Wrapping Up

With fewer than 80 lines of PowerShell you can turn raw Security log noise into a daily actionable report: top offending accounts, source IPs, logon types, and a scheduled delivery before the helpdesk opens. Extend the script by adding email delivery via Send-MailMessage or integrating the CSV output with your ticketing system to auto-create incidents on threshold breaches.

Send-Item -To