Emergency server help: get in touch

AD Health Check Report: dcdiag and repadmin PowerShell Script

Free PowerShell script that runs dcdiag, repadmin /replsummary, w32tm and SYSVOL DFSR checks on every DC and saves a colour-coded HTML report.

Version
1.0.0
Last updated
October 6, 2026
Language
PowerShell
Tested on
Run on 6 Oct 2026 on a Windows Server 2025 DC (build 26100.33438, Windows PowerShell 5.1) in our lab domain with a Windows 11 Pro member; syntax-checked with PowerShell 7.4.6 and PSScriptAnalyzer 1.25.
License
MIT
Pricing
Free

Short answer: run .\Invoke-ADHealthReport.ps1 as a Domain Admin. For every domain controller it runs dcdiag /q, checks the time source with w32tm and reads the SYSVOL DFSR state, then adds repadmin /replsummary and the FSMO role holders, and saves one colour-coded HTML report. Add -SmtpServer, -From and -To to e-mail it, or just schedule it and open the file each morning.

We ran this script on 6 October 2026 on a Windows Server 2025 Standard domain controller (build 26100.33438, September 2026 update, Windows PowerShell 5.1) and a Windows 11 Pro member PC in our lab domain contoso.com. The example output below is from that run; the lab domain is small and new, so your numbers will be bigger. It was also syntax-checked with PowerShell 7.4.6 and PSScriptAnalyzer 1.25.

What it does

The daily DC check most admins do by hand is the same handful of commands. This script runs them for all DCs, turns the results into a Pass / Warning / Fail table, and keeps the raw output underneath for when you need detail. It is read-only: it runs diagnostic commands and queries, nothing else.

CheckHowPass / Warning / Fail
dcdiagdcdiag /s:<DC> /q per DCQuiet mode prints only errors, so any output is a Fail; the text goes into the report
Time sourcew32tm /query /computer:<DC> /source“Local CMOS Clock” or “Free-running System Clock” is a Warning, and a Fail on the forest-root PDC emulator, which should sync from an external source
SYSVOL replicationCIM class DfsrReplicatedFolderInfo (namespace root\MicrosoftDfs), folder “SYSVOL Share”Pass only in state 4 (Normal); 5 (In Error) is a Fail; 0 to 3 and query failures are Warnings
Replicationrepadmin /replsummary onceFail if any DC shows failures in the fails/total column or the command fails
FSMO rolesGet-ADForest and Get-ADDomainListed per DC and in a separate table

Each DC row also shows site, operating system, global catalog and read-only status from Get-ADDomainController. If you want to know what each dcdiag test means and how to fix the usual failures, read dcdiag and repadmin health check: 7 critical tests explained.

Requirements

  • Windows PowerShell 5.1 or PowerShell 7 on Windows, with the ActiveDirectory module.
  • dcdiag.exe and repadmin.exe: present on every DC, and on management machines with the RSAT AD DS tools. w32tm.exe is built into Windows. The script stops with a clear message if one is missing (use -SkipDcdiag to run without dcdiag).
  • A Domain Admin or equivalent account: several dcdiag tests and the DFSR WMI namespace need administrative rights on the DCs.
  • WinRM (WS-Management) access to the DCs for the CIM query; it is on by default on Windows Server.
  • For e-mail: an SMTP relay that accepts mail from the machine running the script.

Download and first run

  1. Copy the script from the box on this page (or use the download button) and save it as C:\Scripts\Invoke-ADHealthReport.ps1.
  2. If you downloaded the file, remove the “downloaded from the internet” mark so the execution policy lets it run: Unblock-File C:\Scripts\Invoke-ADHealthReport.ps1.
  3. Run it on a domain-joined machine that has the ActiveDirectory module: a domain controller, a management server, or a Windows 11 PC with RSAT (see Install RSAT on Windows 11).
  4. Read the built-in help once, then run it with no options to see the result on screen before you export anything.
cd C:\Scripts
Get-Help .\Invoke-ADHealthReport.ps1 -Full
.\Invoke-ADHealthReport.ps1
.\Invoke-ADHealthReport.ps1 -OutputPath C:\Reports\ad-health.html

Options

