Active Directory Forest Health Monitoring with PowerShell

Use Case: Proactive Forest-Level Health Visibility in Multi-Domain Environments

In medium to large enterprise environments, Active Directory is not just a directory service — it is the authentication backbone of the organization. Every login, every Kerberos ticket, every group policy application, and most application authentications depend on it.

Now imagine this scenario:

  • Multiple domains in a single forest
  • Domain Controllers spread across different AD Sites
  • Limited WinRM access between segments
  • Security restrictions blocking remote PowerShell remoting
  • No centralized AD monitoring tool

Yet, leadership expects high availability and zero authentication failures.

This is where this script comes into play.

The purpose of Get-ADHealth.ps1 v8 is to provide a forest-wide, domain-by-domain, Domain Controller health snapshot using native tools — without depending entirely on WinRM-based remoting. It leverages WMI and native utilities such as DCDIAG to gather structured health metrics and generate a consolidated HTML report.

The result is a clear, color-coded, operationally useful health dashboard.

Architecture Overview

The script performs the following logical flow:

  1. Discover all domains in the forest.
  2. Identify the PDC Emulator of each domain.
  3. Execute a data-collection script block remotely on each PDC.
  4. From the PDC, enumerate all Domain Controllers in that domain.
  5. Run health checks against each DC.
  6. Build a structured HTML report with visual status indicators or Save a single consolidated forest-level report.

It relies on:

  • Get-ADForest
  • Get-ADDomain
  • Get-ADDomainController
  • WMI (Win32_OperatingSystem, Win32_LogicalDisk, Win32_Processor)
  • Dcdiag.exe
What Exactly Is Being Checked?
  • Connectivity – Basic DC communication validation
  • DFSREvent – SYSVOL replication health
  • KccEvent – Knowledge Consistency Checker replication topology
  • KnowsOfRoleHolders (FSMO awareness) – Verifies role holder visibility
  • NetLogons – Secure channel validation
  • ObjectsReplicated – Replication integrity
  • OS Free Space (System Drive)
  • CPU Utilization
  • Memory Utilization
  • Uptime in Days
<#
    .SYNOPSIS
    Get-ADHealth.ps1 v8
	# Powershell Health Check Script to gather the Health information of Active Directory Forest 
	# Gathers information via wmi-object so even if Win-RM is blocked you will be able run the script and gather information 
	# Created on  26-2-2026
	
#>

# ============ CONFIGURATION ============

$now = Get-Date
$date = $now.ToShortDateString()
$reportTime = $now
$allDomains = (Get-ADForest).Domains
$reportemailsubject = "Active Directory Health Check for $($allDomains -join ', ')"

# ============ EMAIL CONFIGURATION ============
$smtpsettings = @{
    To         = 'Recipient-email@email.com'
    From       = 'Sender-email@email.com'
    Subject    = "$reportemailsubject - $date"
    SmtpServer = "SMTP Server Address"
    Port       = "25"   
}

# ========== DISCOVER DOMAIN PDC EMULATORS ==========

$domainPDCs = @{}
foreach ($domain in $allDomains) 
{
    $pdc = (Get-ADDomain -Server $domain).PDCEmulator
    $domainPDCs[$domain] = $pdc
}

# ========== DEFINE THE DATA-COLLECTION BLOCK ==========

