PowerShell Find Inactive AD Users and Computers

PowerShell Find Inactive AD Users and Computers

PowerShell Tips Editor 4 min read
PowerShell Find Inactive AD Users and Computers

Every inactive account in Active Directory is a potential attack vector — a credential waiting to be brute-forced, phished, or used in a pass-the-hash attack. Security audits invariably ask for a list of users who haven’t logged in for 90 days, and the answer should come from PowerShell in seconds. PowerShell find inactive Active Directory users with Search-ADAccount and LastLogonDate filtering gives you the reports and bulk automation to shrink your attack surface systematically.

Search-ADAccount for Inactive Users

Search-ADAccount has a dedicated parameter for inactive accounts. The -AccountInactive switch, combined with -TimeSpan, returns accounts that have not had a logon recorded within the specified period.

# Users inactive for 90 days
$inactive = Search-ADAccount -AccountInactive -TimeSpan 90.00:00:00 -UsersOnly
Write-Output "Found $($inactive.Count) inactive users"

$inactive | Select-Object Name, SamAccountName, LastLogonDate, Enabled |
    Sort-Object LastLogonDate | Format-Table -AutoSize
Name          SamAccountName  LastLogonDate         Enabled
----          --------------  -------------         -------
Old Account   xsmith          10/5/2025 9:00:00 AM  True
Test User     testuser1                             True

Filter by LastLogonDate

LastLogonDate is the AD-replicated version of last logon time (as opposed to LastLogon which is only written to the DC the user authenticated against). Always use LastLogonDate in multi-DC environments.

$cutoffDate = (Get-Date).AddDays(-90)

# Filter using Get-ADUser for more control
Get-ADUser -Filter { Enabled -eq $true } `
    -Properties LastLogonDate |
    Where-Object { $_.LastLogonDate -lt $cutoffDate -or $_.LastLogonDate -eq $null } |
    Select-Object Name, SamAccountName, LastLogonDate |
    Sort-Object LastLogonDate

Notice the -eq $null check — accounts that have never logged on have a null LastLogonDate and should be included in the report.

Find Stale Computer Accounts

Computer accounts go stale just like user accounts — a decommissioned PC still has an active account unless it’s cleaned up. Use -ComputersOnly with Search-ADAccount.

# Computer accounts inactive for 90+ days
$staleComputers = Search-ADAccount -AccountInactive -TimeSpan 90.00:00:00 -ComputersOnly
Write-Output "Stale computer accounts: $($staleComputers.Count)"

$staleComputers | Select-Object Name, SamAccountName, LastLogonDate, DistinguishedName |
    Export-Csv -Path "C:\Logs\stale-computers.csv" -NoTypeInformation

Report Inactive Accounts to CSV

Generate a full inactivity report with extended properties for security review. Include the user’s OU so reviewers know which team owns each account.

$cutoff = (Get-Date).AddDays(-90)
$outputPath = "C:\Logs\inactive-users-$(Get-Date -Format yyyyMMdd).csv"

Get-ADUser -Filter { Enabled -eq $true } `
    -Properties LastLogonDate, Department, Title, Manager, DistinguishedName |
    Where-Object { $_.LastLogonDate -lt $cutoff -or $_.LastLogonDate -eq $null } |
    Select-Object Name, SamAccountName, Department, Title,
        @{ N="Manager"; E={ if ($_.Manager) { (Get-ADUser $_.Manager).Name } else { "" } } },
        LastLogonDate, DistinguishedName |
    Export-Csv -Path $outputPath -NoTypeInformation

Write-Output "Inactive user report saved to $outputPath"

Bulk Disable Inactive Users

After the report is reviewed and approved, disable accounts in bulk. Move them to a “Disabled” OU rather than deleting immediately — preserves group memberships and history for a defined retention period.

$cutoff      = (Get-Date).AddDays(-90)
$disabledOU  = "OU=Disabled-Users,DC=corp,DC=example,DC=com"
$logFile     = "C:\Logs\disabled-accounts-$(Get-Date -Format yyyyMMdd).log"

$toDisable = Get-ADUser -Filter { Enabled -eq $true } -Properties LastLogonDate |
    Where-Object { $_.LastLogonDate -lt $cutoff -or $_.LastLogonDate -eq $null }

foreach ($user in $toDisable) {
    Disable-ADAccount -Identity $user.SamAccountName
    Move-ADObject -Identity $user.DistinguishedName -TargetPath $disabledOU
    "$(Get-Date -Format 'yyyy-MM-dd HH:mm') | DISABLED | $($user.SamAccountName) | LastLogon: $($user.LastLogonDate)" |
        Add-Content -Path $logFile
}

Write-Output "Disabled $($toDisable.Count) accounts. Log: $logFile"

Set Account Expiry Date

Instead of disabling accounts manually, set an expiry date at creation for temporary accounts. The account automatically becomes unusable after the expiry date without any ongoing maintenance.

# Set expiry on an existing account
Set-ADAccountExpiration -Identity "contractor01" -DateTime "2026-12-31"

# Remove expiry (account does not expire)
Clear-ADAccountExpiration -Identity "contractor01"

# Verify expiry
Get-ADUser -Identity "contractor01" -Properties AccountExpirationDate |
    Select-Object Name, AccountExpirationDate

Common Errors and Fixes

  • LastLogonDate vs LastLogon: LastLogon is a non-replicated attribute stored only on the DC the user authenticated against — different DCs have different values. LastLogonDate is replicated across all DCs (updated every 14 days) and is the correct attribute to use in multi-DC domains. Always use LastLogonDate in reports to avoid false results from stale DC-specific data.
  • Never logged on users have null LastLogonDate: New or unused accounts that have never completed a successful logon have $null for LastLogonDate. A comparison like $_.LastLogonDate -lt $cutoff returns $false for null values, causing these accounts to be missed. Always add an explicit null check: -or $_.LastLogonDate -eq $null to catch accounts that have never been used.

Related Cmdlets / See Also

Wrapping Up

Regular inactive account cleanup is one of the highest-impact, lowest-effort security improvements you can make — and automating it with PowerShell removes the friction entirely. Schedule the bulk disable script to run monthly with a 90-day cutoff, review the log, and you’ll maintain a consistently lean AD with minimal manual effort.

Send-Item -To