ParameterWhat it doesDefault
-ServerDomain to checkCurrent domain
-DomainControllerCheck only these DCs (host names)All DCs
-OutputPathHTML file to write.\ADHealth-<domain>-<yyyyMMdd-HHmm>.html
-SkipDcdiagSkip dcdiag, the slowest checkOff
-SmtpServer, -From, -ToE-mail the report (all three needed)No e-mail
-Port, -UseSsl, -CredentialSMTP port, TLS and authentication25, off, none
-PassThruReturn the per-DC summary objectsOff

Usage examples

# All DCs, report in the current folder
.\Invoke-ADHealthReport.ps1

# Quick run without dcdiag, fixed file name
.\Invoke-ADHealthReport.ps1 -SkipDcdiag -OutputPath C:\Reports\ad-health.html

# Two DCs only
.\Invoke-ADHealthReport.ps1 -DomainController DC01.contoso.com, DC02.contoso.com

# E-mail through an internal relay
.\Invoke-ADHealthReport.ps1 -SmtpServer relay.contoso.com -From adreport@contoso.com -To admins@contoso.com

# Use the summary in your own monitoring
.\Invoke-ADHealthReport.ps1 -PassThru | Where-Object Overall -ne 'Pass'

About the e-mail option

E-mail uses Send-MailMessage. Microsoft marks that cmdlet as obsolete because it does not guarantee a secure connection to the SMTP server, and there is no direct replacement built into PowerShell. Use it only with an internal relay you trust, add -UseSsl if the relay supports it, or skip e-mail entirely: save the file to a share and open it, or let your monitoring system pick up the -PassThru objects. The subject line starts with PASS, WARNING or FAIL so mail rules can sort it.

What is in the report

The example output further down is from our lab run. The HTML file contains:

  1. A header with domain, forest, time and number of DCs checked.
  2. The domain controller table, one row per DC with these columns: DomainController, Site, OperatingSystem, GlobalCatalog, ReadOnly, FSMORoles, Dcdiag, TimeSource, TimeStatus, SysvolDfsr, SysvolStatus, Overall. Status cells are coloured green, amber or red.
  3. The FSMO role holders table (schema master, domain naming master, PDC emulator, RID master, infrastructure master).
  4. The replication status and the full repadmin /replsummary output.
  5. For each DC that failed dcdiag, the error text dcdiag printed.

-PassThru returns the same per-DC columns as objects.

Example output

Run with -OutputPath C:\srvs-lab\out\ad-health.html -PassThru on the single lab DC (3 seconds):

Report saved to C:\srvs-lab\out\ad-health.html

DomainController   Site                    OperatingSystem              GlobalCatalog ReadOnly FSMORoles
----------------   ----                    ---------------              ------------- -------- ---------
WinSrv.contoso.com Default-First-Site-Name Windows Server 2025 Standard          True    False Schema master, Domain naming master, PDC emulator, RID master, Infrastructure master

With one DC there is no replication partner, so the replication section of the HTML report is empty. On a real domain it lists each partner and its last result.

Schedule it

Run it every morning before the helpdesk opens. The task account needs Domain Admin-level rights for dcdiag, so a gMSA in Domain Admins is the usual choice; restrict which servers may use that gMSA (see our gMSA guide) and run the task only on a hardened management server or a DC.

