Functions/Test-SysvolReplicationHealth.ps1

<#
.SYNOPSIS
    Checks SYSVOL replication health via DFSR (or legacy FRS) across domain controllers.

.DESCRIPTION
    This function checks the health of SYSVOL replication, which is responsible for
    keeping Group Policy Objects and logon scripts consistent across all domain
    controllers. It reports on DFSR replication backlog per DC, detects whether the
    domain is still using legacy FRS instead of DFSR, and flags DCs where SYSVOL
    content appears out of sync. SYSVOL drift is a common, easy-to-miss cause of
    "this Group Policy setting applies on some machines but not others" symptoms.

.PARAMETER OutputPath
    Optional path to export an HTML report. If omitted, results are only shown in console.

.PARAMETER MaxBacklogCount
    Threshold for flagging a DFSR backlog as excessive. Default is 10 pending files.

.EXAMPLE
    Test-SysvolReplicationHealth

.EXAMPLE
    Test-SysvolReplicationHealth -OutputPath "C:\Reports\SysvolHealth.html" -MaxBacklogCount 25

.NOTES
    Author: Sandeep Kumar Reddy Lingampalli
    GitHub: https://github.com/sandeep6891/AD-HealthCheck-Toolkit
    Requires: ActiveDirectory PowerShell module (RSAT-AD-PowerShell)
    Requires: DFSR management tools (dfsrdiag.exe, present on domain controllers / RSAT-DFS-Mgmt-Con)
    Credit: Feature scoped based on community feedback — see Issue #1 on GitHub
#>


