Shore Up
an inspector pulling a handful of never-activated employee badges from a filing drawer and setting them aside on a desk, the used badges left in place
WindowsSecurity

Find and Report AD Accounts That Have Never Logged In

Ketan Aagja9 min read
No ratings yet

Before you run this

This guide gives you a PowerShell script that reads Active Directory and produces a report (on screen and as a CSV) of user accounts that have never authenticated against any domain controller. It changes nothing — it does not disable, delete, or edit a single account. Its purpose is to hand you a clean list of candidates for review before you decide what to do with them.

Because it only reads, you do not need Domain Admin. Any authenticated domain user can read the attributes involved. You do need the ActiveDirectory PowerShell module (part of RSAT), and I'd run it from an elevated PowerShell session on a domain-joined machine simply so the module loads cleanly and you have a predictable environment. No elevation is required for the AD reads themselves.

Even though this is read-only, read the script before you run it, and run it first against a small scope — a single test OU, or one OU you know well — using the -SearchBase parameter, rather than the whole domain. That confirms the output looks sane and keeps the first run fast. The accurate version of this script queries every domain controller once per user, so on a large directory it can take a while; a scoped test run tells you what to expect.

One honest caveat up front, because it's the whole reason this is trickier than it looks: the convenient LastLogonDate attribute is replicated only periodically (roughly every 14 days), so it can be blank for someone who logged in last week. Trusting it alone will over-report — you'll flag active people as "never logged in." The reliable method is to read the non-replicated lastLogon attribute from each DC and take the newest value. Both approaches are below; use the second one for anything you'll act on.

Assumed environment: Windows Server 2016 or later (or Windows 10/11 with RSAT), the ActiveDirectory module installed, PowerShell 5.1, and a single AD domain. On PowerShell 7 the same cmdlets work through the module's compatibility layer.

Why "never logged in" is a specific query

An account that has genuinely never been used has no logon recorded on any DC. In AD terms, its lastLogon value is either absent or 0 on every domain controller. Two other attributes are worth having in the report for context:

  • whenCreated — how long the account has sat unused. A never-used account created three years ago is a very different thing from one created yesterday.
  • Enabled — a disabled, never-used account is low risk; an enabled, never-used account is exactly what you want to find.

Note pwdLastSet is usually populated at creation, so it does not tell you whether anyone ever signed in. Don't use it as a logon proxy.

The quick, rough pass (use for a first look only)

This uses LastLogonDate and will be fast. Treat its output as a superset — a starting list you'll narrow with the accurate script. Replace the -SearchBase value with a real distinguished name from your directory.

Import-Module ActiveDirectory

# Replace with your own OU. Use the whole domain only when you're ready.
$searchBase = "OU=Staff,DC=example,DC=com"

Get-ADUser -SearchBase $searchBase -Filter * -Properties LastLogonDate, whenCreated, Enabled |
    Where-Object { $null -eq $_.LastLogonDate } |          # never a replicated logon timestamp
    Select-Object SamAccountName, Name, Enabled, whenCreated |
    Sort-Object whenCreated |
    Format-Table -AutoSize

If a name on that list logged in recently, that's the replication lag I warned about — which is why you don't act on this output.

The accurate pass — query every DC

This is the version to trust. It reads lastLogon from each domain controller for each user and reports only those whose newest logon across all DCs is still zero: genuinely never used.

Import-Module ActiveDirectory

# Replace with your own OU, or remove -SearchBase from Get-ADUser to scan the whole domain.
$searchBase = "OU=Staff,DC=example,DC=com"

# Where to write the report. Replace with a real path you can write to.
$outputCsv  = "C:\path\to\never-logged-in.csv"

# Every DC in the domain; lastLogon is per-DC and not replicated, so we must check them all.
$dcs = Get-ADDomainController -Filter * | Select-Object -ExpandProperty HostName

$users = Get-ADUser -SearchBase $searchBase -Filter * -Properties whenCreated, Enabled

$report = foreach ($u in $users) {
    $latest = 0
    foreach ($dc in $dcs) {
        # Read this user's lastLogon as stored on THIS domain controller.
        $ll = (Get-ADUser -Identity $u.DistinguishedName -Server $dc -Properties lastLogon).lastLogon
        if ($ll -and $ll -gt $latest) { $latest = $ll }
    }

    if ($latest -eq 0) {
        # No logon on any DC -> never authenticated.
        [PSCustomObject]@{
            SamAccountName = $u.SamAccountName
            Name           = $u.Name
            Enabled        = $u.Enabled
            WhenCreated    = $u.whenCreated
        }
    }
}

$report |
    Sort-Object WhenCreated |
    Export-Csv -Path $outputCsv -NoTypeInformation -Encoding UTF8

# Also show it on screen so you get immediate feedback.
$report | Sort-Object WhenCreated | Format-Table -AutoSize

Write-Host "Found $($report.Count) never-logged-in account(s). Report saved to $outputCsv"

A few notes on what this does and doesn't do:

  • lastLogon is stored as a Windows FILETIME integer. We don't need to convert it here — we only care whether the largest value across all DCs is still 0. If you do want the real date for accounts that have logged in, [DateTime]::FromFileTime($latest) converts a non-zero value.
  • Querying each user against each DC is a lot of small lookups. On a few hundred users this is seconds to a couple of minutes; on tens of thousands, expect it to run long. Scope it with -SearchBase, or run it out of hours.
  • If a DC is offline, its Get-ADUser -Server call will error for every user. If that happens, filter $dcs down to reachable DCs before the loop, or wrap the inner call in a try/catch — but don't silently skip a DC you needed, or you'll wrongly flag an account as never-used.

Verify the result

The report is only useful if you trust it, so spot-check before acting.

Pick a name from the CSV that you expect to be genuinely unused and confirm it directly:

# Replace testuser with a SamAccountName from your report.
Get-ADUser -Identity testuser -Properties whenCreated, LastLogonDate, Enabled |
    Format-List SamAccountName, Enabled, whenCreated, LastLogonDate

Then do the opposite sanity check: pick an account you know is in daily use and confirm it did not land in the report. If an active account appears in the output, something is off — most likely an unreachable DC — and you should resolve that before trusting the list.

What to do with the list — and how to undo it

This guide stops at the report on purpose. Deciding to disable or delete accounts is a separate, deliberate step, and I'd keep the two apart so a reporting run can never damage anything.

When you do act, disable before you delete. Disabling with Set-ADUser -Enabled $false (or Disable-ADAccount) is fully reversible — re-enable with Enable-ADAccount — and it lets you leave an account dormant for a grace period before removal. Both cmdlets support -WhatIf; use it first. Deletion is not reversible without AD Recycle Bin or a system state restore, so treat Remove-ADUser with real caution and never run it across an OU without a filter and a -WhatIf dry run.

For the exact syntax of any of these, see Microsoft Learn for Get-ADUser, Get-ADDomainController, Disable-ADAccount, and Remove-ADUser. Keep the CSV from this run as your record of what the directory looked like before you changed anything.

Written by
Ketan Aagja

Runs enterprise networks and security for a living, and writes Shore Up to turn two decades of hands-on Linux, Windows and mail-server work into guides you can actually use.

More about the author →

Was this article helpful?

Tap a star — no sign-in needed.

Be the first to rate this article.