$action = New-ScheduledTaskAction -Execute 'powershell.exe' `
    -Argument '-NoProfile -ExecutionPolicy Bypass -File C:\Scripts\Invoke-ADHealthReport.ps1 -OutputPath C:\Reports\ad-health.html -SmtpServer relay.contoso.com -From adreport@contoso.com -To admins@contoso.com'
$trigger = New-ScheduledTaskTrigger -Daily -At 6:30am
$principal = New-ScheduledTaskPrincipal -UserId 'CONTOSO\gmsa-adreport$' -LogonType Password
Register-ScheduledTask -TaskName 'AD daily health report' -Action $action -Trigger $trigger -Principal $principal

With a fixed -OutputPath the file is overwritten daily; leave -OutputPath out to get a new time-stamped file each run (and clean the folder now and then).

How it works

  1. Gets the domain, forest and DC list with Get-ADDomain, Get-ADForest and Get-ADDomainController -Filter *, and builds the FSMO map.
  2. For each DC runs the three per-DC checks. Native commands are called with stderr captured as text, so a failing command produces a status instead of stopping the script.
  3. Reads SYSVOL state with Get-CimInstance -ComputerName <DC> -Namespace root/MicrosoftDfs -ClassName DfsrReplicatedFolderInfo and keeps the “SYSVOL Share” folder. No result means SYSVOL is not replicated by DFSR (still on FRS) or the namespace is not there.
  4. Runs repadmin /replsummary once and checks every “fails / total” pair.
  5. Builds the HTML with encoded text (no external CSS or scripts), saves it, and optionally sends it.

Limitations

  • dcdiag is run with its default tests. Use dcdiag /c by hand when you need the comprehensive set.
  • The repadmin check reads the text output; the error column is shown but not parsed, so read the raw block when it says Fail.
  • Replication is summarised forest-wide from the machine you run it on; very large forests may take a while.
  • DNS, certificate expiry and backup age are not checked.
  • Not yet run against a live domain (see the note at the top).

Official documentation: dcdiag (Microsoft Learn) · repadmin /replsummary · Windows Time service tools (w32tm) · Send-MailMessage (obsolete note)

Related: dcdiag repadmin Health Check: 7 Critical Tests Explained · AD Replication Error 1722 and 8453: Fixes · PDC Emulator NTP Time Sync: Reliable Domain Time · Transfer FSMO Roles with PowerShell and ntdsutil: Safe Steps · Restore Domain Controller Backups: System State and DSRM Steps

See also: Export AD Users to CSV: PowerShell Script with Last Logon · Locked Out AD Users Report: PowerShell Script with Lockout Source · AD Privileged Group Report: Domain Admins and adminCount Audit · AD Nested Group Membership: PowerShell Tree with Loop Detection · Inactive AD Accounts Report: PowerShell Script with Safe Disable

The script

Invoke-ADHealthReport.ps1Download
# AD Health Check Report: dcdiag and repadmin PowerShell Script (v1.0.0) - from srvScripts.com
# Source, docs and updates: https://srvscripts.com/scripts/ad-health-check-report/
# Copyright (c) 2026 srvScripts.com. MIT licence: if you copy, share or adapt this script, keep this notice and credit srvScripts.com.
<#
.SYNOPSIS
    One HTML health report for every domain controller: dcdiag, replication summary, FSMO holders, time source
    and SYSVOL (DFSR) state. Optionally e-mails it.

.DESCRIPTION
    Read-only. For each DC in the domain (Get-ADDomainController -Filter *) the script runs:
      dcdiag /s:<DC> /q              quiet mode: prints only errors, so empty output = all default tests passed
      w32tm /query /computer:<DC> /source
      DfsrReplicatedFolderInfo        (CIM, namespace root\MicrosoftDfs) state of the "SYSVOL Share" folder:
                                      0 Uninitialized, 1 Initialized, 2 Initial Sync, 3 Auto Recovery,
                                      4 Normal, 5 In Error
    Once per run it also captures:
      repadmin /replsummary           largest replication delta and failures per DC
      FSMO role holders               from Get-ADForest and Get-ADDomain
    The HTML report starts with a summary table (one row per DC, Pass / Warning / Fail per check) and keeps
    the raw dcdiag and repadmin output underneath for anyone who needs the detail.

    E-mail: with -SmtpServer, -To and -From the report is sent with Send-MailMessage. Microsoft marks
    Send-MailMessage as obsolete because it does not guarantee a secure connection to the SMTP server; use it
    only with an internal relay you trust, or leave the e-mail options out and collect the saved file.

.PARAMETER Server
    Domain to check (default: current domain).

.PARAMETER DomainController
    Check only these DCs (host names) instead of all DCs in the domain.

.PARAMETER OutputPath
    Where to save the HTML report (default: .\ADHealth-<domain>-<yyyyMMdd-HHmm>.html).

.PARAMETER SkipDcdiag
    Do not run dcdiag (it is the slowest check, run once per DC).

.PARAMETER SmtpServer
    SMTP relay to send the report through. Requires -To and -From.

.PARAMETER To
    Recipient address(es).

.PARAMETER From
    Sender address.

.PARAMETER Port
    SMTP port (default 25).

.PARAMETER UseSsl
    Use TLS for the SMTP connection.

.PARAMETER Credential
    SMTP credentials, if the relay needs authentication.

.PARAMETER PassThru
    Output the per-DC summary objects to the pipeline.

.EXAMPLE
    .\Invoke-ADHealthReport.ps1

.EXAMPLE
    .\Invoke-ADHealthReport.ps1 -OutputPath C:\Reports\ad-health.html -SkipDcdiag

.EXAMPLE
    .\Invoke-ADHealthReport.ps1 -SmtpServer relay.contoso.com -From adreport@contoso.com -To admins@contoso.com

.NOTES
    Name:     Invoke-ADHealthReport.ps1
    Version:  1.0.0
    Source:   https://srvscripts.com/scripts/ad-health-check-report/
    License:  MIT
    Requires: Windows PowerShell 5.1 or PowerShell 7 on Windows, ActiveDirectory module and the AD DS command-line
              tools (dcdiag.exe, repadmin.exe: RSAT AD DS tools or a DC), w32tm.exe (built in), a Domain Admin
              or equivalent account (dcdiag and the DFSR WMI namespace need admin rights on the DCs), and
              WinRM/DCOM access to the DCs for the CIM query.
#>
[CmdletBinding()]
param(
    [ValidateNotNullOrEmpty()]
    [string]$Server,

    [ValidateNotNullOrEmpty()]
    [string[]]$DomainController,

    [ValidateNotNullOrEmpty()]
    [string]$OutputPath,

    [switch]$SkipDcdiag,

    [ValidateNotNullOrEmpty()]
    [string]$SmtpServer,

    [ValidateNotNullOrEmpty()]
    [string[]]$To,

    [ValidateNotNullOrEmpty()]
    [string]$From,

    [ValidateRange(1, 65535)]
    [int]$Port = 25,

    [switch]$UseSsl,

    [System.Management.Automation.PSCredential]
    [System.Management.Automation.Credential()]
    $Credential = [System.Management.Automation.PSCredential]::Empty,

    [switch]$PassThru
)

Set-StrictMode -Version 2
$ErrorActionPreference = 'Stop'

if (-not (Get-Module -ListAvailable -Name ActiveDirectory)) {
    throw "The ActiveDirectory module is not installed. Install RSAT: Active Directory Domain Services tools, then run again."
}
Import-Module ActiveDirectory -Verbose:$false
if ($SmtpServer -and (-not $To -or -not $From)) { throw "-SmtpServer needs -To and -From." }
foreach ($exe in 'repadmin.exe', 'w32tm.exe') {
    if (-not (Get-Command $exe -ErrorAction SilentlyContinue)) { throw "$exe not found. Run on a DC or install RSAT: Active Directory Domain Services tools." }
}
if (-not $SkipDcdiag -and -not (Get-Command 'dcdiag.exe' -ErrorAction SilentlyContinue)) {
    throw "dcdiag.exe not found. Run on a DC, install RSAT: Active Directory Domain Services tools, or use -SkipDcdiag."
}

function ConvertTo-HtmlText { param([string]$Text) [System.Net.WebUtility]::HtmlEncode($Text) }

function Invoke-Native {
    param([string]$FilePath, [string[]]$ArgumentList)
    # Windows PowerShell 5.1 turns redirected stderr into error records; do not let them stop the script.
    $ErrorActionPreference = 'Continue'
    $text = (& $FilePath @ArgumentList 2>&1 | ForEach-Object { [string]$_ }) -join "`n"
    [pscustomobject]@{ ExitCode = $LASTEXITCODE; Output = $text.Trim() }
}

