Private/AriaOpsHelpers.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 AriaOpsHelpers
#
# Aria Operations has full PowerCLI/OpenAPI SDK coverage via VMware.Sdk.Vcf.Ops (nested inside
# VCF.PowerCLI) - unlike VRSLCM, no hand-written REST helper is needed. Checks call the
# Invoke-VcfOps* cmdlets directly (e.g. Invoke-VcfOpsGetAlerts), passing the connection object
# this file resolves as -Server.
#
# Aria Operations is deployed and lifecycle-managed by Aria Suite Lifecycle (VRSLCM), but VRSLCM
# does not hold the Aria Operations credential either - Aria Operations manages its own
# credentials via its own API. SDDC Manager's own credential vault never holds an Aria Operations
# credential, so this file has no SDDC-Manager-based resolution path at all.
# Get-VcfCheckEnvironmentAriaOpsEndpoints resolves the user-declared Integrations list on an
# environment (Private/Environments.ps1) as the only source
# of Aria Operations FQDN/credential. Connect-VcfCheckAriaOpsEndpoint connects to one of those
# directly by FQDN + credential.
#
# Invoke-VcfOpsGetResourcesOfAdapterInstance (VMware.Sdk.Vcf.Ops 13.5.0.25380678, the newest
# release at time of writing) sends a malformed 'adapterId' value for this specific endpoint - a
# resource-kind schema version tag (e.g. "Variant.8,Version.4") instead of the real adapter
# instance GUID - so Aria Operations rejects every call with HTTP 400. Get-VcfCheckAriaOpsAdapterResources
# below bypasses the SDK for this one call and hits '/suite-api/api/adapters/{id}/resources'
# directly via Invoke-VcfCheckAriaOpsApi, the same REST-fallback pattern VrslcmHelpers.ps1 uses -
# except unlike VRSLCM, suite-api does not accept HTTP Basic Auth on ordinary endpoints, so
# Invoke-VcfCheckAriaOpsApi first exchanges the credential for a session token via
# Get-VcfCheckAriaOpsApiToken ('/suite-api/api/auth/token/acquire') and sends it as an
# 'Authorization: vRealizeOpsToken <token>' header instead.
#
# Invoke-VcfOpsGetResource sends the same malformed schema-version-tag value as its 'id', so
# Get-VcfCheckAriaOpsResource below hits '/suite-api/api/resources/{id}' directly via
# Invoke-VcfCheckAriaOpsApi instead. Test-VcfAriaOpsCriticalAlerts guards with [Guid]::TryParse
# before calling it, since Alert.ResourceId can independently arrive non-GUID.

