Public/Test-RsCertBinding.ps1

# =============================================================================
# Script : Public/Test-RsCertBinding.ps1
# Author : Keith Ramsey
# Created : 2026-06-08
# =============================================================================
# Change Log
# -----------------------------------------------------------------------------
# 2026-06-08 Keith Ramsey Initial: Phase 5 RS cert binding verification.
# Calls Get-SqlRsInstance (private helper), reads
# bound thumbprint via ListSSLCertificateBindings WMI
# method (DR-013), looks up cert expiry in
# Cert:\LocalMachine\My. Never throws (DR-005).
# Standalone function per DR-015.
# 2026-06-08 Keith Ramsey Route Invoke-CimMethod dispatch through private
# Invoke-SqlRsCimBinding wrapper for Pester mockability
# (Invoke-CimMethod -InputObject enforces [CimInstance]
# binding before mock intercept).
# =============================================================================
# Decision Contract:
# - Never throws (DR-005). All errors caught and returned as Failed.
# - No hostnames, IPs, service accounts in source (air-gap discipline).
# - WMI API is ListSSLCertificateBindings on MSReportServer_ConfigurationSetting
# (DR-013). Property read is filtered by -Application to match only the
# requested RS endpoint.
# - dbatools must not be re-introduced (commit c2b23a4).
# - Standalone: no change to Install-SqlConnectionCertificate (DR-015).
# =============================================================================

