Collectors/Get-EscCaServerStatus.ps1

function Get-EscCaServerStatus {
    <#
    .SYNOPSIS
        Builds a live CA inventory: reachability + certutil ping + Cert Publishers
        cross-check, with discrepancy detection (read-only, live-only).
    .DESCRIPTION
        For every Enterprise CA discovered under the Enrollment Services container
        this correlates three independent sources of truth and flags where they
        disagree:

          1. Enrollment Services (AD configuration) - the CA is registered.
          2. certutil -ping - the CA service actually answers.
          3. Cert Publishers group - the CA computer object is a member.

        Reachability is a passive TCP probe (RPC endpoint mapper, 135) with an ICMP
        fallback. Nothing is enrolled, submitted, or modified. All checks require a
        live domain / Windows tooling; on non-domain or offline runs this is simply
        not invoked.
    .PARAMETER EnrollmentService
        CA objects from Get-EscEnrollmentService (Name, DnsHostName, ...).
    .PARAMETER CertPublisher
        Members from Get-EscCertPublisher (used for the group cross-check).
    .PARAMETER TimeoutMs
        Per-probe TCP connect timeout (default 1500 ms).
    .OUTPUTS
        [pscustomobject] with Servers[], CertPublishers[], Discrepancies[].
    .EXAMPLE
        Get-EscCaServerStatus -EnrollmentService $cas -CertPublisher $pub
    #>

    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $false)]
        [object[]] $EnrollmentService = @(),

        [Parameter(Mandatory = $false)]
        [object[]] $CertPublisher = @(),

        [Parameter(Mandatory = $false)]
        [int] $TimeoutMs = 1500
    )

    $shortName = {
        param($h)
        if ([string]::IsNullOrWhiteSpace([string]$h)) { return '' }
        return (([string]$h) -split '\.')[0].ToLowerInvariant()
    }

    $cpHosts = @()
    foreach ($cp in $CertPublisher) {
        if ($null -eq $cp) { continue }
        $h = ''
        if ($cp.DnsHostName) { $h = [string]$cp.DnsHostName }
        elseif ($cp.SamAccountName) { $h = ([string]$cp.SamAccountName).TrimEnd('$') }
        elseif ($cp.Name) { $h = [string]$cp.Name }
        if (-not [string]::IsNullOrWhiteSpace($h)) { $cpHosts += $h }
    }
    $cpShort = @($cpHosts | ForEach-Object { & $shortName $_ } | Where-Object { $_ } | Select-Object -Unique)

    $servers = @()
    foreach ($svc in $EnrollmentService) {
        if ($null -eq $svc) { continue }
        $dns = [string]$svc.DnsHostName
        if ([string]::IsNullOrWhiteSpace($dns)) { $dns = [string]$svc.Name }
        $caName = [string]$svc.Name
        $short = & $shortName $dns

        $reachable = $false
        $method = ''
        if (-not [string]::IsNullOrWhiteSpace($dns)) {
            if (Test-EscTcpPort -ComputerName $dns -Port 135 -TimeoutMs $TimeoutMs) {
                $reachable = $true; $method = 'TCP/135'
            }
            else {
                try {
                    if (Test-Connection -ComputerName $dns -Count 1 -Quiet -ErrorAction SilentlyContinue) {
                        $reachable = $true; $method = 'ICMP'
                    }
                }
                catch { }
            }
        }

        $certutilAlive = $null
        $certutilDetail = ''
        if (-not [string]::IsNullOrWhiteSpace($dns) -and -not [string]::IsNullOrWhiteSpace($caName)) {
            $cfg = '{0}\{1}' -f $dns, $caName
            try {
                $raw = & certutil -ping -config $cfg 2>&1
                $text = ($raw | Out-String)
                $exit = $LASTEXITCODE
                $certutilAlive = (($exit -eq 0) -and ($text -match 'interface is alive|completed successfully'))
                $line = @($text -split "`r?`n" | Where-Object { $_ -match 'alive|completed|error|0x8|0x80' } | Select-Object -First 1)
                if ($line.Count -gt 0) { $certutilDetail = ([string]$line[0]).Trim() }
            }
            catch {
                $certutilAlive = $false
                $certutilDetail = $_.Exception.Message
            }
        }

        $inCp = $false
        if ($short) { $inCp = (@($cpShort) -contains $short) }

        $servers += [pscustomobject]@{
            CaName           = $caName
            DnsHostName      = $dns
            Reachable        = $reachable
            ReachableMethod  = $method
            CertutilAlive    = $certutilAlive
            CertutilDetail   = $certutilDetail
            InCertPublishers = $inCp
        }
    }

    $disc = @()
    $esShort = @($servers | ForEach-Object { & $shortName $_.DnsHostName } | Where-Object { $_ } | Select-Object -Unique)

    foreach ($s in $servers) {
        if (-not $s.Reachable) {
            $disc += [pscustomobject]@{
                Type = 'Host unreachable'; Host = $s.DnsHostName
                Detail = 'CA host did not answer TCP/135 or ICMP.'
            }
        }
        if ($s.CertutilAlive -eq $false) {
            $disc += [pscustomobject]@{
                Type = 'CA service down'; Host = $s.DnsHostName
                Detail = ("CA '{0}' does not respond to certutil -ping." -f $s.CaName)
            }
        }
        if (-not $s.InCertPublishers) {
            $disc += [pscustomobject]@{
                Type = 'Missing from Cert Publishers'; Host = $s.DnsHostName
                Detail = 'Enrollment Service CA host is not a member of the Cert Publishers group.'
            }
        }
    }
    foreach ($h in $cpShort) {
        if (@($esShort) -notcontains $h) {
            $disc += [pscustomobject]@{
                Type = 'Orphan Cert Publisher'; Host = $h
                Detail = 'Computer is in Cert Publishers but has no Enrollment Service object (possible stale or rogue CA).'
            }
        }
    }

    Write-EscLog -Component 'CaStatus' -Message ("CA status: {0} server(s), {1} discrepancy(ies)." -f @($servers).Count, @($disc).Count)

    return [pscustomobject]@{
        Servers        = @($servers)
        CertPublishers = @($cpHosts)
        Discrepancies  = @($disc)
    }
}