Private/Invoke-SqlCertHttpBinding.ps1

# =============================================================================
# Script : Private/Invoke-SqlCertHttpBinding.ps1
# Author : Keith Ramsey
# Created : 2026-09-08
# =============================================================================
# Change Log
# -----------------------------------------------------------------------------
# 2026-09-08 Keith Ramsey Initial (A2, GR-009, DR-042): HTTP.sys SSL-certificate
# binding seam -- Get returns the thumbprint bound to an
# IP:port (or $null), Add binds a thumbprint to an
# IP:port. Wraps netsh http; local inline or remote via
# Invoke-Command. The one mockable seam the SSAS
# binding commands (and any future HTTP.sys binding) use.
# =============================================================================
# Decision Contract (Docs/DECISIONS_PHASE7.md DR-042)
# -----------------------------------------------------------------------------
# Must : the single seam for reading/adding an HTTP.sys SSL cert binding; Get ->
# bound thumbprint or $null; Add binds (throws on failure so the caller
# reports Failed); local or remote; the live netsh mechanism is held for
# the GR-009 lab proof, unit tests mock this function.
# =============================================================================

function Invoke-SqlCertHttpBinding {
    <#
    .SYNOPSIS
        Reads or adds an HTTP.sys SSL certificate binding for an IP:port (A2 seam).
    .DESCRIPTION
        Wraps the HTTP.sys SSL-certificate binding (netsh http show/add sslcert). Get returns the
        certificate thumbprint currently bound to the IP:port, or $null when none is bound. Add
        binds the given thumbprint to the IP:port (store LocalMachine\My), throwing on failure so
        the caller turns it into a Failed result. A local target runs inline; a remote target runs
        inside an Invoke-Command block. This is the mockable seam the SSAS binding commands use.
    .PARAMETER Action
        Get (read the bound thumbprint) or Add (bind a thumbprint).
    .PARAMETER IPPort
        The IP:port, e.g. '0.0.0.0:443'.
    .PARAMETER Thumbprint
        The certificate thumbprint to bind (Add only).
    .PARAMETER ComputerName
        The host whose HTTP.sys to touch. Default local machine.
    .PARAMETER Credential
        Credential for a remote binding.
    .OUTPUTS
        For Get: the bound thumbprint (string) or $null. For Add: none.
    .NOTES
        Steps:
        1. Build the netsh scriptblock -- Get: 'netsh http show sslcert ipport=<ipport>' and parse the Certificate Hash; Add: 'netsh http add sslcert ipport=<ipport> certhash=<tp> appid={GUID} certstorename=MY'.
        2. Run it local inline, or remote via Invoke-Command with -ComputerName / -Credential.
        3. Get returns the parsed thumbprint (or $null when no binding); Add throws on a netsh failure so the caller reports Failed.
    #>

    [CmdletBinding()]
    [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseUsingScopeModifierInNewRunspaces', '',
        Justification = 'Remote Invoke-Command block receives values via param() + -ArgumentList, not closure capture.')]
    [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseShouldProcessForStateChangingFunctions', '',
        Justification = 'Private worker; the public Set-SsasHttpsBinding carries ShouldProcess and gates the Add call.')]
    param(
        [Parameter(Mandatory)] [ValidateSet('Get', 'Add')] [string] $Action,
        [Parameter(Mandatory)] [string] $IPPort,
        [string] $Thumbprint,
        [string] $ComputerName = $env:COMPUTERNAME,
        [PSCredential] $Credential
    )

    # 1. netsh scriptblock. appid is a stable SqlCertForge owner GUID.
    $sb = {
        param($action, $ipport, $tp)
        $appId = '{6b1e0f2a-6c3d-4a5e-9b7c-5cf0d1e2a3b4}'
        if ($action -eq 'Get') {
            $out = & netsh http show sslcert ipport=$ipport 2>$null
            if ($LASTEXITCODE -ne 0 -or -not $out) { return $null }
            $line = @($out) | Where-Object { $_ -match 'Certificate Hash\s*:\s*([0-9a-fA-F]{40})' } | Select-Object -First 1
            if ($line -and $line -match '([0-9a-fA-F]{40})') { return $Matches[1] }
            return $null
        }
        else {
            $existing = & netsh http show sslcert ipport=$ipport 2>$null
            if ($LASTEXITCODE -eq 0 -and $existing) { & netsh http delete sslcert ipport=$ipport 2>$null | Out-Null }
            $r = & netsh http add sslcert ipport=$ipport certhash=$tp appid=$appId certstorename=MY 2>&1
            if ($LASTEXITCODE -ne 0) { throw "netsh http add sslcert failed for ${ipport}: $r" }
        }
    }

    # 2/3. Local inline or remote.
    if ($ComputerName -eq $env:COMPUTERNAME) { & $sb $Action $IPPort $Thumbprint }
    else {
        $ic = @{ ComputerName = $ComputerName; ScriptBlock = $sb; ArgumentList = @($Action, $IPPort, $Thumbprint) }
        if ($Credential) { $ic.Credential = $Credential }
        Invoke-Command @ic
    }
}