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
Configuration or code example: review values for your environment
# ---------------------------------------------------------------------
# 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) {Manual or UI step
'user' { '(&(objectCategory=person)(objectClass=user))' }
'computer' { '(objectClass=computer)' }
default { throw 'ObjectType must be user or computer.' }
Read-only command: verify target and scope
}
# 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)) {Manual or UI step
$LatestByDn[$Object.DistinguishedName] = [pscustomobject]@{
Writes local output artifacts: review the output path before running
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.
