Private/AD/Checks/Invoke-ADStaleObjectChecks.ps1
|
# PSGuerrilla - Jim Tyler, Microsoft MVP - CC BY 4.0 # https://github.com/jimrtyler/PSGuerrilla | https://creativecommons.org/licenses/by/4.0/ # AI/LLM use: see AI-USAGE.md for required attribution function Invoke-ADStaleObjectChecks { [CmdletBinding()] param( [Parameter(Mandatory)] [hashtable]$AuditData ) $checkDefs = $null try { $checkDefs = Get-AuditCategoryDefinitions -Category 'ADStaleObjectChecks' } catch { Write-Verbose "ADStaleObjectChecks definitions not found: $_" return @() } $findings = [System.Collections.Generic.List[PSCustomObject]]::new() foreach ($check in $checkDefs.checks) { $funcName = "Test-Recon$($check.id -replace '-', '')" if (Get-Command $funcName -ErrorAction SilentlyContinue) { try { $finding = & $funcName -AuditData $AuditData -CheckDefinition $check if ($finding) { $findings.Add($finding) } } catch { $findings.Add((New-AuditFinding -CheckDefinition $check -Status 'ERROR' ` -CurrentValue "Check failed: $_")) } } else { $findings.Add((New-AuditFinding -CheckDefinition $check -Status 'SKIP' ` -CurrentValue 'Check not yet implemented')) } } return @($findings) } # -- ADSTALE-001: Inactive User Accounts ------------------------------------- function Test-ReconADSTALE001 { [CmdletBinding()] param([hashtable]$AuditData, [hashtable]$CheckDefinition) $stale = $AuditData.StaleObjects if (-not $stale) { return New-AuditFinding -CheckDefinition $CheckDefinition -Status 'SKIP' ` -CurrentValue 'Stale object data not available' } $inactiveUsers = @($stale.InactiveUsers) if ($inactiveUsers.Count -eq 0 -or ($inactiveUsers.Count -eq 1 -and $null -eq $inactiveUsers[0])) { $totalUsers = if ($stale.ContainsKey('TotalUsers')) { $stale.TotalUsers } else { 'unknown' } return New-AuditFinding -CheckDefinition $CheckDefinition -Status 'PASS' ` -CurrentValue "No inactive user accounts found ($totalUsers total user(s))" ` -Details @{ TotalUsers = $totalUsers } } # Build sample list (first 10) $sampleList = @($inactiveUsers | Select-Object -First 10 | ForEach-Object { $_.SamAccountName }) $sampleText = $sampleList -join ', ' if ($inactiveUsers.Count -gt 10) { $sampleText += '...' } $totalUsers = if ($stale.ContainsKey('TotalUsers')) { $stale.TotalUsers } else { 0 } $currentValue = "$($inactiveUsers.Count) inactive user account(s) found: $sampleText" return New-AuditFinding -CheckDefinition $CheckDefinition -Status 'FAIL' ` -CurrentValue $currentValue ` -Details @{ InactiveCount = $inactiveUsers.Count TotalUsers = $totalUsers SampleAccounts = @($inactiveUsers | Select-Object -First 20 | ForEach-Object { @{ SamAccountName = $_.SamAccountName DN = $_.DN LastLogon = $_.LastLogon Enabled = $_.Enabled MemberOf = @($_.MemberOf).Count } }) } } # -- ADSTALE-002: Inactive Computer Accounts --------------------------------- function Test-ReconADSTALE002 { [CmdletBinding()] param([hashtable]$AuditData, [hashtable]$CheckDefinition) $stale = $AuditData.StaleObjects if (-not $stale) { return New-AuditFinding -CheckDefinition $CheckDefinition -Status 'SKIP' ` -CurrentValue 'Stale object data not available' } $inactiveComputers = @($stale.InactiveComputers) if ($inactiveComputers.Count -eq 0 -or ($inactiveComputers.Count -eq 1 -and $null -eq $inactiveComputers[0])) { $totalComputers = if ($stale.ContainsKey('TotalComputers')) { $stale.TotalComputers } else { 'unknown' } return New-AuditFinding -CheckDefinition $CheckDefinition -Status 'PASS' ` -CurrentValue "No inactive computer accounts found ($totalComputers total computer(s))" ` -Details @{ TotalComputers = $totalComputers } } $sampleList = @($inactiveComputers | Select-Object -First 10 | ForEach-Object { $name = if ($_.SamAccountName) { $_.SamAccountName } else { $_.Name } $os = if ($_.OperatingSystem) { " ($($_.OperatingSystem))" } else { '' } "$name$os" }) $sampleText = $sampleList -join ', ' if ($inactiveComputers.Count -gt 10) { $sampleText += '...' } $totalComputers = if ($stale.ContainsKey('TotalComputers')) { $stale.TotalComputers } else { 0 } $currentValue = "$($inactiveComputers.Count) inactive computer account(s) found: $sampleText" return New-AuditFinding -CheckDefinition $CheckDefinition -Status 'FAIL' ` -CurrentValue $currentValue ` -Details @{ InactiveCount = $inactiveComputers.Count TotalComputers = $totalComputers SampleAccounts = @($inactiveComputers | Select-Object -First 20 | ForEach-Object { @{ SamAccountName = if ($_.SamAccountName) { $_.SamAccountName } else { $_.Name } DN = $_.DN LastLogon = $_.LastLogon OperatingSystem = $_.OperatingSystem Enabled = $_.Enabled } }) } } # -- ADSTALE-003: Disabled Accounts with Group Memberships ------------------- function Test-ReconADSTALE003 { [CmdletBinding()] param([hashtable]$AuditData, [hashtable]$CheckDefinition) $stale = $AuditData.StaleObjects if (-not $stale) { return New-AuditFinding -CheckDefinition $CheckDefinition -Status 'SKIP' ` -CurrentValue 'Stale object data not available' } $disabledWithGroups = @($stale.DisabledWithGroups) if ($disabledWithGroups.Count -eq 0 -or ($disabledWithGroups.Count -eq 1 -and $null -eq $disabledWithGroups[0])) { return New-AuditFinding -CheckDefinition $CheckDefinition -Status 'PASS' ` -CurrentValue 'No disabled accounts with residual group memberships found' ` -Details @{ TotalDisabled = if ($stale.ContainsKey('TotalDisabled')) { $stale.TotalDisabled } else { 0 } } } # Identify accounts with the most group memberships $sorted = @($disabledWithGroups | Sort-Object { $_.GroupCount } -Descending) $sampleList = @($sorted | Select-Object -First 10 | ForEach-Object { "$($_.SamAccountName) ($($_.GroupCount) group(s))" }) $sampleText = $sampleList -join ', ' if ($disabledWithGroups.Count -gt 10) { $sampleText += '...' } $totalGroups = 0 foreach ($acct in $disabledWithGroups) { $totalGroups += [int]$acct.GroupCount } $currentValue = "$($disabledWithGroups.Count) disabled account(s) retain $totalGroups group membership(s): $sampleText" return New-AuditFinding -CheckDefinition $CheckDefinition -Status 'WARN' ` -CurrentValue $currentValue ` -Details @{ AffectedCount = $disabledWithGroups.Count TotalMemberships = $totalGroups SampleAccounts = @($sorted | Select-Object -First 20 | ForEach-Object { @{ SamAccountName = $_.SamAccountName DN = $_.DN GroupCount = $_.GroupCount Groups = @($_.Groups | Select-Object -First 5) } }) } } # -- ADSTALE-004: Expired Passwords Not Disabled ----------------------------- function Test-ReconADSTALE004 { [CmdletBinding()] param([hashtable]$AuditData, [hashtable]$CheckDefinition) $stale = $AuditData.StaleObjects if (-not $stale) { return New-AuditFinding -CheckDefinition $CheckDefinition -Status 'SKIP' ` -CurrentValue 'Stale object data not available' } $expiredNotDisabled = @($stale.ExpiredNotDisabled) if ($expiredNotDisabled.Count -eq 0 -or ($expiredNotDisabled.Count -eq 1 -and $null -eq $expiredNotDisabled[0])) { return New-AuditFinding -CheckDefinition $CheckDefinition -Status 'PASS' ` -CurrentValue 'No enabled accounts with expired passwords found' ` -Details @{ TotalUsers = if ($stale.ContainsKey('TotalUsers')) { $stale.TotalUsers } else { 0 } } } $sampleList = @($expiredNotDisabled | Select-Object -First 10 | ForEach-Object { $_.SamAccountName }) $sampleText = $sampleList -join ', ' if ($expiredNotDisabled.Count -gt 10) { $sampleText += '...' } $currentValue = "$($expiredNotDisabled.Count) enabled account(s) have expired passwords: $sampleText" return New-AuditFinding -CheckDefinition $CheckDefinition -Status 'WARN' ` -CurrentValue $currentValue ` -Details @{ AffectedCount = $expiredNotDisabled.Count TotalUsers = if ($stale.ContainsKey('TotalUsers')) { $stale.TotalUsers } else { 0 } SampleAccounts = @($expiredNotDisabled | Select-Object -First 20 | ForEach-Object { @{ SamAccountName = $_.SamAccountName DN = $_.DN PwdLastSet = $_.PwdLastSet Enabled = $_.Enabled } }) } } # -- ADSTALE-005: Obsolete OS Computers ------------------------------------- function Test-ReconADSTALE005 { [CmdletBinding()] param([hashtable]$AuditData, [hashtable]$CheckDefinition) $stale = $AuditData.StaleObjects if (-not $stale) { return New-AuditFinding -CheckDefinition $CheckDefinition -Status 'SKIP' ` -CurrentValue 'Stale object data not available' } $obsoleteComputers = @($stale.ObsoleteOSComputers) if ($obsoleteComputers.Count -eq 0 -or ($obsoleteComputers.Count -eq 1 -and $null -eq $obsoleteComputers[0])) { return New-AuditFinding -CheckDefinition $CheckDefinition -Status 'PASS' ` -CurrentValue 'No computers running obsolete operating systems found' ` -Details @{ TotalComputers = if ($stale.ContainsKey('TotalComputers')) { $stale.TotalComputers } else { 0 } } } # Group by OS $osCounts = @{} foreach ($comp in $obsoleteComputers) { $os = if ($comp.OperatingSystem) { $comp.OperatingSystem } else { 'Unknown' } if (-not $osCounts.ContainsKey($os)) { $osCounts[$os] = 0 } $osCounts[$os]++ } $osBreakdown = @($osCounts.GetEnumerator() | Sort-Object Value -Descending | ForEach-Object { "$($_.Value) x $($_.Key)" }) $currentValue = "$($obsoleteComputers.Count) computer(s) running obsolete operating systems: $($osBreakdown -join '; ')" return New-AuditFinding -CheckDefinition $CheckDefinition -Status 'FAIL' ` -CurrentValue $currentValue ` -Details @{ ObsoleteCount = $obsoleteComputers.Count TotalComputers = if ($stale.ContainsKey('TotalComputers')) { $stale.TotalComputers } else { 0 } OSBreakdown = $osCounts SampleComputers = @($obsoleteComputers | Select-Object -First 20 | ForEach-Object { @{ SamAccountName = if ($_.SamAccountName) { $_.SamAccountName } else { $_.Name } DN = $_.DN OperatingSystem = $_.OperatingSystem OSVersion = $_.OSVersion LastLogon = $_.LastLogon Enabled = $_.Enabled } }) } } # -- ADSTALE-006: Unsupported OS Versions ------------------------------------ function Test-ReconADSTALE006 { [CmdletBinding()] param([hashtable]$AuditData, [hashtable]$CheckDefinition) $stale = $AuditData.StaleObjects if (-not $stale) { return New-AuditFinding -CheckDefinition $CheckDefinition -Status 'SKIP' ` -CurrentValue 'Stale object data not available' } $unsupportedComputers = @($stale.UnsupportedOSComputers) if ($unsupportedComputers.Count -eq 0 -or ($unsupportedComputers.Count -eq 1 -and $null -eq $unsupportedComputers[0])) { return New-AuditFinding -CheckDefinition $CheckDefinition -Status 'PASS' ` -CurrentValue 'No computers running unsupported operating systems found' ` -Details @{ TotalComputers = if ($stale.ContainsKey('TotalComputers')) { $stale.TotalComputers } else { 0 } } } # Group by OS $osCounts = @{} foreach ($comp in $unsupportedComputers) { $os = if ($comp.OperatingSystem) { $comp.OperatingSystem } else { 'Unknown' } if (-not $osCounts.ContainsKey($os)) { $osCounts[$os] = 0 } $osCounts[$os]++ } $osBreakdown = @($osCounts.GetEnumerator() | Sort-Object Value -Descending | ForEach-Object { "$($_.Value) x $($_.Key)" }) $currentValue = "$($unsupportedComputers.Count) computer(s) running unsupported operating systems: $($osBreakdown -join '; ')" return New-AuditFinding -CheckDefinition $CheckDefinition -Status 'FAIL' ` -CurrentValue $currentValue ` -Details @{ UnsupportedCount = $unsupportedComputers.Count TotalComputers = if ($stale.ContainsKey('TotalComputers')) { $stale.TotalComputers } else { 0 } OSBreakdown = $osCounts SampleComputers = @($unsupportedComputers | Select-Object -First 20 | ForEach-Object { @{ SamAccountName = if ($_.SamAccountName) { $_.SamAccountName } else { $_.Name } DN = $_.DN OperatingSystem = $_.OperatingSystem OSVersion = $_.OSVersion LastLogon = $_.LastLogon Enabled = $_.Enabled } }) } } # -- ADSTALE-007: Orphaned Foreign Security Principals ----------------------- function Test-ReconADSTALE007 { [CmdletBinding()] param([hashtable]$AuditData, [hashtable]$CheckDefinition) $stale = $AuditData.StaleObjects if (-not $stale) { return New-AuditFinding -CheckDefinition $CheckDefinition -Status 'SKIP' ` -CurrentValue 'Stale object data not available' } $orphanedFSPs = @($stale.OrphanedFSPs) if ($orphanedFSPs.Count -eq 0 -or ($orphanedFSPs.Count -eq 1 -and $null -eq $orphanedFSPs[0])) { return New-AuditFinding -CheckDefinition $CheckDefinition -Status 'PASS' ` -CurrentValue 'No orphaned Foreign Security Principals found' } $sampleList = @($orphanedFSPs | Select-Object -First 10 | ForEach-Object { $_.SID }) $sampleText = $sampleList -join ', ' if ($orphanedFSPs.Count -gt 10) { $sampleText += '...' } $currentValue = "$($orphanedFSPs.Count) orphaned Foreign Security Principal(s) with unresolvable SIDs: $sampleText" return New-AuditFinding -CheckDefinition $CheckDefinition -Status 'WARN' ` -CurrentValue $currentValue ` -Details @{ OrphanedCount = $orphanedFSPs.Count SampleFSPs = @($orphanedFSPs | Select-Object -First 20 | ForEach-Object { @{ SID = $_.SID DN = $_.DN } }) } } # -- ADSTALE-008: Orphaned SID History ---------------------------------------- function Test-ReconADSTALE008 { [CmdletBinding()] param([hashtable]$AuditData, [hashtable]$CheckDefinition) $stale = $AuditData.StaleObjects if (-not $stale) { return New-AuditFinding -CheckDefinition $CheckDefinition -Status 'SKIP' ` -CurrentValue 'Stale object data not available' } $orphanedSIDHistory = @($stale.OrphanedSIDHistory) if ($orphanedSIDHistory.Count -eq 0 -or ($orphanedSIDHistory.Count -eq 1 -and $null -eq $orphanedSIDHistory[0])) { return New-AuditFinding -CheckDefinition $CheckDefinition -Status 'PASS' ` -CurrentValue 'No orphaned SID History entries found' } $totalOrphanedSIDs = 0 foreach ($entry in $orphanedSIDHistory) { $totalOrphanedSIDs += @($entry.OrphanedSIDs).Count } $sampleList = @($orphanedSIDHistory | Select-Object -First 10 | ForEach-Object { "$($_.SamAccountName) ($(@($_.OrphanedSIDs).Count) orphaned SID(s))" }) $sampleText = $sampleList -join ', ' if ($orphanedSIDHistory.Count -gt 10) { $sampleText += '...' } $currentValue = "$($orphanedSIDHistory.Count) object(s) have $totalOrphanedSIDs orphaned SID History entries: $sampleText" return New-AuditFinding -CheckDefinition $CheckDefinition -Status 'WARN' ` -CurrentValue $currentValue ` -Details @{ AffectedObjects = $orphanedSIDHistory.Count TotalOrphanedSIDs = $totalOrphanedSIDs SampleEntries = @($orphanedSIDHistory | Select-Object -First 20 | ForEach-Object { @{ SamAccountName = $_.SamAccountName DN = $_.DN OrphanedSIDs = @($_.OrphanedSIDs) TotalSIDHistory = $_.TotalSIDHistory } }) } } # -- ADSTALE-009: Abandoned OUs ----------------------------------------------- function Test-ReconADSTALE009 { [CmdletBinding()] param([hashtable]$AuditData, [hashtable]$CheckDefinition) $stale = $AuditData.StaleObjects if (-not $stale) { return New-AuditFinding -CheckDefinition $CheckDefinition -Status 'SKIP' ` -CurrentValue 'Stale object data not available' } $abandonedOUs = @($stale.AbandonedOUs) if ($abandonedOUs.Count -eq 0 -or ($abandonedOUs.Count -eq 1 -and $null -eq $abandonedOUs[0])) { return New-AuditFinding -CheckDefinition $CheckDefinition -Status 'PASS' ` -CurrentValue 'No abandoned (empty) OUs found' } $sampleList = @($abandonedOUs | Select-Object -First 10 | ForEach-Object { if ($_.Name) { $_.Name } else { $_.DN } }) $sampleText = $sampleList -join ', ' if ($abandonedOUs.Count -gt 10) { $sampleText += '...' } $currentValue = "$($abandonedOUs.Count) empty OU(s) found: $sampleText" return New-AuditFinding -CheckDefinition $CheckDefinition -Status 'PASS' ` -CurrentValue $currentValue ` -Details @{ AbandonedCount = $abandonedOUs.Count SampleOUs = @($abandonedOUs | Select-Object -First 20 | ForEach-Object { @{ DN = $_.DN Name = $_.Name Description = $_.Description WhenCreated = $_.WhenCreated } }) } } # -- ADSTALE-010: Printer Objects --------------------------------------------- function Test-ReconADSTALE010 { [CmdletBinding()] param([hashtable]$AuditData, [hashtable]$CheckDefinition) $stale = $AuditData.StaleObjects if (-not $stale) { return New-AuditFinding -CheckDefinition $CheckDefinition -Status 'SKIP' ` -CurrentValue 'Stale object data not available' } $printerObjects = @($stale.PrinterObjects) if ($printerObjects.Count -eq 0 -or ($printerObjects.Count -eq 1 -and $null -eq $printerObjects[0])) { return New-AuditFinding -CheckDefinition $CheckDefinition -Status 'PASS' ` -CurrentValue 'No printer objects published in Active Directory' } # Group by server $serverCounts = @{} foreach ($printer in $printerObjects) { $server = if ($printer.ServerName) { $printer.ServerName } else { '(unknown)' } if (-not $serverCounts.ContainsKey($server)) { $serverCounts[$server] = 0 } $serverCounts[$server]++ } $serverBreakdown = @($serverCounts.GetEnumerator() | Sort-Object Value -Descending | ForEach-Object { "$($_.Value) on $($_.Key)" }) $currentValue = "$($printerObjects.Count) printer object(s) published in AD: $($serverBreakdown -join '; ')" return New-AuditFinding -CheckDefinition $CheckDefinition -Status 'PASS' ` -CurrentValue $currentValue ` -Details @{ PrinterCount = $printerObjects.Count ServerBreakdown = $serverCounts SamplePrinters = @($printerObjects | Select-Object -First 20 | ForEach-Object { @{ Name = $_.Name DN = $_.DN ServerName = $_.ServerName UNCName = $_.UNCName DriverName = $_.DriverName WhenCreated = $_.WhenCreated } }) } } # -- ADSTALE-011: DNS Record Staleness ---------------------------------------- function Test-ReconADSTALE011 { [CmdletBinding()] param([hashtable]$AuditData, [hashtable]$CheckDefinition) $stale = $AuditData.StaleObjects if (-not $stale) { return New-AuditFinding -CheckDefinition $CheckDefinition -Status 'SKIP' ` -CurrentValue 'Stale object data not available' } $staleDNS = @($stale.StaleDNSRecords) if ($staleDNS.Count -eq 0 -or ($staleDNS.Count -eq 1 -and $null -eq $staleDNS[0])) { return New-AuditFinding -CheckDefinition $CheckDefinition -Status 'PASS' ` -CurrentValue 'No stale DNS records found in AD-integrated zones' } $tombstonedCount = @($staleDNS | Where-Object { $_.Tombstoned -eq $true }).Count $sampleList = @($staleDNS | Select-Object -First 10 | ForEach-Object { $_.Name }) $sampleText = $sampleList -join ', ' if ($staleDNS.Count -gt 10) { $sampleText += '...' } $currentValue = "$($staleDNS.Count) stale DNS record(s) found" if ($tombstonedCount -gt 0) { $currentValue += " ($tombstonedCount tombstoned)" } $currentValue += ": $sampleText" return New-AuditFinding -CheckDefinition $CheckDefinition -Status 'WARN' ` -CurrentValue $currentValue ` -Details @{ StaleCount = $staleDNS.Count TombstonedCount = $tombstonedCount SampleRecords = @($staleDNS | Select-Object -First 20 | ForEach-Object { @{ Name = $_.Name DN = $_.DN WhenChanged = $_.WhenChanged Tombstoned = $_.Tombstoned } }) } } |