Emergency server help: get in touch

Locked Out AD Users Report: PowerShell Script with Lockout Source

Free PowerShell script that lists locked-out AD users, reads event 4740 on the PDC emulator for the caller computer, and can unlock safely with -WhatIf.

Version
1.0.0
Last updated
October 6, 2026
Language
PowerShell
Tested on
Run on 6 Oct 2026 on a Windows Server 2025 DC (build 26100.33438, Windows PowerShell 5.1) in our lab domain with a Windows 11 Pro member; syntax-checked with PowerShell 7.4.6 and PSScriptAnalyzer 1.25.
License
MIT
Pricing
Free

Short answer: run .\Get-ADLockoutReport.ps1. It lists every user that is locked out right now (checked on the PDC emulator), and for each one reads event 4740 from the PDC’s Security log to show the Caller Computer Name, the machine whose bad passwords caused the lockout. Use -Hours 72 to look further back, -ExportCsv for a file, and -Unlock -WhatIf then -Unlock to unlock the accounts once the source is fixed.

We ran this script on 6 October 2026 on a Windows Server 2025 Standard domain controller (build 26100.33438, September 2026 update, Windows PowerShell 5.1) and a Windows 11 Pro member PC in our lab domain contoso.com. The example output below is from that run; the lab domain is small and new, so your numbers will be bigger. It was also syntax-checked with PowerShell 7.4.6 and PSScriptAnalyzer 1.25.

What it does

A helpdesk lockout call needs two answers: is the account really locked, and which device keeps locking it. The first is in AD; the second is in event 4740 on the PDC emulator. This script gets both in one run and joins them per user.

  • Locked now: Search-ADAccount -LockedOut -UsersOnly against the PDC emulator gives the candidates. The script then reads msDS-User-Account-Control-Computed for each one and keeps only accounts whose lockout bit (0x10) is still set, so accounts whose lockout duration has already run out are not reported as locked.
  • Lockout source: event 4740 (“A user account was locked out”) from the last -Hours hours. Microsoft documents the Caller Computer Name as the computer the failed logon came from; in the event XML it sits in the second data field, TargetDomainName.
  • Per user: lockout time, bad password count, last bad password time, how many lockouts in the window, the latest caller and every distinct caller seen.
  • History: -IncludeHistory also lists users who were locked out in the window but are unlocked now, which is how you spot the repeat offenders.
  • Unlock: optional, guarded by -WhatIf and -Confirm, and done on the PDC emulator so it takes effect immediately.

For the background on event 4740 and the usual causes (phones with an old password, mapped drives, scheduled tasks, services) read our guide AD account lockout source: event 4740 tracing.

Requirements

  • Windows PowerShell 5.1 or PowerShell 7 on Windows, with the ActiveDirectory module (RSAT).
  • Event 4740 must be audited: “Audit User Account Management” (success) on domain controllers. See Active Directory audit policy.
  • Rights to read the PDC emulator’s Security log: Domain Admins, or membership of the built-in Event Log Readers group (a domain local group, so adding an account there covers the DCs).
  • The “Remote Event Log Management” firewall rules enabled on the PDC emulator if you run the script from another machine.
  • For -Unlock: permission to unlock the accounts (see delegate password reset for a helpdesk setup).

Download and first run

  1. Copy the script from the box on this page (or use the download button) and save it as C:\Scripts\Get-ADLockoutReport.ps1.
  2. If you downloaded the file, remove the “downloaded from the internet” mark so the execution policy lets it run: Unblock-File C:\Scripts\Get-ADLockoutReport.ps1.
  3. Run it on a domain-joined machine that has the ActiveDirectory module: a domain controller, a management server, or a Windows 11 PC with RSAT (see Install RSAT on Windows 11).
  4. Read the built-in help once, then run it with no options to see the result on screen before you export anything.
cd C:\Scripts
Get-Help .\Get-ADLockoutReport.ps1 -Full
.\Get-ADLockoutReport.ps1
.\Get-ADLockoutReport.ps1 -Hours 72 -IncludeHistory

