Public/Get-StaleGPOs.ps1
|
function Get-StaleGPOs { <# .SYNOPSIS Finds Group Policy Objects that have not been modified within a given threshold. .DESCRIPTION Scans every GPO in the domain and compares its ModificationTime against a configurable staleness threshold (default: 365 days). Stale GPOs often represent policies created for a project that ended years ago, test policies that were never cleaned up, or settings that have been superseded by newer GPOs. By default, only GPOs that are BOTH stale AND unlinked are returned (the safest cleanup candidates). Use -IncludeLinked to also show stale GPOs that are still linked to OUs. This function is read-only and never modifies or deletes GPOs. .PARAMETER DaysStale Number of days since last modification to consider a GPO stale. Must be between 30 and 3650. Default is 365. .PARAMETER IncludeLinked Include stale GPOs that are still linked to OUs. By default, only stale GPOs that are also unlinked are returned. .EXAMPLE Get-StaleGPOs Returns GPOs not modified in over 365 days that are also unlinked. .EXAMPLE Get-StaleGPOs -DaysStale 180 -IncludeLinked Returns all GPOs not modified in 180 days, whether linked or not. .OUTPUTS [PSCustomObject] with properties: DisplayName, Id, ModificationTime, DaysSinceModified, IsLinked, Finding #> [CmdletBinding()] param( [Parameter()] [ValidateRange(30, 3650)] [int]$DaysStale = 365, [Parameter()] [switch]$IncludeLinked ) begin { Write-Verbose "Get-StaleGPOs: Starting scan for GPOs not modified in $DaysStale+ days" if (-not (Get-Module -ListAvailable -Name GroupPolicy)) { throw 'The GroupPolicy RSAT module is required but not installed. Install RSAT tools and try again.' } Import-Module GroupPolicy -ErrorAction Stop -Verbose:$false } process { try { $AllGPOs = @(Get-GPO -All -ErrorAction Stop) Write-Verbose "Get-StaleGPOs: Retrieved $($AllGPOs.Count) GPOs from domain" } catch { Write-Error "Failed to retrieve GPOs: $_" return } $CutoffDate = (Get-Date).AddDays(-$DaysStale) $StaleCount = 0 foreach ($GPO in $AllGPOs) { # Skip GPOs modified within the threshold if ($GPO.ModificationTime -gt $CutoffDate) { continue } $DaysSinceModified = [math]::Round(((Get-Date) - $GPO.ModificationTime).TotalDays, 0) Write-Verbose "Get-StaleGPOs: '$($GPO.DisplayName)' last modified $DaysSinceModified days ago" # Determine link status via XML report $IsLinked = $false try { [xml]$Report = Get-GPOReport -Guid $GPO.Id -ReportType Xml -ErrorAction Stop $Links = $Report.GPO.LinksTo if ($null -ne $Links -and @($Links).Count -gt 0) { $IsLinked = $true } } catch { Write-Warning "Get-StaleGPOs: Could not determine link status for '$($GPO.DisplayName)': $_" } # By default, only return stale + unlinked GPOs if ($IsLinked -and -not $IncludeLinked) { continue } $Finding = if ($IsLinked) { 'STALE_LINKED' } else { 'STALE_UNLINKED' } $StaleCount++ [PSCustomObject]@{ DisplayName = $GPO.DisplayName Id = $GPO.Id.ToString() ModificationTime = $GPO.ModificationTime DaysSinceModified = $DaysSinceModified IsLinked = $IsLinked Finding = $Finding } } Write-Verbose "Get-StaleGPOs: Scan complete. Found $StaleCount stale GPO(s) out of $($AllGPOs.Count) total." } } |