# ---- Domain, forest, DCs, FSMO --------------------------------------------------------------------------
$domArgs = @{}
if ($Server) { $domArgs.Server = $Server }
$domain = Get-ADDomain @domArgs
$forest = Get-ADForest -Server $domain.DNSRoot
$dcs = @(Get-ADDomainController -Filter * -Server $domain.DNSRoot | Sort-Object HostName)
if ($DomainController) {
    $dcs = @($dcs | Where-Object { $DomainController -contains $_.HostName -or $DomainController -contains $_.Name })
    if (-not $dcs.Count) { throw "None of the -DomainController names is a DC of $($domain.DNSRoot)." }
}
if (-not $dcs.Count) { throw "No domain controllers returned for $($domain.DNSRoot)." }
$fsmo = [ordered]@{
    'Schema master'         = $forest.SchemaMaster
    'Domain naming master'  = $forest.DomainNamingMaster
    'PDC emulator'          = $domain.PDCEmulator
    'RID master'            = $domain.RIDMaster
    'Infrastructure master' = $domain.InfrastructureMaster
}
if (-not $OutputPath) { $OutputPath = Join-Path (Get-Location).Path ("ADHealth-{0}-{1:yyyyMMdd-HHmm}.html" -f $domain.DNSRoot, (Get-Date)) }

