# Get-StaleADObjects.ps1 # Read-only reporting script -- discovers stale, orphaned, and unmanaged # AD objects in managed OUs. # # Usage: # .\Get-StaleADObjects.ps1 # Default 90-day threshold # .\Get-StaleADObjects.ps1 -StaleDays 60 # Custom threshold [CmdletBinding()] param( [int]$StaleDays = 90 ) $ErrorActionPreference = 'Stop' $ScriptRoot = $PSScriptRoot # ------------------------------------------------------------------- # Load definitions (source of truth) # ------------------------------------------------------------------- $definedUsers = & (Join-Path $ScriptRoot 'users.ps1') $definedGroups = & (Join-Path $ScriptRoot 'groups.ps1') $definedOUs = & (Join-Path $ScriptRoot 'ous.ps1') $managedUserNames = @($definedUsers | ForEach-Object { $_.SamAccountName }) $managedGroupNames = @($definedGroups | ForEach-Object { $_.Name }) $managedOUDNs = @($definedOUs | ForEach-Object { "OU=$($_.Name),$($_.Path)" }) $staleDate = (Get-Date).AddDays(-$StaleDays) $totalFindings = 0 Write-Host "=== Stale AD Object Report ===" -ForegroundColor Cyan Write-Host " Threshold: $StaleDays days (before $(Get-Date $staleDate -Format 'yyyy-MM-dd'))" -ForegroundColor DarkGray Write-Host '' # ------------------------------------------------------------------- # 1. Stale user accounts (no login in N days) # ------------------------------------------------------------------- Write-Host "--- Stale User Accounts (no login in $StaleDays days) ---" -ForegroundColor White $staleCount = 0 foreach ($ouDN in $managedOUDNs) { $users = Get-ADUser -Filter * -SearchBase $ouDN -SearchScope OneLevel ` -Properties LastLogonDate, Enabled -ErrorAction SilentlyContinue foreach ($user in $users) { $isStale = $false if ($null -eq $user.LastLogonDate) { # Never logged in -- stale if account is old enough $created = (Get-ADUser -Identity $user.SamAccountName -Properties WhenCreated).WhenCreated if ($created -lt $staleDate) { $isStale = $true } } elseif ($user.LastLogonDate -lt $staleDate) { $isStale = $true } if ($isStale -and $user.Enabled) { $lastLogin = if ($null -eq $user.LastLogonDate) { 'Never' } else { Get-Date $user.LastLogonDate -Format 'yyyy-MM-dd' } Write-Host " [STALE] $($user.SamAccountName) -- last login: $lastLogin" -ForegroundColor Yellow $staleCount++ } } } if ($staleCount -eq 0) { Write-Host " No stale accounts found." -ForegroundColor Green } $totalFindings += $staleCount Write-Host '' # ------------------------------------------------------------------- # 2. Disabled accounts (not intentionally disabled in users.ps1) # ------------------------------------------------------------------- Write-Host "--- Unexpectedly Disabled Accounts ---" -ForegroundColor White $disabledCount = 0 foreach ($ouDN in $managedOUDNs) { $users = Get-ADUser -Filter { Enabled -eq $false } -SearchBase $ouDN -SearchScope OneLevel ` -ErrorAction SilentlyContinue foreach ($user in $users) { # Check if this user is intentionally disabled in definitions $defined = $definedUsers | Where-Object { $_.SamAccountName -eq $user.SamAccountName } if ($defined -and $defined.Enabled -eq $false) { continue # Intentionally disabled } Write-Host " [DISABLED] $($user.SamAccountName) -- not marked as disabled in definitions" -ForegroundColor Yellow $disabledCount++ } } if ($disabledCount -eq 0) { Write-Host " No unexpectedly disabled accounts found." -ForegroundColor Green } $totalFindings += $disabledCount Write-Host '' # ------------------------------------------------------------------- # 3. Empty groups (zero members in managed OUs) # ------------------------------------------------------------------- Write-Host "--- Empty Groups ---" -ForegroundColor White $emptyCount = 0 foreach ($ouDN in $managedOUDNs) { $groups = Get-ADGroup -Filter * -SearchBase $ouDN -SearchScope OneLevel ` -ErrorAction SilentlyContinue foreach ($group in $groups) { $members = @(Get-ADGroupMember -Identity $group.SamAccountName -ErrorAction SilentlyContinue) if ($members.Count -eq 0) { Write-Host " [EMPTY] $($group.Name) -- zero members" -ForegroundColor Yellow $emptyCount++ } } } if ($emptyCount -eq 0) { Write-Host " No empty groups found." -ForegroundColor Green } $totalFindings += $emptyCount Write-Host '' # ------------------------------------------------------------------- # 4. Unmanaged users (in managed OUs but not in users.ps1) # ------------------------------------------------------------------- Write-Host "--- Unmanaged Users (in managed OUs but not in definitions) ---" -ForegroundColor White $unmanagedUserCount = 0 foreach ($ouDN in $managedOUDNs) { $users = Get-ADUser -Filter * -SearchBase $ouDN -SearchScope OneLevel ` -ErrorAction SilentlyContinue foreach ($user in $users) { if ($user.SamAccountName -notin $managedUserNames) { Write-Host " [UNMANAGED] $($user.SamAccountName) in $ouDN" -ForegroundColor Yellow $unmanagedUserCount++ } } } if ($unmanagedUserCount -eq 0) { Write-Host " All users in managed OUs are defined." -ForegroundColor Green } $totalFindings += $unmanagedUserCount Write-Host '' # ------------------------------------------------------------------- # 5. Unmanaged groups (in managed OUs but not in groups.ps1) # ------------------------------------------------------------------- Write-Host "--- Unmanaged Groups (in managed OUs but not in definitions) ---" -ForegroundColor White $unmanagedGroupCount = 0 foreach ($ouDN in $managedOUDNs) { $groups = Get-ADGroup -Filter * -SearchBase $ouDN -SearchScope OneLevel ` -ErrorAction SilentlyContinue foreach ($group in $groups) { if ($group.Name -notin $managedGroupNames) { Write-Host " [UNMANAGED] $($group.Name) in $ouDN" -ForegroundColor Yellow $unmanagedGroupCount++ } } } if ($unmanagedGroupCount -eq 0) { Write-Host " All groups in managed OUs are defined." -ForegroundColor Green } $totalFindings += $unmanagedGroupCount Write-Host '' # ------------------------------------------------------------------- # 6. Pending credential files # ------------------------------------------------------------------- Write-Host "--- Pending Credential Files ---" -ForegroundColor White $credDir = Join-Path $ScriptRoot '.credentials' $credCount = 0 if (Test-Path $credDir) { $credFiles = Get-ChildItem -Path $credDir -Filter '*.txt' -ErrorAction SilentlyContinue foreach ($f in $credFiles) { $age = [math]::Round(((Get-Date) - $f.LastWriteTime).TotalHours, 1) $color = if ($age -gt 24) { 'Red' } else { 'Yellow' } Write-Host " [PENDING] $($f.Name) -- ${age}h old" -ForegroundColor $color $credCount++ } } if ($credCount -eq 0) { Write-Host " No pending credential files." -ForegroundColor Green } $totalFindings += $credCount Write-Host '' # ------------------------------------------------------------------- # Summary # ------------------------------------------------------------------- if ($totalFindings -eq 0) { Write-Host "No findings. All managed OUs are clean." -ForegroundColor Green } else { Write-Host "$totalFindings finding(s) across all checks." -ForegroundColor Red } Write-Host 'Report complete. No changes were made.' -ForegroundColor DarkGray