Orphaned computer objects are machine accounts that exist in Active Directory but no longer correspond to actual physical or virtual machines in your environment. These typically occur when:
- Machines are decommissioned without proper AD cleanup
- OS reinstalls create new computer objects while old ones remain
- VMs are deleted without removing their AD accounts
Here's a comprehensive PowerShell script that identifies computers inactive for specified days:
# Parameters
$DaysInactive = 90
$SearchBase = "OU=Computers,DC=domain,DC=com"
$ReportPath = "C:\ADReports\InactiveComputers.csv"
# Calculate cutoff date
$CutoffDate = (Get-Date).AddDays(-$DaysInactive)
# Get all computer objects
$Computers = Get-ADComputer -Filter * -Properties LastLogonDate,OperatingSystem,Description -SearchBase $SearchBase
# Filter inactive computers
$InactiveComputers = $Computers | Where-Object {
$_.LastLogonDate -lt $CutoffDate -or $_.LastLogonDate -eq $null
}
# Export results
$InactiveComputers | Select-Object Name,DistinguishedName,LastLogonDate,OperatingSystem |
Export-Csv $ReportPath -NoTypeInformation
To ensure we don't accidentally target still-active computers:
# Ping verification
$VerifiedInactive = @()
foreach ($Computer in $InactiveComputers) {
if (-not (Test-Connection -ComputerName $Computer.Name -Count 1 -Quiet)) {
$VerifiedInactive += $Computer
}
}
For more complex environments:
# Multi-domain forest search
$AllDomains = (Get-ADForest).Domains
$AllInactive = @()
foreach ($Domain in $AllDomains) {
$Inactive = Get-ADComputer -Server $Domain -Filter {
LastLogonDate -lt $CutoffDate -or
LastLogonDate -notlike "*"
} -Properties *
$AllInactive += $Inactive
}
Before deleting, consider these safety measures:
# Move to quarantine OU first
$QuarantineOU = "OU=DisabledComputers,DC=domain,DC=com"
$InactiveComputers | ForEach-Object {
Move-ADObject -Identity $_.DistinguishedName -TargetPath $QuarantineOU
Disable-ADAccount -Identity $_.DistinguishedName
}
# Optional: Delete after verification
Start-Sleep -Days 14 # Wait period
Get-ADComputer -Filter * -SearchBase $QuarantineOU | Remove-ADObject -Confirm:$false
Create a scheduled task for regular maintenance:
$Trigger = New-ScheduledTaskTrigger -Weekly -DaysOfWeek Sunday -At 2am
$Action = New-ScheduledTaskAction -Execute "PowerShell.exe" -Argument "-File C:\Scripts\CleanInactiveComputers.ps1"
Register-ScheduledTask -TaskName "AD Computer Cleanup" -Trigger $Trigger -Action $Action -RunLevel Highest
Orphaned computer objects in Active Directory are those that no longer have a corresponding physical machine in your network. These can accumulate over time and clutter your AD. Here's how to find them using PowerShell:
# Import Active Directory module
Import-Module ActiveDirectory
# Find computer objects that haven't logged on in 90 days
$inactiveDays = 90
$cutoffDate = (Get-Date).AddDays(-$inactiveDays)
# Get all computer objects that haven't logged on since cutoff date
$orphanedComputers = Get-ADComputer -Filter {LastLogonTimeStamp -lt $cutoffDate} -Properties LastLogonTimeStamp, OperatingSystem, Enabled |
Where-Object { $_.Enabled -eq $true } |
Select-Object Name, DistinguishedName, LastLogonTimeStamp, OperatingSystem
# Output results
$orphanedComputers | Format-Table -AutoSize
The LastLogonTimeStamp attribute isn't replicated immediately across all domain controllers. For more accurate results, you might want to query all DCs:
function Get-ADComputerLastLogon {
param(
[string]$ComputerName,
[int]$DaysInactive = 90
)
$cutoffDate = (Get-Date).AddDays(-$DaysInactive)
$domainControllers = Get-ADDomainController -Filter *
$lastLogons = foreach ($dc in $domainControllers) {
try {
Get-ADComputer -Identity $ComputerName -Server $dc.HostName -Properties LastLogon |
Select-Object @{Name='DC';Expression={$dc.HostName}}, LastLogon
}
catch {
Write-Warning "Failed to query $($dc.HostName) for $ComputerName"
}
}
$mostRecent = $lastLogons | Sort-Object LastLogon -Descending | Select-Object -First 1
if ($mostRecent.LastLogon -lt $cutoffDate) {
return $true
}
else {
return $false
}
}
You can extend the basic query to include additional criteria:
# Find computers inactive for 180 days that are not in specific OUs
$excludedOUs = @(
"OU=Servers,DC=domain,DC=com",
"OU=Workstations,DC=domain,DC=com"
)
$inactiveComputers = Get-ADComputer -Filter {LastLogonTimeStamp -lt $cutoffDate} -Properties * |
Where-Object {
$_.Enabled -eq $true -and
$excludedOUs -notcontains $_.DistinguishedName -and
$_.OperatingSystem -notlike "*Server*"
} |
Select-Object Name, DistinguishedName, LastLogonTimeStamp, OperatingSystem, Enabled
For larger environments, you'll want to export the results:
# Export to CSV with timestamp
$exportPath = "C:\ADReports\InactiveComputers_$(Get-Date -Format 'yyyyMMdd').csv"
$inactiveComputers | Export-Csv -Path $exportPath -NoTypeInformation
# Optional: Email the report
$emailParams = @{
From = "adadmin@domain.com"
To = "it-team@domain.com"
Subject = "Inactive Computer Objects Report"
Body = "Attached is the report of computer objects inactive for $inactiveDays days."
SmtpServer = "smtp.domain.com"
Attachments = $exportPath
}
Send-MailMessage @emailParams
After verifying the results, you can disable or delete inactive objects:
# Disable inactive computers
$inactiveComputers | ForEach-Object {
Disable-ADAccount -Identity $_.DistinguishedName
Write-Host "Disabled computer: $($_.Name)"
}
# Or move to a quarantine OU first
$quarantineOU = "OU=Disabled Computers,DC=domain,DC=com"
$inactiveComputers | ForEach-Object {
Move-ADObject -Identity $_.DistinguishedName -TargetPath $quarantineOU
}
- Always test queries in a non-production environment first
- Consider creating a staging OU before deletion
- Maintain documentation of your cleanup process
- Schedule regular reports rather than immediate deletion
- Check for associated DNS records and other dependencies