# ---- Per-DC checks --------------------------------------------------------------------------------------
$summary = [System.Collections.Generic.List[object]]::new()
$details = [System.Collections.Generic.List[string]]::new()
$dfsrStates = @{ 0 = 'Uninitialized'; 1 = 'Initialized'; 2 = 'Initial Sync'; 3 = 'Auto Recovery'; 4 = 'Normal'; 5 = 'In Error' }
$i = 0
foreach ($dc in $dcs) {
    $i++
    $h = $dc.HostName
    Write-Progress -Activity 'AD health check' -Status $h -PercentComplete (100 * ($i - 1) / $dcs.Count)
    $roles = @($fsmo.Keys | Where-Object { $fsmo[$_] -eq $h })

    # dcdiag
    $dcdiagStatus = 'Skipped'; $dcdiagOut = ''
    if (-not $SkipDcdiag) {
        Write-Verbose "dcdiag /s:$h /q"
        $r = Invoke-Native 'dcdiag.exe' @("/s:$h", '/q')
        $dcdiagOut = $r.Output
        $dcdiagStatus = if (-not $r.Output) { 'Pass' } else { 'Fail' }
    }

    # time source
    $t = Invoke-Native 'w32tm.exe' @('/query', "/computer:$h", '/source')
    $timeSource = $t.Output
    $timeStatus = 'Pass'
    if ($t.ExitCode -ne 0 -or -not $timeSource) { $timeStatus = 'Fail' }
    elseif ($timeSource -match 'Local CMOS Clock|Free-running System Clock') {
        $timeStatus = if ($h -eq $domain.PDCEmulator -and $domain.DNSRoot -eq $forest.RootDomain) { 'Fail' } else { 'Warning' }
    }

    # SYSVOL DFSR state
    $sysvol = 'Unknown'; $sysvolStatus = 'Warning'
    try {
        $rf = @(Get-CimInstance -ComputerName $h -Namespace 'root/MicrosoftDfs' -ClassName 'DfsrReplicatedFolderInfo' -OperationTimeoutSec 60 |
            Where-Object { $_.ReplicatedFolderName -eq 'SYSVOL Share' })
        if ($rf.Count) {
            $st = [int]$rf[0].State
            $sysvol = if ($dfsrStates.ContainsKey($st)) { $dfsrStates[$st] } else { "State $st" }
            $sysvolStatus = if ($st -eq 4) { 'Pass' } elseif ($st -eq 5) { 'Fail' } else { 'Warning' }
        } else {
            $sysvol = 'No DFSR SYSVOL folder found (FRS-replicated SYSVOL, or DFSR not set up)'
        }
    } catch {
        $sysvol = "Query failed: $($_.Exception.Message)"
    }

    $overall = if (@($dcdiagStatus, $timeStatus, $sysvolStatus) -contains 'Fail') { 'Fail' }
               elseif (@($dcdiagStatus, $timeStatus, $sysvolStatus) -contains 'Warning') { 'Warning' } else { 'Pass' }
    $summary.Add([pscustomobject]@{
        DomainController = $h
        Site             = $dc.Site
        OperatingSystem  = $dc.OperatingSystem
        GlobalCatalog    = $dc.IsGlobalCatalog
        ReadOnly         = $dc.IsReadOnly
        FSMORoles        = ($roles -join ', ')
        Dcdiag           = $dcdiagStatus
        TimeSource       = $timeSource
        TimeStatus       = $timeStatus
        SysvolDfsr       = $sysvol
        SysvolStatus     = $sysvolStatus
        Overall          = $overall
    })
    if ($dcdiagOut) { $details.Add("<h3>dcdiag errors: $(ConvertTo-HtmlText $h)</h3><pre>$(ConvertTo-HtmlText $dcdiagOut)</pre>") }
}
Write-Progress -Activity 'AD health check' -Completed

