This will not track why the lockout is occurring but more the events of the lockouts which can be handy to track frequency of lockouts, this report will try to call the "Caller Computer" but sometimes this is blank (as I have covered in previous posts)
I have been using this Powershell for a while to get all the lockouts:
$DCs = Get-ADDomainController -Filter * | Select-Object -ExpandProperty Name
$Results = foreach($DC in $DCs) {
Get-WinEvent -ComputerName $DC -FilterHashtable @{LogName='Security';ID=4740} -MaxEvents 10 -ErrorAction SilentlyContinue |
Select-Object @{N='DC';E={$DC}},TimeCreated,@{N='LockedAccount';E={$_.Properties[0].Value}},@{N='CallerComputer';E={$_.Properties[1].Value}}
}
$Results | Sort-Object TimeCreated -Descending | Format-Table -AutoSize
However that is in a Powershell window and requires access to the Domain Controllers to run that report, so lets amend this report so it is saved to a CSV file:
Script : ExtractLockoutData.ps1
# Get list of Domain Controllers
$DCs = Get-ADDomainController -Filter * | Select-Object -ExpandProperty Name
# Collect lockout events from each DC
$Results = foreach($DC in $DCs) {
Get-WinEvent -ComputerName $DC -FilterHashtable @{LogName='Security';ID=4740} -MaxEvents 10 -ErrorAction SilentlyContinue |
Select-Object @{N='DC';E={$DC}},
@{N='TimeCreated';E={$_.TimeCreated}},
@{N='LockedAccount';E={$_.Properties[0].Value}},
@{N='CallerComputer';E={$_.Properties[1].Value}}
}
# Sort results by time
$SortedResults = $Results | Sort-Object TimeCreated -Descending
# Export to CSV
$SortedResults | Export-Csv -Path ".\account_lockouts.csv" -NoTypeInformation
# Display results in console
$SortedResults | Format-Table -AutoSize
This is progression, but now let’s beautify it, this will involve parsing the CSV file and then producing a professional minimalistic report in HTML - the output will look something like this:
Script : BeautifyHTML.ps1
# First, find the latest CSV file in the current directory
$LatestCSV = Get-ChildItem -Path "." -Filter "*.csv" | Sort-Object LastWriteTime -Descending | Select-Object -First 1
if ($null -eq $LatestCSV) {
Write-Error "No CSV files found in the current directory"
exit
}
# Import the CSV data
$Results = Import-Csv -Path $LatestCSV.FullName
# Convert string dates back to DateTime objects and get only the most recent lockout for each account
$Results = $Results | ForEach-Object {
# Parse the date using specific format
$dateTime = [DateTime]::ParseExact($_.TimeCreated, "dd/MM/yyyy HH:mm:ss", [System.Globalization.CultureInfo]::InvariantCulture)
$_ | Add-Member -MemberType NoteProperty -Name 'TimeCreatedDate' -Value $dateTime -Force
$_
} | Group-Object LockedAccount | ForEach-Object {
# Get only the most recent entry for each account
$_.Group | Sort-Object TimeCreatedDate -Descending | Select-Object -First 1
}
# Group results by date categories
$Today = (Get-Date).Date
$Yesterday = $Today.AddDays(-1)
$WeekStart = $Today.AddDays(-([int]$Today.DayOfWeek))
$GroupedResults = @{
Today = $Results | Where-Object { $_.TimeCreatedDate.Date -eq $Today }
Yesterday = $Results | Where-Object { $_.TimeCreatedDate.Date -eq $Yesterday }
ThisWeek = $Results | Where-Object {
$_.TimeCreatedDate.Date -ge $WeekStart -and $_.TimeCreatedDate.Date -lt $Today -and $_.TimeCreatedDate.Date -ne $Yesterday
}
}
# Function to create HTML table from results
function Create-HTMLTable {
param($Data)
if ($null -eq $Data -or $Data.Count -eq 0) {
return '<div class="no-data">No lockouts recorded during this period</div>'
}
$html = @"
<table>
<tr>
<th>Time</th>
<th>Locked Account</th>
<th>Caller Computer</th>
<th>Domain Controller</th>
</tr>
"@
foreach ($item in $Data) {
$html += @"
<tr>
<td class="timestamp">$($item.TimeCreated)</td>
<td>$($item.LockedAccount)</td>
<td>$($item.CallerComputer)</td>
<td>$($item.DC)</td>
</tr>
"@
}
$html += "</table>"
return $html
}
# Create HTML content
$HTMLContent = @"
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Account Lockout Report</title>
<style>
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;
line-height: 1.6;
max-width: 1200px;
margin: 0 auto;
padding: 20px;
background: #f5f5f5;
color: #333;
}
.container {
background: white;
padding: 30px;
border-radius: 8px;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
}
h1 {
color: #2c3e50;
border-bottom: 2px solid #eee;
padding-bottom: 10px;
margin-bottom: 30px;
}
h2 {
color: #34495e;
margin-top: 30px;
padding: 10px;
background: #f8f9fa;
border-radius: 4px;
}
table {
width: 100%;
border-collapse: collapse;
margin: 20px 0;
background: white;
}
th, td {
padding: 12px;
text-align: left;
border-bottom: 1px solid #eee;
}
th {
background: #f8f9fa;
font-weight: 600;
}
tr:hover {
background: #f8f9fa;
}
.timestamp {
color: #666;
font-size: 0.9em;
}
.no-data {
padding: 20px;
text-align: center;
color: #666;
font-style: italic;
}
.summary {
margin-bottom: 30px;
padding: 15px;
background: #e1f5fe;
border-radius: 4px;
}
.csv-info {
color: #666;
font-size: 0.9em;
margin-top: 10px;
}
</style>
</head>
<body>
<div class="container">
<h1>Account Lockout Report</h1>
<div class="summary">
Report generated on: $(Get-Date -Format "yyyy-MM-dd HH:mm:ss")<br>
Total unique accounts locked this week: $($Results.Count)<br>
<span class="csv-info">Data source: $($LatestCSV.Name)</span>
</div>
<h2>Today</h2>
$(Create-HTMLTable $GroupedResults.Today)
<h2>Yesterday</h2>
$(Create-HTMLTable $GroupedResults.Yesterday)
<h2>Earlier This Week</h2>
$(Create-HTMLTable $GroupedResults.ThisWeek)
</div>
</body>
</html>
"@
# Save the HTML report
$HTMLContent | Out-File ".\lockout_report.html" -Encoding UTF8
Write-Host "Report generated successfully from $($LatestCSV.Name)"
Write-Host "Output saved to lockout_report.html"
This will the give you a report on all the lockout for this week or going back up to a week.