All-DC lastLogon collector and stale-user evidence report
Collect non-replicated lastLogon values from every writable domain controller, calculate the newest observed logon per account, and export evidence suitable for stale-user or stale-computer cleanup decisions without relying on replicated lastLogonTimestamp alone.
Good For
- Stale user cleanup planning
- Stale computer review
- Evidence packs for access recertification or deprovisioning tickets
- Cross-checking lastLogonTimestamp before disabling accounts
How to Use It
- Set SearchBase, ObjectClass, InactiveDays, and output path. Use a narrow OU first for timing and permission validation.
- Enumerate writable domain controllers with Get-ADDomainController so only authoritative per-DC lastLogon values are queried.
- Collect target accounts with Get-ADObject or adapt the LDAP filter for computers if reviewing stale device objects.
- For each account, query each DC for lastLogon, keep the newest value and record the DC that supplied it, then compare against replicated lastLogonTimestamp.
- Export the full result set to CSV and attach filtered stale-candidate rows to the cleanup ticket or review workbook.
- Before any disable or delete action, validate exceptions such as service accounts, break-glass identities, newly created objects, and accounts with recent pwdLastSet but no interactive logon.
Execution Modes
- ad-filtered
Inputs and Outputs
Inputs
- SearchBase
- ObjectClass
- InactiveDays
- OutCsv
Outputs
- verbose-console
- csv
Command Starter
Safe to run: read-only
$SearchBase = 'OU=Corp Users,DC=contoso,DC=com'; $InactiveDays = 90; $ObjectClass = 'user'; $OutCsv = '.\AllDC-lastLogon-report.csv'
$dcs = Get-ADDomainController -Filter 'IsReadOnly -eq $false' | Select-Object -ExpandProperty HostName
$targets = Get-ADObject -LDAPFilter "(&(objectClass=$ObjectClass)(!(objectClass=computer)))" -SearchBase $SearchBase -Properties sAMAccountName,userPrincipalName,displayName,enabled,lastLogonTimestamp,whenCreated,pwdLastSet | Select-Object DistinguishedName,ObjectClass,sAMAccountName,userPrincipalName,displayName,Enabled,whenCreated,pwdLastSet,lastLogonTimestamp
$report = foreach ($t in $targets) { $maxLL = 0L; $maxDC = $null; foreach ($dc in $dcs) { try { $r = Get-ADObject -Identity $t.DistinguishedName -Server $dc -Properties lastLogon; if ($r.lastLogon -gt $maxLL) { $maxLL = $r.lastLogon; $maxDC = $dc } } catch {} }; [pscustomobject]@{ ObjectClass=$t.ObjectClass; sAMAccountName=$t.sAMAccountName; UserPrincipalName=$t.userPrincipalName; DisplayName=$t.displayName; Enabled=$t.Enabled; DistinguishedName=$t.DistinguishedName; WhenCreated=$t.whenCreated; PwdLastSet=if($t.pwdLastSet){[datetime]::FromFileTimeUtc($t.pwdLastSet)}else{$null}; LastLogonTimestamp=if($t.lastLogonTimestamp){[datetime]::FromFileTimeUtc($t.lastLogonTimestamp)}else{$null}; LastLogon=if($maxLL -gt 0){[datetime]::FromFileTimeUtc($maxLL)}else{$null}; LastLogonSourceDC=$maxDC; DaysSinceLastLogon=if($maxLL -gt 0){ [math]::Floor(((Get-Date).ToUniversalTime() - [datetime]::FromFileTimeUtc($maxLL)).TotalDays) } else { $null }; StaleCandidate=if($maxLL -gt 0){ (((Get-Date).ToUniversalTime() - [datetime]::FromFileTimeUtc($maxLL)).TotalDays -ge $InactiveDays) } else { $true } } }
$report | Sort-Object StaleCandidate -Descending, DaysSinceLastLogon -Descending | Tee-Object -Variable view | Export-Csv -NoTypeInformation -Encoding UTF8 $OutCsv
$view | Format-Table sAMAccountName,Enabled,LastLogon,LastLogonSourceDC,LastLogonTimestamp,DaysSinceLastLogon,StaleCandidate -AutoSizeValidation
- Confirm the DC list matches expected writable domain controllers and excludes RODCs unless intentionally included.
- Spot-check 3-5 known active accounts and verify the newest LastLogon aligns with expected activity and a source DC is populated.
- Compare LastLogon and LastLogonTimestamp on a sample of rows to show why replicated timestamps alone would be imprecise.
- Verify null LastLogon entries are investigated rather than treated as immediate removal candidates; newly created or never-used accounts are common exceptions.
Reporting
- Attach the CSV to the ticket and note SearchBase, query date/time UTC, InactiveDays threshold, and DC count queried.
- Filter StaleCandidate = True and group by Enabled status to separate review-only disabled accounts from active cleanup targets.
- Include columns LastLogon, LastLogonSourceDC, LastLogonTimestamp, PwdLastSet, and WhenCreated in reviewer-facing evidence packs.
- Record exception decisions in a companion sheet: service account, shared mailbox-linked identity, break-glass, pending leaver, or owner-confirmed inactive.
Safety Notes
- This is read-only but can generate many LDAP queries; test against a small OU before running domain-wide.
- Do not use the CSV alone to disable accounts; correlate with account purpose, service dependencies, owner approval, and recent password-set activity.
- lastLogon is non-replicated and authoritative per DC for logon evidence; lastLogonTimestamp is replicated and may lag by days.
- If querying computers, adjust filters and review machine account behavior separately from users because inactivity patterns differ.