Private/Get-SqlCertFromTargetStore.ps1

# =============================================================================
# Script : Private/Get-SqlCertFromTargetStore.ps1
# Author : Keith Ramsey
# Created : 2026-08-23
# =============================================================================
# Change Log
# -----------------------------------------------------------------------------
# 2026-08-23 Keith Ramsey Initial: look up a certificate in the TARGET
# machine's LocalMachine\My store. Extracted because
# Set-RsCertBinding and Test-RsCertBinding both read
# the LOCAL store while their help and error text
# claimed the -ComputerName target -- so on a remote
# target both could report "not found" for a cert
# that was present, or pass a pre-flight for a cert
# the target did not have.
# =============================================================================
# Decision Contract (see Docs/DECISION_REGISTER.md)
# -----------------------------------------------------------------------------
# Must : read the store on the machine named by -ComputerName, not the
# caller's. Never throw (DR-005). Distinguish "checked and absent"
# from "could not check" -- the RS path needs only CIM/WS-Man, so a
# machine without PowerShell remoting must still be bindable: the
# caller skips its pre-flight rather than failing, and lets the RS
# provider report the authoritative error.
# =============================================================================

function Get-SqlCertFromTargetStore {
    <#
    .SYNOPSIS
        Finds a certificate by thumbprint in LocalMachine\My on the target computer.
    .DESCRIPTION
        Returns a small result describing whether the lookup could be performed
        and, if so, what it found. Never throws.

        Local targets read the store directly. Remote targets read it over
        PowerShell remoting. The RS surface otherwise needs only CIM (WS-Man),
        so remoting may be unavailable on a machine where binding still works --
        in that case Checked is $false and the caller treats the pre-flight as
        indeterminate rather than failing the operation.
    .PARAMETER Thumbprint
        40-character hex thumbprint to find.
    .PARAMETER ComputerName
        Machine whose LocalMachine\My store is searched. Defaults to the local
        machine.
    .PARAMETER Credential
        Credential for the remote lookup, when the target needs one.
    .OUTPUTS
        PSCustomObject:
          Checked $true when the store was actually read.
          Found $true when a matching certificate was present.
          NotAfter expiry of the matched certificate, else $null.
          Reason why Checked is $false, else ''.

        Deliberately narrow: only what the callers consume. The module runs
        Set-StrictMode -Version Latest, so reading a property a certificate
        object may not carry would throw and be swallowed as an indeterminate
        result -- which is exactly how a speculative Subject field masked a
        working lookup during development.
    .EXAMPLE
        $r = Get-SqlCertFromTargetStore -Thumbprint $t -ComputerName 'RS01'
        if ($r.Checked -and -not $r.Found) { 'definitely absent on RS01' }

        Shows the three-state contract: only treat an absent certificate as a
        hard error when the store was actually read.
    .NOTES
        Steps:
          1. Build the store-search scriptblock (thumbprint passed as a bound
             argument, never interpolated into the script text).
          2. Run it locally, or via Invoke-Command against a remote target.
          3. On a remoting failure, return Checked = $false with the reason --
             never throw, and never report a false "not found".
    #>

    [CmdletBinding()]
    [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseUsingScopeModifierInNewRunspaces', '',
        Justification = 'The thumbprint is passed via param() + -ArgumentList, not closure capture; the Using: modifier is not applicable.')]
    param(
        [Parameter(Mandatory)]
        [string] $Thumbprint,

        [string] $ComputerName = $env:COMPUTERNAME,

        [System.Management.Automation.PSCredential] $Credential
    )

    # Remote lookups need a scriptblock for Invoke-Command. The LOCAL lookup is
    # deliberately NOT routed through that scriptblock: calling Get-ChildItem
    # inline keeps the store read in this function's own scope, which is what
    # the existing unit tests mock.
    $sb = {
        param($Tp)
        $c = Get-ChildItem Cert:\LocalMachine\My -ErrorAction SilentlyContinue |
            Where-Object { $_.Thumbprint -ieq $Tp } |
            Select-Object -First 1
        if ($c) {
            [PSCustomObject]@{ Found = $true; NotAfter = $c.NotAfter }
        } else {
            [PSCustomObject]@{ Found = $false; NotAfter = $null }
        }
    }

    try {
        if ($ComputerName -eq $env:COMPUTERNAME -or $ComputerName -eq 'localhost' -or $ComputerName -eq '.') {
            $c = Get-ChildItem Cert:\LocalMachine\My -ErrorAction SilentlyContinue |
                Where-Object { $_.Thumbprint -ieq $Thumbprint } |
                Select-Object -First 1
            $hit = if ($c) {
                [PSCustomObject]@{ Found = $true; NotAfter = $c.NotAfter }
            } else {
                [PSCustomObject]@{ Found = $false; NotAfter = $null }
            }
        } else {
            $icArgs = @{
                ComputerName = $ComputerName
                ScriptBlock  = $sb
                ArgumentList = @($Thumbprint)
                ErrorAction  = 'Stop'
            }
            if ($Credential) { $icArgs['Credential'] = $Credential }
            $hit = Invoke-Command @icArgs
        }

        [PSCustomObject]@{
            Checked  = $true
            Found    = [bool]$hit.Found
            NotAfter = $hit.NotAfter
            Reason   = ''
        }
    } catch {
        # Remoting unavailable or refused. The RS bind path needs only CIM, so
        # this must not fail the operation -- report indeterminate and let the
        # caller fall through to the RS provider's own authoritative error.
        [PSCustomObject]@{
            Checked  = $false
            Found    = $false
            NotAfter = $null
            Reason   = "Certificate pre-flight on '$ComputerName' was skipped: $($_.Exception.Message)"
        }
    }
}