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

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

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