ActiveDirectory/Get-ADDCHealthReport.ps1
|
<#
.SYNOPSIS Reports domain controller inventory and health status via dcdiag. .DESCRIPTION Discovers all domain controllers and collects OS version, site membership, FSMO roles, and Global Catalog status. Optionally runs dcdiag.exe to perform key diagnostic tests (Connectivity, Advertising, Services, SYSVOL, etc.) and parses the results into structured objects. If dcdiag.exe is not available (e.g. RSAT command-line tools not installed), DC inventory is still collected with dcdiag results marked as Skipped. Designed for IT consultants performing AD assessments on SMB environments (10-500 users). All operations are read-only. Requires the ActiveDirectory module (available via RSAT or on domain controllers). .PARAMETER DomainController One or more specific domain controller hostnames to check. If not specified, all DCs are discovered via Get-ADDomainController. .PARAMETER SkipDcdiag Skip dcdiag diagnostic tests. Only DC inventory information is collected. .PARAMETER OutputPath Optional path to export results as CSV. If not specified, results are returned to the pipeline. .EXAMPLE PS> .\ActiveDirectory\Get-ADDCHealthReport.ps1 Reports all DCs with dcdiag health status. .EXAMPLE PS> .\ActiveDirectory\Get-ADDCHealthReport.ps1 -DomainController 'DC01','DC02' Checks only the specified domain controllers. .EXAMPLE PS> .\ActiveDirectory\Get-ADDCHealthReport.ps1 -SkipDcdiag -OutputPath '.\dc-health.csv' Exports DC inventory without running dcdiag. #> [CmdletBinding()] param( [Parameter()] [string[]]$DomainController, [Parameter()] [switch]$SkipDcdiag, [Parameter()] [ValidateNotNullOrEmpty()] [string]$OutputPath ) $ErrorActionPreference = 'Stop' # ------------------------------------------------------------------ # Verify ActiveDirectory module is available # ------------------------------------------------------------------ if (-not (Get-Module -Name ActiveDirectory -ListAvailable)) { Write-Error "The ActiveDirectory module is not installed. Install RSAT or run from a domain controller." return } Import-Module -Name ActiveDirectory -ErrorAction Stop # ------------------------------------------------------------------ # Internal: invoke dcdiag.exe for a domain controller # Defined conditionally so tests can inject a mock before running. # ------------------------------------------------------------------ if (-not (Get-Command -Name 'Invoke-Dcdiag' -CommandType Function -ErrorAction SilentlyContinue)) { function Invoke-Dcdiag { [CmdletBinding()] param( [Parameter(Mandatory)] [string]$Target, [Parameter()] [string[]]$Tests ) $dcdiagExe = Get-Command -Name 'dcdiag.exe' -CommandType Application -ErrorAction SilentlyContinue if (-not $dcdiagExe) { throw "dcdiag.exe is not available on this machine. Install RSAT command-line tools or run from a domain controller." } $testArgs = @("/s:$Target") foreach ($test in $Tests) { $testArgs += "/test:$test" } $output = & dcdiag.exe @testArgs 2>&1 return $output } } # end conditional Invoke-Dcdiag definition # ------------------------------------------------------------------ # Define the dcdiag tests to run (SMB-appropriate subset) # ------------------------------------------------------------------ $dcdiagTests = @( 'Connectivity' 'Advertising' 'Services' 'FrsEvent' 'DFSREvent' 'SysVolCheck' 'KccEvent' 'MachineAccount' 'RidManager' 'VerifyReferences' ) # ------------------------------------------------------------------ # Discover domain controllers # ------------------------------------------------------------------ try { Write-Verbose "Discovering domain controllers..." if ($DomainController) { $dcList = foreach ($dc in $DomainController) { try { Get-ADDomainController -Identity $dc } catch { Write-Warning "Could not find domain controller '$dc': $_" } } $dcList = @($dcList | Where-Object { $_ }) } else { $dcList = @(Get-ADDomainController -Filter *) } if ($dcList.Count -eq 0) { Write-Error "No domain controllers found." return } Write-Verbose "Found $($dcList.Count) domain controller(s)" } catch { Write-Error "Failed to discover domain controllers: $_" return } # ------------------------------------------------------------------ # Collect DC info and run dcdiag # ------------------------------------------------------------------ $report = [System.Collections.Generic.List[PSCustomObject]]::new() foreach ($dc in $dcList) { $dcName = $dc.HostName $fsmoRoles = if ($dc.OperationMasterRoles -and $dc.OperationMasterRoles.Count -gt 0) { ($dc.OperationMasterRoles -join ', ') } else { '' } if ($SkipDcdiag) { # Emit a single summary row with Skipped status $report.Add([PSCustomObject]@{ DomainController = $dcName Site = $dc.Site IPv4Address = $dc.IPv4Address OperatingSystem = $dc.OperatingSystem IsGlobalCatalog = $dc.IsGlobalCatalog IsReadOnly = $dc.IsReadOnly FSMORoles = $fsmoRoles DcdiagTest = 'N/A' DcdiagResult = 'Skipped' DcdiagDetails = 'dcdiag tests skipped via -SkipDcdiag parameter' }) continue } # Run dcdiag try { Write-Verbose "Running dcdiag on $dcName..." $dcdiagOutput = Invoke-Dcdiag -Target $dcName -Tests $dcdiagTests # Parse dcdiag output $testPattern = '^\s*\.+\s+(\S+)\s+(passed|failed)\s+test\s+(.+)\s*$' $parsedTests = [System.Collections.Generic.List[PSCustomObject]]::new() foreach ($line in $dcdiagOutput) { $lineStr = if ($line -is [string]) { $line } else { $line.ToString() } if ($lineStr -match $testPattern) { $testResult = $Matches[2] $testName = $Matches[3].Trim() $details = '' if ($testResult -eq 'failed') { $details = "Test $testName failed on $dcName" } $parsedTests.Add([PSCustomObject]@{ TestName = $testName Result = if ($testResult -eq 'passed') { 'Passed' } else { 'Failed' } Details = $details }) } } if ($parsedTests.Count -gt 0) { foreach ($test in $parsedTests) { $report.Add([PSCustomObject]@{ DomainController = $dcName Site = $dc.Site IPv4Address = $dc.IPv4Address OperatingSystem = $dc.OperatingSystem IsGlobalCatalog = $dc.IsGlobalCatalog IsReadOnly = $dc.IsReadOnly FSMORoles = $fsmoRoles DcdiagTest = $test.TestName DcdiagResult = $test.Result DcdiagDetails = $test.Details }) } } else { # dcdiag ran but no test results parsed (unexpected output format) $report.Add([PSCustomObject]@{ DomainController = $dcName Site = $dc.Site IPv4Address = $dc.IPv4Address OperatingSystem = $dc.OperatingSystem IsGlobalCatalog = $dc.IsGlobalCatalog IsReadOnly = $dc.IsReadOnly FSMORoles = $fsmoRoles DcdiagTest = 'N/A' DcdiagResult = 'Unknown' DcdiagDetails = 'dcdiag completed but no test results could be parsed from the output' }) } } catch { # dcdiag failed entirely (e.g. dcdiag.exe not found) Write-Warning "dcdiag failed for $dcName`: $_" $report.Add([PSCustomObject]@{ DomainController = $dcName Site = $dc.Site IPv4Address = $dc.IPv4Address OperatingSystem = $dc.OperatingSystem IsGlobalCatalog = $dc.IsGlobalCatalog IsReadOnly = $dc.IsReadOnly FSMORoles = $fsmoRoles DcdiagTest = 'N/A' DcdiagResult = 'Skipped' DcdiagDetails = "dcdiag unavailable: $_" }) } } # ------------------------------------------------------------------ # Export or return # ------------------------------------------------------------------ $results = @($report) Write-Verbose "Collected $($results.Count) DC health records" if ($OutputPath) { $results | Export-Csv -Path $OutputPath -NoTypeInformation -Encoding UTF8 Write-Output "Exported $($results.Count) DC health records to $OutputPath" } else { Write-Output $results } |