Introduction
Maintaining the health of your Active Directory (AD) environment is crucial for the stability and security of IT systems. An unhealthy AD can lead to authentication failures, replication issues, and broader disruptions across an organization’s network. This PowerShell script is designed to streamline the process of assessing and troubleshooting Active Directory health checks, offering a structured and automated method for IT professionals and Managed Service Providers (MSPs).
Background
Active Directory is the backbone of many enterprise IT environments, managing user authentication, group policies, and directory services. However, ensuring its proper functioning can be complex, given the number of potential failure points. This script leverages the DCDiag command-line utility to analyze the state of a domain controller, identify potential issues, and report results. For MSPs and IT administrators, this tool is invaluable, providing a clear snapshot of AD health and facilitating proactive maintenance.
The Script:
#Requires -Version 5.1 <# .SYNOPSIS Analyzes the state of a domain controller and reports any problems to help with troubleshooting. Optionally, set a WYSIWYG custom field. By using this script, you indicate your acceptance of the following legal terms as well as our Terms of Use at https://www.ninjaone.com/terms-of-use. Ownership Rights: NinjaOne owns and will continue to own all right, title, and interest in and to the script (including the copyright). NinjaOne is giving you a limited license to use the script in accordance with these legal terms. Use Limitation: You may only use the script for your legitimate personal or internal business purposes, and you may not share the script with another party. Republication Prohibition: Under no circumstances are you permitted to re-publish the script in any script library or website belonging to or under the control of any other software provider. Warranty Disclaimer: The script is provided “as is” and “as available”, without warranty of any kind. NinjaOne makes no promise or guarantee that the script will be free from defects or that it will meet your specific needs or expectations. Assumption of Risk: Your use of the script is at your own risk. You acknowledge that there are certain inherent risks in using the script, and you understand and assume each of those risks. Waiver and Release: You will not hold NinjaOne responsible for any adverse or unintended consequences resulting from your use of the script, and you waive any legal or equitable rights or remedies you may have against NinjaOne relating to your use of the script. EULA: If you are a NinjaOne customer, your use of the script is subject to the End User License Agreement applicable to you (EULA). .DESCRIPTION Analyzes the state of a domain controller and reports any problems to help with troubleshooting. Optionally, set a WYSIWYG custom field. .EXAMPLE (No Parameters) Retrieving Directory Server Diagnosis Test Results. Passing Tests: CheckSDRefDom, Connectivity, CrossRefValidation, DFSREvent, FrsEvent, Intersite, KccEvent, KnowsOfRoleHolders, MachineAccount, NCSecDesc, NetLogons, ObjectsReplicated, Replications, RidManager, Services, SystemLog, SysVolCheck, VerifyReferences [Alert] Failed Tests Detected! Failed Tests: Advertising, LocatorCheck ### Detailed Output ### Directory Server Diagnosis Performing initial setup: Trying to find home server... Home Server = SRV16-DC2-TEST * Identified AD Forest. Done gathering initial info. Doing initial required tests Testing server: Default-First-Site-Name\SRV16-DC2-TEST Starting test: Connectivity ......................... SRV16-DC2-TEST passed test Connectivity Doing primary tests Testing server: Default-First-Site-Name\SRV16-DC2-TEST Starting test: Advertising Warning: SRV16-DC2-TEST is not advertising as a time server. ......................... SRV16-DC2-TEST failed test Advertising Running partition tests on : ForestDnsZones Running partition tests on : DomainDnsZones Running partition tests on : Schema Running partition tests on : Configuration Running partition tests on : test Running enterprise tests on : test.lan Directory Server Diagnosis Performing initial setup: Trying to find home server... Home Server = SRV16-DC2-TEST * Identified AD Forest. Done gathering initial info. Doing initial required tests Testing server: Default-First-Site-Name\SRV16-DC2-TEST Starting test: Connectivity ......................... SRV16-DC2-TEST passed test Connectivity Doing primary tests Testing server: Default-First-Site-Name\SRV16-DC2-TEST Running partition tests on : ForestDnsZones Running partition tests on : DomainDnsZones Running partition tests on : Schema Running partition tests on : Configuration Running partition tests on : test Running enterprise tests on : test.lan Starting test: LocatorCheck Warning: DcGetDcName(TIME_SERVER) call failed, error 1355 A Time Server could not be located. The server holding the PDC role is down. Warning: DcGetDcName(GOOD_TIME_SERVER_PREFERRED) call failed, error 1355 A Good Time Server could not be located. ......................... test.lan failed test LocatorCheck PARAMETER: -wysiwygCustomField "ReplaceMeWithaWYSIWYGcustomField" Name of a WYSIWYG custom field to optionally save the results to. .NOTES Minimum OS Architecture Supported: Windows 10, Windows Server 2016 Release Notes: Initial Release #> [CmdletBinding()] param ( [Parameter()] [String]$wysiwygCustomField ) begin { # If script form variables are used, replace command line parameters with their value. if ($env:wysiwygCustomFieldName -and $env:wysiwygCustomFieldName -notlike "null") { $wysiwygCustomField = $env:wysiwygCustomFieldName } # Function to test if the current machine is a domain controller function Test-IsDomainController { $OS = if ($PSVersionTable.PSVersion.Major -lt 5) { Get-WmiObject -Class Win32_OperatingSystem } else { Get-CimInstance -ClassName Win32_OperatingSystem } # Check if the OS is a domain controller (ProductType 2) if ($OS.ProductType -eq "2") { return $true } } function Get-DCDiagResults { # Define the list of DCDiag tests to run $DCDiagTestsToRun = "Connectivity", "Advertising", "FrsEvent", "DFSREvent", "SysVolCheck", "KccEvent", "KnowsOfRoleHolders", "MachineAccount", "NCSecDesc", "NetLogons", "ObjectsReplicated", "Replications", "RidManager", "Services", "SystemLog", "VerifyReferences", "CheckSDRefDom", "CrossRefValidation", "LocatorCheck", "Intersite" foreach ($DCTest in $DCDiagTestsToRun) { # Run DCDiag for the current test and save the output to a file $DCDiag = Start-Process -FilePath "DCDiag.exe" -ArgumentList "/test:$DCTest", "/f:$env:TEMP\dc-diag-$DCTest.txt" -PassThru -Wait -NoNewWindow # Check if the DCDiag test failed if ($DCDiag.ExitCode -ne 0) { Write-Host "[Error] Running $DCTest!" exit 1 } # Read the raw results from the output file and filter out empty lines $RawResult = Get-Content -Path "$env:TEMP\dc-diag-$DCTest.txt" | Where-Object { $_.Trim() } # Find the status line indicating whether the test passed or failed $StatusLine = $RawResult | Where-Object { $_ -match "\. .* test $DCTest" } # Extract the status (passed or failed) from the status line $Status = $StatusLine -split ' ' | Where-Object { $_ -like "passed" -or $_ -like "failed" } # Create a custom object to store the test results [PSCustomObject]@{ Test = $DCTest Status = $Status Result = $RawResult } # Remove the temporary output file Remove-Item -Path "$env:TEMP\dc-diag-$DCTest.txt" } } function Set-NinjaProperty { [CmdletBinding()] Param( [Parameter(Mandatory = $True)] [String]$Name, [Parameter()] [String]$Type, [Parameter(Mandatory = $True, ValueFromPipeline = $True)] $Value, [Parameter()] [String]$DocumentName ) $Characters = $Value | Out-String | Measure-Object -Character | Select-Object -ExpandProperty Characters if ($Characters -ge 200000) { throw [System.ArgumentOutOfRangeException]::New("Character limit exceeded: the value is greater than or equal to 200,000 characters.") } # If requested to set the field value for a Ninja document, specify it here. $DocumentationParams = @{} if ($DocumentName) { $DocumentationParams["DocumentName"] = $DocumentName } # This is a list of valid fields that can be set. If no type is specified, assume that the input does not need to be changed. $ValidFields = "Attachment", "Checkbox", "Date", "Date or Date Time", "Decimal", "Dropdown", "Email", "Integer", "IP Address", "MultiLine", "MultiSelect", "Phone", "Secure", "Text", "Time", "URL", "WYSIWYG" if ($Type -and $ValidFields -notcontains $Type) { Write-Warning "$Type is an invalid type. Please check here for valid types: https://ninjarmm.zendesk.com/hc/en-us/articles/16973443979789-Command-Line-Interface-CLI-Supported-Fields-and-Functionality" } # The field below requires additional information to set. $NeedsOptions = "Dropdown" if ($DocumentName) { if ($NeedsOptions -contains $Type) { # Redirect error output to the success stream to handle errors more easily if nothing is found or something else goes wrong. $NinjaPropertyOptions = Ninja-Property-Docs-Options -AttributeName $Name @DocumentationParams 2>&1 } } else { if ($NeedsOptions -contains $Type) { $NinjaPropertyOptions = Ninja-Property-Options -Name $Name 2>&1 } } # If an error is received with an exception property, exit the function with that error information. if ($NinjaPropertyOptions.Exception) { throw $NinjaPropertyOptions } # The types below require values not typically given to be set. The code below will convert whatever we're given into a format ninjarmm-cli supports. switch ($Type) { "Checkbox" { # Although it's highly likely we were given a value like "True" or a boolean data type, it's better to be safe than sorry. $NinjaValue = [System.Convert]::ToBoolean($Value) } "Date or Date Time" { # Ninjarmm-cli expects the GUID of the option to be selected. Therefore, match the given value with a GUID. $Date = (Get-Date $Value).ToUniversalTime() $TimeSpan = New-TimeSpan (Get-Date "1970-01-01 00:00:00") $Date $NinjaValue = $TimeSpan.TotalSeconds } "Dropdown" { # Ninjarmm-cli expects the GUID of the option we're trying to select, so match the value we were given with a GUID. $Options = $NinjaPropertyOptions -replace '=', ',' | ConvertFrom-Csv -Header "GUID", "Name" $Selection = $Options | Where-Object { $_.Name -eq $Value } | Select-Object -ExpandProperty GUID if (-not $Selection) { throw [System.ArgumentOutOfRangeException]::New("Value is not present in dropdown options.") } $NinjaValue = $Selection } default { # All the other types shouldn't require additional work on the input. $NinjaValue = $Value } } # Set the field differently depending on whether it's a field in a Ninja Document or not. if ($DocumentName) { $CustomField = Ninja-Property-Docs-Set -AttributeName $Name -AttributeValue $NinjaValue @DocumentationParams 2>&1 } else { $CustomField = $NinjaValue | Ninja-Property-Set-Piped -Name $Name 2>&1 } if ($CustomField.Exception) { throw $CustomField } } function Test-IsElevated { $id = [System.Security.Principal.WindowsIdentity]::GetCurrent() $p = New-Object System.Security.Principal.WindowsPrincipal($id) $p.IsInRole([System.Security.Principal.WindowsBuiltInRole]::Administrator) } if (!$ExitCode) { $ExitCode = 0 } } process { # Check if the script is run with Administrator privileges if (!(Test-IsElevated)) { Write-Host -Object "[Error] Access Denied. Please run with Administrator privileges." exit 1 } # Check if the script is run on a Domain Controller if (!(Test-IsDomainController)) { Write-Host -Object "[Error] This script needs to be run on a Domain Controller." exit 1 } # Initialize lists to store passing and failing tests $PassingTests = New-Object System.Collections.Generic.List[object] $FailedTests = New-Object System.Collections.Generic.List[object] # Notify the user that the tests are being retrieved Write-Host -Object "`nRetrieving Directory Server Diagnosis Test Results." $TestResults = Get-DCDiagResults # Process each test result foreach ($Result in $TestResults) { $TestFailed = $False # Check if any status in the result indicates a failure $Result.Status | ForEach-Object { if ($_ -notmatch "pass") { $TestFailed = $True } } # Add the result to the appropriate list if ($TestFailed) { $FailedTests.Add($Result) } else { $PassingTests.Add($Result) } } # Optionally set a WYSIWYG custom field if specified if ($wysiwygCustomField) { try { Write-Host -Object "`nBuilding HTML for Custom Field." # Create an HTML report for the custom field $HTML = New-Object System.Collections.Generic.List[object] $HTML.Add("<h1 style='text-align: center'>Directory Server Diagnosis Test Results (DCDiag.exe)</h1>") $FailedPercentage = $([math]::Round((($FailedTests.Count / ($FailedTests.Count + $PassingTests.Count)) * 100), 2)) $SuccessPercentage = 100 - $FailedPercentage $HTML.Add( @" <div class='p-3 linechart'> <div style='width: $FailedPercentage%; background-color: #C6313A;'></div> <div style='width: $SuccessPercentage%; background-color: #007644;'></div> </div> <ul class='unstyled p-3' style='display: flex; justify-content: space-between; '> <li><span class='chart-key' style='background-color: #C6313A;'></span><span>Failed ($($FailedTests.Count))</span></li> <li><span class='chart-key' style='background-color: #007644;'></span><span>Passed ($($PassingTests.Count))</span></li> </ul> "@ ) # Add failed tests to the HTML report $FailedTests | Sort-Object Test | ForEach-Object { $HTML.Add( @" <div class='info-card error'> <i class='info-icon fa-solid fa-circle-exclamation'></i> <div class='info-text'> <div class='info-title'>$($_.Test)</div> <div class='info-description'> $($_.Result | Out-String) </div> </div> </div> "@ ) } # Add passing tests to the HTML report $PassingTests | Sort-Object Test | ForEach-Object { $HTML.Add( @" <div class='info-card success'> <i class='info-icon fa-solid fa-circle-check'></i> <div class='info-text'> <div class='info-title'>$($_.Test)</div> <div class='info-description'> Test passed. </div> </div> </div> "@ ) } # Set the custom field with the HTML report Write-Host -Object "Attempting to set Custom Field '$wysiwygCustomField'." Set-NinjaProperty -Name $wysiwygCustomField -Value $HTML Write-Host -Object "Successfully set Custom Field '$wysiwygCustomField'!" } catch { Write-Host -Object "[Error] $($_.Exception.Message)" $ExitCode = 1 } } # Display the list of passing tests if ($PassingTests.Count -gt 0) { Write-Host -Object "" Write-Host -Object "Passing Tests: " -NoNewline Write-Host -Object ($PassingTests.Test | Sort-Object) -Separator ", " Write-Host -Object "" } # Display the list of failed tests with detailed output if ($FailedTests.Count -gt 0) { Write-Host -Object "[Alert] Failed Tests Detected!" Write-Host -Object "Failed Tests: " -NoNewline Write-Host -Object ($FailedTests.Test | Sort-Object) -Separator ", " Write-Host -Object "`n### Detailed Output ###" $FailedTests | Sort-Object Test | ForEach-Object { Write-Host -Object "" Write-Host -Object ($_.Result | Out-String) Write-Host -Object "" } } else { Write-Host -Object "All Directory Server Diagnosis Tests Pass!" } exit $ExitCode } end { }
Save time with over 300+ scripts from the NinjaOne Dojo.
Detailed Breakdown
Script Features
- Administrator Privileges Check
The script begins by verifying that it is running with elevated privileges, ensuring it has the necessary permissions to execute diagnostic commands. - Domain Controller Validation
It checks whether the script is executed on a domain controller. If not, it gracefully exits with an appropriate error message. - Execution of DCDiag Tests
A range of critical DCDiag tests are performed, including checks for connectivity, advertising, replication, system logs, and more. Results are parsed to separate passing and failing tests. - Result Categorization
Results are divided into passing and failing categories, with detailed outputs for any failed tests. This helps administrators focus on issues that require immediate attention. - Optional Custom Field Reporting
The script supports an optional parameter to output results in a WYSIWYG (What You See Is What You Get) custom field, useful for integration with tools like NinjaOne. - HTML Report Generation
For enhanced readability, the script generates an HTML report, summarizing test results in a visually intuitive format, including success and failure percentages.
Workflow Summary
- Validate prerequisites: Administrator privileges and domain controller environment.
- Execute DCDiag tests and capture results.
- Categorize and display results.
- Generate an optional HTML report for custom field integration.
Potential Use Cases
Real-World Scenario
Consider an IT administrator responsible for managing a multi-site AD environment. One of the domain controllers begins experiencing intermittent authentication failures. The administrator runs this script on the affected server to diagnose the issue. The output highlights failures in the Advertising and LocatorCheck tests, indicating problems with time synchronization and PDC role availability. The detailed output helps the administrator quickly identify and resolve the root cause, restoring normal operations.
Comparisons
Manual AD Health Checks
Manually running DCDiag tests requires a significant investment of time, especially in parsing and interpreting results. This script automates the entire process, providing structured outputs and integration options.
GUI-Based Tools
While GUI tools like AD Manager or RSAT provide a user-friendly interface for AD health checks, they often lack the flexibility and automation capabilities of PowerShell scripts. This script is particularly advantageous for MSPs managing multiple client environments.
FAQs
- Can this script be run on any server?
No, it must be run on a domain controller. Non-DC environments will trigger an error. - What permissions are required?
Administrator privileges are mandatory to execute the script successfully. - Can the results be exported to a file?
Yes, the script includes an option to generate an HTML report for custom field integration. - How are failed tests highlighted?
Failed tests are listed separately with detailed diagnostic messages, making it easy to prioritize troubleshooting efforts.
Implications
The results of this script can reveal critical vulnerabilities in your AD environment, such as replication failures or misconfigured roles. Left unresolved, these issues can lead to widespread authentication problems, policy misapplications, and security risks.
Recommendations
- Run Regularly: Schedule the script to run periodically to maintain continuous oversight of AD health.
- Analyze Trends: Compare results over time to identify recurring issues.
- Integrate with Monitoring Tools: Use the custom field option to integrate results into tools like NinjaOne for centralized monitoring.
Final Thoughts
This PowerShell script offers a streamlined, automated solution for performing AD health checks, reducing the time and effort required for manual diagnostics. By identifying issues early, IT professionals can proactively address potential disruptions, ensuring a stable and secure Active Directory environment. For MSPs, tools like NinjaOne complement this script by providing a centralized platform for managing and monitoring IT operations. Together, they form a robust strategy for maintaining organizational IT health.