Infrastructure-as-code framework for Active Directory objects and Group Policy. Sanitized from production deployment for public sharing.
203 lines
7.5 KiB
PowerShell
203 lines
7.5 KiB
PowerShell
# 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
|