Find stale Active Directory user accounts (PowerShell)
Lists enabled AD user accounts that have not signed in for a set number of days, including ones that never signed in. Use it before an access review or a clean-up of leavers.
Not yet tested
The script
<#
.SYNOPSIS
Finds enabled Active Directory user accounts that have not signed in recently.
.DESCRIPTION
Uses LastLogonTimestamp, which domain controllers replicate. It can lag
the real last sign-in by up to 14 days by design, so treat the result as
"at least this stale". Accounts that have never signed in are included.
Read-only: nothing is disabled or moved.
.PARAMETER DaysInactive
Report accounts with no sign-in for this many days. Defaults to 90.
.PARAMETER SearchBase
Optional OU to limit the search, for example "OU=Staff,DC=corp,DC=example".
.EXAMPLE
./Get-StaleADUser.ps1 -DaysInactive 60
.EXAMPLE
./Get-StaleADUser.ps1 -SearchBase 'OU=Staff,DC=corp,DC=example' | Export-Csv stale.csv -NoTypeInformation
#>
[CmdletBinding()]
param(
[ValidateRange(1, 3650)]
[int]$DaysInactive = 90,
[string]$SearchBase
)
try {
Import-Module -Name ActiveDirectory -ErrorAction Stop
}
catch {
Write-Error 'The ActiveDirectory module is not available. Install RSAT first.'
exit 1
}
$cutoff = (Get-Date).AddDays(-$DaysInactive)
$params = @{
Filter = 'Enabled -eq $true'
Properties = 'LastLogonTimestamp', 'WhenCreated', 'Description'
}
if ($SearchBase) { $params.SearchBase = $SearchBase }
try {
$users = Get-ADUser @params -ErrorAction Stop
}
catch {
Write-Error "Active Directory query failed: $($_.Exception.Message)"
exit 1
}
$users |
Select-Object -Property SamAccountName, Name, WhenCreated, Description,
@{ Name = 'LastLogon'; Expression = {
if ($_.LastLogonTimestamp) { [datetime]::FromFileTime($_.LastLogonTimestamp) } else { $null } } } |
Where-Object { $null -eq $_.LastLogon -or $_.LastLogon -lt $cutoff } |
Where-Object { $_.WhenCreated -lt $cutoff } |
Sort-Object -Property LastLogon
-DaysInactive is how many days without a sign-in counts as stale. -SearchBase is optional and limits the search to one OU.
Load the module
Import-Module ActiveDirectory runs inside try/catch. If RSAT is missing you get one clear message and exit code 1, not a wall of red text.
Query AD
The parameters for Get-ADUser are built in a hashtable and passed with splatting (@params). That makes it easy to add SearchBase only when you gave one. The filter asks AD for enabled accounts only, which is much faster than fetching everyone and filtering locally.
Filter and sort
LastLogonTimestamp is stored as a Windows file time, so a calculated property converts it with [datetime]::FromFileTime(). The first Where-Object keeps accounts that never signed in or signed in before the cutoff. The second drops accounts created after the cutoff, so a new starter who has not logged on yet is not flagged.
Why LastLogonTimestamp
This attribute replicates to every domain controller, so one query is enough. The catch is that it can lag the real last sign-in by up to 14 days. Treat the result as “at least this stale” and check before you disable anyone.
Cookies
No adverts, and nothing that follows you to another site. What we set is what signing in
and the forum need. We count page views with Umami, which sets no cookie and identifies nobody. The one exception is Google reCAPTCHA, which checks our forms are being used by
a person, and that one is yours to decline — nothing is loaded from Google until you
say so.
No adverts, and nothing that follows you to another site. We count page views without a cookie and identify nobody. Google reCAPTCHA checks our forms, and that one is yours to decline.
Cookie Policy ·
Privacy Policy