$domainHealthScriptBlock = {
    param([string]$DomainName)
    Import-Module ActiveDirectory -ErrorAction Stop

    Function Get-AllDomainControllers ($ComputerName) {
        Get-ADDomainController -Filter * -Server $ComputerName | Sort-Object HostName
    }
    Function Get-DomainControllerUptimeDays {
        param($ComputerName)
        if (Test-Connection $ComputerName -Count 1 -Quiet) {
            try {
                $os = Get-WmiObject Win32_OperatingSystem -ComputerName $ComputerName -ErrorAction Stop
                if ($os) {
                    $lastBoot = $os.ConvertToDateTime($os.LastBootUpTime)
                    return (New-TimeSpan -Start $lastBoot -End (Get-Date)).Days
                } else {
                    return "Data Error"
                }
            } catch {
                return 'WMI Failure'
            }
        } else {
            return 'Fail'
        }
    }
    Function Get-DomainControllerDCDiagTestResults($ComputerName) {
        $DCDiagTestResults = [PSCustomObject]@{
            ServerName         = $ComputerName
            Connectivity       = $null
            DFSREvent          = $null
            KccEvent           = $null
            KnowsOfRoleHolders = $null
            NetLogons          = $null
            ObjectsReplicated  = $null
        }
        if ((Test-Connection $ComputerName -Count 1 -quiet) -eq $True) {
            $params = @(
                "/s:$ComputerName",
                "/test:Connectivity",
                "/test:DFSREvent",
                "/test:KccEvent",
                "/test:KnowsOfRoleHolders",
                "/test:NetLogons",
                "/test:ObjectsReplicated"
            )
            $DCDiagTest = (Dcdiag.exe @params) -split ('[\r\n]')
            $TestName = $null
            $TestStatus = $null
            $DCDiagTest | ForEach-Object {
                switch -Regex ($_) {
                    "Starting test:" {
                        $TestName = ($_ -replace ".*Starting test:").Trim()
                    }
                    "passed test|failed test" {
                        $TestStatus = if ($_ -match "passed test") { "Passed" } else { "Failed" }
                    }
                }
                if ($TestName -and $TestStatus) {
                    $DCDiagTestResults.$TestName = $TestStatus
                    $TestName = $null
                    $TestStatus = $null
                }
            }
        }
        else {
            foreach ($property in $DCDiagTestResults.PSObject.Properties.Name) {
                if ($property -ne "ServerName") {
                    $DCDiagTestResults.$property = "Failed"
                }
            }
        }
        return $DCDiagTestResults
    }
    Function Get-DomainControllerOSDriveFreeSpaceGB {
        Param($ComputerName)
        if (Test-Connection $ComputerName -Count 1 -Quiet) {
            try {
                $os = Get-WmiObject -Class Win32_OperatingSystem -ComputerName $ComputerName -ErrorAction Stop
                $osDriveLetter = $os.SystemDrive
                $osDrive = Get-WmiObject -Class Win32_LogicalDisk -ComputerName $ComputerName `
                    -Filter "DeviceID='$osDriveLetter'" -ErrorAction Stop
                [math]::Round($osDrive.FreeSpace / 1GB, 2)
            } catch {
                'WMI Failure'
            }
        } else {
            'Fail'
        }
    }
    Function Get-DomainControllerCPUUsage {
        Param($ComputerName)
        if (Test-Connection $ComputerName -Count 1 -Quiet) {
            try {
                $avgProc = Get-WmiObject -Class win32_processor -ComputerName $ComputerName -ErrorAction Stop |
                        Measure-Object -Property LoadPercentage -Average |
                        Select-Object -ExpandProperty Average
                [math]::Round($avgProc, 2)
            } catch {
                'WMI Failure'
            }
        }
        else {
            'Fail'
        }
    }
    Function Get-DomainControllerMemoryUsage {
        Param($ComputerName)
        if (Test-Connection $ComputerName -Count 1 -Quiet) {
            try {
                $os = Get-WmiObject -Class Win32_OperatingSystem -ComputerName $ComputerName -ErrorAction Stop
                $total = $os.TotalVisibleMemorySize
                $free  = $os.FreePhysicalMemory
                $used  = $total - $free
                [math]::Round(($used / $total) * 100, 2)
            }
            catch {
                'WMI Failure'
            }
        }
        else {
            'Fail'
        }
    }
    $Results = @()
    $allDomainControllers = Get-AllDomainControllers $DomainName
    foreach ($domainController in $allDomainControllers) {
        $DCDiagTestResults = Get-DomainControllerDCDiagTestResults $domainController.HostName
        $result = [PSCustomObject]@{
            Server                  = ($domainController.HostName.Split('.')[0]).ToUpper()
            Site                    = $domainController.Site
            "DCDIAG: Connectivity"  = $DCDiagTestResults.Connectivity
            "DCDIAG: DFSREvent"     = $DCDiagTestResults.DFSREvent
            "DCDIAG: KccEvent"      = $DCDiagTestResults.KccEvent
            "DCDIAG: FSMO "         = $DCDiagTestResults.KnowsOfRoleHolders
            "DCDIAG: NetLogons"     = $DCDiagTestResults.NetLogons
            "Replication"           = $DCDiagTestResults.ObjectsReplicated
            "OS Free Space (GB)"    = Get-DomainControllerOSDriveFreeSpaceGB $domainController.HostName
            "CPU Usage (%)"         = Get-DomainControllerCPUUsage $domainController.HostName
            "Memory Usage (%)"      = Get-DomainControllerMemoryUsage $domainController.HostName
            "Uptime (days)"         = Get-DomainControllerUptimeDays $domainController.HostName
        }
        $Results += $result
    }
    return $Results
}

# ========== COLLECT RESULTS FROM ALL PDCs ==========

$perDomainResults = @{}
foreach ($domain in $allDomains) {
    $pdc = $domainPDCs[$domain]
    Write-Host "Collecting health from domain '$domain' PDC '$pdc'..."
    $results = Invoke-Command -ComputerName $pdc -ScriptBlock $domainHealthScriptBlock -ArgumentList $domain
    $perDomainResults[$domain] = $results
}

# ========== BUILD HTML REPORT (UNTOUCHED LAYOUT) ==========

Function New-ServerHealthHTMLTableCell {
    param( $lineitem, $Width = "80px" )
    $cellValue = $reportline."$lineitem"
    switch ($cellValue) {
        "Success" { return "<td style='height:25px;width:$Width; border:1px solid #000;padding:6px; background-color:#6BBF59; color:#000;text-align:center;'>$cellValue</td>" }
        "Passed"  { return "<td style='height:25px;width:$Width; border:1px solid #000;padding:6px; background-color:#6BBF59; color:#000;text-align:center;'>$cellValue</td>" }
        "Pass"    { return "<td style='height:25px;width:$Width; border:1px solid #000;padding:6px; background-color:#6BBF59; color:#000;text-align:center;'>$cellValue</td>" }
        "Warn"    { return "<td style='height:25px;width:$Width; border:1px solid #000;padding:6px; background-color:#FFD966; color:#000;text-align:center;'>$cellValue</td>" }
        "Fail"    { return "<td style='height:25px;width:$Width; border:1px solid #000;padding:6px; background-color:#D9534F; color:#fff;text-align:center;'>$cellValue</td>" }
        "Failed"  { return "<td style='height:25px;width:$Width; border:1px solid #000;padding:6px; background-color:#D9534F; color:#fff;text-align:center;'>$cellValue</td>" }
        default   { return "<td style='height:25px;width:$Width; border:1px solid #000;padding:6px;text-align:center;'>$cellValue</td>" }
    }
}

$htmlhead = @"
<html>
<body style='font-family:Segoe UI,Tahoma,Geneva,Verdana,sans-serif;font-size:10pt;'>
<h1 style='font-size:20px;text-align:left;'>Domain Controller Health Check Report</h1>
<h3 style='font-size:14px;text-align:left;'>Generated: $reportTime</h3>
"@

$htmltableheader = @"
<table border='1' cellpadding='0' cellspacing='0' style='width:1300px;border-collapse:collapse;border-spacing:0;margin:0;font-size:10pt;table-layout:fixed;'>
<tr style='background-color:#f2f2f2; margin:0; border-spacing:0;'>
  <th style='height:25px;width:120px;'>Server</th>
  <th style='height:25px;width:110px;'>Site</th>
  <th style='height:25px;width:70px;'>Connectivity</th>
  <th style='height:25px;width:70px;'>DFSREvent</th>
  <th style='height:25px;width:70px;'>FSMO</th>
  <th style='height:25px;width:70px;'>NetLogons</th>
  <th style='height:25px;width:70px;'>Replication</th>
  <th style='height:25px;width:70px;'>OS Free Space(GB)</th>
  <th style='height:25px;width:70px;'>CPU Usage (%)</th>
  <th style='height:25px;width:70px;'>Memory Usage (%)</th>
  <th style='height:25px;width:70px;'>Uptime (days)</th>
</tr>
"@

$explanationTable = @"
<h3 style='font-family: Arial, sans-serif; color: #0056b3;'>Column Reference</h3>
<table border='1' cellpadding='0' cellspacing='0' style='border-collapse:collapse; width:50%; font-family:Arial, sans-serif; font-size:12px;'>
  <thead>
    <tr style='background-color:#f2f2f2;'>
      <th style='height:25px;'>Field</th>
      <th style='height:25px;'>Description</th>
    </tr>
  </thead>
  <tbody>
    <tr><td>Connectivity</td><td>Checks basic connectivity between DCs.</td></tr>
    <tr><td>DFSREvent</td><td>Checks DFS Replication health for SYSVOL.</td></tr>
    <tr><td>FSMO</td><td>Confirms the DC knows FSMO role holders.</td></tr>
    <tr><td>NetLogons</td><td>Validates secure channel (Netlogon).</td></tr>
    <tr><td>Replication</td><td>Confirms that AD objects replicate properly.</td></tr>
    <tr><td>OS Free Space (GB)</td><td>Available disk space on the system drive (in GB).</td></tr>
    <tr><td>CPU Usage (%)</td><td>Current CPU utilization of the server.</td></tr>
    <tr><td>Memory Usage (%)</td><td>Current RAM utilization.</td></tr>
    <tr><td>Uptime (days)</td><td>How many days the DC has been running since the last reboot.</td></tr>
  </tbody>
</table>
"@

$htmltail = "<p style='font-size:11px; color:#555;'>* DNS test is performed using <code>Resolve-DnsName</code>. This cmdlet is only available from Windows Server 2012 onwards.</p>
</body>
</html>"

# --------- ASSEMBLE ALL TABLES -----------

$allDomainTables = @()
foreach ($domain in $allDomains) {
    $domainDCResults = $perDomainResults[$domain]
    $serverhealthhtmltable = "<h2 style='color:#174ea6;'>Domain: $domain</h2>" + $htmltableheader
    foreach ($reportline in $domainDCResults) {
        $htmltablerow = "<tr>"
        $htmltablerow += "<td style='text-align:center;'><b>$($reportline.Server)</b></td>"
        $htmltablerow += "<td style='text-align:center;'><b>$($reportline.Site)</b></td>"
        $htmltablerow += (New-ServerHealthHTMLTableCell "DCDIAG: Connectivity" -Width "70px")
        $htmltablerow += (New-ServerHealthHTMLTableCell "DCDIAG: DFSREvent" -Width "70px")
        $htmltablerow += (New-ServerHealthHTMLTableCell "DCDIAG: FSMO " -Width "70px")
        $htmltablerow += (New-ServerHealthHTMLTableCell "DCDIAG: NetLogons" -Width "70px")
        $htmltablerow += (New-ServerHealthHTMLTableCell "Replication" -Width "70px")

        $osFree = $reportline.'OS Free Space (GB)'
        if ($osFree -is [double] -or $osFree -is [int]) {
            if ($osFree -lt 20) {
                $htmltablerow += "<td style='background-color:#D9534F; color:#fff; text-align:center; margin:0; border-spacing:0;'>$osFree</td>"
            } else {
                $htmltablerow += "<td style='background-color:#6BBF59; color:#000; text-align:center; margin:0; border-spacing:0;'>$osFree</td>"
            }
        } else {
            $htmltablerow += "<td>$osFree</td>"
        }
        $cpu = $reportline.'CPU Usage (%)'
        if ($cpu -is [double] -or $cpu -is [int]) {
            if ($cpu -le 75) {
                $htmltablerow += "<td style='background-color:#6BBF59; color:#000; text-align:center; margin:0; border-spacing:0;'>$cpu</td>"
            } elseif ($cpu -le 90) {
                $htmltablerow += "<td style='background-color:#FFD966; color:#000; text-align:center; margin:0; border-spacing:0;'>$cpu</td>"
            } else {
                $htmltablerow += "<td style='background-color:#D9534F; color:#fff; text-align:center; margin:0; border-spacing:0;'>$cpu</td>"
            }
        } else {
            $htmltablerow += "<td>$cpu</td>"
        }
        $mem = $reportline.'Memory Usage (%)'
        if ($mem -is [double] -or $mem -is [int]) {
            if ($mem -le 75) {
                $htmltablerow += "<td style='background-color:#6BBF59; color:#000; text-align:center; margin:0; border-spacing:0;'>$mem</td>"
            } elseif ($mem -le 90) {
                $htmltablerow += "<td style='background-color:#FFD966; color:#000; text-align:center; margin:0; border-spacing:0;'>$mem</td>"
            } else {
                $htmltablerow += "<td style='background-color:#D9534F; color:#fff; text-align:center; margin:0; border-spacing:0;'>$mem</td>"
            }
        } else {
            $htmltablerow += "<td>$mem</td>"
        }
        $uptimeDays = $reportline.'Uptime (days)'
        if ($uptimeDays -eq "CIM Failure" -or $uptimeDays -eq "Fail") {
            $htmltablerow += "<td style='background-color:#D9534F; color:#fff; text-align:center; margin:0; border-spacing:0;'>$uptimeDays</td>"
        } elseif ($uptimeDays -is [int] -or $uptimeDays -is [double]) {
            if ($uptimeDays -le 30) {
                $htmltablerow += "<td style='background-color:#6BBF59; color:#000; text-align:center; margin:0; border-spacing:0;'>$uptimeDays</td>"
            } elseif ($uptimeDays -le 45) {
                $htmltablerow += "<td style='background-color:#FFD966; color:#000; text-align:center; margin:0; border-spacing:0;'>$uptimeDays</td>"
            } else {
                $htmltablerow += "<td style='background-color:#D9534F; color:#fff; text-align:center; margin:0; border-spacing:0;'>$uptimeDays</td>"
            }
        } else {
            $htmltablerow += "<td>$uptimeDays</td>"
        }
        $htmltablerow += "</tr>"
        $serverhealthhtmltable += $htmltablerow
    }
    $serverhealthhtmltable += "</table>"
    $allDomainTables += $serverhealthhtmltable
}

$htmlbody = $htmlhead + ($allDomainTables -join "<br/><br/>") + $explanationTable + $htmltail

# SEND THE CONSOLIDATED REPORT

#Send-MailMessage @smtpsettings -Body $htmlbody -BodyAsHtml -Encoding ([System.Text.Encoding]::UTF8) -ErrorAction Stop
#Write-Host "Combined Email sent successfully with per-domain health tables." -ForegroundColor Green


# ===== WRITE REPORT TO CENTRAL FOLDER (same folder, unique file per forest) =====
$OutputFolder = "\\Computername\C$\Scripts\Health Check\Reports\" 
New-Item -Path $OutputFolder -ItemType Directory -Force | Out-Null

$ForestName = (Get-ADForest).Name

# Make filename safe for Windows (replace characters like :)
$safeForest = $ForestName -replace '[\\/:*?"<>| ]','_'

# Overwrite a single file per forest (no file buildup)
$OutFile = Join-Path $OutputFolder ("ADHealth_Latest_{0}.html" -f $safeForest)

$htmlbody | Out-File -FilePath $OutFile -Encoding UTF8
Write-Host "Report written to: $OutFile" -ForegroundColor Green
K Shankar R Karanth
K Shankar R Karanth
Articles: 8