Options

ParameterWhat it doesDefault
-HoursHow far back to read 4740 events (1 to 720)24
-IdentityOnly these users (sAMAccountName, one or more)All users
-ServerDomain to query; the script always talks to its PDC emulatorCurrent domain
-EventComputerRead 4740 from another computer, e.g. an event collector with forwarded Security eventsPDC emulator
-IncludeHistoryAlso list users locked out in the window but not locked nowOff
-UnlockUnlock every reported account that is still locked (supports -WhatIf, -Confirm)Off
-ExportCsvWrite a CSV fileNot written
-PassThruSend the row objects down the pipelineOff

Usage examples

# Who is locked out right now, and from where
.\Get-ADLockoutReport.ps1

# Three days of lockouts, including users already unlocked, to CSV
.\Get-ADLockoutReport.ps1 -Hours 72 -IncludeHistory -ExportCsv C:\Reports\lockouts.csv

# One user: see what an unlock would do, then do it
.\Get-ADLockoutReport.ps1 -Identity bob -Unlock -WhatIf
.\Get-ADLockoutReport.ps1 -Identity bob -Unlock

# Users locked out more than three times this week
.\Get-ADLockoutReport.ps1 -Hours 168 -IncludeHistory -PassThru | Where-Object LockoutEvents -gt 3

Before you unlock: check the CallerComputers column and fix the device first (update the stored password on the phone, mapped drive, service or scheduled task). Unlocking alone usually means the same account is locked again within minutes. Run with -WhatIf first; the CSV from -ExportCsv records what was unlocked in the Action column.

CSV columns

The example output further down is from our lab run. The CSV has these columns:

ColumnMeaning
SamAccountName, DisplayNameThe user
LockedNowTrue if the lockout bit in msDS-User-Account-Control-Computed is set on the PDC
LockoutTimeFrom lockoutTime
BadPwdCount, LastBadPasswordFrom badPwdCount and badPasswordTime as stored on the PDC emulator
LockoutEventsNumber of 4740 events for this user in the window
LastEventTime, LastCallerTime and caller computer of the newest 4740 event
CallerComputersEvery distinct caller seen, separated by “; ” (“(blank)” if the event had none)
EventSourceComputer the events were read from
DistinguishedNameFull DN of the user
ActionEmpty, Unlocked, “WhatIf / skipped” or the unlock error

Example output

We locked out the test user erin with six wrong passwords from the member PC, then ran .\Get-ADLockoutReport.ps1 -Hours 2 -ExportCsv C:\srvs-lab\out\lockouts.csv on the DC:

1 row(s) written to C:\srvs-lab\out\lockouts.csv

SamAccountName LockedNow LockoutTime          BadPwdCount LockoutEvents LastCaller CallerComputers Action
-------------- --------- -----------          ----------- ------------- ---------- --------------- ------
erin                True 10/6/2026 2:19:21 PM           5             1 WINCLIENT  WINCLIENT

The caller computer comes from event 4740 on the PDC emulator, so it named the PC the bad passwords came from.

Schedule it

Run it as a scheduled task so the report is waiting for you. A group managed service account (gMSA) is the cleanest identity for this: no password to store and nothing to expire. See our gMSA guide to create one and allow this server to retrieve its password.

