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

  1. Set SearchBase, ObjectClass, InactiveDays, and output path. Use a narrow OU first for timing and permission validation.
  2. Enumerate writable domain controllers with Get-ADDomainController so only authoritative per-DC lastLogon values are queried.
  3. Collect target accounts with Get-ADObject or adapt the LDAP filter for computers if reviewing stale device objects.
  4. For each account, query each DC for lastLogon, keep the newest value and record the DC that supplied it, then compare against replicated lastLogonTimestamp.
  5. Export the full result set to CSV and attach filtered stale-candidate rows to the cleanup ticket or review workbook.
  6. 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 -AutoSize

Validation

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