Public/Invoke-GPOHealthAudit.ps1
|
function Invoke-GPOHealthAudit { <# .SYNOPSIS Runs a comprehensive Group Policy health audit and generates an HTML dashboard. .DESCRIPTION Orchestrates all GPO health check functions (Get-UnlinkedGPOs, Get-EmptyGPOs, Get-GPOPermissionReport, Get-StaleGPOs), collects their findings, and compiles them into a single HTML dashboard report. The report uses a dark theme with purple accents and includes summary cards, per-section finding tables, and status indicators. This function is read-only and never modifies or deletes GPOs. .PARAMETER OutputPath Directory where the HTML report will be written. The file is named GPO-HealthAudit_<domain>_<date>.html. Defaults to the current directory. .PARAMETER DaysStale Number of days since last modification to flag a GPO as stale. Must be between 30 and 3650. Default is 365. .PARAMETER IncludeDisabled Include GPOs with disabled-but-configured sections in the Empty GPOs check. .EXAMPLE Invoke-GPOHealthAudit Runs the full audit and writes the HTML report to the current directory. .EXAMPLE Invoke-GPOHealthAudit -OutputPath C:\Reports -DaysStale 180 -IncludeDisabled Runs the audit with a 180-day staleness threshold, includes disabled sections, and saves the report to C:\Reports. .OUTPUTS [System.IO.FileInfo] The generated HTML report file. #> [CmdletBinding()] param( [Parameter()] [ValidateScript({ if (-not (Test-Path -Path $_ -PathType Container)) { throw "OutputPath '$_' does not exist or is not a directory." } $true })] [string]$OutputPath = (Get-Location).Path, [Parameter()] [ValidateRange(30, 3650)] [int]$DaysStale = 365, [Parameter()] [switch]$IncludeDisabled ) begin { Write-Verbose 'Invoke-GPOHealthAudit: Starting comprehensive GPO health audit' $StartTime = Get-Date 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 { $Sections = [System.Collections.ArrayList]::new() $DomainName = try { (Get-ADDomain -ErrorAction Stop).DNSRoot } catch { $env:USERDNSDOMAIN } if ([string]::IsNullOrEmpty($DomainName)) { $DomainName = 'Unknown' } # ---- Section 1: Unlinked GPOs ---- Write-Verbose 'Invoke-GPOHealthAudit: Running unlinked GPO check...' try { $UnlinkedResults = @(Get-UnlinkedGPOs -Verbose:($PSBoundParameters['Verbose'] -eq $true)) $UnlinkedStatus = if ($UnlinkedResults.Count -gt 5) { 'CRITICAL' } elseif ($UnlinkedResults.Count -gt 0) { 'WARNING' } else { 'OK' } [void]$Sections.Add(@{ Name = 'Unlinked GPOs' Summary = 'GPOs not linked to any OU, site, or domain. These consume SYSVOL space but never apply. Safe deletion candidates.' Findings = $UnlinkedResults Status = $UnlinkedStatus }) Write-Verbose "Invoke-GPOHealthAudit: Found $($UnlinkedResults.Count) unlinked GPO(s)" } catch { Write-Warning "Invoke-GPOHealthAudit: Unlinked GPO check failed: $_" [void]$Sections.Add(@{ Name = 'Unlinked GPOs' Summary = "Check failed: $_" Findings = @() Status = 'WARNING' }) } # ---- Section 2: Empty GPOs ---- Write-Verbose 'Invoke-GPOHealthAudit: Running empty GPO check...' try { $EmptyParams = @{} if ($IncludeDisabled) { $EmptyParams['IncludeDisabledSections'] = $true } $EmptyResults = @(Get-EmptyGPOs @EmptyParams -Verbose:($PSBoundParameters['Verbose'] -eq $true)) $EmptyStatus = if ($EmptyResults.Count -gt 5) { 'CRITICAL' } elseif ($EmptyResults.Count -gt 0) { 'WARNING' } else { 'OK' } [void]$Sections.Add(@{ Name = 'Empty GPOs' Summary = 'GPOs with no settings configured in either Computer or User Configuration. These add GPMC clutter with no policy effect.' Findings = $EmptyResults Status = $EmptyStatus }) Write-Verbose "Invoke-GPOHealthAudit: Found $($EmptyResults.Count) empty GPO(s)" } catch { Write-Warning "Invoke-GPOHealthAudit: Empty GPO check failed: $_" [void]$Sections.Add(@{ Name = 'Empty GPOs' Summary = "Check failed: $_" Findings = @() Status = 'WARNING' }) } # ---- Section 3: Permission Issues ---- Write-Verbose 'Invoke-GPOHealthAudit: Running GPO permission audit...' try { $PermResults = @(Get-GPOPermissionReport -Verbose:($PSBoundParameters['Verbose'] -eq $true)) $HasCriticalPerm = ($PermResults | Where-Object { $_.Finding -eq 'CRITICAL' }).Count -gt 0 $PermStatus = if ($HasCriticalPerm) { 'CRITICAL' } elseif ($PermResults.Count -gt 0) { 'WARNING' } else { 'OK' } [void]$Sections.Add(@{ Name = 'Permission Issues' Summary = 'GPOs with missing apply permissions, excessive edit rights, or broken delegation. Missing apply rights is the #1 reason GPOs silently fail.' Findings = $PermResults Status = $PermStatus }) Write-Verbose "Invoke-GPOHealthAudit: Found $($PermResults.Count) permission issue(s)" } catch { Write-Warning "Invoke-GPOHealthAudit: Permission audit failed: $_" [void]$Sections.Add(@{ Name = 'Permission Issues' Summary = "Check failed: $_" Findings = @() Status = 'WARNING' }) } # ---- Section 4: Stale GPOs ---- Write-Verbose "Invoke-GPOHealthAudit: Running stale GPO check ($DaysStale day threshold)..." try { $StaleResults = @(Get-StaleGPOs -DaysStale $DaysStale -IncludeLinked -Verbose:($PSBoundParameters['Verbose'] -eq $true)) $StaleStatus = if ($StaleResults.Count -gt 10) { 'CRITICAL' } elseif ($StaleResults.Count -gt 0) { 'WARNING' } else { 'OK' } [void]$Sections.Add(@{ Name = 'Stale GPOs' Summary = "GPOs not modified in $DaysStale+ days. Long-dormant policies may conflict with newer settings or reference obsolete configurations." Findings = $StaleResults Status = $StaleStatus }) Write-Verbose "Invoke-GPOHealthAudit: Found $($StaleResults.Count) stale GPO(s)" } catch { Write-Warning "Invoke-GPOHealthAudit: Stale GPO check failed: $_" [void]$Sections.Add(@{ Name = 'Stale GPOs' Summary = "Check failed: $_" Findings = @() Status = 'WARNING' }) } # ---- Generate HTML Dashboard ---- $DateStamp = Get-Date -Format 'yyyyMMdd_HHmmss' $SafeDomain = ($DomainName -replace '[^\w.-]', '_') $ReportFileName = "GPO-HealthAudit_${SafeDomain}_${DateStamp}.html" $ReportFullPath = Join-Path -Path $OutputPath -ChildPath $ReportFileName Write-Verbose "Invoke-GPOHealthAudit: Generating HTML dashboard at $ReportFullPath" try { $ReportFile = New-HtmlDashboard -Title 'GPO Health Audit Report' ` -Sections $Sections.ToArray() ` -OutputPath $ReportFullPath ` -DomainName $DomainName $Elapsed = (Get-Date) - $StartTime Write-Verbose "Invoke-GPOHealthAudit: Audit complete in $([math]::Round($Elapsed.TotalSeconds, 1)) seconds" Write-Verbose "Invoke-GPOHealthAudit: Report saved to $($ReportFile.FullName)" return $ReportFile } catch { Write-Error "Invoke-GPOHealthAudit: Failed to generate HTML dashboard: $_" } } } |