# ---- Replication summary --------------------------------------------------------------------------------
Write-Verbose 'repadmin /replsummary'
$rep = Invoke-Native 'repadmin.exe' @('/replsummary')
$repStatus = 'Pass'
# Each DC row has a "fails / total" pair. Any row with fails above zero, or a non-zero exit code, is a failure.
$failCounts = @(($rep.Output -split "`n") | Where-Object { $_ -notmatch 'Start Time' } | ForEach-Object {
    $m = [regex]::Match($_, '(?<![\d/])(\d+)\s*/\s*(\d+)\s+(\d{1,3})(?![\d/:])')
    if ($m.Success) { [int]$m.Groups[1].Value }
})
if ($rep.ExitCode -ne 0 -or -not $rep.Output) { $repStatus = 'Fail' }
elseif (@($failCounts | Where-Object { $_ -gt 0 }).Count) { $repStatus = 'Fail' }
elseif ($rep.Output -match 'operational errors') { $repStatus = 'Warning' }

# ---- HTML -----------------------------------------------------------------------------------------------
$css = @'
body{font-family:Segoe UI,Arial,sans-serif;font-size:13px;margin:20px;color:#222}
table{border-collapse:collapse;margin-bottom:16px}th,td{border:1px solid #ccc;padding:4px 8px;text-align:left;vertical-align:top}
th{background:#f0f0f0}pre{background:#f7f7f7;border:1px solid #ddd;padding:8px;overflow:auto;font-size:12px}
.Pass{background:#e3f4e3}.Warning{background:#fff4d6}.Fail{background:#fbe0e0}
'@
$cell = { param($v) $c = [string]$v; if ($c -in 'Pass', 'Warning', 'Fail') { "<td class=`"$c`">$c</td>" } else { "<td>$(ConvertTo-HtmlText $c)</td>" } }
$cols = 'DomainController', 'Site', 'OperatingSystem', 'GlobalCatalog', 'ReadOnly', 'FSMORoles', 'Dcdiag', 'TimeSource', 'TimeStatus', 'SysvolDfsr', 'SysvolStatus', 'Overall'
$sb = New-Object System.Text.StringBuilder
[void]$sb.Append("<!DOCTYPE html><html><head><meta charset=`"utf-8`"><title>AD health report $(ConvertTo-HtmlText $domain.DNSRoot)</title><style>$css</style></head><body>")
[void]$sb.Append(("<h1>AD health report: {0}</h1><p>Generated {1:yyyy-MM-dd HH:mm} on {2}. Forest {3}, {4} DC(s) checked.</p>" -f (ConvertTo-HtmlText $domain.DNSRoot), (Get-Date), $env:COMPUTERNAME, (ConvertTo-HtmlText $forest.Name), $summary.Count))
[void]$sb.Append('<h2>Domain controllers</h2><table><tr>' + (($cols | ForEach-Object { "<th>$_</th>" }) -join '') + '</tr>')
foreach ($s in $summary) { [void]$sb.Append('<tr>' + (($cols | ForEach-Object { & $cell $s.$_ }) -join '') + '</tr>') }
[void]$sb.Append('</table><h2>FSMO role holders</h2><table><tr><th>Role</th><th>Holder</th></tr>')
foreach ($k in $fsmo.Keys) { [void]$sb.Append("<tr><td>$k</td><td>$(ConvertTo-HtmlText $fsmo[$k])</td></tr>") }
[void]$sb.Append("</table><h2>Replication summary (repadmin /replsummary)</h2><table><tr><th>Status</th></tr><tr>$(& $cell $repStatus)</tr></table><pre>$(ConvertTo-HtmlText $rep.Output)</pre>")
if ($details.Count) { [void]$sb.Append('<h2>dcdiag details</h2>' + ($details -join '')) }
[void]$sb.Append('<p>dcdiag ran in quiet mode (/q): only failing tests print text. Status rules: time source "Local CMOS Clock" or "Free-running System Clock" is a warning on a DC and a failure on the forest-root PDC emulator; SYSVOL is Pass only in DFSR state 4 (Normal).</p></body></html>')
$sb.ToString() | Out-File -LiteralPath $OutputPath -Encoding utf8
Write-Information ("Report saved to {0}" -f $OutputPath) -InformationAction Continue

# ---- Optional e-mail ------------------------------------------------------------------------------------
if ($SmtpServer) {
    $states = @($summary | ForEach-Object { $_.Overall })
    $worst = if ($states -contains 'Fail' -or $repStatus -eq 'Fail') { 'FAIL' } elseif ($states -contains 'Warning' -or $repStatus -eq 'Warning') { 'WARNING' } else { 'PASS' }
    $mail = @{
        SmtpServer = $SmtpServer; Port = $Port; To = $To; From = $From
        Subject = "AD health $($domain.DNSRoot): $worst"; Body = $sb.ToString(); BodyAsHtml = $true; Attachments = $OutputPath
    }
    if ($UseSsl) { $mail.UseSsl = $true }
    if ($Credential -ne [System.Management.Automation.PSCredential]::Empty) { $mail.Credential = $Credential }
    try {
        Send-MailMessage @mail -WarningAction SilentlyContinue
        Write-Information "Report e-mailed to $($To -join ', ')" -InformationAction Continue
    } catch {
        Write-Warning "E-mail failed: $($_.Exception.Message). The report is still saved at $OutputPath."
    }
}

if ($PassThru) { return $summary }
$summary | Format-Table DomainController, Dcdiag, TimeStatus, SysvolStatus, Overall, FSMORoles -AutoSize
Write-Information "Replication summary: $repStatus" -InformationAction Continue
Version 1.0.0 · SHA-256 002f207dce5c5e970985b8293f36228966f7f90967ed0e4578016f7d8a850250
Download and verify on Linux or macOS
curl -fsSL -o Invoke-ADHealthReport.ps1 https://scr.srvscripts.com/ad-health-check-report/Invoke-ADHealthReport.ps1 && curl -fsSL https://scr.srvscripts.com/ad-health-check-report/Invoke-ADHealthReport.ps1.sha256 | sha256sum -c
Download and verify in Windows PowerShell
Invoke-WebRequest -Uri 'https://scr.srvscripts.com/ad-health-check-report/Invoke-ADHealthReport.ps1' -OutFile 'Invoke-ADHealthReport.ps1'; if ((Get-FileHash 'Invoke-ADHealthReport.ps1' -Algorithm SHA256).Hash -eq '002F207DCE5C5E970985B8293F36228966F7F90967ED0E4578016F7D8A850250') { 'OK: the file is intact' } else { 'MISMATCH: do not run this file' }
Copy the whole line. In Windows PowerShell, curl and sha256sum are not the Linux tools, so use the PowerShell line there.
Also on GitHub: github.com/srvscripts/scripts

Frequently asked questions

How do I check Active Directory health with PowerShell?

Run Invoke-ADHealthReport.ps1. It wraps dcdiag, repadmin /replsummary and w32tm, reads SYSVOL DFSR state and FSMO holders, and writes one HTML report for all DCs.

Why does the report say the time source is a warning?

A DC that reports Local CMOS Clock or Free-running System Clock is not syncing from the domain hierarchy or an external NTP server. Only the forest-root PDC emulator should sync externally; the rest should follow the domain hierarchy.

What does SYSVOL state 4 mean?

State 4 is Normal for the DfsrReplicatedFolderInfo class. 5 means In Error, and 0 to 3 mean the folder is still initialising or recovering.

Is Send-MailMessage safe to use?

Microsoft marks it obsolete because it cannot guarantee a secure SMTP connection. Use it only with a trusted internal relay, or skip e-mail and save the report to a share.

How long does the script take?

Most of the time goes to dcdiag, the slowest check, which runs once per DC. Use -SkipDcdiag for a quick check of time, SYSVOL and replication.

Does it fix anything?

No. It only runs diagnostic commands and queries and writes the report.

Changelog

  • 1.0.0 — First release.

Free website test

Is your website set up right?

Check SSL, security headers, redirects, robots.txt, sitemap, llms.txt and security.txt in one test. It takes about 30 seconds.