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
# ---------------------------------------------------------------------
# Operator inputs
# ---------------------------------------------------------------------
$ObjectType = 'user' # Supported starter values: user or computer
$SearchBase = 'DC=contoso,DC=com'
$DaysInactive = 45
$Cutoff = (Get-Date).AddDays(-$DaysInactive)
$OutputPath = '.\all-dc-lastlogon-evidence.csv'
# Build a non-contradictory LDAP filter for the selected object type.
$LdapFilter = switch ($ObjectType) {
'user' { '(&(objectCategory=person)(objectClass=user))' }
'computer' { '(objectClass=computer)' }
default { throw 'ObjectType must be user or computer.' }
}
# Writable DCs provide the non-replicated lastLogon evidence we need to compare.
$DomainControllers = Get-ADDomainController -Filter * | Where-Object { -not $_.IsReadOnly }
$LatestByDn = @{}
foreach ($Dc in $DomainControllers) {
Write-Verbose "Querying $($Dc.HostName)"
$Objects = Get-ADObject -Server $Dc.HostName -SearchBase $SearchBase -LDAPFilter $LdapFilter -Properties sAMAccountName,lastLogon
foreach ($Object in $Objects) {
$LastLogonValue = [int64]$Object.lastLogon
$LastLogonDate = if ($LastLogonValue -gt 0) { [DateTime]::FromFileTimeUtc($LastLogonValue) } else { $null }
$Existing = $LatestByDn[$Object.DistinguishedName]
$CandidateDate = if ($LastLogonDate) { $LastLogonDate } else { [datetime]::MinValue }
$ExistingDate = if ($Existing -and $Existing.LastLogonUtc) { $Existing.LastLogonUtc } else { [datetime]::MinValue }
if (-not $Existing -or ($CandidateDate -gt $ExistingDate)) {
$LatestByDn[$Object.DistinguishedName] = [pscustomobject]@{
DistinguishedName = $Object.DistinguishedName
SamAccountName = $Object.sAMAccountName
LastLogonUtc = $LastLogonDate
SourceDomainController = $Dc.HostName
}
}
}
}
$Results = foreach ($Entry in $LatestByDn.Values) {
$ReviewStatus = if (-not $Entry.LastLogonUtc) {
'NeedsReview-NoObservedLastLogon'
} elseif ($Entry.LastLogonUtc -lt $Cutoff.ToUniversalTime()) {
'StaleByCutoff'
} else {
'Recent'
}
[pscustomobject]@{
DistinguishedName = $Entry.DistinguishedName
SamAccountName = $Entry.SamAccountName
LastLogonUtc = $Entry.LastLogonUtc
SourceDomainController = $Entry.SourceDomainController
ReviewStatus = $ReviewStatus
}
}
$Results | Export-Csv -Path $OutputPath -NoTypeInformation -Encoding UTF8
$Results | Sort-Object ReviewStatus, LastLogonUtc | Format-Table -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
- Export exact newest observed lastLogon evidence from all queried writable domain controllers.
- Keep null/no-observed-lastLogon rows in a review bucket rather than automatically calling them stale-removal candidates.
- Promote this into a parallelized evidence collector pack when you need large-domain runtime optimization and execution logging.
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.