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:
- Discover all domains in the forest.
- Identify the PDC Emulator of each domain.
- Execute a data-collection script block remotely on each PDC.
- From the PDC, enumerate all Domain Controllers in that domain.
- Run health checks against each DC.
- Build a structured HTML report with visual status indicators or Save a single consolidated forest-level report.
It relies on:
Get-ADForestGet-ADDomainGet-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