function Test-SysvolReplicationHealth {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $false)]
        [string]$OutputPath,

        [Parameter(Mandatory = $false)]
        [int]$MaxBacklogCount = 10
    )

    if (-not (Get-Module -ListAvailable -Name ActiveDirectory)) {
        Write-Error "ActiveDirectory module not found. Install RSAT-AD-PowerShell and try again."
        return
    }
    Import-Module ActiveDirectory -ErrorAction Stop

    $domain = Get-ADDomain
    $domainControllers = Get-ADDomainController -Filter *

    if (-not $domainControllers) {
        Write-Error "No domain controllers found. Check connectivity and permissions."
        return
    }

    $results = @()

    # --- Check 1: Detect legacy FRS vs DFSR migration state ---
    Write-Verbose "Checking SYSVOL replication migration state (FRS vs DFSR)..."
    try {
        $ntfrsMigration = Get-ADObject -Identity "CN=DFSR-GlobalSettings,CN=System,$($domain.DistinguishedName)" -Properties msDFSR-Flags -ErrorAction Stop
        $migrationState = "DFSR (migrated)"
        $migrationStatus = "OK"
        $migrationDetail = "Domain is using DFSR for SYSVOL replication."
    }
    catch {
        $migrationState = "Unknown / Possibly legacy FRS"
        $migrationStatus = "REVIEW"
        $migrationDetail = "Could not confirm DFSR-GlobalSettings object. Domain may still be using legacy FRS, which Microsoft has deprecated."
    }

    $results += [PSCustomObject]@{
        CheckType = "Migration State"
        Target    = $domain.DNSRoot
        Status    = $migrationStatus
        Detail    = "$migrationDetail (State: $migrationState)"
    }

    # --- Check 2: DFSR replication backlog per DC ---
    Write-Verbose "Checking DFSR replication backlog per domain controller..."
    foreach ($dc in $domainControllers) {
        try {
            $backlogOutput = dfsrdiag backlog /rgname:"Domain System Volume" /rfname:"SYSVOL Share" /sendingmember:$($dc.HostName) /receivingmember:$($dc.HostName) 2>&1

            $backlogCount = 0
            if ($backlogOutput -match "Backlog File count:\s*(\d+)") {
                $backlogCount = [int]$Matches[1]
            }

            $status = "OK"
            $detail = "Backlog: $backlogCount file(s)"

            if ($backlogCount -gt $MaxBacklogCount) {
                $status = "BACKLOG"
                $detail = "Backlog of $backlogCount file(s) exceeds threshold ($MaxBacklogCount). May indicate replication lag or failure."
            }

            $results += [PSCustomObject]@{
                CheckType = "DFSR Backlog"
                Target    = $dc.HostName
                Status    = $status
                Detail    = $detail
            }
        }
        catch {
            $results += [PSCustomObject]@{
                CheckType = "DFSR Backlog"
                Target    = $dc.HostName
                Status    = "ERROR"
                Detail    = "Could not query DFSR backlog: $($_.Exception.Message)"
            }
        }
    }

    # --- Check 3: SYSVOL content consistency across DCs (file/folder count comparison) ---
    Write-Verbose "Checking SYSVOL content consistency across domain controllers..."
    $sysvolCounts = @()

    foreach ($dc in $domainControllers) {
        try {
            $sysvolPath = "\\$($dc.HostName)\SYSVOL\$($domain.DNSRoot)\Policies"
            $itemCount = (Get-ChildItem -Path $sysvolPath -Recurse -ErrorAction Stop -Force).Count

            $sysvolCounts += [PSCustomObject]@{
                DC    = $dc.HostName
                Count = $itemCount
            }
        }
        catch {
            $results += [PSCustomObject]@{
                CheckType = "SYSVOL Content"
                Target    = $dc.HostName
                Status    = "ERROR"
                Detail    = "Could not access SYSVOL path: $($_.Exception.Message)"
            }
        }
    }

    if ($sysvolCounts.Count -gt 1) {
        $baseline = ($sysvolCounts | Group-Object Count | Sort-Object Count -Descending | Select-Object -First 1).Name

        foreach ($entry in $sysvolCounts) {
            $status = "OK"
            $detail = "$($entry.Count) items under Policies (matches majority baseline)"

            if ($entry.Count -ne [int]$baseline) {
                $status = "MISMATCH"
                $detail = "$($entry.Count) items under Policies, but majority baseline is $baseline — possible SYSVOL drift."
            }

            $results += [PSCustomObject]@{
                CheckType = "SYSVOL Content"
                Target    = $entry.DC
                Status    = $status
                Detail    = $detail
            }
        }
    }

    # --- Console summary ---
    $issues = $results | Where-Object { $_.Status -in @("BACKLOG", "MISMATCH", "ERROR", "REVIEW") }

    Write-Host "`n=== SYSVOL Replication Health Summary ===" -ForegroundColor Cyan
    Write-Host "Domain: $($domain.DNSRoot)"
    Write-Host "Domain Controllers: $($domainControllers.Count)"
    Write-Host "Checks performed: $($results.Count)"
    Write-Host "Issues found: $($issues.Count)`n"

    if ($issues.Count -gt 0) {
        Write-Host "Issues detected:" -ForegroundColor Yellow
        $issues | Format-Table CheckType, Target, Status, Detail -AutoSize
    } else {
        Write-Host "No SYSVOL replication issues detected." -ForegroundColor Green
    }

    # --- Optional HTML export ---
    if ($OutputPath) {
        $htmlHeader = @"
<style>
    body { font-family: Segoe UI, Arial, sans-serif; }
    table { border-collapse: collapse; width: 100%; }
    th, td { border: 1px solid #ccc; padding: 6px 10px; text-align: left; }
    th { background-color: #2c3e50; color: white; }
    tr.OK { background-color: #eafaf1; }
    tr.BACKLOG, tr.MISMATCH, tr.ERROR { background-color: #fdecea; }
    tr.REVIEW { background-color: #fff8e1; }
</style>
"@

        $htmlBody = $results | ConvertTo-Html -Head $htmlHeader -Title "SYSVOL Replication Health Report - $(Get-Date)" -PreContent "<h2>SYSVOL Replication Health Report</h2><p>Domain: $($domain.DNSRoot)</p><p>Generated: $(Get-Date)</p>"
        $htmlBody | Out-File -FilePath $OutputPath -Encoding UTF8
        Write-Host "`nHTML report saved to: $OutputPath" -ForegroundColor Cyan
    }

    return $results
}