ActiveDirectory/Get-ADReplicationReport.ps1

<#
.SYNOPSIS
    Reports Active Directory replication health, partner status, and site link topology.
.DESCRIPTION
    Collects replication partner metadata, replication failure history, and site link
    configuration from Active Directory. Identifies DCs with replication lag or failures,
    site links with non-standard schedules, and missing replication connections.
 
    Designed for IT consultants performing AD assessments on SMB environments
    (10-500 users). All operations are read-only.
 
    Requires the ActiveDirectory module (available via RSAT or on domain controllers).
.PARAMETER DomainController
    One or more specific domain controller hostnames to check replication for.
    If not specified, all DCs are discovered and checked.
.PARAMETER OutputPath
    Optional path to export results as CSV. If not specified, results are returned
    to the pipeline.
.EXAMPLE
    PS> .\ActiveDirectory\Get-ADReplicationReport.ps1
 
    Reports replication status for all domain controllers.
.EXAMPLE
    PS> .\ActiveDirectory\Get-ADReplicationReport.ps1 -DomainController 'DC01'
 
    Reports replication status for a specific domain controller.
.EXAMPLE
    PS> .\ActiveDirectory\Get-ADReplicationReport.ps1 -OutputPath '.\replication.csv'
 
    Exports the replication report to CSV.
#>

[CmdletBinding()]
param(
    [Parameter()]
    [string[]]$DomainController,

    [Parameter()]
    [ValidateNotNullOrEmpty()]
    [string]$OutputPath
)

$ErrorActionPreference = 'Stop'

# ------------------------------------------------------------------
# Verify ActiveDirectory module is available
# ------------------------------------------------------------------
if (-not (Get-Module -Name ActiveDirectory -ListAvailable)) {
    Write-Error "The ActiveDirectory module is not installed. Install RSAT or run from a domain controller."
    return
}

Import-Module -Name ActiveDirectory -ErrorAction Stop

$report = [System.Collections.Generic.List[PSCustomObject]]::new()

# ------------------------------------------------------------------
# Discover target domain controllers
# ------------------------------------------------------------------
try {
    Write-Verbose "Discovering domain controllers..."
    if ($DomainController) {
        $dcList = foreach ($dc in $DomainController) {
            try {
                Get-ADDomainController -Identity $dc
            }
            catch {
                Write-Warning "Could not find domain controller '$dc': $_"
            }
        }
        $dcList = @($dcList | Where-Object { $_ })
    }
    else {
        $dcList = @(Get-ADDomainController -Filter *)
    }

    if ($dcList.Count -eq 0) {
        Write-Error "No domain controllers found."
        return
    }

    Write-Verbose "Found $($dcList.Count) domain controller(s)"
}
catch {
    Write-Error "Failed to discover domain controllers: $_"
    return
}

# ------------------------------------------------------------------
# Collect replication partner metadata for each DC
# ------------------------------------------------------------------
foreach ($dc in $dcList) {
    $dcName = $dc.HostName

    try {
        Write-Verbose "Querying replication partners for $dcName..."
        $partners = @(Get-ADReplicationPartnerMetadata -Target $dcName -ErrorAction Stop)

        if ($partners.Count -eq 0) {
            $report.Add([PSCustomObject]@{
                RecordType            = 'ReplicationPartner'
                DomainController      = $dcName
                Partner               = 'N/A'
                PartnerType           = 'N/A'
                LastReplicationAttempt = $null
                LastReplicationSuccess = $null
                LastReplicationResult  = 0
                ConsecutiveFailures   = 0
                ReplicationStatus     = 'No Partners'
                Detail                = 'No replication partners found for this DC'
            })
            continue
        }

        foreach ($partner in $partners) {
            $lastAttempt = $partner.LastReplicationAttempt
            $lastSuccess = $partner.LastReplicationSuccess
            $lastResult = $partner.LastReplicationResult
            $failures = $partner.ConsecutiveReplicationFailures
            $partnerName = $partner.Partner

            # Determine replication health status
            $replStatus = if ($lastResult -eq 0 -and $failures -eq 0) {
                'Healthy'
            }
            elseif ($failures -gt 0 -and $failures -le 3) {
                'Warning'
            }
            else {
                'Error'
            }

            # Calculate replication lag if we have timestamps
            $lagDetail = ''
            if ($lastSuccess -and $lastAttempt) {
                $lag = $lastAttempt - $lastSuccess
                if ($lag.TotalHours -gt 24) {
                    $lagDetail = "ReplicationLag=$([math]::Round($lag.TotalHours, 1))h"
                    $replStatus = 'Error'
                }
                elseif ($lag.TotalHours -gt 1) {
                    $lagDetail = "ReplicationLag=$([math]::Round($lag.TotalMinutes, 0))min"
                    if ($replStatus -eq 'Healthy') { $replStatus = 'Warning' }
                }
            }

            $detail = @()
            if ($lagDetail) { $detail += $lagDetail }
            if ($failures -gt 0) { $detail += "ConsecutiveFailures=$failures" }
            if ($lastResult -ne 0) { $detail += "LastResultCode=$lastResult" }

            $report.Add([PSCustomObject]@{
                RecordType            = 'ReplicationPartner'
                DomainController      = $dcName
                Partner               = $partnerName
                PartnerType           = $partner.PartnerType
                LastReplicationAttempt = $lastAttempt
                LastReplicationSuccess = $lastSuccess
                LastReplicationResult  = $lastResult
                ConsecutiveFailures   = $failures
                ReplicationStatus     = $replStatus
                Detail                = ($detail -join '; ')
            })
        }
    }
    catch {
        Write-Warning "Failed to query replication partners for $dcName`: $_"
        $report.Add([PSCustomObject]@{
            RecordType            = 'ReplicationPartner'
            DomainController      = $dcName
            Partner               = 'N/A'
            PartnerType           = 'N/A'
            LastReplicationAttempt = $null
            LastReplicationSuccess = $null
            LastReplicationResult  = -1
            ConsecutiveFailures   = -1
            ReplicationStatus     = 'QueryFailed'
            Detail                = "Failed to query: $_"
        })
    }
}