function Test-RsCertBinding {
    <#
    .SYNOPSIS
        Reports the TLS certificate binding for a Reporting Services endpoint.
    .DESCRIPTION
        Calls Get-SqlRsInstance to resolve the MSReportServer_ConfigurationSetting
        WMI object, then invokes ListSSLCertificateBindings to read the bound
        certificate thumbprint for the specified RS application endpoint. Looks
        up the certificate in Cert:\LocalMachine\My, checks its expiry, and
        returns a SqlCert.Result.

        Returns Status=Success with Thumbprint=null and an informational Detail
        when RS has no TLS certificate bound (HTTP-only mode). Returns
        Status=Failed if the RS instance cannot be found, if the bound
        certificate has expired, or if any WMI call fails.

        Use -Credential to supply alternate credentials for the WMI connection
        when the calling account lacks RS admin rights on the target machine.
        Never throws.
    .PARAMETER RsInstance
        Reporting Services instance name. Use 'MSSQLSERVER' for the default
        SSRS 2016 instance, 'SSRS' for SSRS 2017+ standalone, or 'PBIRS' for
        Power BI Report Server. Defaults to 'MSSQLSERVER'.
    .PARAMETER Application
        RS application endpoint to inspect. Must be one of:
          ReportServerWebService -- the Report Server web service (default)
          ReportServerWebApp -- the web portal
        These are the application name strings used by CreateSSLCertificateBinding
        and ListSSLCertificateBindings (DR-013; SSRS 2016+ naming convention).
    .PARAMETER ComputerName
        Target machine. Defaults to the local computer. Used by
        Get-SqlRsInstance to construct the CIM connection. Air-gap rule: pass
        the value; never hard-code a hostname.
    .PARAMETER Credential
        Credential for the CIM/WMI session when the calling account does not
        have RS admin rights on the target machine.
    .OUTPUTS
        SqlCert.Result
        Data keys: RsInstance, Application, Thumbprint, ExpiresOn, IsExpired.
    .EXAMPLE
        Test-RsCertBinding
        # Checks the default SSRS instance (MSSQLSERVER), ReportServerWebService
        # application, on the local machine.
    .EXAMPLE
        Test-RsCertBinding -RsInstance 'PBIRS' -Application 'ReportServerWebApp'
        # Checks the web portal endpoint on a PBIRS installation.
    .EXAMPLE
        Test-RsCertBinding -RsInstance 'SSRS' -ComputerName $node -Credential $cred
        # Checks a remote SSRS 2017+ node with explicit credentials.
    .NOTES
    Steps:
      1. Call Get-SqlRsInstance to resolve the RS WMI namespace and config object.
      2. If RS not found, return Failed.
      3. Read bound cert thumbprint from the WMI config object via
         ListSSLCertificateBindings; filter results by Application name.
      4. If no thumbprint (HTTP only): return Success with Thumbprint=null and
         informational Detail.
      5. Look up cert in Cert:\LocalMachine\My by thumbprint; check NotAfter.
      6. Build per-binding data row: RsInstance, Application, Thumbprint,
         ExpiresOn, IsExpired.
      7. If IsExpired=true, return Failed with expiry detail.
      8. Else return Success.
    #>

    [CmdletBinding()]
    [OutputType('SqlCert.Result')]
    param(
        [string] $RsInstance = 'MSSQLSERVER',
        [ValidateSet('ReportServerWebService', 'ReportServerWebApp')]
        [string] $Application = 'ReportServerWebService',
        [string] $ComputerName = $env:COMPUTERNAME,
        [PSCredential] $Credential
    )

    try {
        # ------------------------------------------------------------------
        # Step 1 — Resolve the RS WMI config object via the private helper.
        # ------------------------------------------------------------------

        $getRsArgs = @{
            InstanceName = $RsInstance
            ComputerName = $ComputerName
        }
        if ($Credential) { $getRsArgs['Credential'] = $Credential }

        $configObj = Get-SqlRsInstance @getRsArgs

        # ------------------------------------------------------------------
        # Step 2 — Return Failed if the RS instance was not found.
        # ------------------------------------------------------------------

        if (-not $configObj) {
            return New-SqlCertResult -Stage Verify -Status Failed `
                -Detail "RS instance '$RsInstance' not found on '$ComputerName'. Verify that Reporting Services is installed and the instance name is correct." `
                -Data $null
        }

        # ------------------------------------------------------------------
        # Step 3 — Call ListSSLCertificateBindings to read the bound cert
        # thumbprint for the requested Application endpoint (DR-013).
        # The method returns parallel arrays: Application[], CertificateHash[],
        # IPAddress[], Port[], HRESULT.
        # ------------------------------------------------------------------

        $thumbprint = $null
        try {
            $bindResult = Invoke-SqlRsCimBinding -ConfigObject $configObj

            if ($bindResult.HRESULT -eq 0 -and $bindResult.Application) {
                $apps   = @($bindResult.Application)
                $hashes = @($bindResult.CertificateHash)
                for ($i = 0; $i -lt $apps.Count; $i++) {
                    if ($apps[$i] -ieq $Application) {
                        $thumbprint = $hashes[$i]
                        break
                    }
                }
            }
        }
        catch {
            return New-SqlCertResult -Stage Verify -Status Failed `
                -Detail "ListSSLCertificateBindings WMI call failed for RS instance '$RsInstance' on '$ComputerName': $($_.Exception.Message)" `
                -Data $null
        }

        # ------------------------------------------------------------------
        # Step 4 — No thumbprint: RS is running HTTP-only. This is not an
        # error condition; return Success with an informational Detail.
        # ------------------------------------------------------------------

        if ([string]::IsNullOrEmpty($thumbprint)) {
            $data = [pscustomobject]@{
                RsInstance  = $RsInstance
                Application = $Application
                Thumbprint  = $null
                ExpiresOn   = $null
                IsExpired   = $false
            }
            return New-SqlCertResult -Stage Verify -Status Success `
                -Detail "No certificate is bound to RS instance '$RsInstance' application '$Application'. RS is using HTTP only." `
                -Data $data
        }

        # ------------------------------------------------------------------
        # Step 5 — Look up the bound cert in Cert:\LocalMachine\My on the
        # TARGET (that is the store RS binds from), and check NotAfter
        # vs current time. When the target cannot be read over
        # remoting the expiry is reported as unknown ($null) rather
        # than guessed from the caller's own store.
        # ------------------------------------------------------------------

        $certArgs = @{ Thumbprint = $thumbprint; ComputerName = $ComputerName }
        if ($Credential) { $certArgs['Credential'] = $Credential }
        $certLookup = Get-SqlCertFromTargetStore @certArgs

        $expiresOn = if ($certLookup.Checked -and $certLookup.Found) { $certLookup.NotAfter } else { $null }
        $isExpired = [bool]($null -ne $expiresOn -and $expiresOn -lt [datetime]::Now)

        # ------------------------------------------------------------------
        # Step 6 — Build the data row.
        # ------------------------------------------------------------------

        $data = [pscustomobject]@{
            RsInstance  = $RsInstance
            Application = $Application
            Thumbprint  = $thumbprint
            ExpiresOn   = $expiresOn
            IsExpired   = $isExpired
        }

        # ------------------------------------------------------------------
        # Step 7 — Return Failed if the bound certificate is expired.
        # ------------------------------------------------------------------

        if ($isExpired) {
            $expDetail = if ($expiresOn) {
                $expiresOn.ToString('yyyy-MM-dd')
            } else {
                'unknown expiry'
            }
            return New-SqlCertResult -Stage Verify -Status Failed `
                -Detail "Bound RS certificate is expired ($expDetail) -- RS will reject HTTPS connections for '$Application' on instance '$RsInstance'. Renew or replace the certificate and re-run Set-RsCertBinding." `
                -Data $data
        }

        # ------------------------------------------------------------------
        # Step 8 — Return Success.
        # ------------------------------------------------------------------

        New-SqlCertResult -Stage Verify -Status Success `
            -Detail "RS instance '$RsInstance' application '$Application' has a valid TLS certificate bound (thumbprint: $thumbprint)." `
            -Data $data
    }
    catch {
        New-SqlCertResult -Stage Verify -Status Failed `
            -Detail "Test-RsCertBinding encountered an unexpected error: $($_.Exception.Message)" `
            -Data $null
    }
}