In our previous article i have explained to generate Comprehensive report for SCCM server and its components. which will make administrator easy to find and troubleshoot the error. At Here i will be explaining you to generate report from your domain controllers and along to score card of the server, recommendation and remediations.
This script is safe.
Copy the below code and paste in your domain controller with any name with extension .ps1
example: ADHealthCheck.ps1
# ==========================================================
# COMPREHENSIVE ACTIVE DIRECTORY HEALTH CHECK SCRIPT
# M Server Pro Pvt. Ltd. - Infrastructure & EUC Practice
# Author: Habib
# Version: 3.2
# ==========================================================
# ================= CONFIGURATION ==========================
param(
[string]$OutputPath = "C:\ADHealthReports",
[switch]$OpenReport,
[switch]$ExportCSV
)
# Company Information
$CompanyInfo = @{
Name = "M Server Pro Pvt. Ltd."
Practice = "Infrastructure & EUC Practice"
Author = "Habib"
Version = "3.2"
SupportEmail = "infrastructure@techoneglobal.com"
}
# Compliance thresholds
$Thresholds = @{
MaxCPU = 85
MaxMemory = 90
MinDiskSpaceGB = 15
MaxUptimeDays = 30
InactiveUserDays = 90
InactiveComputerDays = 60
GPOStaleDays = 180
MaxGPOSizeMB = 100
}
# ================= INITIALIZATION =========================
function Initialize-Script {
Write-Host "Initializing AD Health Check..." -ForegroundColor Cyan
# Create output directory if they don't exist
$directories = @($OutputPath, "$OutputPath\Logs", "$OutputPath\Archives", "$OutputPath\CSV")
foreach ($dir in $directories) {
if (!(Test-Path $dir)) {
New-Item -ItemType Directory -Path $dir -Force | Out-Null
Write-Host "Created directory: $dir" -ForegroundColor Green
}
}
# Import required modules
$requiredModules = @("ActiveDirectory", "GroupPolicy")
foreach ($module in $requiredModules) {
try {
Import-Module $module -ErrorAction Stop
Write-Host "Imported module: $module" -ForegroundColor Green
}
catch {
Write-Host "ERROR: Failed to import module $module" -ForegroundColor Red
Write-Host "Please install RSAT tools: Install-WindowsFeature RSAT-AD-PowerShell" -ForegroundColor Yellow
exit 1
}
}
# Try to import DNS module but don't fail if not available
try {
Import-Module DnsServer -ErrorAction SilentlyContinue
Write-Host "Imported module: DnsServer" -ForegroundColor Green
}
catch {
Write-Host "Note: DnsServer module not available. DNS checks will be limited." -ForegroundColor Yellow
}
}
# ================= HTML REPORT FUNCTIONS ==================
function Get-HTMLHeader {
param($DomainName, $ForestName)
$ReportDate = Get-Date -Format "MMMM dd, yyyy"
$ReportTime = Get-Date -Format "HH:mm:ss"
return @"
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Active Directory Health Check Report - $DomainName</title>
<style>
body {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
background-color: #f8f9fa;
margin: 0;
padding: 0;
color: #333;
line-height: 1.6;
}
.container {
max-width: 1400px;
margin: 0 auto;
padding: 20px;
}
/* Header Styles */
.report-header {
background: linear-gradient(135deg, #0b5394 0%, #083e6b 100%);
color: white;
padding: 30px;
border-bottom: 5px solid #ffc107;
margin-bottom: 30px;
border-radius: 8px;
box-shadow: 0 4px 6px rgba(0,0,0,0.1);
}
.company-info {
margin-bottom: 20px;
}
.company-name {
font-size: 1.8rem;
font-weight: 700;
margin: 0;
color: white;
}
.company-practice {
font-size: 1rem;
opacity: 0.9;
margin: 5px 0 0 0;
font-style: italic;
}
.report-header h1 {
margin: 10px 0 0 0;
font-size: 2.2rem;
font-weight: 600;
color: white;
}
.report-meta {
display: flex;
flex-wrap: wrap;
gap: 20px;
margin-top: 20px;
background: rgba(255, 255, 255, 0.1);
padding: 15px;
border-radius: 6px;
}
.meta-item {
flex: 1;
min-width: 200px;
}
.meta-label {
font-size: 0.85rem;
opacity: 0.9;
text-transform: uppercase;
letter-spacing: 0.5px;
margin-bottom: 5px;
}
.meta-value {
font-size: 1.1rem;
font-weight: 600;
}
.author-info {
font-size: 0.9rem;
margin-top: 10px;
opacity: 0.8;
}
/* Card Styles */
.card {
background: white;
border-radius: 8px;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
margin-bottom: 25px;
overflow: hidden;
border: 1px solid #dee2e6;
}
.card-header {
background: linear-gradient(to right, #e6f2ff, white);
padding: 15px 20px;
border-bottom: 2px solid #dee2e6;
}
.card-header h2 {
margin: 0;
color: #0b5394;
font-size: 1.3rem;
font-weight: 600;
}
.card-body {
padding: 20px;
}
/* Table Styles */
.data-table {
width: 100%;
border-collapse: collapse;
font-size: 0.95rem;
}
.data-table th {
background-color: #e9ecef;
color: #495057;
font-weight: 600;
padding: 12px 15px;
text-align: left;
border-bottom: 2px solid #dee2e6;
}
.data-table td {
padding: 10px 15px;
border-bottom: 1px solid #dee2e6;
vertical-align: middle;
}
.data-table tr:hover {
background-color: #f8f9fa;
}
/* Status Indicators */
.status {
display: inline-block;
padding: 4px 10px;
border-radius: 20px;
font-size: 0.8rem;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.5px;
}
.status-healthy {
background-color: #d4edda;
color: #155724;
border: 1px solid #c3e6cb;
}
.status-warning {
background-color: #fff3cd;
color: #856404;
border: 1px solid #ffeaa7;
}
.status-critical {
background-color: #f8d7da;
color: #721c24;
border: 1px solid #f5c6cb;
}
.status-info {
background-color: #d1ecf1;
color: #0c5460;
border: 1px solid #bee5eb;
}
/* Progress Bars */
.progress-container {
width: 100%;
background-color: #e9ecef;
border-radius: 10px;
overflow: hidden;
height: 20px;
}
.progress-bar {
height: 100%;
border-radius: 10px;
transition: width 0.3s ease;
}
.progress-cpu {
background: linear-gradient(90deg, #28a745, #17a2b8);
}
.progress-memory {
background: linear-gradient(90deg, #28a745, #ffc107);
}
.progress-disk {
background: linear-gradient(90deg, #28a745, #dc3545);
}
/* Summary Grid */
.summary-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 20px;
margin: 20px 0;
}
.summary-card {
text-align: center;
padding: 20px;
border-radius: 8px;
background: white;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
border-top: 4px solid;
}
.summary-card .count {
font-size: 2.2rem;
font-weight: 700;
margin: 10px 0;
color: #0b5394;
}
.summary-card .label {
font-size: 0.9rem;
color: #6c757d;
text-transform: uppercase;
letter-spacing: 1px;
}
.summary-card.total { border-top-color: #0b5394; }
.summary-card.healthy { border-top-color: #28a745; }
.summary-card.warning { border-top-color: #ffc107; }
.summary-card.critical { border-top-color: #dc3545; }
/* Footer */
.report-footer {
background: #343a40;
color: white;
padding: 20px;
margin-top: 40px;
border-radius: 8px;
text-align: center;
}
.company-footer {
margin-bottom: 15px;
font-size: 1.1rem;
font-weight: 600;
color: #ffc107;
}
.report-footer small {
color: #adb5bd;
font-size: 0.9rem;
}
/* Responsive Design */
@media (max-width: 768px) {
.container {
padding: 10px;
}
.report-header {
padding: 20px;
}
.report-header h1 {
font-size: 1.8rem;
}
.data-table {
font-size: 0.85rem;
}
.summary-grid {
grid-template-columns: 1fr;
}
}
/* Print Styles */
@media print {
body {
background: white;
}
.card {
box-shadow: none;
border: 1px solid #ddd;
}
.report-footer {
background: white;
color: black;
border-top: 2px solid #ddd;
}
}
</style>
</head>
<body>
<div class="report-header">
<div class="container">
<div class="company-info">
<div class="company-name">$($CompanyInfo.Name)</div>
<div class="company-practice">$($CompanyInfo.Practice)</div>
</div>
<h1>Active Directory Health Check Report</h1>
<div class="author-info">Prepared by: $($CompanyInfo.Author) | Version: $($CompanyInfo.Version)</div>
<div class="report-meta">
<div class="meta-item">
<div class="meta-label">Domain</div>
<div class="meta-value">$DomainName</div>
</div>
<div class="meta-item">
<div class="meta-label">Forest</div>
<div class="meta-value">$ForestName</div>
</div>
<div class="meta-item">
<div class="meta-label">Report Date</div>
<div class="meta-value">$ReportDate</div>
</div>
<div class="meta-item">
<div class="meta-label">Report Time</div>
<div class="meta-value">$ReportTime</div>
</div>
</div>
</div>
</div>
<div class="container">
"@
}
function Get-HTMLFooter {
return @"
</div>
<div class="report-footer">
<div class="container">
<div class="company-footer">$($CompanyInfo.Name)</div>
<small>Report generated: $(Get-Date -Format 'yyyy-MM-dd HH:mm:ss') | Active Directory Health Check v$($CompanyInfo.Version)</small><br>
<small>Prepared by: $($CompanyInfo.Author) | $($CompanyInfo.Practice)</small><br>
<small>This report contains confidential information for authorized personnel only.</small>
</div>
</div>
<script>
// Simple interactive features
document.addEventListener('DOMContentLoaded', function() {
// Toggle table rows on mobile
if (window.innerWidth <= 768) {
document.querySelectorAll('.card-header').forEach(header => {
header.style.cursor = 'pointer';
header.addEventListener('click', function() {
const cardBody = this.nextElementSibling;
cardBody.style.display = cardBody.style.display === 'none' ? 'block' : 'none';
});
});
}
// Highlight critical rows
document.querySelectorAll('.status-critical').forEach(element => {
const row = element.closest('tr');
if (row) {
row.style.backgroundColor = 'rgba(220, 53, 69, 0.05)';
}
});
// Highlight warning rows
document.querySelectorAll('.status-warning').forEach(element => {
const row = element.closest('tr');
if (row) {
row.style.backgroundColor = 'rgba(255, 193, 7, 0.05)';
}
});
});
</script>
</body>
</html>
"@
}
# ================= HEALTH CHECK FUNCTIONS ================
function Get-DCHealth {
param($DCs)
Write-Host "Checking Domain Controller Health..." -ForegroundColor Cyan
$dcResults = @()
foreach ($DC in $DCs) {
try {
$DCName = $DC.HostName
Write-Host " Checking $DCName..." -ForegroundColor Gray
# Get DC info
$Roles = @()
if ($DC.OperationMasterRoles -contains "PDCEmulator") { $Roles += "PDC" }
if ($DC.OperationMasterRoles -contains "RIDMaster") { $Roles += "RID" }
if ($DC.OperationMasterRoles -contains "InfrastructureMaster") { $Roles += "Infra" }
if ($DC.OperationMasterRoles -contains "SchemaMaster") { $Roles += "Schema" }
if ($DC.OperationMasterRoles -contains "DomainNamingMaster") { $Roles += "DomainNaming" }
$RolesString = if ($Roles.Count -gt 0) { $Roles -join ", " } else { "Member" }
# Get performance metrics
$OS = Get-CimInstance Win32_OperatingSystem -ComputerName $DCName -ErrorAction Stop
$CPU = [math]::Round((Get-Counter -ComputerName $DCName '\Processor(_Total)\% Processor Time' -ErrorAction Stop).CounterSamples.CookedValue, 1)
$Mem = [math]::Round((($OS.TotalVisibleMemorySize - $OS.FreePhysicalMemory) / $OS.TotalVisibleMemorySize) * 100, 1)
$Disk = Get-CimInstance Win32_LogicalDisk -ComputerName $DCName -Filter "DeviceID='C:'" -ErrorAction Stop
$DiskFree = [math]::Round($Disk.FreeSpace / 1GB, 1)
$DiskTotal = [math]::Round($Disk.Size / 1GB, 1)
$Uptime = (New-TimeSpan -Start $OS.LastBootUpTime -End (Get-Date)).Days
# Determine status
$Status = "Healthy"
$StatusClass = "status-healthy"
if ($CPU -gt $Thresholds.MaxCPU -or $Mem -gt $Thresholds.MaxMemory -or $DiskFree -lt $Thresholds.MinDiskSpaceGB) {
$Status = "Critical"
$StatusClass = "status-critical"
}
elseif ($CPU -gt 70 -or $Mem -gt 80 -or $DiskFree -lt 25 -or $Uptime -gt $Thresholds.MaxUptimeDays) {
$Status = "Warning"
$StatusClass = "status-warning"
}
$dcResults += [PSCustomObject]@{
DCName = $DCName
Site = $DC.Site
Roles = $RolesString
CPU = $CPU
Memory = $Mem
DiskFreeGB = $DiskFree
DiskTotalGB = $DiskTotal
UptimeDays = $Uptime
Status = $Status
StatusClass = $StatusClass
}
Write-Host " CPU: $CPU%, Memory: $Mem%, Disk: $DiskFree GB free, Status: $Status" -ForegroundColor Green
}
catch {
Write-Host " ERROR: Failed to query $($DC.HostName): $($_.Exception.Message)" -ForegroundColor Red
$dcResults += [PSCustomObject]@{
DCName = $DC.HostName
Site = "Error"
Roles = "Error"
CPU = 0
Memory = 0
DiskFreeGB = 0
DiskTotalGB = 0
UptimeDays = 0
Status = "Error"
StatusClass = "status-critical"
}
}
}
return $dcResults
}
function Get-ADServicesStatus {
param($DCs)
Write-Host "Checking AD Services Status..." -ForegroundColor Cyan
$servicesResults = @()
$Services = @("NTDS", "DNS", "Netlogon", "KDC", "W32Time")
foreach ($DC in $DCs) {
Write-Host " Checking services on $($DC.HostName)..." -ForegroundColor Gray
$serviceStatus = @{}
$allRunning = $true
$runningCount = 0
foreach ($Service in $Services) {
try {
$Status = (Get-Service -ComputerName $DC.HostName -Name $Service -ErrorAction Stop).Status
$serviceStatus[$Service] = if ($Status -eq "Running") { "Running" } else { "Stopped" }
$allRunning = $allRunning -and ($Status -eq "Running")
if ($Status -eq "Running") { $runningCount++ }
}
catch {
$serviceStatus[$Service] = "Error"
$allRunning = $false
}
}
$OverallStatus = if ($allRunning) { "Healthy" } elseif ($runningCount -eq 0) { "Critical" } else { "Degraded" }
$OverallClass = if ($allRunning) { "status-healthy" } elseif ($runningCount -eq 0) { "status-critical" } else { "status-warning" }
$servicesResults += [PSCustomObject]@{
DCName = $DC.HostName
NTDS = $serviceStatus["NTDS"]
DNS = $serviceStatus["DNS"]
Netlogon = $serviceStatus["Netlogon"]
KDC = $serviceStatus["KDC"]
W32Time = $serviceStatus["W32Time"]
Overall = $OverallStatus
OverallClass = $OverallClass
}
Write-Host " Services status: $OverallStatus ($runningCount/$($Services.Count) running)" -ForegroundColor Green
}
return $servicesResults
}
function Get-ReplicationStatus {
Write-Host "Checking AD Replication..." -ForegroundColor Cyan
try {
$replication = Get-ADReplicationFailure -Target * -ErrorAction Stop
$failures = ($replication | Where-Object {$_.FailureCount -gt 0}).Count
if ($failures -eq 0) {
Write-Host " Replication: Healthy (No failures detected)" -ForegroundColor Green
}
else {
Write-Host " Replication: Warning ($failures failures detected)" -ForegroundColor Yellow
}
return $failures
}
catch {
Write-Host " ERROR: Failed to check replication: $($_.Exception.Message)" -ForegroundColor Red
return -1
}
}
function Get-UserAccountAnalysis {
Write-Host "Analyzing User Accounts..." -ForegroundColor Cyan
try {
$Users = Get-ADUser -Filter * -Properties Enabled, LastLogonDate, PasswordNeverExpires, PasswordExpired, LockedOut, whenCreated -ErrorAction Stop
$analysis = @{
Total = $Users.Count
Enabled = ($Users | Where-Object {$_.Enabled}).Count
Disabled = ($Users | Where-Object {-not $_.Enabled}).Count
Locked = ($Users | Where-Object {$_.LockedOut}).Count
PasswordNeverExpires = ($Users | Where-Object {$_.PasswordNeverExpires}).Count
PasswordExpired = ($Users | Where-Object {$_.PasswordExpired}).Count
Inactive30 = ($Users | Where-Object {$_.Enabled -and $_.LastLogonDate -and $_.LastLogonDate -lt (Get-Date).AddDays(-30)}).Count
Inactive60 = ($Users | Where-Object {$_.Enabled -and $_.LastLogonDate -and $_.LastLogonDate -lt (Get-Date).AddDays(-60)}).Count
Inactive90 = ($Users | Where-Object {$_.Enabled -and $_.LastLogonDate -and $_.LastLogonDate -lt (Get-Date).AddDays(-90)}).Count
}
Write-Host " Total Users: $($analysis.Total)" -ForegroundColor Green
Write-Host " Enabled Users: $($analysis.Enabled)" -ForegroundColor Green
Write-Host " Password Never Expires: $($analysis.PasswordNeverExpires)" -ForegroundColor $(if ($analysis.PasswordNeverExpires -eq 0) { "Green" } else { "Yellow" })
Write-Host " Inactive (90+ days): $($analysis.Inactive90)" -ForegroundColor $(if ($analysis.Inactive90 -eq 0) { "Green" } else { "Yellow" })
return $analysis
}
catch {
Write-Host " ERROR: Failed to analyze user accounts: $($_.Exception.Message)" -ForegroundColor Red
return $null
}
}
function Get-ComputerAccountAnalysis {
Write-Host "Analyzing Computer Accounts..." -ForegroundColor Cyan
try {
$Computers = Get-ADComputer -Filter * -Properties Enabled, LastLogonDate, OperatingSystem, whenCreated -ErrorAction Stop
$analysis = @{
Total = $Computers.Count
Enabled = ($Computers | Where-Object {$_.Enabled}).Count
Disabled = ($Computers | Where-Object {-not $_.Enabled}).Count
Servers = ($Computers | Where-Object {$_.OperatingSystem -match 'Server'}).Count
Workstations = ($Computers | Where-Object {$_.OperatingSystem -notmatch 'Server' -and $_.OperatingSystem -notmatch '$'}).Count
DCs = ($Computers | Where-Object {$_.OperatingSystem -match 'Server' -and $_.DNSHostName -match 'DC'}).Count
Inactive30 = 0
Inactive60 = 0
Inactive90 = 0
}
foreach ($Computer in $Computers) {
if ($Computer.LastLogonDate -and $Computer.LastLogonDate -lt (Get-Date).AddDays(-30)) {
$analysis.Inactive30++
}
if ($Computer.LastLogonDate -and $Computer.LastLogonDate -lt (Get-Date).AddDays(-60)) {
$analysis.Inactive60++
}
if (($Computer.LastLogonDate -and $Computer.LastLogonDate -lt (Get-Date).AddDays(-90)) -or
(-not $Computer.LastLogonDate -and $Computer.whenCreated -lt (Get-Date).AddDays(-90))) {
$analysis.Inactive90++
}
}
Write-Host " Total Computers: $($analysis.Total)" -ForegroundColor Green
Write-Host " Servers: $($analysis.Servers)" -ForegroundColor Green
Write-Host " Domain Controllers: $($analysis.DCs)" -ForegroundColor Green
Write-Host " Inactive (90+ days): $($analysis.Inactive90)" -ForegroundColor $(if ($analysis.Inactive90 -eq 0) { "Green" } else { "Yellow" })
return $analysis
}
catch {
Write-Host " ERROR: Failed to analyze computer accounts: $($_.Exception.Message)" -ForegroundColor Red
return $null
}
}
function Get-ADRecycleBinStatus {
Write-Host "Checking AD Recycle Bin..." -ForegroundColor Cyan
try {
$RecycleBin = Get-ADOptionalFeature -Filter 'name -like "Recycle Bin Feature"' -ErrorAction Stop
$RBEnabled = $RecycleBin.EnabledScopes.Count -gt 0
if ($RBEnabled) {
try {
$DeletedUsers = (Get-ADObject -Filter 'isDeleted -eq $true -and objectClass -eq "user"' -IncludeDeletedObjects -ErrorAction Stop).Count
}
catch {
$DeletedUsers = "Access Denied"
}
try {
$DeletedComputers = (Get-ADObject -Filter 'isDeleted -eq $true -and objectClass -eq "computer"' -IncludeDeletedObjects -ErrorAction Stop).Count
}
catch {
$DeletedComputers = "Access Denied"
}
try {
$DeletedGroups = (Get-ADObject -Filter 'isDeleted -eq $true -and objectClass -eq "group"' -IncludeDeletedObjects -ErrorAction Stop).Count
}
catch {
$DeletedGroups = "Access Denied"
}
Write-Host " Recycle Bin: Enabled" -ForegroundColor Green
Write-Host " Deleted Users: $DeletedUsers" -ForegroundColor Gray
Write-Host " Deleted Computers: $DeletedComputers" -ForegroundColor Gray
return [PSCustomObject]@{
Enabled = $true
Status = "Enabled"
StatusClass = "status-healthy"
DeletedUsers = $DeletedUsers
DeletedComputers = $DeletedComputers
DeletedGroups = $DeletedGroups
}
}
else {
Write-Host " Recycle Bin: Disabled" -ForegroundColor Yellow
return [PSCustomObject]@{
Enabled = $false
Status = "Disabled"
StatusClass = "status-warning"
DeletedUsers = "N/A"
DeletedComputers = "N/A"
DeletedGroups = "N/A"
}
}
}
catch {
Write-Host " ERROR: Failed to check Recycle Bin: $($_.Exception.Message)" -ForegroundColor Red
return [PSCustomObject]@{
Enabled = $false
Status = "Error"
StatusClass = "status-critical"
DeletedUsers = "Error"
DeletedComputers = "Error"
DeletedGroups = "Error"
}
}
}
function Get-PasswordPolicyStatus {
Write-Host "Checking Password Policy..." -ForegroundColor Cyan
try {
$Policy = Get-ADDefaultDomainPasswordPolicy -ErrorAction Stop
$status = [PSCustomObject]@{
MinLength = $Policy.MinPasswordLength
Complexity = $Policy.ComplexityEnabled
MaxAge = $Policy.MaxPasswordAge.Days
HistoryCount = $Policy.PasswordHistoryCount
LockoutThreshold = $Policy.LockoutThreshold
LockoutDuration = $Policy.LockoutDuration
LockoutObservationWindow = $Policy.LockoutObservationWindow
}
Write-Host " Password Policy retrieved successfully" -ForegroundColor Green
return $status
}
catch {
Write-Host " ERROR: Failed to get password policy: $($_.Exception.Message)" -ForegroundColor Red
return $null
}
}
# ================= GPO ANALYSIS FUNCTIONS =================
function Get-GPOAnalysis {
Write-Host "Analyzing Group Policy Objects..." -ForegroundColor Cyan
try {
$AllGPOs = Get-GPO -All -ErrorAction Stop
Write-Host " Found $($AllGPOs.Count) GPOs in domain" -ForegroundColor Green
$GPOAnalysis = @()
$GPOStatistics = @{
Total = $AllGPOs.Count
Unlinked = 0
Stale = 0
Large = 0
Disabled = 0
MissingPermissions = 0
}
foreach ($GPO in $AllGPOs) {
$GPODetails = @{
Name = $GPO.DisplayName
ID = $GPO.Id
Owner = $GPO.Owner
Created = $GPO.CreationTime
Modified = $GPO.ModificationTime
Enabled = $GPO.GpoStatus -eq "AllSettingsEnabled"
WmiFilter = $GPO.WmiFilter.Name
}
# Get GPO links
try {
$GPOLinks = Get-GPOReport -Guid $GPO.Id -ReportType XML
$XMLReport = [xml]$GPOLinks
$Links = $XMLReport.GPO.LinksTo
$LinkCount = if ($Links) { $Links.Count } else { 0 }
$GPODetails.LinkCount = $LinkCount
if ($LinkCount -eq 0) {
$GPOStatistics.Unlinked++
$GPODetails.IsUnlinked = $true
}
else {
$GPODetails.IsUnlinked = $false
}
}
catch {
$GPODetails.LinkCount = "Error"
$GPODetails.IsUnlinked = $null
}
# Check if GPO is stale (not modified in X days)
$DaysSinceModification = (Get-Date) - $GPO.ModificationTime
$GPODetails.DaysSinceModification = $DaysSinceModification.Days
if ($DaysSinceModification.Days -gt $Thresholds.GPOStaleDays) {
$GPOStatistics.Stale++
$GPODetails.IsStale = $true
}
else {
$GPODetails.IsStale = $false
}
# Check GPO size
try {
$GPOFolder = "\\$DomainName\SYSVOL\$DomainName\Policies\{$($GPO.Id)}\"
if (Test-Path $GPOFolder) {
$GPOSize = (Get-ChildItem $GPOFolder -Recurse | Measure-Object Length -Sum).Sum / 1MB
$GPODetails.SizeMB = [math]::Round($GPOSize, 2)
if ($GPOSize -gt $Thresholds.MaxGPOSizeMB) {
$GPOStatistics.Large++
$GPODetails.IsLarge = $true
}
else {
$GPODetails.IsLarge = $false
}
}
else {
$GPODetails.SizeMB = "N/A"
$GPODetails.IsLarge = $false
}
}
catch {
$GPODetails.SizeMB = "Error"
$GPODetails.IsLarge = $false
}
if (-not $GPODetails.Enabled) {
$GPOStatistics.Disabled++
}
$GPOAnalysis += [PSCustomObject]$GPODetails
}
# Get orphaned GPOs (in SYSVOL but not in AD)
Write-Host " Checking for orphaned GPOs in SYSVOL..." -ForegroundColor Gray
$OrphanedGPOs = Get-OrphanedGPOs -DomainName $DomainName
$GPOStatistics.Orphaned = $OrphanedGPOs.Count
return @{
GPOs = $GPOAnalysis
Statistics = $GPOStatistics
OrphanedGPOs = $OrphanedGPOs
}
}
catch {
Write-Host " ERROR: Failed to analyze GPOs: $($_.Exception.Message)" -ForegroundColor Red
return $null
}
}
function Get-OrphanedGPOs {
param($DomainName)
$Orphaned = @()
try {
$SYSVOLPath = "\\$DomainName\SYSVOL\$DomainName\Policies\"
if (Test-Path $SYSVOLPath) {
$GPODirs = Get-ChildItem -Path $SYSVOLPath -Directory -Filter "{*}"
foreach ($Dir in $GPODirs) {
$GPOID = $Dir.Name.Trim('{}')
$GPO = $null
try {
$GPO = Get-GPO -Guid $GPOID -ErrorAction SilentlyContinue
}
catch {}
if (-not $GPO) {
$DirSize = (Get-ChildItem $Dir.FullName -Recurse | Measure-Object Length -Sum).Sum / 1MB
$Orphaned += [PSCustomObject]@{
ID = $GPOID
Path = $Dir.FullName
SizeMB = [math]::Round($DirSize, 2)
LastModified = $Dir.LastWriteTime
}
}
}
}
}
catch {
Write-Host " WARNING: Could not check for orphaned GPOs" -ForegroundColor Yellow
}
return $Orphaned
}
# ================= DNS HEALTH FUNCTIONS ===================
function Get-DNSHealth {
param($DCs)
Write-Host "Checking DNS Health..." -ForegroundColor Cyan
$DNSResults = @()
foreach ($DC in $DCs) {
try {
Write-Host " Checking DNS on $($DC.HostName)..." -ForegroundColor Gray
# Check if DNS module is available
if (Get-Module -Name DnsServer -ListAvailable) {
Import-Module DnsServer -ErrorAction SilentlyContinue
# Get DNS zones
$Zones = Get-DnsServerZone -ComputerName $DC.HostName -ErrorAction Stop
$ZoneStats = @{
ForwardZones = ($Zones | Where-Object { -not $_.IsReverseLookupZone }).Count
ReverseZones = ($Zones | Where-Object { $_.IsReverseLookupZone }).Count
ADIntegrated = ($Zones | Where-Object { $_.ZoneType -eq "Primary" -and $_.IsDsIntegrated }).Count
}
# Check for stale records
$StaleRecords = 0
foreach ($Zone in ($Zones | Where-Object { -not $_.IsReverseLookupZone -and $_.ZoneName -ne "TrustAnchors" })) {
try {
$Records = Get-DnsServerResourceRecord -ComputerName $DC.HostName -ZoneName $Zone.ZoneName -ErrorAction SilentlyContinue
$StaleRecords += ($Records | Where-Object {
$_.Timestamp -and $_.Timestamp -lt (Get-Date).AddDays(-30)
}).Count
}
catch {}
}
# Check scavenging settings
$Scavenging = Get-DnsServerScavenging -ComputerName $DC.HostName -ErrorAction SilentlyContinue
$DNSResults += [PSCustomObject]@{
DCName = $DC.HostName
Zones = $Zones.Count
ForwardZones = $ZoneStats.ForwardZones
ReverseZones = $ZoneStats.ReverseZones
ADIntegrated = $ZoneStats.ADIntegrated
StaleRecords = $StaleRecords
ScavengingEnabled = if ($Scavenging) { $Scavenging.ScavengingState } else { $false }
NoRefreshInterval = if ($Scavenging) { $Scavenging.NoRefreshInterval.Days } else { 0 }
RefreshInterval = if ($Scavenging) { $Scavenging.RefreshInterval.Days } else { 0 }
Status = "Checked"
}
Write-Host " Zones: $($Zones.Count) (Forward: $($ZoneStats.ForwardZones), Reverse: $($ZoneStats.ReverseZones))" -ForegroundColor Green
}
else {
$DNSResults += [PSCustomObject]@{
DCName = $DC.HostName
Zones = "DNS Module Not Available"
ForwardZones = "N/A"
ReverseZones = "N/A"
ADIntegrated = "N/A"
StaleRecords = "N/A"
ScavengingEnabled = "N/A"
NoRefreshInterval = "N/A"
RefreshInterval = "N/A"
Status = "DNS Module Missing"
}
Write-Host " DNS Server module not installed on $($DC.HostName)" -ForegroundColor Yellow
}
}
catch {
Write-Host " ERROR: Failed to check DNS on $($DC.HostName): $($_.Exception.Message)" -ForegroundColor Red
$DNSResults += [PSCustomObject]@{
DCName = $DC.HostName
Zones = "Error"
ForwardZones = "Error"
ReverseZones = "Error"
ADIntegrated = "Error"
StaleRecords = "Error"
ScavengingEnabled = "Error"
NoRefreshInterval = "Error"
RefreshInterval = "Error"
Status = "Error"
}
}
}
return $DNSResults
}
# ================= SITE & REPLICATION FUNCTIONS ===========
function Get-ADSiteReplicationInfo {
Write-Host "Checking AD Sites & Replication..." -ForegroundColor Cyan
try {
# Alternative method to get site information using ADSI
Write-Host " Getting AD Sites information..." -ForegroundColor Gray
# Method 1: Try using Get-ADObject if Get-ADSite is not available
$Sites = @()
try {
# Try to get sites using ADSI
$SitesContainer = [ADSI]"LDAP://CN=Sites,CN=Configuration,$((Get-ADDomain).DistinguishedName)"
$SiteObjects = $SitesContainer.Children | Where-Object { $_.SchemaClassName -eq "site" }
foreach ($Site in $SiteObjects) {
$Sites += [PSCustomObject]@{
Name = $Site.Name
Location = $Site.Location
Description = $Site.Description
}
}
Write-Host " Found $($Sites.Count) AD sites using ADSI" -ForegroundColor Green
}
catch {
Write-Host " WARNING: Could not retrieve detailed site information" -ForegroundColor Yellow
# Create a simple site object
$Sites = @([PSCustomObject]@{
Name = "Default-First-Site-Name"
Location = ""
Description = "Default site"
})
}
# Get replication failures
$ReplicationFailures = 0
try {
$replication = Get-ADReplicationFailure -Target * -ErrorAction Stop
$ReplicationFailures = ($replication | Where-Object {$_.FailureCount -gt 0}).Count
}
catch {
Write-Host " WARNING: Could not get replication failures" -ForegroundColor Yellow
}
# Get replication summary
$ReplicationSummary = @()
try {
$ReplicationSummary = Get-ADReplicationSummary -ErrorAction Stop | Select-Object -First 5
}
catch {
Write-Host " WARNING: Could not get detailed replication summary" -ForegroundColor Yellow
}
return @{
Sites = $Sites
ReplicationFailures = $ReplicationFailures
ReplicationSummary = $ReplicationSummary
}
}
catch {
Write-Host " ERROR: Failed to get site/replication info: $($_.Exception.Message)" -ForegroundColor Red
return $null
}
}
# ================= FSMO ROLES FUNCTIONS ===================
function Get-FSMORoles {
Write-Host "Checking FSMO Roles..." -ForegroundColor Cyan
try {
$FSMORoles = @()
# Get all FSMO role holders
$Forest = Get-ADForest -ErrorAction Stop
$Domain = Get-ADDomain -ErrorAction Stop
$FSMORoles += [PSCustomObject]@{
Role = "Schema Master"
Holder = $Forest.SchemaMaster
RoleType = "Forest"
}
$FSMORoles += [PSCustomObject]@{
Role = "Domain Naming Master"
Holder = $Forest.DomainNamingMaster
RoleType = "Forest"
}
$FSMORoles += [PSCustomObject]@{
Role = "PDC Emulator"
Holder = $Domain.PDCEmulator
RoleType = "Domain"
}
$FSMORoles += [PSCustomObject]@{
Role = "RID Master"
Holder = $Domain.RIDMaster
RoleType = "Domain"
}
$FSMORoles += [PSCustomObject]@{
Role = "Infrastructure Master"
Holder = $Domain.InfrastructureMaster
RoleType = "Domain"
}
# Check if roles are online
foreach ($Role in $FSMORoles) {
try {
$HostName = $Role.Holder -replace "^.*\\", ""
$Test = Test-Connection -ComputerName $HostName -Count 1 -Quiet -ErrorAction Stop
$Role | Add-Member -NotePropertyName IsOnline -NotePropertyValue $Test -Force
}
catch {
$Role | Add-Member -NotePropertyName IsOnline -NotePropertyValue $false -Force
}
}
Write-Host " All FSMO roles located" -ForegroundColor Green
return $FSMORoles
}
catch {
Write-Host " ERROR: Failed to get FSMO roles: $($_.Exception.Message)" -ForegroundColor Red
return $null
}
}
# ================= BACKUP & RECOVERY FUNCTIONS ============
function Get-BackupStatus {
param($DCs)
Write-Host "Checking Backup Status..." -ForegroundColor Cyan
$BackupStatus = @()
foreach ($DC in $DCs) {
try {
Write-Host " Checking backups on $($DC.HostName)..." -ForegroundColor Gray
# Check for Windows Server Backup events
$BackupEvents = $null
try {
$BackupEvents = Get-WinEvent -ComputerName $DC.HostName -FilterHashtable @{
LogName = 'Application'
ProviderName = 'Microsoft-Windows-Backup'
ID = 4, 5
StartTime = (Get-Date).AddDays(-7)
} -MaxEvents 5 -ErrorAction SilentlyContinue
}
catch {
Write-Host " WARNING: Could not access backup events on $($DC.HostName)" -ForegroundColor Yellow
}
$LastBackup = if ($BackupEvents) {
($BackupEvents | Sort-Object TimeCreated -Descending | Select-Object -First 1).TimeCreated
} else {
$null
}
$BackupStatus += [PSCustomObject]@{
DCName = $DC.HostName
LastBackup = $LastBackup
DaysSinceLastBackup = if ($LastBackup) { ((Get-Date) - $LastBackup).Days } else { 999 }
BackupEventsFound = if ($BackupEvents) { $BackupEvents.Count } else { 0 }
}
$StatusText = if ($LastBackup) { "Last backup: $($LastBackup.ToString('yyyy-MM-dd HH:mm'))" } else { "No recent backup events found" }
Write-Host " $StatusText" -ForegroundColor $(if ($LastBackup) { "Green" } else { "Yellow" })
}
catch {
Write-Host " ERROR: Failed to check backups on $($DC.HostName)" -ForegroundColor Red
$BackupStatus += [PSCustomObject]@{
DCName = $DC.HostName
LastBackup = $null
DaysSinceLastBackup = 999
BackupEventsFound = 0
}
}
}
return $BackupStatus
}
# ================= TRUST RELATIONSHIPS ====================
function Get-TrustRelationships {
Write-Host "Checking Trust Relationships..." -ForegroundColor Cyan
try {
$Trusts = Get-ADTrust -Filter * -ErrorAction Stop
$TrustAnalysis = foreach ($Trust in $Trusts) {
[PSCustomObject]@{
Name = $Trust.Name
Direction = $Trust.Direction
Type = $Trust.TrustType
Transitive = $Trust.Transitive
Created = $Trust.whenCreated
}
}
Write-Host " Found $($Trusts.Count) trust relationships" -ForegroundColor Green
return $TrustAnalysis
}
catch {
Write-Host " WARNING: Failed to get trust relationships: $($_.Exception.Message)" -ForegroundColor Yellow
return $null
}
}
# ================= MAIN SCRIPT ============================
Write-Host "=========================================" -ForegroundColor Cyan
Write-Host "COMPREHENSIVE AD HEALTH CHECK v$($CompanyInfo.Version)" -ForegroundColor Cyan
Write-Host "Company: $($CompanyInfo.Name)" -ForegroundColor Cyan
Write-Host "Author: $($CompanyInfo.Author)" -ForegroundColor Cyan
Write-Host "Practice: $($CompanyInfo.Practice)" -ForegroundColor Cyan
Write-Host "=========================================" -ForegroundColor Cyan
# Initialize
Initialize-Script
try {
# Get domain information
Write-Host "Getting domain information..." -ForegroundColor Cyan
$DomainInfo = Get-ADDomain -ErrorAction Stop
$ForestInfo = Get-ADForest -ErrorAction Stop
$DomainName = $DomainInfo.DNSRoot
$ForestName = $ForestInfo.Name
Write-Host "Domain: $DomainName" -ForegroundColor Green
Write-Host "Forest: $ForestName" -ForegroundColor Green
Write-Host "Domain Mode: $($DomainInfo.DomainMode)" -ForegroundColor Green
Write-Host "Forest Mode: $($ForestInfo.ForestMode)" -ForegroundColor Green
# Get all Domain Controllers
$DCs = Get-ADDomainController -Filter *
Write-Host "Found $($DCs.Count) Domain Controller(s):" -ForegroundColor Green
$DCs | ForEach-Object { Write-Host " - $($_.HostName) ($($_.Site))" -ForegroundColor White }
# Run comprehensive health checks
Write-Host "`nRunning comprehensive health checks..." -ForegroundColor Cyan
# 1. DC Health
$dcHealth = Get-DCHealth -DCs $DCs
# 2. AD Services Status
$adServices = Get-ADServicesStatus -DCs $DCs
# 3. Replication Status
$replicationFailures = Get-ReplicationStatus
# 4. User Account Analysis
$userAnalysis = Get-UserAccountAnalysis
# 5. Computer Account Analysis
$computerAnalysis = Get-ComputerAccountAnalysis
# 6. AD Recycle Bin Status
$recycleBinStatus = Get-ADRecycleBinStatus
# 7. Password Policy Status
$passwordPolicy = Get-PasswordPolicyStatus
# 8. GPO Analysis
$gpoAnalysis = Get-GPOAnalysis
# 9. DNS Health
$dnsHealth = Get-DNSHealth -DCs $DCs
# 10. AD Site & Replication Info
$siteReplicationInfo = Get-ADSiteReplicationInfo
# 11. FSMO Roles
$fsmoRoles = Get-FSMORoles
# 12. Backup Status
$backupStatus = Get-BackupStatus -DCs $DCs
# 13. Trust Relationships
$trustRelationships = Get-TrustRelationships
# Calculate overall scores
$criticalCount = ($dcHealth | Where-Object { $_.Status -eq "Critical" }).Count
$warningCount = ($dcHealth | Where-Object { $_.Status -eq "Warning" }).Count
$overallScore = 100
if ($dcHealth.Count -gt 0) {
$healthyPercent = ($dcHealth | Where-Object { $_.Status -eq "Healthy" }).Count / $dcHealth.Count * 100
$overallScore = [math]::Round($healthyPercent * 0.6, 1) # DC health contributes 60%
}
# Additional scoring components
if ($userAnalysis) {
$userScore = 100
if ($userAnalysis.PasswordNeverExpires -gt 0) { $userScore -= 10 }
if ($userAnalysis.Inactive90 -gt 0) { $userScore -= 10 }
if ($userAnalysis.Locked -gt 0) { $userScore -= 5 }
$overallScore += [math]::Round($userScore * 0.1, 1) # User health contributes 10%
}
if ($gpoAnalysis) {
$gpoScore = 100
if ($gpoAnalysis.Statistics.Unlinked -gt 0) { $gpoScore -= 10 }
if ($gpoAnalysis.Statistics.Stale -gt 0) { $gpoScore -= 10 }
if ($gpoAnalysis.Statistics.Orphaned -gt 0) { $gpoScore -= 20 }
$overallScore += [math]::Round($gpoScore * 0.1, 1) # GPO health contributes 10%
}
if ($siteReplicationInfo -and $siteReplicationInfo.ReplicationFailures -gt 0) {
$overallScore -= 10 # Replication failures deduct 10%
}
$overallScore = [math]::Max(0, [math]::Min(100, $overallScore))
# Generate HTML report
$ReportFile = "$OutputPath\AD_Comprehensive_HealthCheck_$DomainName_$(Get-Date -Format 'yyyyMMdd_HHmmss').html"
Write-Host "`nGenerating comprehensive HTML report..." -ForegroundColor Cyan
# Start HTML content
$HTML = Get-HTMLHeader -DomainName $DomainName -ForestName $ForestName
# ================= EXECUTIVE SUMMARY =====================
$HTML += @"
<div class="card">
<div class="card-header">
<h2>Executive Summary</h2>
</div>
<div class="card-body">
<p>This comprehensive report provides a complete assessment of the Active Directory environment for <strong>$DomainName</strong> across all $($DCs.Count) Domain Controllers.</p>
<div class="summary-grid">
<div class="summary-card total">
<div class="label">Domain Controllers</div>
<div class="count">$($DCs.Count)</div>
</div>
<div class="summary-card $(if ($criticalCount -eq 0) {'healthy'} else {'critical'})">
<div class="label">Critical Issues</div>
<div class="count">$criticalCount</div>
</div>
<div class="summary-card $(if ($warningCount -eq 0) {'healthy'} else {'warning'})">
<div class="label">Warnings</div>
<div class="count">$warningCount</div>
</div>
<div class="summary-card $(if ($overallScore -ge 90) {'healthy'} elseif ($overallScore -ge 70) {'warning'} else {'critical'})">
<div class="label">Overall Score</div>
<div class="count">$overallScore%</div>
</div>
</div>
<div class="summary-grid">
<div class="summary-card total">
<div class="label">Total Users</div>
<div class="count">$(if ($userAnalysis) {$userAnalysis.Total} else {'N/A'})</div>
</div>
<div class="summary-card total">
<div class="label">Total Computers</div>
<div class="count">$(if ($computerAnalysis) {$computerAnalysis.Total} else {'N/A'})</div>
</div>
<div class="summary-card $(if ($gpoAnalysis.Statistics.Total) {'total'} else {'warning'})">
<div class="label">Total GPOs</div>
<div class="count">$(if ($gpoAnalysis) {$gpoAnalysis.Statistics.Total} else {'N/A'})</div>
</div>
<div class="summary-card $(if ($siteReplicationInfo -and $siteReplicationInfo.ReplicationFailures -eq 0) {'healthy'} else {'critical'})">
<div class="label">Replication Issues</div>
<div class="count">$(if ($siteReplicationInfo) {$siteReplicationInfo.ReplicationFailures} else {'N/A'})</div>
</div>
</div>
<h3>Environment Overview</h3>
<table class="data-table">
<tr><th>Property</th><th>Value</th></tr>
<tr><td>Domain Functional Level</td><td>$($DomainInfo.DomainMode)</td></tr>
<tr><td>Forest Functional Level</td><td>$($ForestInfo.ForestMode)</td></tr>
<tr><td>Domain SID</td><td>$($DomainInfo.DomainSID.Value)</td></tr>
<tr><td>Child Domains</td><td>$(($ForestInfo.Domains | Where-Object { $_ -ne $DomainName }).Count)</td></tr>
<tr><td>Sites</td><td>$(if ($siteReplicationInfo) {$siteReplicationInfo.Sites.Count} else {'N/A'})</td></tr>
<tr><td>Trust Relationships</td><td>$(if ($trustRelationships) {$trustRelationships.Count} else {'N/A'})</td></tr>
</table>
</div>
</div>
"@
# ================= DOMAIN CONTROLLER HEALTH ==============
$HTML += "<div class='card'><div class='card-header'><h2>Domain Controller Health & Performance</h2></div><div class='card-body'><table class='data-table'><thead><tr><th>DC Name</th><th>Site</th><th>Roles</th><th>CPU Usage</th><th>Memory Usage</th><th>Disk Free (C:)</th><th>Uptime</th><th>Status</th></tr></thead><tbody>"
foreach ($dc in $dcHealth) {
$diskPercent = if ($dc.DiskTotalGB -gt 0) { [math]::Round(($dc.DiskFreeGB / $dc.DiskTotalGB) * 100, 1) } else { 0 }
$HTML += @"
<tr>
<td><strong>$($dc.DCName)</strong></td>
<td>$($dc.Site)</td>
<td>$($dc.Roles)</td>
<td>
<div class="progress-container">
<div class="progress-bar progress-cpu" style="width: $($dc.CPU)%"></div>
</div>
<span>$($dc.CPU)%</span>
</td>
<td>
<div class="progress-container">
<div class="progress-bar progress-memory" style="width: $($dc.Memory)%"></div>
</div>
<span>$($dc.Memory)%</span>
</td>
<td>
<div class="progress-container">
<div class="progress-bar progress-disk" style="width: $diskPercent%"></div>
</div>
<span>$($dc.DiskFreeGB) GB free / $($dc.DiskTotalGB) GB total</span>
</td>
<td>$($dc.UptimeDays) days</td>
<td><span class="status $($dc.StatusClass)">$($dc.Status)</span></td>
</tr>
"@
}
$HTML += "</tbody></table></div></div>"
# ================= AD SERVICES STATUS ====================
$HTML += "<div class='card'><div class='card-header'><h2>Active Directory Services Status</h2></div><div class='card-body'><table class='data-table'><thead><tr><th>DC Name</th><th>NTDS</th><th>DNS</th><th>Netlogon</th><th>KDC</th><th>W32Time</th><th>Overall</th></tr></thead><tbody>"
foreach ($service in $adServices) {
$HTML += "<tr>
<td><strong>$($service.DCName)</strong></td>
<td><span class='status $(if($service.NTDS -eq 'Running'){'status-healthy'}else{'status-critical'})'>$($service.NTDS)</span></td>
<td><span class='status $(if($service.DNS -eq 'Running'){'status-healthy'}else{'status-critical'})'>$($service.DNS)</span></td>
<td><span class='status $(if($service.Netlogon -eq 'Running'){'status-healthy'}else{'status-critical'})'>$($service.Netlogon)</span></td>
<td><span class='status $(if($service.KDC -eq 'Running'){'status-healthy'}else{'status-critical'})'>$($service.KDC)</span></td>
<td><span class='status $(if($service.W32Time -eq 'Running'){'status-healthy'}else{'status-critical'})'>$($service.W32Time)</span></td>
<td><span class='status $($service.OverallClass)'>$($service.Overall)</span></td>
</tr>"
}
$HTML += "</tbody></table></div></div>"
# ================= GPO ANALYSIS ==========================
if ($gpoAnalysis) {
$HTML += "<div class='card'><div class='card-header'><h2>Group Policy Analysis</h2></div><div class='card-body'>
<div class='summary-grid'>
<div class='summary-card total'><div class='label'>Total GPOs</div><div class='count'>$($gpoAnalysis.Statistics.Total)</div></div>
<div class='summary-card $(if($gpoAnalysis.Statistics.Unlinked -eq 0){'healthy'}else{'warning'})'><div class='label'>Unlinked GPOs</div><div class='count'>$($gpoAnalysis.Statistics.Unlinked)</div></div>
<div class='summary-card $(if($gpoAnalysis.Statistics.Stale -eq 0){'healthy'}else{'warning'})'><div class='label'>Stale GPOs</div><div class='count'>$($gpoAnalysis.Statistics.Stale)</div></div>
<div class='summary-card $(if($gpoAnalysis.Statistics.Orphaned -eq 0){'healthy'}else{'critical'})'><div class='label'>Orphaned GPOs</div><div class='count'>$($gpoAnalysis.Statistics.Orphaned)</div></div>
</div>
<h3>Top 10 Largest GPOs</h3>
<table class='data-table'>
<thead><tr><th>GPO Name</th><th>Size (MB)</th><th>Links</th><th>Last Modified</th><th>Status</th></tr></thead>
<tbody>"
$topGPOs = $gpoAnalysis.GPOs | Sort-Object @{Expression={if($_.SizeMB -is [double]){$_.SizeMB}else{0}}; Ascending=$false} | Select-Object -First 10
foreach ($gpo in $topGPOs) {
$statusClass = if ($gpo.IsUnlinked) { "status-warning" }
elseif ($gpo.IsStale) { "status-info" }
elseif ($gpo.IsLarge) { "status-info" }
else { "status-healthy" }
$statusText = if ($gpo.IsUnlinked) { "Unlinked" }
elseif ($gpo.IsStale) { "Stale" }
elseif ($gpo.IsLarge) { "Large" }
else { "OK" }
$lastModified = if ($gpo.Modified) { $gpo.Modified.ToString('yyyy-MM-dd') } else { "Unknown" }
$HTML += "<tr>
<td>$($gpo.Name)</td>
<td>$($gpo.SizeMB)</td>
<td>$($gpo.LinkCount)</td>
<td>$lastModified</td>
<td><span class='status $statusClass'>$statusText</span></td>
</tr>"
}
$HTML += "</tbody></table>"
# Orphaned GPOs section
if ($gpoAnalysis.OrphanedGPOs.Count -gt 0) {
$HTML += "<h3>Orphaned GPOs (Require Cleanup)</h3>
<table class='data-table'>
<thead><tr><th>GPO ID</th><th>Size (MB)</th><th>Last Modified</th></tr></thead>
<tbody>"
foreach ($orphan in $gpoAnalysis.OrphanedGPOs) {
$HTML += "<tr>
<td>$($orphan.ID)</td>
<td>$($orphan.SizeMB)</td>
<td>$($orphan.LastModified.ToString('yyyy-MM-dd'))</td>
</tr>"
}
$HTML += "</tbody></table>"
}
$HTML += "</div></div>"
}
# ================= DNS HEALTH ============================
if ($dnsHealth) {
$HTML += "<div class='card'><div class='card-header'><h2>DNS Health Status</h2></div><div class='card-body'><table class='data-table'><thead><tr><th>DC Name</th><th>Total Zones</th><th>Forward Zones</th><th>Reverse Zones</th><th>AD Integrated</th><th>Stale Records</th><th>Scavenging</th><th>Status</th></tr></thead><tbody>"
foreach ($dns in $dnsHealth) {
$statusClass = if ($dns.StaleRecords -is [int] -and $dns.StaleRecords -gt 100) { "status-warning" }
elseif ($dns.ScavengingEnabled -eq $true) { "status-healthy" }
elseif ($dns.Status -eq "DNS Module Missing") { "status-info" }
else { "status-info" }
$statusText = if ($dns.StaleRecords -is [int] -and $dns.StaleRecords -gt 100) { "Stale Records" }
elseif ($dns.ScavengingEnabled -eq $true) { "Healthy" }
elseif ($dns.Status -eq "DNS Module Missing") { "Module Missing" }
else { "Not Checked" }
$HTML += "<tr>
<td><strong>$($dns.DCName)</strong></td>
<td>$($dns.Zones)</td>
<td>$($dns.ForwardZones)</td>
<td>$($dns.ReverseZones)</td>
<td>$($dns.ADIntegrated)</td>
<td>$($dns.StaleRecords)</td>
<td>$(if($dns.ScavengingEnabled -eq $true){'Enabled'}elseif($dns.ScavengingEnabled -eq $false){'Disabled'}else{$dns.ScavengingEnabled})</td>
<td><span class='status $statusClass'>$statusText</span></td>
</tr>"
}
$HTML += "</tbody></table></div></div>"
}
# ================= AD SITES & REPLICATION ================
if ($siteReplicationInfo) {
$HTML += "<div class='card'><div class='card-header'><h2>AD Sites & Replication</h2></div><div class='card-body'>
<div class='summary-grid'>
<div class='summary-card total'><div class='label'>Total Sites</div><div class='count'>$($siteReplicationInfo.Sites.Count)</div></div>
<div class='summary-card $(if($siteReplicationInfo.ReplicationFailures -eq 0){'healthy'}else{'critical'})'><div class='label'>Replication Issues</div><div class='count'>$($siteReplicationInfo.ReplicationFailures)</div></div>
</div>
<h3>Site List</h3>
<table class='data-table'>
<thead><tr><th>Site Name</th><th>Location</th><th>Description</th></tr></thead>
<tbody>"
foreach ($site in $siteReplicationInfo.Sites) {
$HTML += "<tr>
<td>$($site.Name)</td>
<td>$($site.Location)</td>
<td>$($site.Description)</td>
</tr>"
}
$HTML += "</tbody></table>"
# Replication summary
if ($siteReplicationInfo.ReplicationSummary.Count -gt 0) {
$HTML += "<h3>Recent Replication Summary</h3>
<table class='data-table'>
<thead><tr><th>Source Server</th><th>Partner Server</th><th>Last Attempt</th><th>Failures (24h)</th></tr></thead>
<tbody>"
foreach ($rep in $siteReplicationInfo.ReplicationSummary) {
$HTML += "<tr>
<td>$($rep.Server)</td>
<td>$($rep.Partner)</td>
<td>$($rep.LastAttemptTime)</td>
<td>$($rep.FailureCount)</td>
</tr>"
}
$HTML += "</tbody></table>"
}
$HTML += "</div></div>"
}
# ================= FSMO ROLES ============================
if ($fsmoRoles) {
$HTML += "<div class='card'><div class='card-header'><h2>FSMO Roles Status</h2></div><div class='card-body'><table class='data-table'><thead><tr><th>Role</th><th>Role Type</th><th>Current Holder</th><th>Online Status</th><th>Recommendation</th></tr></thead><tbody>"
foreach ($role in $fsmoRoles) {
$statusClass = if ($role.IsOnline) { "status-healthy" } else { "status-critical" }
$statusText = if ($role.IsOnline) { "Online" } else { "Offline" }
$recommendation = if ($role.Role -in @("PDC Emulator", "RID Master") -and -not $role.IsOnline) {
"Critical: $($role.Role) is offline!"
} elseif ($role.Role -eq "Infrastructure Master" -and $role.Holder -match "DC" -and $DCs.Count -gt 1) {
"Ensure this role is NOT on a Global Catalog server"
} else {
"No action required"
}
$HTML += "<tr>
<td><strong>$($role.Role)</strong></td>
<td>$($role.RoleType)</td>
<td>$($role.Holder)</td>
<td><span class='status $statusClass'>$statusText</span></td>
<td>$recommendation</td>
</tr>"
}
$HTML += "</tbody></table></div></div>"
}
# ================= TRUST RELATIONSHIPS ====================
if ($trustRelationships) {
$HTML += "<div class='card'><div class='card-header'><h2>Trust Relationships</h2></div><div class='card-body'><table class='data-table'><thead><tr><th>Trust Name</th><th>Type</th><th>Direction</th><th>Transitive</th><th>Created</th></tr></thead><tbody>"
foreach ($trust in $trustRelationships) {
$HTML += "<tr>
<td><strong>$($trust.Name)</strong></td>
<td>$($trust.Type)</td>
<td>$($trust.Direction)</td>
<td>$($trust.Transitive)</td>
<td>$($trust.Created.ToString('yyyy-MM-dd'))</td>
</tr>"
}
$HTML += "</tbody></table></div></div>"
}
# ================= BACKUP STATUS =========================
if ($backupStatus) {
$HTML += "<div class='card'><div class='card-header'><h2>Backup Status</h2></div><div class='card-body'><table class='data-table'><thead><tr><th>DC Name</th><th>Last Backup</th><th>Days Since</th><th>Status</th><th>Action Required</th></tr></thead><tbody>"
foreach ($backup in $backupStatus) {
$statusClass = if ($backup.DaysSinceLastBackup -le 1) { "status-healthy" }
elseif ($backup.DaysSinceLastBackup -le 3) { "status-warning" }
else { "status-critical" }
$statusText = if ($backup.DaysSinceLastBackup -le 1) { "Recent" }
elseif ($backup.DaysSinceLastBackup -le 3) { "Warning" }
else { "Critical" }
$action = if ($backup.DaysSinceLastBackup -gt 7) { "Immediate backup required" }
elseif ($backup.DaysSinceLastBackup -gt 3) { "Schedule backup soon" }
else { "No action" }
$lastBackupText = if ($backup.LastBackup) { $backup.LastBackup.ToString('yyyy-MM-dd HH:mm') } else { "No recent backups" }
$HTML += "<tr>
<td><strong>$($backup.DCName)</strong></td>
<td>$lastBackupText</td>
<td>$($backup.DaysSinceLastBackup)</td>
<td><span class='status $statusClass'>$statusText</span></td>
<td>$action</td>
</tr>"
}
$HTML += "</tbody></table></div></div>"
}
# ================= USER ACCOUNT ANALYSIS ================
if ($userAnalysis) {
$HTML += "<div class='card'><div class='card-header'><h2>User Account Analysis</h2></div><div class='card-body'>
<div class='summary-grid'>
<div class='summary-card total'><div class='label'>Total Users</div><div class='count'>$($userAnalysis.Total)</div></div>
<div class='summary-card healthy'><div class='label'>Enabled</div><div class='count'>$($userAnalysis.Enabled)</div></div>
<div class='summary-card warning'><div class='label'>Disabled</div><div class='count'>$($userAnalysis.Disabled)</div></div>
<div class='summary-card $(if($userAnalysis.Locked -eq 0){'healthy'}else{'critical'})'><div class='label'>Locked</div><div class='count'>$($userAnalysis.Locked)</div></div>
</div>
<table class='data-table'>
<thead><tr><th>Metric</th><th>Count</th><th>Status</th><th>Recommendation</th></tr></thead>
<tbody>
<tr><td>Password Never Expires</td><td>$($userAnalysis.PasswordNeverExpires)</td><td><span class='status $(if($userAnalysis.PasswordNeverExpires -eq 0){'status-healthy'}else{'status-warning'})'>$(if($userAnalysis.PasswordNeverExpires -eq 0){'Compliant'}else{'Non-Compliant'})</span></td><td>$(if($userAnalysis.PasswordNeverExpires -eq 0){'No action'}else{'Review and update password policies'})</td></tr>
<tr><td>Password Expired</td><td>$($userAnalysis.PasswordExpired)</td><td><span class='status $(if($userAnalysis.PasswordExpired -eq 0){'status-healthy'}else{'status-warning'})'>$(if($userAnalysis.PasswordExpired -eq 0){'OK'}else{'Warning'})</span></td><td>$(if($userAnalysis.PasswordExpired -eq 0){'No action'}else{'Notify users to change passwords'})</td></tr>
<tr><td>Inactive 30+ days</td><td>$($userAnalysis.Inactive30)</td><td><span class='status $(if($userAnalysis.Inactive30 -eq 0){'status-healthy'}else{'status-info'})'>Monitor</span></td><td>$(if($userAnalysis.Inactive30 -eq 0){'No action'}else{'Monitor for extended inactivity'})</td></tr>
<tr><td>Inactive 60+ days</td><td>$($userAnalysis.Inactive60)</td><td><span class='status $(if($userAnalysis.Inactive60 -eq 0){'status-healthy'}else{'status-warning'})'>$(if($userAnalysis.Inactive60 -eq 0){'OK'}else{'Review'})</span></td><td>$(if($userAnalysis.Inactive60 -eq 0){'No action'}else{'Investigate inactivity'})</td></tr>
<tr><td>Inactive 90+ days</td><td>$($userAnalysis.Inactive90)</td><td><span class='status $(if($userAnalysis.Inactive90 -eq 0){'status-healthy'}else{'status-critical'})'>$(if($userAnalysis.Inactive90 -eq 0){'OK'}else{'Cleanup Required'})</span></td><td>$(if($userAnalysis.Inactive90 -eq 0){'No action'}else{'Disable or remove accounts'})</td></tr>
</tbody>
</table>
</div></div>"
}
# ================= COMPUTER ACCOUNT ANALYSIS ============
if ($computerAnalysis) {
$HTML += "<div class='card'><div class='card-header'><h2>Computer Account Analysis</h2></div><div class='card-body'>
<div class='summary-grid'>
<div class='summary-card total'><div class='label'>Total Computers</div><div class='count'>$($computerAnalysis.Total)</div></div>
<div class='summary-card healthy'><div class='label'>Servers</div><div class='count'>$($computerAnalysis.Servers)</div></div>
<div class='summary-card healthy'><div class='label'>Workstations</div><div class='count'>$($computerAnalysis.Workstations)</div></div>
<div class='summary-card warning'><div class='label'>Disabled</div><div class='count'>$($computerAnalysis.Disabled)</div></div>
<div class='summary-card healthy'><div class='label'>Domain Controllers</div><div class='count'>$($computerAnalysis.DCs)</div></div>
</div>
<table class='data-table'>
<thead><tr><th>Inactivity Period</th><th>Count</th><th>Status</th><th>Action</th></tr></thead>
<tbody>
<tr><td>30+ days inactive</td><td>$($computerAnalysis.Inactive30)</td><td><span class='status $(if($computerAnalysis.Inactive30 -eq 0){'status-healthy'}else{'status-info'})'>Monitor</span></td><td>$(if($computerAnalysis.Inactive30 -eq 0){'No action'}else{'Review'})</td></tr>
<tr><td>60+ days inactive</td><td>$($computerAnalysis.Inactive60)</td><td><span class='status $(if($computerAnalysis.Inactive60 -eq 0){'status-healthy'}else{'status-warning'})'>$(if($computerAnalysis.Inactive60 -eq 0){'OK'}else{'Warning'})</span></td><td>$(if($computerAnalysis.Inactive60 -eq 0){'No action'}else{'Investigate'})</td></tr>
<tr><td>90+ days inactive</td><td>$($computerAnalysis.Inactive90)</td><td><span class='status $(if($computerAnalysis.Inactive90 -eq 0){'status-healthy'}else{'status-critical'})'>$(if($computerAnalysis.Inactive90 -eq 0){'OK'}else{'Stale'})</span></td><td>$(if($computerAnalysis.Inactive90 -eq 0){'No action'}else{'Cleanup required'})</td></tr>
</tbody>
</table>
</div></div>"
}
# ================= RECOMMENDATIONS ======================
$HTML += "<div class='card'><div class='card-header'><h2>Comprehensive Recommendations</h2></div><div class='card-body'><h3>Priority Actions (1-3 days):</h3><ul>"
if ($criticalCount -gt 0) {
$HTML += "<li><strong>CRITICAL:</strong> Address $criticalCount critical DC issues immediately</li>"
}
if ($siteReplicationInfo -and $siteReplicationInfo.ReplicationFailures -gt 0) {
$HTML += "<li><strong>CRITICAL:</strong> Resolve $($siteReplicationInfo.ReplicationFailures) replication failures</li>"
}
if ($gpoAnalysis -and $gpoAnalysis.Statistics.Orphaned -gt 0) {
$HTML += "<li><strong>SECURITY:</strong> Clean up $($gpoAnalysis.Statistics.Orphaned) orphaned GPOs from SYSVOL</li>"
}
if ($backupStatus -and ($backupStatus | Where-Object { $_.DaysSinceLastBackup -gt 7 }).Count -gt 0) {
$HTML += "<li><strong>RECOVERY:</strong> Perform immediate backups on DCs without recent backups</li>"
}
$HTML += "</ul><h3>Medium Priority Actions (1-2 weeks):</h3><ul>"
if ($userAnalysis -and $userAnalysis.PasswordNeverExpires -gt 0) {
$HTML += "<li><strong>SECURITY:</strong> Review $($userAnalysis.PasswordNeverExpires) accounts with non-expiring passwords</li>"
}
if ($userAnalysis -and $userAnalysis.Inactive90 -gt 0) {
$HTML += "<li><strong>HOUSEKEEPING:</strong> Clean up $($userAnalysis.Inactive90) user accounts inactive for 90+ days</li>"
}
if ($computerAnalysis -and $computerAnalysis.Inactive90 -gt 0) {
$HTML += "<li><strong>ASSET MANAGEMENT:</strong> Remove $($computerAnalysis.Inactive90) computer accounts inactive for 90+ days</li>"
}
if ($gpoAnalysis -and $gpoAnalysis.Statistics.Unlinked -gt 0) {
$HTML += "<li><strong>OPTIMIZATION:</strong> Review $($gpoAnalysis.Statistics.Unlinked) unlinked GPOs for removal</li>"
}
$HTML += "</ul><h3>Long-term Improvements (1 month):</h3><ul>"
if (-not $recycleBinStatus.Enabled) {
$HTML += "<li><strong>RECOVERY:</strong> Enable Active Directory Recycle Bin</li>"
}
if ($passwordPolicy -and $passwordPolicy.MinLength -lt 14) {
$HTML += "<li><strong>SECURITY:</strong> Increase minimum password length to 14 characters</li>"
}
$HTML += @"
<li><strong>AUTOMATION:</strong> Implement automated AD health monitoring</li>
<li><strong>DOCUMENTATION:</strong> Update AD recovery and maintenance procedures</li>
<li><strong>TRAINING:</strong> Conduct AD administration best practices training</li>
</ul>
<h3>Next Steps:</h3>
<ol>
<li>Review this report with IT leadership within 24 hours</li>
<li>Create action plan with priorities and owners</li>
<li>Schedule daily follow-ups on critical items</li>
<li>Schedule comprehensive reassessment in 30 days</li>
<li>Consider third-party AD management tools for advanced monitoring</li>
</ol>
</div></div>
"@
# Add footer
$HTML += Get-HTMLFooter
# Save HTML report
$HTML | Out-File $ReportFile -Encoding UTF8
Write-Host "`n=========================================" -ForegroundColor Green
Write-Host "COMPREHENSIVE REPORT GENERATED SUCCESSFULLY!" -ForegroundColor Green
Write-Host "=========================================" -ForegroundColor Green
Write-Host "Company: $($CompanyInfo.Name)" -ForegroundColor Yellow
Write-Host "Author: $($CompanyInfo.Author)" -ForegroundColor Yellow
Write-Host "Practice: $($CompanyInfo.Practice)" -ForegroundColor Yellow
Write-Host "Report saved to: $ReportFile" -ForegroundColor Yellow
Write-Host "`nExecutive Summary:" -ForegroundColor Cyan
Write-Host " Domain Controllers: $($DCs.Count)" -ForegroundColor White
Write-Host " Critical Issues: $criticalCount" -ForegroundColor $(if ($criticalCount -eq 0) { "Green" } else { "Red" })
Write-Host " Warnings: $warningCount" -ForegroundColor $(if ($warningCount -eq 0) { "Green" } else { "Yellow" })
Write-Host " Overall Health Score: $overallScore%" -ForegroundColor $(if ($overallScore -ge 90) { "Green" } elseif ($overallScore -ge 70) { "Yellow" } else { "Red" })
Write-Host " Total Users: $(if ($userAnalysis) {$userAnalysis.Total} else {'N/A'})" -ForegroundColor White
Write-Host " Total Computers: $(if ($computerAnalysis) {$computerAnalysis.Total} else {'N/A'})" -ForegroundColor White
Write-Host " Total GPOs: $(if ($gpoAnalysis) {$gpoAnalysis.Statistics.Total} else {'N/A'})" -ForegroundColor White
Write-Host " AD Sites: $(if ($siteReplicationInfo) {$siteReplicationInfo.Sites.Count} else {'N/A'})" -ForegroundColor White
# Open report in browser if requested
if ($OpenReport -and (Test-Path $ReportFile)) {
Write-Host "`nOpening report in default browser..." -ForegroundColor Cyan
Start-Process $ReportFile
}
}
catch {
Write-Host "`nERROR: $($_.Exception.Message)" -ForegroundColor Red
Write-Host "Script execution failed." -ForegroundColor Red
exit 1
}
Write-Host "`nScript completed successfully!" -ForegroundColor Green
Write-Host "Report prepared by: $($CompanyInfo.Author)" -ForegroundColor Cyan
Write-Host "Support: $($CompanyInfo.SupportEmail)" -ForegroundColor Cyan
Write-Host "$($CompanyInfo.Name) - $($CompanyInfo.Practice)" -ForegroundColor Cyan
Save the script at any location in the domain controller. Open PowerShell with administrative privilege and go to directory where the script is saved.
Run the script
.\V4.ps1

below details will appear in the powershell console.

Report is generated successfully and save in OS drive with name ADHealthReports. Open the report and review.

Download the sample report.
In this article i have explained component services and roles which need to be run and check in active directory. how to run and generate a brief report of the domain controllers to view and take action on time.
Did you enjoy the article? you also may want to understand more about how to check and block NTLM authentication in active directory which will force users to authenticate via Kerberos.