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: $_"
        }
    }
}