# ------------------------------------------------------------------
# Collect replication failure history
# ------------------------------------------------------------------
foreach ($dc in $dcList) {
    $dcName = $dc.HostName

    try {
        Write-Verbose "Querying replication failures for $dcName..."
        $failures = @(Get-ADReplicationFailure -Target $dcName -ErrorAction Stop)

        foreach ($failure in $failures) {
            $report.Add([PSCustomObject]@{
                RecordType            = 'ReplicationFailure'
                DomainController      = $dcName
                Partner               = $failure.Partner
                PartnerType           = 'N/A'
                LastReplicationAttempt = $failure.FirstFailureTime
                LastReplicationSuccess = $null
                LastReplicationResult  = $failure.LastError
                ConsecutiveFailures   = $failure.FailureCount
                ReplicationStatus     = 'FailureRecord'
                Detail                = "FailureType=$($failure.FailureType); FirstFailure=$($failure.FirstFailureTime)"
            })
        }
    }
    catch {
        Write-Warning "Failed to query replication failures for $dcName`: $_"
    }
}

# ------------------------------------------------------------------
# Collect site link topology
# ------------------------------------------------------------------
try {
    Write-Verbose "Querying replication site links..."
    $siteLinks = @(Get-ADReplicationSiteLink -Filter * -ErrorAction Stop)

    foreach ($link in $siteLinks) {
        $sites = if ($link.SitesIncluded) {
            ($link.SitesIncluded | ForEach-Object {
                # Extract CN from distinguished name
                if ($_ -match '^CN=([^,]+),') { $Matches[1] } else { $_ }
            }) -join '; '
        }
        else { '' }

        $detail = @(
            "Sites=$sites"
            "Cost=$($link.Cost)"
            "ReplicationFrequency=$($link.ReplicationFrequencyInMinutes)min"
        ) -join '; '

        $report.Add([PSCustomObject]@{
            RecordType            = 'SiteLink'
            DomainController      = 'N/A'
            Partner               = $link.Name
            PartnerType           = 'N/A'
            LastReplicationAttempt = $null
            LastReplicationSuccess = $null
            LastReplicationResult  = 0
            ConsecutiveFailures   = 0
            ReplicationStatus     = 'Configured'
            Detail                = $detail
        })
    }

    Write-Verbose "Found $($siteLinks.Count) site link(s)"
}
catch {
    Write-Warning "Failed to query site links: $_"
}

# ------------------------------------------------------------------
# Export or return
# ------------------------------------------------------------------
$results = @($report)

Write-Verbose "Collected $($results.Count) replication records"

if ($OutputPath) {
    $results | Export-Csv -Path $OutputPath -NoTypeInformation -Encoding UTF8
    Write-Output "Exported $($results.Count) replication records to $OutputPath"
}
else {
    Write-Output $results
}