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:
LastLogonis a non-replicated attribute stored only on the DC the user authenticated against — different DCs have different values.LastLogonDateis replicated across all DCs (updated every 14 days) and is the correct attribute to use in multi-DC domains. Always useLastLogonDatein 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
$nullforLastLogonDate. A comparison like$_.LastLogonDate -lt $cutoffreturns$falsefor null values, causing these accounts to be missed. Always add an explicit null check:-or $_.LastLogonDate -eq $nullto catch accounts that have never been used.
Related Cmdlets / See Also
- PowerShell Active Directory: Get-ADUser Examples
- PowerShell Active Directory User Management: Create and Modify
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.


