Private/Checks/Aria/Test-VcfVrslcmCertificateExpiration.ps1

# Copyright (c) 2026 Broadcom. All Rights Reserved.
# Broadcom Confidential. The term "Broadcom" refers to Broadcom Inc.
# and/or its subsidiaries.
#
# =============================================================================
#
# SOFTWARE LICENSE AGREEMENT
#
# Copyright (c) CA, Inc. All rights reserved.
#
# You are hereby granted a non-exclusive, worldwide, royalty-free license
# under CA, Inc.'s copyrights to use, copy, modify, and distribute this
# software in source code or binary form for use in connection with CA, Inc.
# products.
#
# This copyright notice shall be included in all copies or substantial
# portions of the software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
# IN THE SOFTWARE.
#
# =============================================================================
#region Aria

function Get-VcfCheckVrslcmCertificateEnvironmentMap {

    <#
        .SYNOPSIS
        Builds a vmid -> environment/product label lookup from vRSLCM's environments API.

        .DESCRIPTION
        Helper for Test-VcfVrslcmCertificateExpiration. vRSLCM's locker certificate list
        ('/lcm/locker/api/v2/certificates') returns only a bare "vmid" per certificate - no
        product or environment name. Calling '/lcm/lcops/api/v2/environments?status=COMPLETED'
        separately and indexing every product's "certificateId" back to its environment/product
        name is the only way to label which appliance a given certificate belongs to.

        .PARAMETER Connection
        The object returned by Get-VcfCheckVrslcmConnection.

        .OUTPUTS
        [Hashtable] keyed by certificate vmid, each value a "<environmentName>/<productId>" label.
    #>


    [CmdletBinding()]
    [OutputType([Hashtable])]
    Param (
        [Parameter(Mandatory = $true)] [PSObject]$Connection
    )

    $map = @{}
    $environments = Invoke-VcfCheckVrslcmApi -Connection $Connection -Path '/lcm/lcops/api/v2/environments?status=COMPLETED'
    foreach ($environment in @($environments)) {
        foreach ($product in @($environment.products)) {
            if ($product.certificateId) {
                $map[$product.certificateId] = "$($environment.environmentName)/$($product.id)"
            }
        }
    }
    return $map
}
function Test-VcfVrslcmCertificateExpiration {

    <#
        .SYNOPSIS
        Checks certificate expiration for Aria Suite Lifecycle Manager and every registered Aria product.

        .DESCRIPTION
        Queries vRSLCM's certificate locker ('/lcm/locker/api/v2/certificates') for every
        certificate it manages—covering vRSLCM itself and every Aria product (VRLI, VROPS,
        VRA, WSA) it has deployed or registered. The list endpoint returns bare certificate IDs,
        so expiration dates ("validity.expiresOn") are queried via the per-certificate detail
        endpoint ('/lcm/locker/api/v2/certificates/{vmid}'). Sub-progress is reported via
        Write-VcfCheckSubProgress during iterations. Fails if any certificate has expired;
        warns if any certificate expires within the warning threshold (default 30 days).
        Labels each certificate with its owning environment and product via
        Get-VcfCheckVrslcmCertificateEnvironmentMap.

        Outcome behavior:
        - Skipped: Returns 'Skipped' if Aria Suite Lifecycle Manager is not deployed in the
          environment.
        - Pass: Returns 'Pass' if every certificate is valid beyond the warning threshold.
        - Warning: Returns 'Warning' if one or more certificates expire within
          -WarningThresholdDays, or one or more could not be checked due to an API or data issue,
          and none has expired.
        - Fail: Returns 'Fail' if one or more certificates have already expired.
        - Error: Returns 'Error' if SDDC Manager's credential inventory or the vRSLCM API call
          fails.

        The Detail message provides categorized findings for expired, expiring-soon, and uncheckable
        certificates, attaching remediation documentation where applicable. Constructs a breakdown
        table ('Rows') containing Endpoint, Alias, ExpiryDate, DaysRemaining, and Status per
        certificate.

        .PARAMETER Context
        The VcfCheck.Context object. Must already be connected to SDDC Manager.

        .PARAMETER DisplayName
        Optional friendly display name for the check result.

        .PARAMETER WarningThresholdDays
        Number of days before expiry to raise a Warning instead of a Pass. Default 30.

        .OUTPUTS
        [PSObject] A single VcfCheck.Result object.
    #>


    [CmdletBinding()]
    [OutputType([PSObject])]
    Param (
        [Parameter(Mandatory = $true)] [PSObject]$Context,
        [Parameter(Mandatory = $false)] [String]$DisplayName = '',
        [Parameter(Mandatory = $false)] [ValidateRange(1, 3650)] [Int]$WarningThresholdDays = 30
    )

    $startedAt = Get-Date
    $checkId = 'vrslcm_certificate_expiration'

    try {
        $connection = Get-VcfCheckVrslcmConnection -Context $Context
    } catch {
        return New-VcfCheckResult -CheckId $checkId -Status Error `
            -Exception $_.Exception.Message `
            -StartedAt $startedAt -CompletedAt (Get-Date) -DisplayName $DisplayName
    }

    if (-not $connection) {
        return New-VcfCheckResult -CheckId $checkId -Status Skipped `
            -Detail 'Aria Suite Lifecycle Manager is not deployed in this environment.' -SkipReasonTag 'vRSLCM not deployed' `
            -StartedAt $startedAt -CompletedAt (Get-Date) -DisplayName $DisplayName
    }

    try {
        $certificateList = Invoke-VcfCheckVrslcmApi -Connection $connection -Path '/lcm/locker/api/v2/certificates'
        $certificates = @($certificateList.certificates)
        $environmentMap = Get-VcfCheckVrslcmCertificateEnvironmentMap -Connection $connection
    } catch {
        $friendlyMessage = ConvertTo-VcfCheckFriendlyVrslcmError -Fqdn $connection.Fqdn -ErrorMessage $_.Exception.Message
        return New-VcfCheckResult -CheckId $checkId -Status Error `
            -TargetComponent $connection.Fqdn -Exception $friendlyMessage `
            -StartedAt $startedAt -CompletedAt (Get-Date) -DisplayName $DisplayName
    }

    if ($certificates.Count -eq 0) {
        return New-VcfCheckResult -CheckId $checkId -Status Pass `
            -TargetComponent $connection.Fqdn `
            -Detail 'No certificates were found in the Aria Suite Lifecycle Manager certificate locker.' `
            -StartedAt $startedAt -CompletedAt (Get-Date) -DisplayName $DisplayName
    }

    $now = Get-Date
    $replaceKbLink = 'https://techdocs.broadcom.com/us/en/vmware-cis/aria/aria-suite-lifecycle/8-12/vmware-aria-suite-lifecycle-installation-upgrade-and-management-8-12/managing-environments/managing-vrealize-suite-products-in-a-private-cloud/replace-certificate-for-vrslcm-products.html'
    $replaceKbTitle = 'Replace certificate for VMware Aria Suite Lifecycle products'

    $rows = [System.Collections.Generic.List[PSCustomObject]]::new()
    $expired = [System.Collections.Generic.List[String]]::new()
    $expiringSoon = [System.Collections.Generic.List[String]]::new()
    $unableToCheck = [System.Collections.Generic.List[String]]::new()
    $certIndex = 0
    foreach ($certificate in $certificates) {
        $certIndex++
        Write-VcfCheckSubProgress -Context $Context -Current $certIndex -Total $certificates.Count `
            -Label $certificate.alias -Unit 'certificates'

        $endpoint = 'Aria Suite Lifecycle Manager'
        if ($certificate.vmid -and $environmentMap.ContainsKey($certificate.vmid)) {
            $endpoint = $environmentMap[$certificate.vmid]
        }

        try {
            $certDetail = Invoke-VcfCheckVrslcmApi -Connection $connection -Path "/lcm/locker/api/v2/certificates/$($certificate.vmid)"
        } catch {
            # A failed detail call or unparseable date indicates an API/data issue rather than
            # an expiring certificate, so it is tracked separately from expiration warnings.
            $rows.Add([PSCustomObject]@{ Endpoint = $endpoint; Alias = $certificate.alias; ExpiryDate = 'Unknown'; DaysRemaining = 'Unknown'; Status = 'Error' })
            $unableToCheck.Add("$endpoint ($($certificate.alias)): unable to retrieve certificate details: $($_.Exception.Message)")
            continue
        }

        $expiryDate = Get-Date
        if (-not [DateTime]::TryParse($certDetail.validity.expiresOn, [ref]$expiryDate)) {
            $rows.Add([PSCustomObject]@{ Endpoint = $endpoint; Alias = $certificate.alias; ExpiryDate = $certDetail.validity.expiresOn; DaysRemaining = 'Unknown'; Status = 'Error' })
            $unableToCheck.Add("$endpoint ($($certificate.alias)): unparseable expiration date `"$($certDetail.validity.expiresOn)`"")
            continue
        }

        $daysRemaining = [Math]::Floor(($expiryDate - $now).TotalDays)
        $certStatus = 'Pass'
        if ($expiryDate -lt $now) {
            $certStatus = 'Expired'
            $expired.Add("$endpoint ($($certificate.alias)) expired on $expiryDate")
        } elseif ($expiryDate -lt $now.AddDays($WarningThresholdDays)) {
            $certStatus = 'Expiring Soon'
            $expiringSoon.Add("$endpoint ($($certificate.alias)) expires on $expiryDate ($daysRemaining day(s) remaining)")
        }

        $rows.Add([PSCustomObject]@{ Endpoint = $endpoint; Alias = $certificate.alias; ExpiryDate = $expiryDate; DaysRemaining = [Math]::Max(0, $daysRemaining); Status = $certStatus })
    }

    $rows = @($rows | Sort-Object -Property Endpoint)

    # Output distinct lines for each finding category to ensure clear reporting.
    $detailLines = [System.Collections.Generic.List[String]]::new()
    $status = $null
    if ($expired.Count -gt 0) {
        $status = 'Fail'
        $detailLines.Add("Expired - replace before proceeding: $($expired -join '; '). See [$replaceKbTitle]($replaceKbLink).")
    }
    if ($expiringSoon.Count -gt 0) {
        if (-not $status) { $status = 'Warning' }
        $detailLines.Add("Expiring within $WarningThresholdDays day(s) - replace before they expire: $($expiringSoon -join '; '). See [$replaceKbTitle]($replaceKbLink).")
    }
    if ($unableToCheck.Count -gt 0) {
        if (-not $status) { $status = 'Warning' }
        $detailLines.Add("Could not be checked due to an Aria Suite Lifecycle Manager API/data issue (not a certificate expiration problem) - investigate Aria Suite Lifecycle Manager's connectivity and locker health: $($unableToCheck -join '; ')")
    }

    if ($detailLines.Count -gt 0) {
        $detail = $detailLines -join "`n"
    } else {
        $status = 'Pass'
        $detail = "Checked $($certificates.Count) certificate(s) in the Aria Suite Lifecycle Manager certificate locker; none expired or expiring soon."
    }

    return New-VcfCheckResult -CheckId $checkId -Status $status `
        -TargetComponent $connection.Fqdn -Detail $detail -Rows @($rows) `
        -StartedAt $startedAt -CompletedAt (Get-Date) -DisplayName $DisplayName
}
#endregion