Public/System/Get-VergeCertificate.ps1

function Get-VergeCertificate {
    <#
    .SYNOPSIS
        Retrieves SSL/TLS certificates from VergeOS.

    .DESCRIPTION
        Get-VergeCertificate retrieves one or more SSL/TLS certificates from a VergeOS system.
        Certificates can be manual uploads, Let's Encrypt (ACME), or self-signed.

    .PARAMETER Domain
        The primary domain of the certificate to retrieve. Supports wildcards (* and ?).
        If not specified, all certificates are returned.

    .PARAMETER Key
        The unique key (ID) of the certificate to retrieve.

    .PARAMETER Type
        Filter certificates by type: Manual, LetsEncrypt, or SelfSigned.

    .PARAMETER Valid
        Filter to show only valid (unexpired) certificates.

    .PARAMETER IncludeKeys
        Include the public key, private key, and chain in the output.
        Use with caution as this exposes sensitive key material.

    .PARAMETER Server
        The VergeOS connection to use. Defaults to the current default connection.

    .EXAMPLE
        Get-VergeCertificate

        Retrieves all certificates from the connected VergeOS system.

    .EXAMPLE
        Get-VergeCertificate -Domain "example.com"

        Retrieves a specific certificate by domain.

    .EXAMPLE
        Get-VergeCertificate -Domain "*.example.com"

        Retrieves wildcard certificates for example.com.

    .EXAMPLE
        Get-VergeCertificate -Type LetsEncrypt -Valid

        Retrieves all valid Let's Encrypt certificates.

    .EXAMPLE
        Get-VergeCertificate -Key 1 -IncludeKeys

        Retrieves a certificate by key including the key material.

    .OUTPUTS
        PSCustomObject with PSTypeName 'Verge.Certificate'

    .NOTES
        Certificate types:
        - Manual: Uploaded certificates with public/private keys
        - LetsEncrypt: Automatically managed via ACME protocol
        - SelfSigned: Self-signed certificates generated by VergeOS
    #>

    [CmdletBinding(DefaultParameterSetName = 'Filter')]
    [OutputType([PSCustomObject])]
    param(
        [Parameter(Position = 0, ParameterSetName = 'Filter')]
        [SupportsWildcards()]
        [string]$Domain,

        [Parameter(Mandatory, ParameterSetName = 'ByKey', ValueFromPipelineByPropertyName)]
        [Alias('Id', '$key')]
        [int]$Key,

        [Parameter(ParameterSetName = 'Filter')]
        [ValidateSet('Manual', 'LetsEncrypt', 'SelfSigned')]
        [string]$Type,

        [Parameter(ParameterSetName = 'Filter')]
        [switch]$Valid,

        [Parameter()]
        [switch]$IncludeKeys,

        [Parameter()]
        [object]$Server
    )

    begin {
        # Resolve connection
        if (-not $Server) {
            $Server = $script:DefaultConnection
        }
        if (-not $Server) {
            throw [System.InvalidOperationException]::new(
                'Not connected to VergeOS. Use Connect-VergeOS to establish a connection.'
            )
        }

        # Map friendly type names to API values
        $typeMapping = @{
            'Manual'      = 'manual'
            'LetsEncrypt' = 'letsencrypt'
            'SelfSigned'  = 'self_signed'
        }

        # Map API type values to friendly names
        $typeDisplayMapping = @{
            'manual'      = 'Manual'
            'letsencrypt' = "Let's Encrypt"
            'self_signed' = 'Self-Signed'
        }
    }

    process {
        try {
            Write-Verbose "Querying certificates from $($Server.Server)"

            # Build query parameters
            $queryParams = @{}
            $filters = [System.Collections.Generic.List[string]]::new()

            # Filter by key
            if ($PSCmdlet.ParameterSetName -eq 'ByKey') {
                $filters.Add("`$key eq $Key")
            }
            else {
                # Filter by domain
                if ($Domain) {
                    if ($Domain -match '[\*\?]' -and $Domain -notmatch '^\*\.') {
                        # Wildcard search (but not domain wildcards like *.example.com)
                        $searchTerm = $Domain -replace '[\*\?]', ''
                        if ($searchTerm) {
                            $filters.Add("domain ct '$searchTerm'")
                        }
                    }
                    else {
                        $filters.Add("domain eq '$Domain'")
                    }
                }

                # Filter by type
                if ($Type) {
                    $apiType = $typeMapping[$Type]
                    $filters.Add("type eq '$apiType'")
                }

                # Filter by valid status
                if ($Valid) {
                    $filters.Add("valid eq true")
                }
            }

            # Apply filters
            if ($filters.Count -gt 0) {
                $queryParams['filter'] = $filters -join ' and '
            }

            # Request fields
            $fieldList = @(
                '$key'
                'domain'
                'domainname'
                'domainlist'
                'description'
                'type'
                'acme_server'
                'key_type'
                'rsa_key_size'
                'contact'
                'agree_tos'
                'valid'
                'autocreated'
                'expires'
                'created'
                'modified'
            )

            # Include sensitive key material if requested
            if ($IncludeKeys) {
                $fieldList += @('public', 'private', 'chain')
                Write-Warning "Including sensitive key material in output. Handle with care."
            }

            $queryParams['fields'] = $fieldList -join ','

            $response = Invoke-VergeAPI -Method GET -Endpoint 'certificates' -Query $queryParams -Connection $Server

            # Handle both single object and array responses
            $certificates = if ($response -is [array]) { $response } else { @($response) }

            foreach ($cert in $certificates) {
                # Skip null entries - check for $key since domain might be empty for new certs
                if (-not $cert -or -not $cert.'$key') {
                    continue
                }

                # Determine the primary domain - use domainname if domain is empty
                $primaryDomain = if ($cert.domain) { $cert.domain } else { $cert.domainname }

                # Apply wildcard filtering for client-side matching (for non-domain wildcards)
                if ($Domain -and ($Domain -match '[\*\?]') -and ($Domain -notmatch '^\*\.')) {
                    if ($primaryDomain -notlike $Domain) {
                        continue
                    }
                }

                # Parse domain list
                $domainListArray = @()
                if ($cert.domainlist) {
                    $domainListArray = ($cert.domainlist -split ',').Trim() | Where-Object { $_ }
                }

                # Get type display name
                $typeDisplay = $typeDisplayMapping[$cert.type]
                if (-not $typeDisplay) {
                    $typeDisplay = $cert.type
                }

                # Create output object
                $output = [PSCustomObject]@{
                    PSTypeName   = 'Verge.Certificate'
                    Key          = [int]$cert.'$key'
                    Domain       = $primaryDomain
                    DomainName   = $cert.domainname
                    DomainList   = $domainListArray
                    Description  = $cert.description
                    Type         = $typeDisplay
                    TypeValue    = $cert.type
                    KeyType      = if ($cert.key_type) { $cert.key_type.ToUpper() } else { $null }
                    RSAKeySize   = $cert.rsa_key_size
                    ACMEServer   = $cert.acme_server
                    ContactId    = if ($cert.contact) { [int]$cert.contact } else { $null }
                    AgreeTOS     = [bool]$cert.agree_tos
                    Valid        = [bool]$cert.valid
                    AutoCreated  = [bool]$cert.autocreated
                    Expires      = if ($cert.expires) { [DateTimeOffset]::FromUnixTimeSeconds($cert.expires).LocalDateTime } else { $null }
                    Created      = if ($cert.created) { [DateTimeOffset]::FromUnixTimeSeconds($cert.created).LocalDateTime } else { $null }
                    Modified     = if ($cert.modified) { [DateTimeOffset]::FromUnixTimeSeconds($cert.modified).LocalDateTime } else { $null }
                }

                # Add key material if requested
                if ($IncludeKeys) {
                    $output | Add-Member -MemberType NoteProperty -Name 'PublicKey' -Value $cert.public
                    $output | Add-Member -MemberType NoteProperty -Name 'PrivateKey' -Value $cert.private
                    $output | Add-Member -MemberType NoteProperty -Name 'Chain' -Value $cert.chain
                }

                # Calculate days until expiration
                if ($output.Expires) {
                    $daysUntilExpiry = ($output.Expires - (Get-Date)).Days
                    $output | Add-Member -MemberType NoteProperty -Name 'DaysUntilExpiry' -Value $daysUntilExpiry
                }

                # Add hidden properties for pipeline support
                $output | Add-Member -MemberType NoteProperty -Name '_Connection' -Value $Server -Force

                Write-Output $output
            }
        }
        catch {
            $PSCmdlet.ThrowTerminatingError($_)
        }
    }
}