ActiveDirectory/Find-StaleADObjects.ps1
|
function Find-StaleADObjects { <# .SYNOPSIS Identifies inactive and stale objects in Active Directory .DESCRIPTION Finds stale user accounts, computer accounts, and groups that haven't been used within specified time periods .PARAMETER InactiveDays Number of days to consider an object stale (default: 90) .PARAMETER IncludeUsers Check for stale user accounts .PARAMETER IncludeComputers Check for stale computer accounts .PARAMETER IncludeGroups Check for empty/unused groups .PARAMETER Domain Target domain .EXAMPLE Find-StaleADObjects -InactiveDays 90 -IncludeUsers -IncludeComputers Find-StaleADObjects -InactiveDays 180 -IncludeUsers -IncludeComputers -IncludeGroups .OUTPUTS PSCustomObject with stale object information #> [CmdletBinding()] param( [Parameter()] [int]$InactiveDays = 90, [Parameter()] [switch]$IncludeUsers, [Parameter()] [switch]$IncludeComputers, [Parameter()] [switch]$IncludeGroups, [Parameter()] [string]$Domain ) begin { Write-Host "=== STALE AD OBJECTS SCAN ===" -ForegroundColor Cyan if (-not (Get-Module -ListAvailable -Name ActiveDirectory)) { throw "ActiveDirectory module required" } Import-Module ActiveDirectory -ErrorAction Stop if (-not $Domain) { $Domain = (Get-ADDomain).DNSRoot } Write-Host "Domain: $Domain" -ForegroundColor Yellow Write-Host "Inactive Threshold: $InactiveDays days" -ForegroundColor Yellow $cutoffDate = (Get-Date).AddDays(-$InactiveDays) $results = [PSCustomObject]@{ ScanDate = Get-Date -Format "yyyy-MM-dd HH:mm:ss" Domain = $Domain InactiveDays = $InactiveDays StaleUsers = @() StaleComputers = @() StaleGroups = @() Statistics = @{ TotalStaleUsers = 0 TotalStaleComputers = 0 EmptyGroups = 0 TotalObjectsToReview = 0 } Recommendations = @() } } process { try { # 1. Find Stale User Accounts if ($IncludeUsers) { Write-Host "`n[1/3] Scanning for stale user accounts..." -ForegroundColor Cyan try { # Get users that haven't logged on in X days (@() wraps $null so .Count is safe under strict mode) $staleUsers = @(Get-ADUser -Filter { Enabled -eq $true -and LastLogonDate -lt $cutoffDate } -Properties LastLogonDate, PasswordLastSet, whenCreated, Description -Server $Domain) foreach ($user in $staleUsers) { $daysSinceLogon = if ($user.LastLogonDate) { (New-TimeSpan -Start $user.LastLogonDate -End (Get-Date)).Days } else { "Never" } $accountAge = (New-TimeSpan -Start $user.whenCreated -End (Get-Date)).Days $results.StaleUsers += [PSCustomObject]@{ Username = $user.SamAccountName DisplayName = $user.Name Description = $user.Description Enabled = $user.Enabled LastLogon = $user.LastLogonDate DaysSinceLogon = $daysSinceLogon PasswordLastSet = $user.PasswordLastSet Created = $user.whenCreated AccountAge = $accountAge DistinguishedName = $user.DistinguishedName } } $results.Statistics.TotalStaleUsers = $staleUsers.Count Write-Host " Found $($staleUsers.Count) stale user accounts" -ForegroundColor $( if ($staleUsers.Count -gt 0) { 'Yellow' } else { 'Green' } ) if ($staleUsers.Count -gt 0) { Write-Host " Top 5 oldest:" -ForegroundColor Yellow $staleUsers | Sort-Object LastLogonDate | Select-Object -First 5 | ForEach-Object { $lastLogon = if ($_.LastLogonDate) { $_.LastLogonDate.ToString("yyyy-MM-dd") } else { "Never" } Write-Host " - $($_.SamAccountName) (Last: $lastLogon)" -ForegroundColor White } } } catch { Write-Warning "Error scanning stale users: $_" } } # 2. Find Stale Computer Accounts if ($IncludeComputers) { Write-Host "`n[2/3] Scanning for stale computer accounts..." -ForegroundColor Cyan try { # Get computers that haven't connected in X days (@() wraps $null so .Count is safe under strict mode) $staleComputers = @(Get-ADComputer -Filter { Enabled -eq $true -and LastLogonDate -lt $cutoffDate } -Properties LastLogonDate, OperatingSystem, whenCreated, Description -Server $Domain) foreach ($computer in $staleComputers) { $daysSinceLogon = if ($computer.LastLogonDate) { (New-TimeSpan -Start $computer.LastLogonDate -End (Get-Date)).Days } else { "Never" } $results.StaleComputers += [PSCustomObject]@{ ComputerName = $computer.Name Description = $computer.Description OperatingSystem = $computer.OperatingSystem Enabled = $computer.Enabled LastLogon = $computer.LastLogonDate DaysSinceLogon = $daysSinceLogon Created = $computer.whenCreated DistinguishedName = $computer.DistinguishedName } } $results.Statistics.TotalStaleComputers = $staleComputers.Count Write-Host " Found $($staleComputers.Count) stale computer accounts" -ForegroundColor $( if ($staleComputers.Count -gt 0) { 'Yellow' } else { 'Green' } ) if ($staleComputers.Count -gt 0) { Write-Host " Top 5 oldest:" -ForegroundColor Yellow $staleComputers | Sort-Object LastLogonDate | Select-Object -First 5 | ForEach-Object { $lastLogon = if ($_.LastLogonDate) { $_.LastLogonDate.ToString("yyyy-MM-dd") } else { "Never" } Write-Host " - $($_.Name) (Last: $lastLogon)" -ForegroundColor White } } } catch { Write-Warning "Error scanning stale computers: $_" } } # 3. Find Empty/Unused Groups if ($IncludeGroups) { Write-Host "`n[3/3] Scanning for empty/unused groups..." -ForegroundColor Cyan try { # Get all groups $allGroups = Get-ADGroup -Filter * -Properties Members, whenCreated, Description -Server $Domain foreach ($group in $allGroups) { $members = Get-ADGroupMember -Identity $group -Server $Domain -ErrorAction SilentlyContinue # Get-ADGroupMember returns $null (not an empty array) for empty # groups, and $null.Count is $null, so a .Count -eq 0 check never fires if (-not $members) { $groupAge = (New-TimeSpan -Start $group.whenCreated -End (Get-Date)).Days $results.StaleGroups += [PSCustomObject]@{ GroupName = $group.Name Description = $group.Description MemberCount = 0 Created = $group.whenCreated GroupAge = $groupAge GroupScope = $group.GroupScope GroupCategory = $group.GroupCategory DistinguishedName = $group.DistinguishedName } } } $results.Statistics.EmptyGroups = $results.StaleGroups.Count Write-Host " Found $($results.StaleGroups.Count) empty groups" -ForegroundColor $( if ($results.StaleGroups.Count -gt 0) { 'Yellow' } else { 'Green' } ) if ($results.StaleGroups.Count -gt 0) { Write-Host " Top 5 oldest empty groups:" -ForegroundColor Yellow $results.StaleGroups | Sort-Object Created | Select-Object -First 5 | ForEach-Object { Write-Host " - $($_.GroupName) (Created: $($_.Created.ToString('yyyy-MM-dd')))" -ForegroundColor White } } } catch { Write-Warning "Error scanning groups: $_" } } # Calculate total objects to review $results.Statistics.TotalObjectsToReview = $results.Statistics.TotalStaleUsers + $results.Statistics.TotalStaleComputers + $results.Statistics.EmptyGroups # Generate recommendations if ($results.Statistics.TotalStaleUsers -gt 0) { $results.Recommendations += "Review and disable $($results.Statistics.TotalStaleUsers) stale user accounts" $results.Recommendations += "Move disabled accounts to separate OU after 30 days" $results.Recommendations += "Delete disabled accounts after 90 days (with backup)" } if ($results.Statistics.TotalStaleComputers -gt 0) { $results.Recommendations += "Remove $($results.Statistics.TotalStaleComputers) stale computer accounts" } if ($results.Statistics.EmptyGroups -gt 0) { $results.Recommendations += "Review and remove $($results.Statistics.EmptyGroups) empty groups" } $results.Recommendations += "Implement automated stale object cleanup policy" $results.Recommendations += "Schedule monthly stale object reviews" } catch { Write-Error "Error scanning for stale objects: $_" throw } } end { Write-Host "`n=== SCAN COMPLETE ===" -ForegroundColor Green Write-Host "`nStatistics:" -ForegroundColor Cyan Write-Host " Stale Users: $($results.Statistics.TotalStaleUsers)" -ForegroundColor White Write-Host " Stale Computers: $($results.Statistics.TotalStaleComputers)" -ForegroundColor White Write-Host " Empty Groups: $($results.Statistics.EmptyGroups)" -ForegroundColor White Write-Host " Total Objects to Review: $($results.Statistics.TotalObjectsToReview)" -ForegroundColor $( if ($results.Statistics.TotalObjectsToReview -gt 0) { 'Yellow' } else { 'Green' } ) if ($results.Recommendations.Count -gt 0) { Write-Host "`nRecommendations:" -ForegroundColor Cyan $results.Recommendations | ForEach-Object { Write-Host " ! $_" -ForegroundColor Yellow } } Write-Host "`nCleanup Impact:" -ForegroundColor Cyan if ($results.Statistics.TotalObjectsToReview -gt 0) { Write-Host " Removing stale objects will improve AD performance" -ForegroundColor White Write-Host " Reduce AD replication traffic" -ForegroundColor White Write-Host " Improve security posture" -ForegroundColor White } return $results } } |