Public/Get-sqmTlsStatus.ps1

<#
.SYNOPSIS
    Audits TLS/SSL configuration and certificate status for all SQL Server instances on one or more computers.

.DESCRIPTION
    Get-sqmTlsStatus connects to each target computer (locally or via Invoke-Command for remotes),
    reads the SQL Server instance list from the registry, and for each instance checks:

    - The TLS certificate thumbprint bound in SuperSocketNetLib (empty = auto-generated self-signed)
    - Whether ForceEncryption is enabled (0 = Warning, 1 = required)
    - Certificate details from the local machine certificate store (Cert:\LocalMachine\My):
        Expiry date, days remaining, Subject/CN, SAN entries, chain trust validation, private key presence
    - TLS protocol version state at the OS/SCHANNEL level:
        TLS 1.0, TLS 1.1, TLS 1.2, TLS 1.3 -- each reported as Enabled, Disabled, or NotConfigured

    Status is calculated per instance:
    - Critical : cert expired, cert not found in store, or cert chain not trusted
    - Warning : cert expires within 60 days, ForceEncryption = 0, or TLS 1.0 / TLS 1.1 enabled
    - OK : cert trusted, not expiring soon, ForceEncryption = 1, TLS 1.0 and TLS 1.1 disabled

    Results are written to a CSV and a TXT summary report in OutputPath, and returned as PSCustomObjects.

.PARAMETER ComputerName
    One or more computer names to audit. Default: current computer ($env:COMPUTERNAME).

.PARAMETER Credential
    Optional PSCredential used for Invoke-Command when auditing remote computers.

.PARAMETER OutputPath
    Directory where the CSV and TXT report files are saved.
    Default: $env:ProgramData\sqmSQLTool\Logs

.PARAMETER WarnDaysBeforeExpiry
    Number of days before certificate expiry that triggers a Warning status.
    Default: 60

.EXAMPLE
    Get-sqmTlsStatus

    Audits all SQL Server instances on the local computer and saves results to the default log folder.

.EXAMPLE
    Get-sqmTlsStatus -ComputerName "SQL01", "SQL02" -OutputPath "D:\Reports"

    Audits SQL01 and SQL02, saves reports to D:\Reports.

.EXAMPLE
    $cred = Get-Credential
    Get-sqmTlsStatus -ComputerName "SQL01" -Credential $cred | Where-Object Status -ne "OK"

    Audits SQL01 with explicit credentials and filters for non-OK results.

.NOTES
    Author: sqmSQLTool
    Prerequisites: None (pure PowerShell, Registry, CertStore). PowerShell 5.1 compatible.
    Remote access: Uses Invoke-Command (WinRM) for remote computers.
    Default output path: $env:ProgramData\sqmSQLTool\Logs
#>

