Public/Get-DCDecommissionReadiness.ps1
|
function Get-DCDecommissionReadiness { <# .SYNOPSIS Pre-decommission readiness scan for a Domain Controller. .DESCRIPTION Collects all relevant information needed before decommissioning a DC: - FSMO role holdings - Replication health and partners - Logon activity (recent authentications) - DNS role and zone hosting - SYSVOL/DFSR health - Site/subnet associations - Dependent clients (recent Kerberos/NTLM logons from this DC) - Service dependencies (LDAP binds, trust status) - Pending replication changes Output is written to a timestamped HTML report and a structured PS object. .PARAMETER DCName FQDN or hostname of the DC to assess. .PARAMETER DaysBack How many days of event log history to scan. Default: 7. .PARAMETER OutputPath Folder to write the HTML report. Default: C:\ADOpsKit\Reports\Get-DCDecommissionReadiness. .PARAMETER Credential Alternate credentials if running from a non-domain-joined machine. .EXAMPLE Get-DCDecommissionReadiness -DCName "DC01.corp.contoso.com" .EXAMPLE Get-DCDecommissionReadiness -DCName "DC01.corp.contoso.com" -DaysBack 14 -OutputPath "D:\Reports" .NOTES Author: K Shankar R Karanth Website: https://karanth.ovh Version: 1.0 Run this from a machine with RSAT installed and Domain Admin (or delegated read) rights. The account running this script needs remote event log access on the target DC. #> [CmdletBinding()] param( [Parameter(Mandatory)] [string]$DCName, [int]$DaysBack = 7, [string]$OutputPath = "C:\ADOpsKit\Reports\Get-DCDecommissionReadiness", [System.Management.Automation.PSCredential]$Credential ) Set-StrictMode -Version Latest $ErrorActionPreference = "Continue" #region ── Helpers ────────────────────────────────────────────────────────────── function Write-Section { param([string]$Title) $line = "=" * 70 Write-Host "`n$line" -ForegroundColor Cyan Write-Host " $Title" -ForegroundColor Cyan Write-Host "$line" -ForegroundColor Cyan } function Write-Check { param([string]$Label, [string]$Status, [string]$Detail = "") $colour = switch ($Status) { "OK" { "Green" } "WARN" { "Yellow" } "FAIL" { "Red" } "INFO" { "White" } default { "Gray" } } $pad = $Label.PadRight(45) Write-Host " $pad [$Status] $Detail" -ForegroundColor $colour } function Get-EventsRemotely { param( [string]$ComputerName, [string]$LogName, [int[]]$EventIds, [datetime]$After, [int]$MaxResults = 200 ) $filter = @{ LogName = $LogName Id = $EventIds StartTime = $After } try { $splatArgs = @{ ComputerName = $ComputerName FilterHashtable = $filter MaxEvents = $MaxResults ErrorAction = "Stop" } if ($Credential) { $splatArgs.Credential = $Credential } Get-WinEvent @splatArgs } catch [System.Exception] { if ($_.Exception.Message -match "No events were found") { return @() } Write-Warning " [EventLog] $LogName on $ComputerName : $($_.Exception.Message)" return @() } } #endregion #region ── Init ───────────────────────────────────────────────────────────────── Import-Module ActiveDirectory -ErrorAction Stop $StartTime = Get-Date $Since = $StartTime.AddDays(-$DaysBack) $Timestamp = $StartTime.ToString("yyyyMMdd_HHmmss") $ReportFile = Join-Path $OutputPath "DCDecomm_${DCName}_${Timestamp}.html" $Results = [ordered]@{} # Accumulates all findings if (-not (Test-Path $OutputPath)) { New-Item -ItemType Directory -Path $OutputPath -Force | Out-Null } Write-Host "`n DC Pre-Decommission Readiness Scanner" -ForegroundColor Magenta Write-Host " Target : $DCName" -ForegroundColor Magenta Write-Host " Window : Last $DaysBack days (since $($Since.ToString('yyyy-MM-dd')))" -ForegroundColor Magenta Write-Host " Report : $ReportFile`n" -ForegroundColor Magenta # Verify DC is reachable if (-not (Test-Connection -ComputerName $DCName -Count 1 -Quiet)) { throw "Cannot reach $DCName." } #endregion #region ── 1. BASIC DC INFO ───────────────────────────────────────────────────── Write-Section "1. Basic DC Information" try { $splatDC = @{ Identity = $DCName; ErrorAction = "Stop" } if ($Credential) { $splatDC.Credential = $Credential } $DC = Get-ADDomainController @splatDC $dcInfo = [ordered]@{ Hostname = $DC.HostName Site = $DC.Site IPAddress = $DC.IPv4Address OS = $DC.OperatingSystem OSVersion = $DC.OperatingSystemVersion IsGlobalCatalog = $DC.IsGlobalCatalog IsRODC = $DC.IsReadOnly LdapPort = $DC.LdapPort Domain = $DC.Domain Forest = $DC.Forest } $Results["BasicInfo"] = $dcInfo $dcInfo.GetEnumerator() | ForEach-Object { Write-Check $_.Key "INFO" $_.Value } } catch { Write-Check "Get-ADDomainController" "FAIL" $_.Exception.Message $Results["BasicInfo"] = @{ Error = $_.Exception.Message } } #endregion #region ── 2. FSMO ROLES ──────────────────────────────────────────────────────── Write-Section "2. FSMO Role Holdings" try { $domain = Get-ADDomain -ErrorAction Stop $forest = Get-ADForest -ErrorAction Stop $fsmoRoles = [ordered]@{ PDCEmulator = $domain.PDCEmulator RIDMaster = $domain.RIDMaster InfrastructureMaster = $domain.InfrastructureMaster SchemaMaster = $forest.SchemaMaster DomainNamingMaster = $forest.DomainNamingMaster } $heldRoles = @() foreach ($role in $fsmoRoles.GetEnumerator()) { $holder = $role.Value -replace "\..*", "" # strip FQDN for comparison $target = $DCName -replace "\..*", "" if ($holder -ieq $target) { Write-Check $role.Key "WARN" "*** HELD BY THIS DC — must transfer before demote ***" $heldRoles += $role.Key } else { Write-Check $role.Key "OK" $role.Value } } $Results["FSMO"] = @{ Roles = $fsmoRoles; HeldByTargetDC = $heldRoles } } catch { Write-Check "FSMO Query" "FAIL" $_.Exception.Message } #endregion #region ── 3. REPLICATION HEALTH ──────────────────────────────────────────────── Write-Section "3. Replication Health" try { $replPartners = Get-ADReplicationPartnerMetadata -Target $DCName -ErrorAction Stop $Results["ReplicationPartners"] = $replPartners | Select-Object Partner, LastReplicationSuccess, LastReplicationAttempt, LastReplicationResult, ConsecutiveReplicationFailures foreach ($p in $replPartners) { $failures = $p.ConsecutiveReplicationFailures $lastOK = $p.LastReplicationSuccess $status = if ($failures -gt 0) { "WARN" } else { "OK" } Write-Check "Partner: $($p.Partner -replace 'CN=NTDS Settings,CN=|,.*','')" ` $status "LastOK=$($lastOK.ToString('yyyy-MM-dd HH:mm')) Failures=$failures" } # Replication queue / pending changes $replQueue = @(Get-ADReplicationQueueOperation -Server $DCName -ErrorAction Stop) $queueCount = ($replQueue | Measure-Object).Count $qStatus = if ($queueCount -gt 0) { "WARN" } else { "OK" } Write-Check "Replication Queue" $qStatus "$queueCount pending operations" $Results["ReplicationQueue"] = $replQueue # Replication failures $replFail = @(Get-ADReplicationFailure -Target $DCName -ErrorAction Stop) $failCount = ($replFail | Measure-Object).Count $fStatus = if ($failCount -gt 0) { "FAIL" } else { "OK" } Write-Check "Replication Failures" $fStatus "$failCount active failures" $Results["ReplicationFailures"] = $replFail } catch { Write-Check "Replication Query" "FAIL" $_.Exception.Message } #endregion #region ── 4. LOGON ACTIVITY — SECURITY EVENT LOG ─────────────────────────────── Write-Section "4. Logon Activity (Last $DaysBack Days)" # 4606/4624 = Logon; 4768 = Kerberos TGT; 4769 = Kerberos TGS; 4776 = NTLM $logonEventIds = @(4624, 4625, 4648, 4768, 4769, 4776) $logonEvents = Get-EventsRemotely -ComputerName $DCName -LogName "Security" ` -EventIds $logonEventIds -After $Since -MaxResults 1000 $logonSummary = $logonEvents | Group-Object Id | Sort-Object Count -Descending | Select-Object @{N="EventId";E={$_.Name}}, @{N="Description";E={ switch ($_.Name) { "4624" { "Interactive/Network Logon Success" } "4625" { "Logon Failure" } "4648" { "Logon with Explicit Credentials" } "4768" { "Kerberos TGT Request (AS-REQ)" } "4769" { "Kerberos Service Ticket (TGS-REQ)" } "4776" { "NTLM Authentication" } default { "Unknown" } } }}, Count $logonSummary | ForEach-Object { $status = if ($_.Count -gt 100) { "WARN" } else { "INFO" } Write-Check "Event $($_.EventId) — $($_.Description)" $status "Count=$($_.Count)" } $Results["LogonActivity"] = $logonSummary # Top authenticating accounts (from 4768 TGT requests) $tgtEvents = $logonEvents | Where-Object { $_.Id -eq 4768 } if ($tgtEvents) { $topAccounts = $tgtEvents | ForEach-Object { try { ([xml]$_.ToXml()).Event.EventData.Data | Where-Object { $_.Name -eq "TargetUserName" } | Select-Object -ExpandProperty "#text" } catch { $null } } | Where-Object { $_ } | Group-Object | Sort-Object Count -Descending | Select-Object -First 20 Write-Host "`n Top 20 Accounts Requesting Kerberos TGTs from this DC:" -ForegroundColor Yellow $topAccounts | ForEach-Object { Write-Check " $($_.Name)" "INFO" "TGT Requests: $($_.Count)" } $Results["TopKerberosAccounts"] = $topAccounts } #endregion #region ── 5. DNS ROLE ────────────────────────────────────────────────────────── Write-Section "5. DNS Role & Zone Hosting" try { $dnsService = Get-Service -ComputerName $DCName -Name DNS -ErrorAction Stop $dnsStatus = if ($dnsService.Status -eq "Running") { "WARN" } else { "OK" } Write-Check "DNS Server Service" $dnsStatus $dnsService.Status if ($dnsService.Status -eq "Running") { Write-Host " *** This DC is running DNS. Verify zone replication and remove from client DNS settings before decommission. ***" -ForegroundColor Yellow # Get zones hosted on this DC $zones = Get-DnsServerZone -ComputerName $DCName -ErrorAction Stop | Select-Object ZoneName, ZoneType, IsDsIntegrated, IsAutoCreated, ReplicationScope $zones | ForEach-Object { Write-Check "Zone: $($_.ZoneName)" "INFO" "Type=$($_.ZoneType) DSIntegrated=$($_.IsDsIntegrated)" } $Results["DNSZones"] = $zones } } catch { Write-Check "DNS Query" "FAIL" $_.Exception.Message } #endregion #region ── 6. SYSVOL / DFSR HEALTH ───────────────────────────────────────────── Write-Section "6. SYSVOL / DFSR Health" $dfsrEventIds = @(2213, 4012, 5008, 1202, 6016, 6018) $dfsrEvents = @(Get-EventsRemotely -ComputerName $DCName -LogName "DFS Replication" ` -EventIds $dfsrEventIds -After $Since -MaxResults 100) if ($dfsrEvents.Count -gt 0) { Write-Check "DFSR Critical Events" "WARN" "$($dfsrEvents.Count) events found in last $DaysBack days" $dfsrEvents | Select-Object TimeCreated, Id, Message | ForEach-Object { Write-Host " [$($_.TimeCreated.ToString('yyyy-MM-dd HH:mm'))] Event $($_.Id): $($_.Message.Substring(0,[math]::Min(120,$_.Message.Length)))" -ForegroundColor Yellow } } else { Write-Check "DFSR Critical Events" "OK" "No critical DFSR events in last $DaysBack days" } $Results["DFSREvents"] = @($dfsrEvents | Select-Object TimeCreated, Id, Message) # SYSVOL share availability $sysvol = Test-Path -LiteralPath "\\$DCName\SYSVOL" -ErrorAction SilentlyContinue if ($sysvol) { Write-Check "SYSVOL Share" "OK" "Share is present and accessible" } else { Write-Check "SYSVOL Share" "WARN" "SYSVOL share not found or inaccessible remotely" } $netlogon = Test-Path -LiteralPath "\\$DCName\NETLOGON" -ErrorAction SilentlyContinue $nlStatus = if ($netlogon) { "OK" } else { "WARN" } Write-Check "NETLOGON Share" $nlStatus $(if ($netlogon) { "Present" } else { "Not found" }) #endregion #region ── 7. SITE & SUBNET ASSOCIATIONS ──────────────────────────────────────── Write-Section "7. Site & Subnet Associations" try { $siteName = (Get-ADDomainController -Identity $DCName).Site Write-Check "DC Site" "INFO" $siteName $subnets = Get-ADReplicationSubnet -Filter * -Properties Site | Where-Object { $_.Site -match $siteName } $subnetCount = ($subnets | Measure-Object).Count $snStatus = if ($subnetCount -gt 0) { "WARN" } else { "OK" } Write-Check "Subnets mapped to this site" $snStatus "$subnetCount subnet(s) — reassign or remove before decommission" $subnets | ForEach-Object { Write-Host " $($_.Name) → $($_.Site)" -ForegroundColor Gray } $Results["Subnets"] = @($subnets | Select-Object Name, Location, Site) # Site links referencing this site $siteLinks = Get-ADReplicationSiteLink -Filter * | Where-Object { $_.SitesIncluded -match $siteName } $slCount = ($siteLinks | Measure-Object).Count Write-Check "Site Links referencing this site" "INFO" "$slCount link(s)" $siteLinks | ForEach-Object { Write-Host " $($_.Name) — Sites: $($_.SitesIncluded -join ', ')" -ForegroundColor Gray } $Results["SiteLinks"] = $siteLinks | Select-Object Name, Cost, ReplicationFrequencyInMinutes, SitesIncluded } catch { Write-Check "Site/Subnet Query" "FAIL" $_.Exception.Message } #endregion #region ── 8. NETLOGON / SECURE CHANNEL ───────────────────────────────────────── Write-Section "8. Netlogon & Secure Channel Activity" $netlogonEventIds = @(5722, 5805, 5723, 3210, 5806) $netlogonEvents = @(Get-EventsRemotely -ComputerName $DCName -LogName "System" ` -EventIds $netlogonEventIds -After $Since -MaxResults 200) if ($netlogonEvents.Count -gt 0) { Write-Check "Secure Channel Issues (5722/5805)" "WARN" "$($netlogonEvents.Count) events found" $netlogonEvents | Select-Object TimeCreated, Id, Message | ForEach-Object { Write-Host " [$($_.TimeCreated.ToString('yyyy-MM-dd HH:mm'))] Event $($_.Id): $($_.Message.Substring(0,[math]::Min(120,$_.Message.Length)))" -ForegroundColor Yellow } } else { Write-Check "Secure Channel Issues" "OK" "No Netlogon errors in last $DaysBack days" } # Check Netlogon.log for this DC for LOGON entries (via UNC) $netlogonLog = "\\$DCName\ADMIN$\debug\netlogon.log" try { if (Test-Path $netlogonLog) { $logLines = Get-Content $netlogonLog -Tail 500 $logonLines = @($logLines | Select-String "LOGON" | Select-Object -Last 50) Write-Check "Netlogon.log LOGON entries (last 500 lines)" "INFO" "$($logonLines.Count) LOGON entries" $Results["NetlogonLog"] = $logonLines | Select-Object -ExpandProperty Line } else { Write-Check "Netlogon.log" "WARN" "Not accessible at $netlogonLog" } } catch { Write-Check "Netlogon.log" "WARN" "Access denied or path unavailable" } #endregion #region ── 9. DIRECTORY SERVICE ERRORS ────────────────────────────────────────── Write-Section "9. Directory Service Event Log" $dsErrorIds = @(1311, 1566, 2088, 1084, 1925, 2042) $dsEvents = @(Get-EventsRemotely -ComputerName $DCName -LogName "Directory Service" ` -EventIds $dsErrorIds -After $Since -MaxResults 200) if ($dsEvents.Count -gt 0) { Write-Check "Directory Service Errors/Warnings" "WARN" "$($dsEvents.Count) events found" $dsEvents | Select-Object TimeCreated, Id, Message | ForEach-Object { Write-Host " [$($_.TimeCreated.ToString('yyyy-MM-dd HH:mm'))] Event $($_.Id): $($_.Message.Substring(0,[math]::Min(150,$_.Message.Length)))" -ForegroundColor Yellow } } else { Write-Check "Directory Service Errors" "OK" "No critical DS events in last $DaysBack days" } $Results["DirectoryServiceEvents"] = @($dsEvents | Select-Object TimeCreated, Id, Message) #endregion #region ── 10. ACTIVE SESSIONS ON THIS DC ─────────────────────────────────────── Write-Section "10. Active RDP / Interactive Sessions" try { $sessions = query session /server:$DCName 2>&1 Write-Host " Active Sessions on $DCName :" -ForegroundColor Yellow $sessions | ForEach-Object { Write-Host " $_" -ForegroundColor Gray } $Results["ActiveSessions"] = $sessions } catch { Write-Check "Session Query" "WARN" $_.Exception.Message } #endregion #region ── 11. TRUST RELATIONSHIPS ────────────────────────────────────────────── Write-Section "11. Trust Relationships" try { $trusts = Get-ADTrust -Filter * -ErrorAction Stop | Select-Object Name, Direction, TrustType, IntraForest $trustCount = ($trusts | Measure-Object).Count Write-Check "Domain Trusts" "INFO" "$trustCount trust(s) — verify none depend solely on this DC" $trusts | ForEach-Object { Write-Host " $($_.Name) [$($_.Direction)] Type=$($_.TrustType) IntraForest=$($_.IntraForest)" -ForegroundColor Gray } $Results["Trusts"] = $trusts } catch { Write-Check "Trust Query" "FAIL" $_.Exception.Message } #endregion #region ── 12. GLOBAL CATALOG STATUS ──────────────────────────────────────────── Write-Section "12. Global Catalog Status" try { $isGC = (Get-ADDomainController -Identity $DCName).IsGlobalCatalog $allGCs = (Get-ADForest).GlobalCatalogs $gcCount = ($allGCs | Measure-Object).Count if ($isGC) { Write-Check "Is Global Catalog" "WARN" "*** This DC is a GC. Ensure another GC exists in the site before decommission. ***" Write-Check "Other GCs in forest" "INFO" "$gcCount total — $($allGCs -join ', ')" } else { Write-Check "Is Global Catalog" "OK" "Not a GC — no GC transfer needed" } $Results["GlobalCatalog"] = @{ IsGC = $isGC; AllGCs = $allGCs } } catch { Write-Check "GC Query" "FAIL" $_.Exception.Message } #endregion #region ── 13. PENDING DECOMMISSION CHECKLIST ─────────────────────────────────── Write-Section "13. Pre-Decommission Checklist Summary" $checklist = [ordered]@{ "Transfer FSMO roles off this DC" = if ($Results["FSMO"].HeldByTargetDC.Count -gt 0) { "ACTION REQUIRED" } else { "OK — No roles held" } "Resolve replication failures" = if ($Results["ReplicationFailures"].Count -gt 0) { "ACTION REQUIRED" } else { "OK" } "Drain replication queue" = if ($Results["ReplicationQueue"].Count -gt 0) { "ACTION REQUIRED" } else { "OK" } "Reassign/remove subnets from site" = if ($Results["Subnets"].Count -gt 0) { "ACTION REQUIRED" } else { "OK" } "Migrate DNS zones / remove from client DNS" = if ($DC.IsGlobalCatalog) { "VERIFY" } else { "VERIFY" } "Ensure another GC exists in this site" = if ($Results["GlobalCatalog"].IsGC) { "ACTION REQUIRED" } else { "OK" } "Verify no clients homed solely to this DC" = "VERIFY — see LogonActivity section" "Check for active user sessions" = "VERIFY — see ActiveSessions section" "Resolve Directory Service errors" = if ($Results["DirectoryServiceEvents"].Count -gt 0) { "REVIEW" } else { "OK" } "Resolve DFSR/SYSVOL errors" = if ($Results["DFSREvents"].Count -gt 0) { "REVIEW" } else { "OK" } "Notify dependent applications / service accounts" = "MANUAL — review TopKerberosAccounts list" "Run: dcpromo /forceremoval only as last resort" = "NOTE — use Uninstall-ADDSDomainController" } foreach ($item in $checklist.GetEnumerator()) { $status = switch -Wildcard ($item.Value) { "OK*" { "OK" } "ACTION REQUIRED*" { "WARN" } "VERIFY*" { "INFO" } "REVIEW*" { "INFO" } "MANUAL*" { "INFO" } "NOTE*" { "INFO" } default { "INFO" } } Write-Check $item.Key $status $item.Value } #endregion #region ── HTML REPORT ─────────────────────────────────────────────────────────── function ConvertTo-HtmlTable { param([object[]]$Data, [string]$Title) if (-not $Data -or $Data.Count -eq 0) { return "<p><em>No data.</em></p>" } $html = "<h3>$Title</h3><table><thead><tr>" $props = $Data[0].PSObject.Properties.Name $props | ForEach-Object { $html += "<th>$_</th>" } $html += "</tr></thead><tbody>" foreach ($row in $Data) { $html += "<tr>" $props | ForEach-Object { $html += "<td>$($row.$_)</td>" } $html += "</tr>" } $html += "</tbody></table>" return $html } $html = @" <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>DC Decommission Readiness — $DCName</title> <style> body { font-family: Consolas, monospace; background:#1e1e2e; color:#cdd6f4; margin:2rem; } h1 { color:#cba6f7; border-bottom:2px solid #cba6f7; padding-bottom:.4rem; } h2 { color:#89b4fa; margin-top:2rem; } h3 { color:#94e2d5; } table { border-collapse:collapse; width:100%; margin-bottom:1.5rem; font-size:.85rem; } th { background:#313244; color:#cba6f7; padding:.4rem .6rem; text-align:left; } td { padding:.35rem .6rem; border-bottom:1px solid #313244; } tr:hover td { background:#313244; } .warn { color:#f9e2af; } .fail { color:#f38ba8; } .ok { color:#a6e3a1; } .meta { color:#6c7086; font-size:.8rem; margin-bottom:2rem; } </style> </head> <body> <h1>DC Pre-Decommission Readiness Report</h1> <p class="meta"> Target DC : <strong>$DCName</strong><br> Generated : <strong>$($StartTime.ToString('yyyy-MM-dd HH:mm:ss'))</strong><br> Scan window : <strong>Last $DaysBack days</strong> </p> <h2>1. Basic DC Information</h2> $(ConvertTo-HtmlTable -Data ($Results["BasicInfo"].GetEnumerator() | Select-Object @{N="Property";E={$_.Key}},@{N="Value";E={$_.Value}}) -Title "") <h2>2. FSMO Roles</h2> $(ConvertTo-HtmlTable -Data ($Results["FSMO"].Roles.GetEnumerator() | Select-Object @{N="Role";E={$_.Key}},@{N="Holder";E={$_.Value}}) -Title "") <p class="warn">Roles held by this DC: <strong>$($Results["FSMO"].HeldByTargetDC -join ", ")</strong></p> <h2>3. Replication Partners</h2> $(ConvertTo-HtmlTable -Data $Results["ReplicationPartners"] -Title "") <h2>4. Logon Activity Summary</h2> $(ConvertTo-HtmlTable -Data $Results["LogonActivity"] -Title "") <h2>4b. Top Kerberos Accounts (TGT Requests)</h2> $(ConvertTo-HtmlTable -Data ($Results["TopKerberosAccounts"] | Select-Object @{N="Account";E={$_.Name}},@{N="TGT Requests";E={$_.Count}}) -Title "") <h2>5. DNS Zones</h2> $(ConvertTo-HtmlTable -Data $Results["DNSZones"] -Title "") <h2>6. DFSR Events</h2> $(ConvertTo-HtmlTable -Data $Results["DFSREvents"] -Title "") <h2>7. Site Subnets</h2> $(ConvertTo-HtmlTable -Data $Results["Subnets"] -Title "") <h2>8. Netlogon Log (last 50 LOGON entries)</h2> <pre>$(($Results["NetlogonLog"] -join "`n"))</pre> <h2>9. Directory Service Events</h2> $(ConvertTo-HtmlTable -Data $Results["DirectoryServiceEvents"] -Title "") <h2>10. Trust Relationships</h2> $(ConvertTo-HtmlTable -Data $Results["Trusts"] -Title "") <h2>11. Global Catalog</h2> $(ConvertTo-HtmlTable -Data @([pscustomobject]@{ IsGC = $Results["GlobalCatalog"].IsGC; AllGCsInForest = ($Results["GlobalCatalog"].AllGCs -join ", ") }) -Title "") <h2>12. Pre-Decommission Checklist</h2> $(ConvertTo-HtmlTable -Data ($checklist.GetEnumerator() | Select-Object @{N="Item";E={$_.Key}},@{N="Status";E={$_.Value}}) -Title "") <hr> <p class="meta">Generated by Get-DCDecommissionReadiness.ps1 — ADOpsKit</p> </body> </html> "@ $html | Out-File -FilePath $ReportFile -Encoding UTF8 Write-Host "`n [DONE] Report written to: $ReportFile" -ForegroundColor Green Write-Host " Total runtime: $([math]::Round(((Get-Date) - $StartTime).TotalSeconds, 1))s`n" -ForegroundColor Green # Return structured results object for pipeline use return [pscustomobject]$Results #endregion } |