function Get-VcfCheckAriaOpsConnectionFailureCategory {
    <#
        .SYNOPSIS
        Classifies a Connect-VcfCheckAriaOpsEndpoint failure message so the caller can show an
        accurate, specific remediation instead of one generic "check your network" message for
        every failure.

        .DESCRIPTION
        A rejected credential (bad password) surfaces from Connect-VcfOpsServer as an ordinary
        caught exception, indistinguishable at a glance from a real network problem. Left
        unclassified, it was being reported as an "Error" result telling the user to verify
        network connectivity - misleading when the endpoint is reachable and the only problem is
        the entered credential. This inspects the exception message for a recognized credential/
        authorization rejection signature and returns 'AuthenticationFailed', else 'Unknown'.

        .PARAMETER ErrorMessage
        The exception message from a failed Connect-VcfCheckAriaOpsEndpoint call.

        .OUTPUTS
        [String] one of 'AuthenticationFailed', 'Unknown'.

        .EXAMPLE
        Get-VcfCheckAriaOpsConnectionFailureCategory -ErrorMessage $_.Exception.Message
    #>

    [CmdletBinding()]
    [OutputType([String])]
    Param (
        [Parameter(Mandatory = $true)] [ValidateNotNullOrEmpty()] [String]$ErrorMessage
    )

    if ($ErrorMessage -match '(?i)UNAUTHORIZED|not authorized|invalid credentials|incorrect user ?name or password|authentication failed|401\b') {
        return 'AuthenticationFailed'
    }
    return 'Unknown'
}
function Get-VcfCheckEnvironmentAriaOpsEndpoints {
    <#
        .SYNOPSIS
        Resolves the user-declared, standalone Aria Operations endpoints attached to an
        environment (Private/Environments.ps1's Integrations field) - components SDDC Manager and
        VRSLCM have zero knowledge of.

        .DESCRIPTION
        Filters $Environment.Integrations to entries with Type -eq 'AriaOperations' and flattens
        their Endpoints into one object per endpoint, applying each integration's
        SharedCredentials setting to resolve which Username applies (the integration-level one
        when shared, else the endpoint's own). Never resolves a password - see
        Connect-VcfCheckAriaOpsEndpoint.

        When the integration has EnableGuestOsChecks set, also resolves VCenterFqdn/
        VCenterUsername for the vCenter that hosts this endpoint - the integration-level
        AriaVCenterFqdn/AriaVCenterUsername when AriaVCenterSharedAcrossEndpoints is set, else the
        endpoint's own VCenterFqdn/VCenterUsername. Both are left $null when guestOS checks are
        not enabled, so Get-VcfCheckAllVCenterFqdns' dedup loop skips the endpoint. When guestOS
        checks are enabled, not shared, and the endpoint has no VCenterFqdn of its own, both are
        left $null and a warning is logged.

        .PARAMETER Environment
        An environment object as returned by Get-VcfCheckEnvironments.

        .OUTPUTS
        [Object[]] of PSCustomObject with Name, Fqdn, Username, VCenterFqdn, VCenterUsername,
        VmNames. VmNames is the user-declared, comma-delimited list of guestOS VM names for this
        endpoint - a single Aria appliance is not always one VM, and its VM name(s) cannot be
        assumed to match the FQDN shortname, so it is entered explicitly rather than derived.
        Empty array if the environment has no AriaOperations integration.

        .EXAMPLE
        Get-VcfCheckEnvironmentAriaOpsEndpoints -Environment $environment
    #>

    [CmdletBinding()]
    [OutputType([Object[]])]
    Param (
        [Parameter(Mandatory = $true)] [PSObject]$Environment
    )

    $results = [System.Collections.Generic.List[PSObject]]::new()
    $ariaIntegrations = @($Environment.Integrations) | Where-Object { $_ -and $_.Type -eq 'AriaOperations' }

    foreach ($integration in $ariaIntegrations) {
        foreach ($endpoint in @($integration.Endpoints)) {
            if (-not $endpoint) {
                continue
            }
            $username = if ($integration.SharedCredentials) { $integration.Username } else { $endpoint.Username }
            $vCenterFqdn = $null
            $vCenterUsername = $null
            if ($integration.EnableGuestOsChecks) {
                if ($integration.AriaVCenterSharedAcrossEndpoints) {
                    $vCenterFqdn = $integration.AriaVCenterFqdn
                    $vCenterUsername = $integration.AriaVCenterUsername
                } else {
                    $vCenterFqdn = $endpoint.VCenterFqdn
                    $vCenterUsername = $endpoint.VCenterUsername
                    if ([String]::IsNullOrWhiteSpace($vCenterFqdn)) {
                        Write-LogMessage -Type WARNING -Message "GuestOS checks are enabled for Aria Operations endpoint '$($endpoint.Name)' but no VCenterFqdn is configured; skipping guestOS checks for this endpoint."
                    }
                }
            }
            $results.Add([PSCustomObject]@{
                PSTypeName      = 'VcfCheck.AriaOpsEndpoint'
                Name            = $endpoint.Name
                Fqdn            = $endpoint.Fqdn
                Username        = $username
                VCenterFqdn     = $vCenterFqdn
                VCenterUsername = $vCenterUsername
                VmNames         = @(if ($endpoint.VmNames) { $endpoint.VmNames } else { @() })
            })
        }
    }

    return $results.ToArray()
}
function Connect-VcfCheckAriaOpsEndpoint {
    <#
        .SYNOPSIS
        Connects directly to a user-declared, standalone Aria Operations endpoint by FQDN and
        credential - no SDDC Manager lookup.

        .DESCRIPTION
        Connects to an Aria Operations instance declared on the environment (see
        Get-VcfCheckEnvironmentAriaOpsEndpoints) - the only source of Aria Operations FQDN/
        credential, since SDDC Manager never holds one. Runs a TCP reachability pre-flight and
        caches the resulting connection on $Context.AriaOpsEndpointConnections
        (keyed by Fqdn, since there can be more than one) so repeat checks against the same
        endpoint reuse it. Unreachable/failed endpoints are cached on
        $Context.UnreachableAriaOpsEndpoints (also keyed by Fqdn) for the same fail-fast reason
        Connect-VcfCheckVCenter caches per-vCenter failures.

        .PARAMETER Context
        The VcfCheck.Context object.

        .PARAMETER Fqdn
        The endpoint's FQDN, as entered by the user.

        .PARAMETER Credential
        The credential to connect with. The caller is responsible for resolving the password
        (session-only prompt or launcher parameter) - this function never persists or looks one
        up itself.

        .PARAMETER ConnectivityTimeoutSeconds
        Maximum time to wait for the TCP reachability pre-flight check. Defaults to 15 seconds.

        .OUTPUTS
        The Aria Operations connection object returned by Connect-VcfOpsServer.

        .EXAMPLE
        Connect-VcfCheckAriaOpsEndpoint -Context $Context -Fqdn 'vrops-standalone.example.com' -Credential $credential
    #>

    [CmdletBinding()]
    [OutputType([PSObject])]
    Param (
        [Parameter(Mandatory = $true)] [PSObject]$Context,
        [Parameter(Mandatory = $true)] [ValidateNotNullOrEmpty()] [String]$Fqdn,
        [Parameter(Mandatory = $true)] [PSCredential]$Credential,
        [Parameter(Mandatory = $false)] [Int]$ConnectivityTimeoutSeconds = 15
    )

    if (-not $Context.AriaOpsEndpointConnections) {
        $Context.AriaOpsEndpointConnections = @{}
    }
    if (-not $Context.UnreachableAriaOpsEndpoints) {
        $Context.UnreachableAriaOpsEndpoints = @{}
    }

    if ($Context.UnreachableAriaOpsEndpoints.ContainsKey($Fqdn)) {
        throw [System.InvalidOperationException]::new($Context.UnreachableAriaOpsEndpoints[$Fqdn])
    }
    if ($Context.AriaOpsEndpointConnections.ContainsKey($Fqdn)) {
        return $Context.AriaOpsEndpointConnections[$Fqdn]
    }

    if (-not (Test-VcfCheckTcpConnectivity -ComputerName $Fqdn -Port 443 -TimeoutSeconds $ConnectivityTimeoutSeconds)) {
        $reason = "Could not reach Aria Operations `"$Fqdn`" on port 443 within $ConnectivityTimeoutSeconds second(s). Check VPN/network connectivity to the environment, firewall rules, and that the FQDN resolves to the correct address, then retry."
        $Context.UnreachableAriaOpsEndpoints[$Fqdn] = $reason
        throw [System.InvalidOperationException]::new($reason)
    }

    Write-LogMessage -Type INFO -Message "Connecting to Aria Operations `"$Fqdn`"..." -NoNewline
    try {
        $connection = Connect-VcfOpsServer -Server $Fqdn -Credential $Credential -NotDefault -IgnoreInvalidCertificate:$Context.AllowInsecureTls -ErrorAction Stop
        Write-Host " Connected" -ForegroundColor White
    } catch {
        Write-Host " Failed" -ForegroundColor Red
        $reason = Get-VcfCheckTlsTrustErrorMessage -ComponentName 'Aria Operations' -Fqdn $Fqdn -ErrorMessage $_.Exception.Message
        if (-not $reason) {
            $category = Get-VcfCheckAriaOpsConnectionFailureCategory -ErrorMessage $_.Exception.Message
            $reason = switch ($category) {
                'AuthenticationFailed' { "Authentication failed for Aria Operations `"$Fqdn`". Verify the username and password entered for this endpoint are correct." }
                default { "Failed to connect to Aria Operations `"$Fqdn`". Verify network connectivity and the credential entered for this endpoint." }
            }
        }
        $Context.UnreachableAriaOpsEndpoints[$Fqdn] = $reason
        throw [System.InvalidOperationException]::new($reason)
    }

    $Context.AriaOpsEndpointConnections[$Fqdn] = $connection
    return $connection
}
function Resolve-VcfCheckAriaOpsEndpointCredentials {
    <#
        .SYNOPSIS
        Resolves and caches a [PSCredential] per standalone Aria Operations endpoint's Username.

        .DESCRIPTION
        For each endpoint returned by Get-VcfCheckEnvironmentAriaOpsEndpoints, uses a matching
        entry (by Fqdn) in PreSuppliedCredentials (the launcher/browser session-only password
        path - see Tools/Invoke-VcfCheckLauncher.ps1) when present, otherwise prompts once via
        Read-Host -AsSecureString (the CLI path, matching Get-VcfCheckSddcManagerRootCredential's
        prompt style). Results are cached on $Context.AriaOpsEndpointCredentials, keyed by Fqdn,
        so a second run within the same $Context (e.g. re-invoking with a different -CheckId
        filter) never re-prompts.

        .PARAMETER Context
        The VcfCheck.Context object.

        .PARAMETER Endpoints
        Endpoint objects as returned by Get-VcfCheckEnvironmentAriaOpsEndpoints (Name/Fqdn/Username).

        .PARAMETER PreSuppliedCredentials
        Optional array of PSCustomObject/Hashtable with Fqdn/Username/Password (SecureString),
        e.g. resolved from the browser's per-endpoint session-only password field. Endpoints not
        matched here fall back to an interactive prompt.

        .EXAMPLE
        Resolve-VcfCheckAriaOpsEndpointCredentials -Context $Context -Endpoints $endpoints -PreSuppliedCredentials $AriaOpsEndpointCredentials
    #>

    [CmdletBinding()]
    [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSAvoidUsingPlainTextForPassword', 'PreSuppliedCredentials', Justification = 'Object[] of Fqdn/Username/Password triplets, not a password itself - each element''s Password field is already a SecureString.')]
    Param (
        [Parameter(Mandatory = $true)] [PSObject]$Context,
        [Parameter(Mandatory = $true)] [Object[]]$Endpoints,
        [Parameter(Mandatory = $false)] [Object[]]$PreSuppliedCredentials = @()
    )

    foreach ($endpoint in @($Endpoints)) {
        if (-not $endpoint -or $Context.AriaOpsEndpointCredentials.ContainsKey($endpoint.Fqdn)) {
            continue
        }

        $preSupplied = @($PreSuppliedCredentials) | Where-Object { $_ -and $_.Fqdn -eq $endpoint.Fqdn } | Select-Object -First 1
        if ($preSupplied) {
            $Context.AriaOpsEndpointCredentials[$endpoint.Fqdn] = [PSCredential]::new($endpoint.Username, $preSupplied.Password)
            continue
        }

        $securePassword = Read-Host -Prompt "Enter the password for $($endpoint.Username)@$($endpoint.Fqdn) (Aria Operations - $($endpoint.Name))" -AsSecureString
        if ($securePassword.Length -eq 0) {
            throw [System.InvalidOperationException]::new("Password for Aria Operations endpoint `"$($endpoint.Fqdn)`" must not be empty.")
        }
        $Context.AriaOpsEndpointCredentials[$endpoint.Fqdn] = [PSCredential]::new($endpoint.Username, $securePassword)
    }
}
function Get-VcfCheckAriaOpsTargets {
    <#
        .SYNOPSIS
        Resolves every Aria Operations instance a check should evaluate - every standalone
        endpoint declared on the current environment - and connects to each.

        .DESCRIPTION
        Every Test-VcfAriaOps* check calls this once instead of Connect-VcfCheckAriaOpsEndpoint
        directly, so a single check fans out across every declared Aria Operations instance.
        SDDC Manager never holds an Aria Operations credential (it is lifecycle-managed by Aria
        Suite Lifecycle, not SDDC Manager), so the environment's declared Integrations endpoints
        are the only source of targets. A connection failure for one target (unreachable, bad
        credential) never blocks evaluation of the others - it is surfaced on that target's
        ConnectError instead of throwing, so the caller can emit one Error result per failed
        target and keep evaluating the rest.

        .PARAMETER Context
        The VcfCheck.Context object. $Context.AriaOpsEndpoints and
        $Context.AriaOpsEndpointCredentials must already be populated by Invoke-VcfCheck (via
        Resolve-VcfCheckAriaOpsEndpointCredentials) for standalone endpoints to be included.

        .PARAMETER ConnectivityTimeoutSeconds
        Maximum time to wait for each target's TCP reachability pre-flight check. Defaults to 15
        seconds.

        .OUTPUTS
        [Object[]] of PSCustomObject with Name, Fqdn, Connection, CredentialInfo, ConnectError.
        CredentialInfo (Fqdn/Credential) - pass it to the REST-fallback helpers
        (Get-VcfCheckAriaOpsResource, Get-VcfCheckAriaOpsAdapterResources) so those calls target
        the right instance. Empty array if the environment has no Aria Operations Integration
        declared - callers must treat that as Skipped.

        .EXAMPLE
        Get-VcfCheckAriaOpsTargets -Context $Context
    #>

    [CmdletBinding()]
    [OutputType([Object[]])]
    Param (
        [Parameter(Mandatory = $true)] [PSObject]$Context,
        [Parameter(Mandatory = $false)] [Int]$ConnectivityTimeoutSeconds = 15
    )

    $targets = [System.Collections.Generic.List[PSObject]]::new()

    foreach ($endpoint in @($Context.AriaOpsEndpoints) | Where-Object { $_ }) {
        $credential = $Context.AriaOpsEndpointCredentials[$endpoint.Fqdn]
        if (-not $credential) {
            $targets.Add([PSCustomObject]@{ Name = $endpoint.Name; Fqdn = $endpoint.Fqdn; Connection = $null; CredentialInfo = $null; ConnectError = "No password was supplied for Aria Operations endpoint `"$($endpoint.Fqdn)`"." })
            continue
        }
        $credentialInfo = [PSCustomObject]@{ PSTypeName = 'VcfCheck.AriaOpsCredential'; Fqdn = $endpoint.Fqdn; Credential = $credential }
        try {
            $connection = Connect-VcfCheckAriaOpsEndpoint -Context $Context -Fqdn $endpoint.Fqdn -Credential $credential -ConnectivityTimeoutSeconds $ConnectivityTimeoutSeconds
            $targets.Add([PSCustomObject]@{ Name = $endpoint.Name; Fqdn = $endpoint.Fqdn; Connection = $connection; CredentialInfo = $credentialInfo; ConnectError = $null })
        } catch {
            $targets.Add([PSCustomObject]@{ Name = $endpoint.Name; Fqdn = $endpoint.Fqdn; Connection = $null; CredentialInfo = $credentialInfo; ConnectError = $_.Exception.Message })
        }
    }

    return $targets.ToArray()
}
function Get-VcfCheckAriaOpsCollectors {
    <#
        .SYNOPSIS
        Thin, mockable wrapper around Invoke-VcfOpsGetCollectors (see AriaOpsHelpers.ps1's file
        header for why this wrapper exists).

        .PARAMETER Connection
        The Aria Operations connection object returned by
        Connect-VcfCheckAriaOpsEndpoint.

        .OUTPUTS
        The Collectors response object returned by Invoke-VcfOpsGetCollectors.
    #>

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

    return Invoke-VcfOpsGetCollectors -Server $Connection -ErrorAction Stop
}
function Get-VcfCheckAriaOpsCriticalAlerts {
    <#
        .SYNOPSIS
        Thin, mockable wrapper around Invoke-VcfOpsQueryAlert (see AriaOpsHelpers.ps1's file
        header for why this wrapper exists), pre-built for active Critical/Immediate alerts.

        .DESCRIPTION
        Builds an AlertQuery via Initialize-VcfOpsAlertQuery filtered to ActiveOnly with
        AlertCriticality 'CRITICAL'/'IMMEDIATE', then calls Invoke-VcfOpsQueryAlert. The response
        object's alert collection is exposed as its '_Alerts' property - a naming quirk of the
        generated SDK model, not a typo.

        .PARAMETER Connection
        The Aria Operations connection object returned by
        Connect-VcfCheckAriaOpsEndpoint.

        .OUTPUTS
        The Alerts response object returned by Invoke-VcfOpsQueryAlert.
    #>

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

    $alertQuery = Initialize-VcfOpsAlertQuery -ActiveOnly $true -AlertCriticality @('CRITICAL', 'IMMEDIATE')
    return Invoke-VcfOpsQueryAlert -Server $Connection -AlertQuery $alertQuery -ErrorAction Stop
}
function Get-VcfCheckAriaOpsResource {
    <#
        .SYNOPSIS
        Thin, mockable wrapper around Aria Operations' '/suite-api/api/resources/{id}' REST
        endpoint - see AriaOpsHelpers.ps1's file header for why this bypasses the SDK.

        .DESCRIPTION
        Resolves a resource id (as returned on an Alert's ResourceId property) to its Resource
        object, whose ResourceKey carries the human-readable Name and AdapterKindKey/
        ResourceKindKey - fields the Alert model itself does not expose. Invoke-VcfOpsGetResource
        (the SDK equivalent) hits the same "Variant,<n>,Version,4" schema-tag defect as
        Invoke-VcfOpsGetResourcesOfAdapterInstance - it malforms its own request path regardless
        of the id passed in - so this call goes straight to suite-api instead, the same
        REST-fallback pattern Get-VcfCheckAriaOpsAdapterResources uses.

        .PARAMETER CredentialInfo
        The AriaOpsCredential object from a target's CredentialInfo (Fqdn + Credential).

        .PARAMETER ResourceId
        The resource id to resolve.

        .OUTPUTS
        The Resource object returned by the suite-api.
    #>

    [CmdletBinding()]
    Param (
        [Parameter(Mandatory = $true)] [PSObject]$CredentialInfo,
        [Parameter(Mandatory = $true)] [Guid]$ResourceId
    )

    return Invoke-VcfCheckAriaOpsApi -CredentialInfo $CredentialInfo -Path "/suite-api/api/resources/$($ResourceId.ToString())" -ErrorAction Stop
}
function Test-VcfCheckAriaOpsValueLooksLikeFqdn {
    <#
        .SYNOPSIS
        Returns whether a string value is shaped like an FQDN rather than a bare IP address.

        .PARAMETER Value
        The candidate string.

        .OUTPUTS
        [Bool]
    #>

    [CmdletBinding()]
    [OutputType([Bool])]
    Param (
        [Parameter(Mandatory = $false)] [String]$Value
    )

    if ([String]::IsNullOrWhiteSpace($Value)) {
        return $false
    }

    $parsedIp = $null
    if ([System.Net.IPAddress]::TryParse($Value, [ref]$parsedIp)) {
        return $false
    }

    return $Value -match '^[a-zA-Z0-9]([a-zA-Z0-9\-]{0,61}[a-zA-Z0-9])?(\.[a-zA-Z0-9]([a-zA-Z0-9\-]{0,61}[a-zA-Z0-9])?)+$'
}
function Resolve-VcfCheckAriaOpsCollectorFqdn {
    <#
        .SYNOPSIS
        Best-effort resolution of an Aria Operations collector's FQDN from its node resource.

        .DESCRIPTION
        A Collector's HostName property is documented by Aria Operations as "host name or IP
        address" and in practice is usually an IP address - the Collector object itself has no
        FQDN field. This resolves the collector's NodeIdentifier to its Resource object via
        Get-VcfCheckAriaOpsResource and scans ResourceKey.ResourceIdentifiers for a
        network-address-type identifier whose value is shaped like an FQDN (see
        Test-VcfCheckAriaOpsValueLooksLikeFqdn). Falls back to the collector's own HostName -
        logged via Write-LogMessage - when the resource lookup fails, the NodeIdentifier is not a
        GUID, or no FQDN-shaped identifier is found. This is best-effort enrichment only; it never
        throws.

        .PARAMETER CredentialInfo
        The AriaOpsCredential object from a target's CredentialInfo (Fqdn + Credential).

        .PARAMETER Collector
        A Collector object as returned by Get-VcfCheckAriaOpsCollectors.

        .OUTPUTS
        [String] The resolved FQDN, or the collector's HostName if none could be resolved.
    #>

    [CmdletBinding()]
    [OutputType([String])]
    Param (
        [Parameter(Mandatory = $true)] [PSObject]$CredentialInfo,
        [Parameter(Mandatory = $true)] [PSObject]$Collector
    )

    $nodeIdentifier = [Guid]::Empty
    if (-not [Guid]::TryParse($Collector.NodeIdentifier, [ref]$nodeIdentifier)) {
        return $Collector.HostName
    }

    try {
        $resource = Get-VcfCheckAriaOpsResource -CredentialInfo $CredentialInfo -ResourceId $nodeIdentifier
    } catch {
        Write-LogMessage -Type DEBUG -Message "Could not resolve FQDN for Aria Operations collector `"$($Collector.Name)`" - falling back to its reported HostName `"$($Collector.HostName)`": $($_.Exception.Message)"
        return $Collector.HostName
    }

    $identifiers = @($resource.ResourceKey.ResourceIdentifiers | Where-Object { $_.IdentifierType.Name -match '(?i)network|address|dns|host' })
    $fqdn = $identifiers | ForEach-Object { $_.Value } | Where-Object { Test-VcfCheckAriaOpsValueLooksLikeFqdn -Value $_ } | Select-Object -First 1

    if (-not $fqdn) {
        Write-LogMessage -Type DEBUG -Message "No FQDN-shaped network identifier found for Aria Operations collector `"$($Collector.Name)`" - falling back to its reported HostName `"$($Collector.HostName)`"."
        return $Collector.HostName
    }

    return $fqdn
}
function Get-VcfCheckAriaOpsCertificates {
    <#
        .SYNOPSIS
        Thin, mockable wrapper around Invoke-VcfOpsGetAllCertificates (see AriaOpsHelpers.ps1's
        file header for why this wrapper exists).

        .DESCRIPTION
        The response object's certificate collection is exposed as its '_Certificates' property -
        a naming quirk of the generated SDK model, not a typo.

        .PARAMETER Connection
        The Aria Operations connection object returned by
        Connect-VcfCheckAriaOpsEndpoint.

        .OUTPUTS
        The Certificates response object returned by Invoke-VcfOpsGetAllCertificates.
    #>

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

    return Invoke-VcfOpsGetAllCertificates -Server $Connection -ErrorAction Stop
}
function Get-VcfCheckAriaOpsApiToken {
    <#
        .SYNOPSIS
        Acquires (and caches) an Aria Operations suite-api session token.

        .DESCRIPTION
        Aria Operations' suite-api does not accept HTTP Basic Auth on ordinary endpoints (unlike
        VRSLCM) - it requires a short-lived token acquired via POST '/suite-api/api/auth/token/acquire'
        with the username/password/authSource, then presented on every later call as
        'Authorization: vRealizeOpsToken <token>'. The token is cached as an 'ApiToken' member added
        directly onto the CredentialInfo object (a target's cached credential info from
        Get-VcfCheckAriaOpsTargets), so every call within a run re-uses it instead of
        re-authenticating.

        Aria Operations accounts are not always local - the credential's username determines the
        authSource to authenticate against: 'DOMAIN\user' or 'user@domain' is treated as a
        directory-backed account (authSource 'domain', username 'user' with the domain stripped,
        matching how Aria Operations' own login screen splits the same formats); a bare username
        with neither separator is treated as a local account (authSource 'LOCAL').

        .PARAMETER CredentialInfo
        The AriaOpsCredential object from a target's CredentialInfo (Fqdn + Credential).

        .OUTPUTS
        [String] the session token.
    #>

    [CmdletBinding()]
    [OutputType([String])]
    Param (
        [Parameter(Mandatory = $true)] [PSObject]$CredentialInfo
    )

    if ($CredentialInfo.PSObject.Properties['ApiToken'] -and $CredentialInfo.ApiToken) {
        return $CredentialInfo.ApiToken
    }

    $rawUsername = $CredentialInfo.Credential.UserName
    if ($rawUsername -match '^(?<domain>[^\\]+)\\(?<user>.+)$') {
        $username = $Matches.user
        $authSource = $Matches.domain
    } elseif ($rawUsername -match '^(?<user>[^@]+)@(?<domain>.+)$') {
        $username = $Matches.user
        $authSource = $Matches.domain
    } else {
        $username = $rawUsername
        $authSource = 'LOCAL'
    }

    $body = @{
        username   = $username
        password   = $CredentialInfo.Credential.GetNetworkCredential().Password
        authSource = $authSource
    } | ConvertTo-Json -Depth 2

    try {
        $response = Invoke-RestMethod -Uri "https://$($CredentialInfo.Fqdn)/suite-api/api/auth/token/acquire" `
            -Method POST -Body $body -ContentType 'application/json' -Headers @{ Accept = 'application/json' } `
            -SkipCertificateCheck:$CredentialInfo.AllowInsecureTls -ErrorAction Stop
    } catch {
        $message = Get-VcfCheckTlsTrustErrorMessage -ComponentName 'Aria Operations' -Fqdn $CredentialInfo.Fqdn -ErrorMessage $_.Exception.Message
        if (-not $message) {
            throw
        }
        throw [System.InvalidOperationException]::new($message)
    }

    if ($CredentialInfo.PSObject.Properties['ApiToken']) {
        $CredentialInfo.ApiToken = $response.token
    } else {
        $CredentialInfo | Add-Member -NotePropertyName 'ApiToken' -NotePropertyValue $response.token
    }

    return $response.token
}
function Invoke-VcfCheckAriaOpsApi {
    <#
        .SYNOPSIS
        Calls Aria Operations' suite-api REST API directly with a token acquired via
        Get-VcfCheckAriaOpsApiToken, bypassing the VMware.Sdk.Vcf.Ops SDK.

        .DESCRIPTION
        Thin Invoke-RestMethod wrapper for the handful of suite-api endpoints where the
        VMware.Sdk.Vcf.Ops SDK cannot be used - see AriaOpsHelpers.ps1's file header.
        -SkipCertificateCheck is only passed when CredentialInfo.AllowInsecureTls is $true (the
        run's resolved AllowInsecureTls value, derived from PowerCLI's InvalidCertificateAction
        setting, see Invoke-VcfCheck in Orchestrator.ps1) rather than being unconditional; an untrusted
        certificate encountered while that is $false surfaces as a clear, actionable error via
        Get-VcfCheckTlsTrustErrorMessage instead of being silently accepted. Retries up to 3 times
        with a 10-second delay on transient network failures (timeouts, connection resets).

        .PARAMETER CredentialInfo
        The AriaOpsCredential object from a target's CredentialInfo (Fqdn + Credential).

        .PARAMETER Method
        HTTP method. Defaults to GET.

        .PARAMETER Path
        The suite-api path, e.g. '/suite-api/api/adapters/<guid>/resources'.

        .OUTPUTS
        The parsed JSON response.
    #>

    [CmdletBinding()]
    Param (
        [Parameter(Mandatory = $true)] [PSObject]$CredentialInfo,
        [Parameter(Mandatory = $false)] [ValidateSet('GET', 'POST')] [String]$Method = 'GET',
        [Parameter(Mandatory = $true)] [ValidateNotNullOrEmpty()] [String]$Path
    )

    $token = Get-VcfCheckAriaOpsApiToken -CredentialInfo $CredentialInfo
    $uri = "https://$($CredentialInfo.Fqdn)$Path"
    $parameters = @{
        Uri                  = $uri
        Method               = $Method
        Headers              = @{ Accept = 'application/json'; Authorization = "vRealizeOpsToken $token" }
        SkipCertificateCheck = $CredentialInfo.AllowInsecureTls
        ErrorAction          = 'Stop'
    }

    $maxAttempts = 3
    $retryDelaySeconds = 10
    for ($attempt = 1; $attempt -le $maxAttempts; $attempt++) {
        try {
            return Invoke-RestMethod @parameters
        } catch {
            $tlsMessage = Get-VcfCheckTlsTrustErrorMessage -ComponentName 'Aria Operations' -Fqdn $CredentialInfo.Fqdn -ErrorMessage $_.Exception.Message
            if ($tlsMessage) {
                throw [System.InvalidOperationException]::new($tlsMessage)
            }
            $isTransient = $_.Exception.Message -match '(?i)timed out|timeout|actively refused|connection refused|reset by peer|handshake'
            if (-not $isTransient -or $attempt -eq $maxAttempts) {
                throw
            }
            Write-LogMessage -Type DEBUG -Message "Transient error calling Aria Operations suite-api '$Path' (attempt $attempt of $maxAttempts): $($_.Exception.Message). Retrying in $retryDelaySeconds seconds."
            Start-Sleep -Seconds $retryDelaySeconds
        }
    }
}
function Get-VcfCheckAriaOpsAdapterInstances {
    <#
        .SYNOPSIS
        Thin, mockable wrapper around Invoke-VcfOpsEnumerateAdapterInstances (see
        AriaOpsHelpers.ps1's file header for why this wrapper exists).

        .DESCRIPTION
        The response object's adapter instance collection is exposed as its
        'AdapterInstancesInfoDto' property, per the generated SDK model.

        .PARAMETER Connection
        The Aria Operations connection object returned by
        Connect-VcfCheckAriaOpsEndpoint.

        .OUTPUTS
        The AdapterInstances response object returned by Invoke-VcfOpsEnumerateAdapterInstances.
    #>

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

    return Invoke-VcfOpsEnumerateAdapterInstances -Server $Connection -ErrorAction Stop
}
function Get-VcfCheckAriaOpsAdapterResources {
    <#
        .SYNOPSIS
        Thin, mockable wrapper around Aria Operations' '/suite-api/api/adapters/{id}/resources'
        REST endpoint - see AriaOpsHelpers.ps1's file header for why this bypasses the SDK.

        .DESCRIPTION
        The response object's resource collection is exposed as its 'ResourceList' property
        (PowerShell's property access on a JSON-deserialized PSCustomObject is case-insensitive,
        so this matches the suite-api response's camelCase 'resourceList' as well as the
        generated SDK model's PascalCase naming callers were already written against). Each
        resource's ResourceStatusStates carries the collection status (e.g. DATA_RECEIVING,
        COLLECTOR_DOWN) per adapter instance monitoring it.

        .PARAMETER CredentialInfo
        The AriaOpsCredential object from a target's CredentialInfo (Fqdn + Credential).

        .PARAMETER AdapterId
        The identifier of the adapter instance to enumerate resources for.

        .OUTPUTS
        The Resources response object returned by the suite-api.
    #>

    [CmdletBinding()]
    Param (
        [Parameter(Mandatory = $true)] [PSObject]$CredentialInfo,
        [Parameter(Mandatory = $true)] [Guid]$AdapterId
    )

    return Invoke-VcfCheckAriaOpsApi -CredentialInfo $CredentialInfo -Path "/suite-api/api/adapters/$($AdapterId.ToString())/resources" -ErrorAction Stop
}
function Get-VcfCheckAriaOpsLicenseEntitlement {
    <#
        .SYNOPSIS
        Thin, mockable wrapper around Aria Operations' '/suite-api/api/product/licensing/entitlement'
        REST endpoint - see AriaOpsHelpers.ps1's file header for why this bypasses the SDK.

        .DESCRIPTION
        No VMware.Sdk.Vcf.Ops cmdlet covers this endpoint (only Invoke-VcfOpsGetLicenseInfo exists,
        and it 404s on every real instance tested - see Docs/CheckConfidence.md's
        aria_ops_license entry), so this goes straight through Invoke-VcfCheckAriaOpsApi. The
        response object's license collection is exposed as its 'solutionLicenses' property, each
        entry carrying id/licenseKey/expirationDate (epoch milliseconds)/capacity/usage/edition/
        licenseType/statuses per the 8.18 swagger spec's 'solution-licenses' schema.

        .PARAMETER CredentialInfo
        The AriaOpsCredential object from a target's CredentialInfo (Fqdn + Credential).

        .OUTPUTS
        The solution-licenses response object returned by the suite-api.
    #>

    [CmdletBinding()]
    Param (
        [Parameter(Mandatory = $true)] [PSObject]$CredentialInfo
    )

    return Invoke-VcfCheckAriaOpsApi -CredentialInfo $CredentialInfo -Path '/suite-api/api/product/licensing/entitlement' -ErrorAction Stop
}
function Get-VcfCheckAriaOpsVersion {
    <#
        .SYNOPSIS
        Thin, mockable wrapper around Invoke-VcfOpsGetCurrentVersionOfServer, returning Aria
        Operations' own reported marketing version as a dotted string.

        .DESCRIPTION
        The SDK's Version model (VMware.Bindings.Vcf.Ops.Model.Version) exposes Major/Minor/
        MinorMinor/Patch as separate integers, but those are the internal build-numbering
        scheme (e.g. 1.70.0), not the marketing product version (e.g. 8.18.0) that the shipped
        Interop Matrix snapshot (Data/Interoperability/Vrops.json) uses. The marketing version
        is embedded as free text in the model's ReleaseName property (e.g. 'VMware Aria
        Operations 8.18.0'), so this extracts the trailing dotted-number group from it.

        .PARAMETER Connection
        The Aria Operations connection object returned by
        Connect-VcfCheckAriaOpsEndpoint.

        .OUTPUTS
        [String] the dotted marketing version string, or $null if it could not be determined.
    #>

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

    $version = Invoke-VcfOpsGetCurrentVersionOfServer -Server $Connection -ErrorAction Stop
    if ($null -eq $version -or [String]::IsNullOrWhiteSpace($version.ReleaseName)) {
        return $null
    }

    $match = [Regex]::Match($version.ReleaseName, '\d+(?:\.\d+){1,3}$')
    if (-not $match.Success) {
        return $null
    }

    return $match.Value
}
function Get-VcfCheckAriaOpsSelfMonitoringResources {
    <#
        .SYNOPSIS
        Thin, mockable wrapper around Invoke-VcfOpsGetResourcesWithAdapterKind (see
        AriaOpsHelpers.ps1's file header for why this wrapper exists), pre-built for Aria
        Operations' own self-monitoring resources (its cluster and node(s)).

        .DESCRIPTION
        Aria Operations monitors itself under a built-in adapter kind - the same resources the
        product's own "vRealize Operations Manager Cluster" dashboards chart. The exact adapter
        kind key varies in shape by version and is resolved by the caller (see
        Get-VcfCheckAriaOpsSelfMonitoringAdapterKind) rather than hardcoded here, since a
        hardcoded guess ('VCOPS_VCOPS_ADAPTER') was confirmed live to return zero resources
        against a real instance - see ARIA_OPS_CONNECTOR_PLAN.md. The response object's resource
        collection is exposed as its 'ResourceList' property, per the generated SDK model.

        .PARAMETER Connection
        The Aria Operations connection object returned by
        Connect-VcfCheckAriaOpsEndpoint.

        .PARAMETER AdapterKindKey
        The adapter kind key to query, as resolved by Get-VcfCheckAriaOpsSelfMonitoringAdapterKind.

        .OUTPUTS
        The Resources response object returned by Invoke-VcfOpsGetResourcesWithAdapterKind.
    #>

    [CmdletBinding()]
    Param (
        [Parameter(Mandatory = $true)] [PSObject]$Connection,
        [Parameter(Mandatory = $true)] [String]$AdapterKindKey
    )

    return Invoke-VcfOpsGetResourcesWithAdapterKind -Server $Connection -AdapterKindKey $AdapterKindKey -ErrorAction Stop
}
function Get-VcfCheckAriaOpsSelfMonitoringAdapterKind {
    <#
        .SYNOPSIS
        Resolves the adapter kind key of Aria Operations' own self-monitoring adapter instance by
        matching on name/kind rather than a hardcoded key.

        .DESCRIPTION
        Calls Get-VcfCheckAriaOpsAdapterInstances and returns the AdapterKindKey of the first
        configured adapter instance whose AdapterKindKey or resource name matches the
        self-monitoring adapter's known naming pattern ('VCOPS', 'vRealize Operations Manager', or
        'vRealize Operations Adapter' - a real instance's self-monitoring adapters are named
        'vRealize Operations Adapter - <node>', one per node, confirmed live against
        xint-ops01.rainpole.io). A hardcoded key ('VCOPS_VCOPS_ADAPTER') was tried previously and
        confirmed live to return zero resources - see ARIA_OPS_CONNECTOR_PLAN.md - so this
        discovers the key from the instance's own adapter inventory instead of guessing.

        .PARAMETER Connection
        The Aria Operations connection object returned by
        Connect-VcfCheckAriaOpsEndpoint.

        .OUTPUTS
        [String] The discovered adapter kind key, or $null if no self-monitoring adapter instance
        is configured.
    #>

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

    $response = Get-VcfCheckAriaOpsAdapterInstances -Connection $Connection
    $adapters = @($response.AdapterInstancesInfoDto) | Where-Object { $_ -and $_.ResourceKey }
    $selfMonitoring = $adapters | Where-Object {
        $_.ResourceKey.AdapterKindKey -match '(?i)vcops' -or $_.ResourceKey.Name -match '(?i)vRealize Operations Manager|vRealize Operations Adapter|Aria Operations'
    } | Select-Object -First 1

    if (-not $selfMonitoring) {
        return $null
    }

    return $selfMonitoring.ResourceKey.AdapterKindKey
}
function Get-VcfCheckAriaOpsResourceStats {
    <#
        .SYNOPSIS
        Thin, mockable wrapper around Invoke-VcfOpsGetLatestStatsOfResources (see
        AriaOpsHelpers.ps1's file header for why this wrapper exists).

        .DESCRIPTION
        Returns every stat Aria Operations currently reports for the given resource id(s) - no
        -StatKey filter is passed, since the exact stat key names for CPU/memory/disk/network/
        object-count self-monitoring metrics have not been confirmed live against a real
        instance (see ARIA_OPS_CONNECTOR_PLAN.md). Callers filter the returned stats by name
        pattern instead of relying on hardcoded key strings. The response object's per-resource
        stat collection is exposed as its 'Values' property, each entry's 'StatList' property
        being a wrapper object (not itself enumerable) whose 'Stat' property holds one Stats
        object per reported metric (StatKey.Key/Data/Timestamps), per the generated SDK model.

        .PARAMETER Connection
        The Aria Operations connection object returned by
        Connect-VcfCheckAriaOpsEndpoint.

        .PARAMETER ResourceId
        Array of resource ids to fetch the latest stats for.

        .OUTPUTS
        The StatsOfResources response object returned by Invoke-VcfOpsGetLatestStatsOfResources.
    #>

    [CmdletBinding()]
    Param (
        [Parameter(Mandatory = $true)] [PSObject]$Connection,
        [Parameter(Mandatory = $true)] [Guid[]]$ResourceId
    )

    return Invoke-VcfOpsGetLatestStatsOfResources -Server $Connection -ResourceId $ResourceId -CurrentOnly $true -ErrorAction Stop
}