function Get-sqmTlsStatus
{
    [CmdletBinding()]
    [OutputType([PSCustomObject])]
    param (
        [Parameter(Mandatory = $false, ValueFromPipeline = $true)]
        [string[]]$ComputerName = @($env:COMPUTERNAME),

        [Parameter(Mandatory = $false)]
        [System.Management.Automation.PSCredential]$Credential,

        [Parameter(Mandatory = $false)]
        [string]$OutputPath = '$env:ProgramData\sqmSQLTool\Logs',

        [Parameter(Mandatory = $false)]
        [int]$WarnDaysBeforeExpiry = 60
    )

    begin
    {
        $functionName = $MyInvocation.MyCommand.Name
        $allResults = [System.Collections.Generic.List[PSCustomObject]]::new()

        # Resolve literal $env:... in OutputPath at runtime
        if ($OutputPath -like '*$env:*')
        {
            $OutputPath = $OutputPath -replace '\$env:ProgramData', $env:ProgramData
        }

        if (-not (Test-Path -Path $OutputPath))
        {
            New-Item -ItemType Directory -Path $OutputPath -Force | Out-Null
        }

        Invoke-sqmLogging -Message "Starting $functionName - ComputerName: $($ComputerName -join ', ') - OutputPath: $OutputPath" -FunctionName $functionName -Level "INFO"

        # Script block executed on each target (local or remote via Invoke-Command)
        $remoteCollectBlock = {
            param([string]$TargetComputer)

            $collected = [System.Collections.Generic.List[hashtable]]::new()

            # --- Read SQL Server instances from registry ---
            $instanceRegPath = 'HKLM:\SOFTWARE\Microsoft\Microsoft SQL Server\Instance Names\SQL'
            $instanceNames = @()
            try
            {
                $regKey = Get-Item -Path $instanceRegPath -ErrorAction Stop
                $instanceNames = $regKey.GetValueNames()
            }
            catch
            {
                # No SQL Server installed or registry not accessible
                return $collected
            }

            # --- Read SCHANNEL TLS protocol state (computer-wide) ---
            $tlsProtocols = @('TLS 1.0', 'TLS 1.1', 'TLS 1.2', 'TLS 1.3')
            $tlsState = @{}
            foreach ($proto in $tlsProtocols)
            {
                $clientPath = "HKLM:\SYSTEM\CurrentControlSet\Control\SecurityProviders\SCHANNEL\Protocols\$proto\Client"
                $serverPath = "HKLM:\SYSTEM\CurrentControlSet\Control\SecurityProviders\SCHANNEL\Protocols\$proto\Server"

                # Server subkey is authoritative for SQL Server; fall back to Client if absent
                $enabled = $null
                $disabledByDefault = $null

                foreach ($subPath in @($serverPath, $clientPath))
                {
                    if (Test-Path -Path $subPath)
                    {
                        try
                        {
                            $subKey = Get-Item -Path $subPath -ErrorAction Stop
                            $eVal = $subKey.GetValue('Enabled', $null)
                            $dVal = $subKey.GetValue('DisabledByDefault', $null)
                            if ($null -ne $eVal)
                            {
                                $enabled = [int]$eVal
                                $disabledByDefault = if ($null -ne $dVal) { [int]$dVal } else { $null }
                                break
                            }
                        }
                        catch { }
                    }
                }

                if ($null -eq $enabled)
                {
                    $tlsState[$proto] = 'NotConfigured'
                }
                elseif ($enabled -eq 1)
                {
                    $tlsState[$proto] = 'Enabled'
                }
                else
                {
                    $tlsState[$proto] = 'Disabled'
                }
            }

            # --- Per-instance data ---
            foreach ($instName in $instanceNames)
            {
                # Resolve key name (e.g. MSSQL15.MSSQLSERVER)
                try
                {
                    $keyName = (Get-ItemProperty -Path $instanceRegPath -Name $instName -ErrorAction Stop).$instName
                }
                catch
                {
                    continue
                }

                $superSockPath = "HKLM:\SOFTWARE\Microsoft\Microsoft SQL Server\$keyName\MSSQLServer\SuperSocketNetLib"

                $thumbprint = ''
                $forceEncryption = 0

                if (Test-Path -Path $superSockPath)
                {
                    try
                    {
                        $ssnKey = Get-Item -Path $superSockPath -ErrorAction Stop
                        $rawThumb = $ssnKey.GetValue('Certificate', '')
                        $thumbprint = if ($rawThumb) { $rawThumb.ToString().ToUpper() } else { '' }
                        $feVal = $ssnKey.GetValue('ForceEncryption', 0)
                        $forceEncryption = [int]$feVal
                    }
                    catch { }
                }

                $entry = @{
                    ComputerName     = $TargetComputer
                    InstanceName     = $instName
                    KeyName          = $keyName
                    ForceEncryption  = $forceEncryption
                    CertThumbprint   = $thumbprint
                    TLS10            = $tlsState['TLS 1.0']
                    TLS11            = $tlsState['TLS 1.1']
                    TLS12            = $tlsState['TLS 1.2']
                    TLS13            = $tlsState['TLS 1.3']
                }
                $collected.Add($entry)
            }

            return $collected
        }
    }

    process
    {
        foreach ($computer in $ComputerName)
        {
            Invoke-sqmLogging -Message "[$computer] Starting TLS audit ..." -FunctionName $functionName -Level "INFO"

            # --- Collect registry data (local or remote) ---
            $rawData = $null
            $isLocal = ($computer -eq $env:COMPUTERNAME) -or ($computer -eq 'localhost') -or ($computer -eq '.')

            try
            {
                if ($isLocal)
                {
                    $rawData = & $remoteCollectBlock -TargetComputer $computer
                }
                else
                {
                    $invokeParams = @{
                        ComputerName = $computer
                        ScriptBlock  = $remoteCollectBlock
                        ArgumentList = $computer
                        ErrorAction  = 'Stop'
                    }
                    if ($Credential) { $invokeParams['Credential'] = $Credential }
                    $rawData = Invoke-Command @invokeParams
                }
            }
            catch
            {
                Invoke-sqmLogging -Message "[$computer] Failed to collect registry data: $($_.Exception.Message)" -FunctionName $functionName -Level "ERROR"
                $allResults.Add([PSCustomObject]@{
                    ComputerName   = $computer
                    InstanceName   = 'N/A'
                    ForceEncryption = $null
                    CertThumbprint = ''
                    CertSubject    = ''
                    CertExpiry     = $null
                    CertDaysLeft   = $null
                    CertTrusted    = $null
                    CertInStore    = $null
                    TLS10          = 'Unknown'
                    TLS11          = 'Unknown'
                    TLS12          = 'Unknown'
                    TLS13          = 'Unknown'
                    Status         = 'Critical'
                    StatusDetail   = "Registry collection failed - $($_.Exception.Message)"
                })
                continue
            }

            if (-not $rawData -or $rawData.Count -eq 0)
            {
                Invoke-sqmLogging -Message "[$computer] No SQL Server instances found in registry." -FunctionName $functionName -Level "WARNING"
                continue
            }

            # --- Validate certificates locally (cert store on THIS machine or remote) ---
            foreach ($entry in $rawData)
            {
                $certSubject    = ''
                $certExpiry     = $null
                $certDaysLeft   = $null
                $certTrusted    = $null
                $certInStore    = $false
                $sanEntries     = ''
                $hasPrivateKey  = $null
                $statusList     = [System.Collections.Generic.List[string]]::new()

                $thumb = $entry['CertThumbprint']

                if ($thumb -ne '')
                {
                    # Look up cert in LocalMachine\My on the target computer
                    # For remote: run cert lookup via Invoke-Command
                    $certLookupBlock = {
                        param([string]$Thumbprint)
                        $result = @{
                            Found        = $false
                            Subject      = ''
                            Expiry       = $null
                            SAN          = ''
                            HasPrivKey   = $false
                            ChainValid   = $false
                            ChainStatus  = ''
                        }
                        $cert = Get-Item -Path "Cert:\LocalMachine\My\$Thumbprint" -ErrorAction SilentlyContinue
                        if ($cert)
                        {
                            $result['Found']      = $true
                            $result['Subject']    = $cert.Subject
                            $result['Expiry']     = $cert.NotAfter
                            $result['HasPrivKey'] = $cert.HasPrivateKey

                            # SAN
                            $sanExt = $cert.Extensions | Where-Object { $_.Oid.FriendlyName -eq 'Subject Alternative Name' }
                            if ($sanExt)
                            {
                                $result['SAN'] = $sanExt.Format($false)
                            }

                            # Chain validation
                            $chain = New-Object System.Security.Cryptography.X509Certificates.X509Chain
                            $chain.ChainPolicy.RevocationMode = [System.Security.Cryptography.X509Certificates.X509RevocationMode]::NoCheck
                            $chainBuilt = $chain.Build($cert)
                            $result['ChainValid'] = $chainBuilt
                            if (-not $chainBuilt)
                            {
                                $statusTexts = @($chain.ChainStatus | ForEach-Object { $_.StatusInformation.Trim() })
                                $result['ChainStatus'] = $statusTexts -join '; '
                            }
                        }
                        return $result
                    }

                    try
                    {
                        if ($isLocal)
                        {
                            $certInfo = & $certLookupBlock -Thumbprint $thumb
                        }
                        else
                        {
                            $certInvokeParams = @{
                                ComputerName = $computer
                                ScriptBlock  = $certLookupBlock
                                ArgumentList = $thumb
                                ErrorAction  = 'Stop'
                            }
                            if ($Credential) { $certInvokeParams['Credential'] = $Credential }
                            $certInfo = Invoke-Command @certInvokeParams
                        }

                        if ($certInfo['Found'])
                        {
                            $certInStore    = $true
                            $certSubject    = $certInfo['Subject']
                            $certExpiry     = $certInfo['Expiry']
                            $certDaysLeft   = [int](($certExpiry - (Get-Date)).TotalDays)
                            $certTrusted    = $certInfo['ChainValid']
                            $hasPrivateKey  = $certInfo['HasPrivKey']
                            $sanEntries     = $certInfo['SAN']
                        }
                        else
                        {
                            $certInStore = $false
                        }
                    }
                    catch
                    {
                        Invoke-sqmLogging -Message "[$computer][$($entry['InstanceName'])] Certificate lookup failed: $($_.Exception.Message)" -FunctionName $functionName -Level "WARNING"
                    }
                }

                # --- Determine status ---
                $overallStatus = 'OK'

                # Critical conditions
                if ($thumb -ne '' -and -not $certInStore)
                {
                    $statusList.Add('Cert not found in LocalMachine\My')
                    $overallStatus = 'Critical'
                }
                if ($certTrusted -eq $false)
                {
                    $statusList.Add('Cert chain not trusted')
                    $overallStatus = 'Critical'
                }
                if ($null -ne $certDaysLeft -and $certDaysLeft -lt 0)
                {
                    $statusList.Add("Cert expired $([Math]::Abs($certDaysLeft)) days ago")
                    $overallStatus = 'Critical'
                }

                # Warning conditions (only downgrade to Warning if not already Critical)
                if ($overallStatus -ne 'Critical')
                {
                    if ($null -ne $certDaysLeft -and $certDaysLeft -ge 0 -and $certDaysLeft -lt $WarnDaysBeforeExpiry)
                    {
                        $statusList.Add("Cert expires in $certDaysLeft days")
                        $overallStatus = 'Warning'
                    }
                    if ($entry['ForceEncryption'] -eq 0)
                    {
                        $statusList.Add('ForceEncryption = 0')
                        $overallStatus = 'Warning'
                    }
                    if ($entry['TLS10'] -eq 'Enabled')
                    {
                        $statusList.Add('TLS 1.0 enabled')
                        $overallStatus = 'Warning'
                    }
                    if ($entry['TLS11'] -eq 'Enabled')
                    {
                        $statusList.Add('TLS 1.1 enabled')
                        $overallStatus = 'Warning'
                    }
                    if ($thumb -eq '')
                    {
                        $statusList.Add('No cert bound - auto-generated self-signed cert in use')
                        if ($overallStatus -eq 'OK') { $overallStatus = 'Warning' }
                    }
                }

                if ($statusList.Count -eq 0) { $statusList.Add('All checks passed') }

                $resultObj = [PSCustomObject]@{
                    ComputerName    = $entry['ComputerName']
                    InstanceName    = $entry['InstanceName']
                    ForceEncryption = $entry['ForceEncryption']
                    CertThumbprint  = $thumb
                    CertSubject     = $certSubject
                    CertExpiry      = $certExpiry
                    CertDaysLeft    = $certDaysLeft
                    CertTrusted     = $certTrusted
                    CertInStore     = $certInStore
                    SANEntries      = $sanEntries
                    HasPrivateKey   = $hasPrivateKey
                    TLS10           = $entry['TLS10']
                    TLS11           = $entry['TLS11']
                    TLS12           = $entry['TLS12']
                    TLS13           = $entry['TLS13']
                    Status          = $overallStatus
                    StatusDetail    = $statusList -join ' | '
                }

                $allResults.Add($resultObj)

                Invoke-sqmLogging -Message "[$computer][$($entry['InstanceName'])] Status: $overallStatus - $($resultObj.StatusDetail)" -FunctionName $functionName -Level $(if ($overallStatus -eq 'OK') { 'INFO' } elseif ($overallStatus -eq 'Warning') { 'WARNING' } else { 'ERROR' })
            }
        }
    }

    end
    {
        if ($allResults.Count -eq 0)
        {
            Invoke-sqmLogging -Message "$functionName completed - no instances found." -FunctionName $functionName -Level "WARNING"
            return
        }

        $timestamp = Get-Date -Format 'yyyyMMdd_HHmmss'
        $csvPath   = Join-Path -Path $OutputPath -ChildPath "TlsStatus_$timestamp.csv"
        $txtPath   = Join-Path -Path $OutputPath -ChildPath "TlsStatus_$timestamp.txt"

        # --- CSV export ---
        try
        {
            $allResults | Export-Csv -Path $csvPath -NoTypeInformation -Encoding UTF8
            Invoke-sqmLogging -Message "CSV saved: $csvPath" -FunctionName $functionName -Level "INFO"
        }
        catch
        {
            Invoke-sqmLogging -Message "Failed to write CSV: $($_.Exception.Message)" -FunctionName $functionName -Level "ERROR"
        }

        # --- TXT report ---
        try
        {
            $lines = [System.Collections.Generic.List[string]]::new()
            $lines.Add("TLS / SSL Status Report")
            $lines.Add("Generated : $(Get-Date -Format 'yyyy-MM-dd HH:mm:ss')")
            $lines.Add("Computers : $($ComputerName -join ', ')")
            $lines.Add(('-' * 80))
            $lines.Add('')

            $criticalCount = ($allResults | Where-Object { $_.Status -eq 'Critical' }).Count
            $warningCount  = ($allResults | Where-Object { $_.Status -eq 'Warning'  }).Count
            $okCount       = ($allResults | Where-Object { $_.Status -eq 'OK'       }).Count

            $lines.Add("Summary : OK=$okCount Warning=$warningCount Critical=$criticalCount")
            $lines.Add('')

            foreach ($r in $allResults)
            {
                $lines.Add("[[$($r.Status)]] $($r.ComputerName) \ $($r.InstanceName)")
                $lines.Add(" ForceEncryption : $($r.ForceEncryption)")
                $lines.Add(" CertThumbprint : $(if ($r.CertThumbprint) { $r.CertThumbprint } else { '(none - self-signed)' })")
                $lines.Add(" CertSubject : $($r.CertSubject)")
                $lines.Add(" CertExpiry : $(if ($r.CertExpiry) { $r.CertExpiry.ToString('yyyy-MM-dd') + ' (' + $r.CertDaysLeft + ' days)' } else { 'N/A' })")
                $lines.Add(" CertTrusted : $($r.CertTrusted)")
                $lines.Add(" CertInStore : $($r.CertInStore)")
                $lines.Add(" HasPrivateKey : $($r.HasPrivateKey)")
                $lines.Add(" SAN Entries : $($r.SANEntries)")
                $lines.Add(" TLS 1.0 : $($r.TLS10)")
                $lines.Add(" TLS 1.1 : $($r.TLS11)")
                $lines.Add(" TLS 1.2 : $($r.TLS12)")
                $lines.Add(" TLS 1.3 : $($r.TLS13)")
                $lines.Add(" Detail : $($r.StatusDetail)")
                $lines.Add('')
            }

            $lines | Out-File -FilePath $txtPath -Encoding UTF8
            Invoke-sqmLogging -Message "TXT report saved: $txtPath" -FunctionName $functionName -Level "INFO"
        }
        catch
        {
            Invoke-sqmLogging -Message "Failed to write TXT report: $($_.Exception.Message)" -FunctionName $functionName -Level "ERROR"
        }

        Invoke-sqmLogging -Message "$functionName completed. Total instances audited: $($allResults.Count). Critical=$criticalCount Warning=$warningCount OK=$okCount" -FunctionName $functionName -Level "INFO"

        return $allResults
    }
}