$action = New-ScheduledTaskAction -Execute 'powershell.exe' `
    -Argument '-NoProfile -ExecutionPolicy Bypass -File C:\Scripts\Get-ADLockoutReport.ps1 -Hours 24 -IncludeHistory -ExportCsv C:\Reports\lockouts.csv'
$trigger = New-ScheduledTaskTrigger -Daily -At 7am
$principal = New-ScheduledTaskPrincipal -UserId 'CONTOSO\gmsa-adreport$' -LogonType Password
Register-ScheduledTask -TaskName 'AD lockout report' -Action $action -Trigger $trigger -Principal $principal

The account the task runs as needs to read the PDC emulator’s Security log, so add the gMSA to Event Log Readers. Do not schedule -Unlock: automatic unlocking hides password-guessing attacks.

How it works

  1. Get-ADDomain returns the PDC emulator name. All AD reads go to that DC, because bad password attempts are forwarded to it and it holds the most current lockout state.
  2. Search-ADAccount -LockedOut -UsersOnly -Server <PDC> finds candidates; Get-ADUser then reads lockoutTime, badPwdCount, badPasswordTime and the computed attribute msDS-User-Account-Control-Computed.
  3. Get-WinEvent -ComputerName <PDC> -FilterHashtable @{LogName='Security'; Id=4740; StartTime=...} reads the events. Each event is turned into XML and the named data fields TargetUserName and TargetDomainName (the caller computer) are taken from it, so the script does not depend on the language of the event message text.
  4. Events are grouped per user and joined to the locked accounts.
  5. With -Unlock, Unlock-ADAccount runs against the PDC emulator for each locked account, only after ShouldProcess approves it.

Limitations

  • Blank Caller Computer Name. Some lockouts arrive with an empty caller, for example from some non-Windows clients and services in front of AD. The script shows “(blank)”; you then need the DC’s 4771/4776 failure events or the logs of the device in front (VPN, mail server) to go further.
  • Log size. If the PDC’s Security log wraps faster than -Hours, older lockouts are simply not there. Increase the log size or forward events to a collector and use -EventComputer.
  • One domain per run. Each domain has its own PDC emulator.
  • badPwdCount is per DC. The value shown is the PDC’s copy, which is normally the highest, but it is not summed across DCs.
  • Not yet run against a live domain (see the note at the top).

Official documentation: Event 4740: a user account was locked out · Search-ADAccount · UserAccountControl and msDS-User-Account-Control-Computed

Related: AD Account Lockout Source: Event 4740 Tracing · Active Directory Audit Policy: DC Settings and 35 Key Event IDs · Delegate Password Reset in Active Directory: Secure 5-Step Helpdesk Setup · Fine-Grained Password Policy (PSO) in Active Directory: 2026 Setup · Get-ADUser PowerShell Examples: 25 Queries for Active Directory

See also: Export AD Users to CSV: PowerShell Script with Last Logon · AD Privileged Group Report: Domain Admins and adminCount Audit · AD Nested Group Membership: PowerShell Tree with Loop Detection · Inactive AD Accounts Report: PowerShell Script with Safe Disable · AD Health Check Report: dcdiag and repadmin PowerShell Script

The script

Get-ADLockoutReport.ps1Download
# Locked Out AD Users Report: PowerShell Script with Lockout Source (v1.0.0) - from srvScripts.com
# Source, docs and updates: https://srvscripts.com/scripts/ad-locked-out-users-report/
# Copyright (c) 2026 srvScripts.com. MIT licence: if you copy, share or adapt this script, keep this notice and credit srvScripts.com.
<#
.SYNOPSIS
    List locked-out AD users and the computer each lockout came from (event 4740 on the PDC emulator).

.DESCRIPTION
    Read-only unless -Unlock is used.
    1. Finds locked-out users with Search-ADAccount -LockedOut against the PDC emulator, then keeps only the
       accounts whose msDS-User-Account-Control-Computed lockout bit (0x10) is still set, so accounts whose
       lockout duration has already expired are not reported as locked.
    2. Reads event 4740 ("A user account was locked out") from the Security log of the PDC emulator for the
       last -Hours hours. The Caller Computer Name is read from the event's second data field
       (TargetDomainName in the event XML), which is the machine the failed logons came from.
    3. Joins the two: one row per locked user with lockout time, bad password count, the lockout events in
       the window and every caller computer seen.

    -Unlock unlocks the reported accounts on the PDC emulator. It supports -WhatIf and -Confirm. Unlocking
    does not fix the cause: find and fix the device in CallerComputers first, or the account locks again.

.PARAMETER Hours
    How far back to read 4740 events (default 24, maximum 720).

.PARAMETER Identity
    Only report these users (sAMAccountName). Wildcards are not supported here.

.PARAMETER Server
    Domain to query (default: current domain). The script always talks to that domain's PDC emulator.

.PARAMETER EventComputer
    Read 4740 events from this computer instead of the PDC emulator (for example a log collector that
    receives forwarded Security events).

.PARAMETER IncludeHistory
    Also output users that were locked out in the window but are no longer locked.

.PARAMETER Unlock
    Unlock every reported account that is currently locked. Use -WhatIf first.

.PARAMETER ExportCsv
    Write the report to this CSV file (UTF-8).

.PARAMETER PassThru
    Output the row objects to the pipeline.

.EXAMPLE
    .\Get-ADLockoutReport.ps1

.EXAMPLE
    .\Get-ADLockoutReport.ps1 -Hours 72 -IncludeHistory -ExportCsv C:\Reports\lockouts.csv

.EXAMPLE
    .\Get-ADLockoutReport.ps1 -Identity bob -Unlock -WhatIf

.NOTES
    Name:     Get-ADLockoutReport.ps1
    Version:  1.0.0
    Source:   https://srvscripts.com/scripts/ad-locked-out-users-report/
    License:  MIT
    Requires: Windows PowerShell 5.1 or PowerShell 7 on Windows, ActiveDirectory module (RSAT), rights to read
              the PDC emulator's Security log (Domain Admins, or Event Log Readers on the DCs), the Remote Event
              Log Management firewall rules on the PDC, "Audit User Account Management" success auditing on DCs
              (needed for 4740), and for -Unlock the right to unlock the accounts.
#>
[CmdletBinding(SupportsShouldProcess = $true, ConfirmImpact = 'Medium')]
param(
    [ValidateRange(1, 720)]
    [int]$Hours = 24,

    [ValidateNotNullOrEmpty()]
    [string[]]$Identity,

    [ValidateNotNullOrEmpty()]
    [string]$Server,

    [ValidateNotNullOrEmpty()]
    [string]$EventComputer,

    [switch]$IncludeHistory,

    [switch]$Unlock,

    [ValidateNotNullOrEmpty()]
    [string]$ExportCsv,

    [switch]$PassThru
)

Set-StrictMode -Version 2
$ErrorActionPreference = 'Stop'

if (-not (Get-Module -ListAvailable -Name ActiveDirectory)) {
    throw "The ActiveDirectory module is not installed. Install RSAT: Active Directory Domain Services tools, then run again."
}
Import-Module ActiveDirectory -Verbose:$false

function ConvertFrom-FileTimeValue {
    param($Value)
    if ($null -eq $Value) { return $null }
    $v = [int64]$Value
    if ($v -le 0 -or $v -eq [int64]::MaxValue) { return $null }
    [DateTime]::FromFileTime($v)
}

function Get-AttributeValue {
    param($Entity, [string]$Name)
    if ($Entity.PropertyNames -contains $Name) { return $Entity[$Name].Value }
    $null
}

# ---- Find the PDC emulator ------------------------------------------------------------------------------
$domainArgs = @{}
if ($Server) { $domainArgs.Server = $Server }
$domain = Get-ADDomain @domainArgs
$pdc = $domain.PDCEmulator
$logHost = if ($EventComputer) { $EventComputer } else { $pdc }
Write-Verbose "Domain $($domain.DNSRoot), PDC emulator $pdc, reading 4740 events from $logHost"

# ---- Currently locked accounts (from the PDC) -----------------------------------------------------------
$lockProps = 'lockoutTime', 'badPwdCount', 'badPasswordTime', 'msDS-User-Account-Control-Computed', 'displayName'
$locked = @{}
$candidates = @(Search-ADAccount -LockedOut -UsersOnly -Server $pdc)
foreach ($c in $candidates) {
    if ($Identity -and ($Identity -notcontains $c.SamAccountName)) { continue }
    $u = Get-ADUser -Identity $c.DistinguishedName -Server $pdc -Properties $lockProps
    $computed = Get-AttributeValue $u 'msDS-User-Account-Control-Computed'
    if ($null -ne $computed -and -not ([int]$computed -band 0x10)) {
        Write-Verbose "$($u.SamAccountName): lockoutTime is set but the lockout has expired; not reported as locked."
        continue
    }
    $locked[$u.SamAccountName.ToLowerInvariant()] = $u
}

# ---- 4740 events in the window --------------------------------------------------------------------------
$start = (Get-Date).AddHours(-$Hours)
$events = @()
try {
    $events = @(Get-WinEvent -ComputerName $logHost -FilterHashtable @{ LogName = 'Security'; Id = 4740; StartTime = $start })
} catch {
    $msg = $_.Exception.Message
    if ($_.FullyQualifiedErrorId -match 'NoMatchingEventsFound' -or $msg -match 'No events were found') {
        $events = @()
    } elseif ($msg -match 'denied|unauthori') {
        Write-Warning "Access denied reading the Security log on $logHost. Run as Domain Admin or a member of Event Log Readers on the DCs. Lockout sources will be empty."
    } else {
        Write-Warning "Could not read the Security log on ${logHost}: $msg. Lockout sources will be empty."
    }
}

$byUser = @{}
foreach ($e in $events) {
    $x = [xml]$e.ToXml()
    $d = @{}
    foreach ($n in $x.Event.EventData.Data) { $d[$n.Name] = [string]$n.InnerText }
    $name = [string]$d['TargetUserName']
    if (-not $name) { continue }
    $key = $name.ToLowerInvariant()
    if ($Identity -and ($Identity -notcontains $name)) { continue }
    if (-not $byUser.ContainsKey($key)) { $byUser[$key] = [System.Collections.Generic.List[object]]::new() }
    $byUser[$key].Add([pscustomobject]@{ Time = $e.TimeCreated; Caller = [string]$d['TargetDomainName']; DC = $e.MachineName })
}
Write-Verbose ("{0} lockout event(s) since {1:yyyy-MM-dd HH:mm}" -f $events.Count, $start)

# ---- Join -----------------------------------------------------------------------------------------------
$keys = [System.Collections.Generic.List[string]]::new()
foreach ($k in $locked.Keys) { $keys.Add($k) }
if ($IncludeHistory) { foreach ($k in $byUser.Keys) { if (-not $keys.Contains($k)) { $keys.Add($k) } } }

$rows = foreach ($k in $keys) {
    $u = $null
    if ($locked.ContainsKey($k)) { $u = $locked[$k] }
    $ev = @()
    if ($byUser.ContainsKey($k)) { $ev = @($byUser[$k] | Sort-Object Time -Descending) }
    $callers = @($ev | ForEach-Object { if ($_.Caller) { $_.Caller } else { '(blank)' } } | Select-Object -Unique)
    $name = $k; $display = ''; $dn = ''
    if ($u) { $name = $u.SamAccountName; $display = [string](Get-AttributeValue $u 'displayName'); $dn = $u.DistinguishedName }
    [pscustomobject]@{
        SamAccountName    = $name
        DisplayName       = $display
        LockedNow         = [bool]$u
        LockoutTime       = if ($u) { ConvertFrom-FileTimeValue (Get-AttributeValue $u 'lockoutTime') } else { $null }
        BadPwdCount       = if ($u) { Get-AttributeValue $u 'badPwdCount' } else { $null }
        LastBadPassword   = if ($u) { ConvertFrom-FileTimeValue (Get-AttributeValue $u 'badPasswordTime') } else { $null }
        LockoutEvents     = $ev.Count
        LastEventTime     = if ($ev.Count) { $ev[0].Time } else { $null }
        LastCaller        = if ($ev.Count) { $ev[0].Caller } else { '' }
        CallerComputers   = ($callers -join '; ')
        EventSource       = $logHost
        DistinguishedName = $dn
        Action            = ''
    }
}
$rows = @($rows | Sort-Object @{ Expression = 'LockedNow'; Descending = $true }, SamAccountName)

# ---- Optional unlock ------------------------------------------------------------------------------------
if ($Unlock) {
    foreach ($r in $rows | Where-Object { $_.LockedNow }) {
        if ($PSCmdlet.ShouldProcess("$($r.SamAccountName) on $pdc", 'Unlock-ADAccount')) {
            try {
                Unlock-ADAccount -Identity $r.DistinguishedName -Server $pdc -Confirm:$false
                $r.Action = 'Unlocked'
            } catch {
                $r.Action = "Unlock failed: $($_.Exception.Message)"
                Write-Warning "Could not unlock $($r.SamAccountName): $($_.Exception.Message)"
            }
        } else {
            $r.Action = 'WhatIf / skipped'
        }
    }
}

# ---- Output ---------------------------------------------------------------------------------------------
if ($ExportCsv) {
    $rows | Export-Csv -LiteralPath $ExportCsv -NoTypeInformation -Encoding UTF8
    Write-Information ("{0} row(s) written to {1}" -f $rows.Count, $ExportCsv) -InformationAction Continue
}
if ($PassThru) { return $rows }
if (-not $rows.Count) {
    Write-Information ("No locked-out users found on {0}. 4740 events read since {1:yyyy-MM-dd HH:mm}: {2}." -f $pdc, $start, $events.Count) -InformationAction Continue
    return
}
$rows | Format-Table SamAccountName, LockedNow, LockoutTime, BadPwdCount, LockoutEvents, LastCaller, CallerComputers, Action -AutoSize -Wrap
Version 1.0.0 · SHA-256 7ec7bb73e5d28de582ea2e9aa6a564452551b9e2de6f2827b4a897201e863a70
Download and verify on Linux or macOS
curl -fsSL -o Get-ADLockoutReport.ps1 https://scr.srvscripts.com/ad-locked-out-users-report/Get-ADLockoutReport.ps1 && curl -fsSL https://scr.srvscripts.com/ad-locked-out-users-report/Get-ADLockoutReport.ps1.sha256 | sha256sum -c
Download and verify in Windows PowerShell
Invoke-WebRequest -Uri 'https://scr.srvscripts.com/ad-locked-out-users-report/Get-ADLockoutReport.ps1' -OutFile 'Get-ADLockoutReport.ps1'; if ((Get-FileHash 'Get-ADLockoutReport.ps1' -Algorithm SHA256).Hash -eq '7EC7BB73E5D28DE582EA2E9AA6A564452551B9E2DE6F2827B4A897201E863A70') { 'OK: the file is intact' } else { 'MISMATCH: do not run this file' }
Copy the whole line. In Windows PowerShell, curl and sha256sum are not the Linux tools, so use the PowerShell line there.
Also on GitHub: github.com/srvscripts/scripts

Frequently asked questions

How do I find locked out users in Active Directory with PowerShell?

Search-ADAccount -LockedOut -UsersOnly lists them. This script adds the lockout source from event 4740 and filters out accounts whose lockout has already expired.

Where is event 4740 logged?

On the domain controller that processed the lockout, and the lockout is also recorded on the PDC emulator, which is why the script reads the PDC. Auditing of User Account Management must be enabled.

What is the Caller Computer Name in event 4740?

The computer the failed logon attempts came from. Find the device or service on that machine that still uses the old password.

Why is the Caller Computer Name empty?

Some clients and services do not pass a workstation name. Look at failure events 4771 and 4776 on the DCs or at the logs of the server in front of AD, such as a VPN or mail server.

Does the script unlock accounts automatically?

Only with -Unlock, and it asks for confirmation unless you pass -Confirm:$false. Use -WhatIf first to see what would be unlocked.

Can a helpdesk account run it?

Yes, if it can read the PDC emulator Security log (Event Log Readers) and, for -Unlock, has been delegated the right to unlock the accounts.

Changelog

  • 1.0.0 — First release.

Free website test

Is your website set up right?

Check SSL, security headers, redirects, robots.txt, sitemap, llms.txt and security.txt in one test. It takes about 30 seconds.