Tested script

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

Run it

./Get-StaleADUser.ps1 -DaysInactive 60
./Get-StaleADUser.ps1 -SearchBase 'OU=Staff,DC=corp,DC=example' | Export-Csv stale.csv -NoTypeInformation

How it works

Parameters

-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.