Tools/Invoke-VcfCheckVrslcmLockerDiagnostic.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.
#
# =============================================================================
<#
    .SYNOPSIS
    Diagnoses credential mismatch results from Test-VcfVrslcmValidateVcenterCredentials by inspecting all locker entries for a vCenter username.

    .DESCRIPTION
    Queries all locker password entries stored in Aria Suite Lifecycle Manager for a target vCenter username via
    `/lcm/locker/api/passwords`.

    Decrypts each matching locker entry using appliance root credentials via `/lcm/locker/api/passwords/view/{vmid}` and
    validates the decrypted password directly against the target vCenter Server REST session endpoint (`https://{VcHost}/rest/com/vmware/cis/session`).

    Distinguishes genuine password mismatches from false positives caused by duplicate or stale locker entries for the same username.

    Connects to SDDC Manager to retrieve registered Aria Suite Lifecycle Manager connection details and appliance root credentials.

    .PARAMETER SddcManagerFqdn
    Fully qualified domain name of the SDDC Manager appliance.

    .PARAMETER SddcManagerUser
    Username for SDDC Manager authentication.

    .PARAMETER Username
    The target vCenter account username to inspect in Aria Suite Lifecycle Manager locker entries.

    .PARAMETER VcHost
    Fully qualified domain name or IP address of the target vCenter Server to validate credentials against.

    .NOTES
    Reads SDDC Manager password credentials directly from process environment variable `VCFCHECK_SDDC_PASSWORD`.

    .EXAMPLE
    $env:VCFCHECK_SDDC_PASSWORD = '...'
    pwsh -NoProfile -NonInteractive -File Invoke-VcfCheckVrslcmLockerDiagnostic.ps1 `
        -SddcManagerFqdn m01-sddcmgr01.example.com -SddcManagerUser administrator@vsphere.local `
        -Username svc-xint-lcm01-m01-vc01@vsphere.local -VcHost m01-vc01.example.com
#>


[CmdletBinding()]
Param (
    [Parameter(Mandatory = $true)] [ValidateNotNullOrEmpty()] [String]$SddcManagerFqdn,
    [Parameter(Mandatory = $true)] [ValidateNotNullOrEmpty()] [String]$SddcManagerUser,
    [Parameter(Mandatory = $true)] [ValidateNotNullOrEmpty()] [String]$Username,
    [Parameter(Mandatory = $true)] [ValidateNotNullOrEmpty()] [String]$VcHost
)

if ($null -ne $PSStyle) { $PSStyle.OutputRendering = 'PlainText' }

$modulePath = Join-Path -Path (Split-Path -Path $PSScriptRoot -Parent) -ChildPath 'VcfCheck.psd1'
Import-Module -Name $modulePath -Force -ErrorAction Stop

$sddcPasswordPlainText = $env:VCFCHECK_SDDC_PASSWORD
if ([String]::IsNullOrEmpty($sddcPasswordPlainText)) {
    Write-Error 'VCFCHECK_SDDC_PASSWORD environment variable was not set.'
    exit 1
}

$Context = $null
try {
    $sddcPassword = ConvertTo-SecureString -String $sddcPasswordPlainText -AsPlainText -Force
    $Context = New-VcfCheckContext
    $Context.AllowInsecureTls = Resolve-VcfCheckAllowInsecureTls
    Connect-VcfCheckSddcManager -Context $Context -Fqdn $SddcManagerFqdn -User $SddcManagerUser -Password $sddcPassword `
        -IgnoreInvalidCertificate:$Context.AllowInsecureTls

    $connection = Get-VcfCheckVrslcmConnection -Context $Context
    if (-not $connection) {
        Write-Error 'Aria Suite Lifecycle Manager is not registered with this SDDC Manager.'
        exit 1
    }

    $rootCredential = Get-VcfCheckVrslcmRootCredential -Context $Context
    if (-not $rootCredential) {
        Write-Error 'SDDC Manager returned no root (SSH) credential for Aria Suite Lifecycle Manager.'
        exit 1
    }
    $rootPassword = $rootCredential.GetNetworkCredential().Password

    $lockerEntries = @(Invoke-VcfCheckVrslcmApi -Connection $connection -Path '/lcm/locker/api/passwords')
    $matchingEntries = @($lockerEntries | Where-Object { $_.userName -eq $Username })

    Write-Host "Found $($matchingEntries.Count) locker entry(ies) for username `"$Username`" in Aria Suite Lifecycle Manager `"$($connection.Fqdn)`"." -ForegroundColor Cyan
    if ($matchingEntries.Count -eq 0) {
        exit 0
    }
    if ($matchingEntries.Count -gt 1) {
        Write-Host "More than one locker entry exists for this username - validation checks evaluate the first entry returned, so stale duplicate entries may conceal valid credentials." -ForegroundColor Yellow
    }

    $results = [System.Collections.Generic.List[PSCustomObject]]::new()
    for ($i = 0; $i -lt $matchingEntries.Count; $i++) {
        $entry = $matchingEntries[$i]
        $vmid = @($entry.vmid) | Select-Object -First 1
        $outcome = 'Unvalidated'
        $note = $null

        if ([String]::IsNullOrWhiteSpace($vmid)) {
            $note = 'Missing vmid.'
        } else {
            try {
                $passwordResponse = Invoke-VcfCheckVrslcmApi -Connection $connection -Method POST `
                    -Path "/lcm/locker/api/passwords/view/$vmid" -Body @{ rootPassword = $rootPassword }
            } catch {
                $passwordResponse = $null
                $note = "Decrypt call failed: $($_.Exception.Message)"
            }

            if ($passwordResponse -and $passwordResponse.password) {
                $credential = [PSCredential]::new($Username, (ConvertTo-SecureStringForCredential -PlainText $passwordResponse.password))
                try {
                    Invoke-RestMethod -Uri "https://$VcHost/rest/com/vmware/cis/session" -Method POST `
                        -Credential $credential -Authentication Basic -SkipCertificateCheck:$Context.AllowInsecureTls -TimeoutSec 10 -ErrorAction Stop | Out-Null
                    $outcome = 'MatchesVCenter'
                } catch {
                    $outcome = 'DoesNotMatchVCenter'
                    $note = $_.Exception.Message
                }
                Remove-Variable -Name credential -ErrorAction SilentlyContinue
            } elseif (-not $note) {
                $note = 'Decrypt call returned no password.'
            }
            Remove-Variable -Name passwordResponse -ErrorAction SilentlyContinue
        }

        $results.Add([PSCustomObject]@{
            Index   = $i
            Vmid    = $vmid
            Outcome = $outcome
            Note    = $note
        })
    }

    $results | Format-Table -AutoSize | Out-Host

    if (@($results | Where-Object { $_.Outcome -eq 'MatchesVCenter' }).Count -gt 0) {
        Write-Host "At least one locker entry's decrypted password IS accepted by `"$VcHost`" - if CredentialMismatch was reported, a different duplicate entry was likely evaluated." -ForegroundColor Green
    } else {
        Write-Host "No locker entry's decrypted password was accepted by `"$VcHost`" - this indicates a genuine credential mismatch." -ForegroundColor Red
    }
} finally {
    $sddcPasswordPlainText = $null
    if ($Context) { Disconnect-VcfCheckAll -Context $Context }
    Remove-Variable -Name sddcPasswordPlainText, sddcPassword, rootPassword, rootCredential -ErrorAction SilentlyContinue
}