Private/Checks/Aria/Test-VcfVrslcmRootPasswordExpiration.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 ConvertFrom-VcfCheckChageOutput { <# .SYNOPSIS Parses `chage -l <user>` output into a name/value field list. .DESCRIPTION Helper for Test-VcfVrslcmRootPasswordExpiration. `chage -l` prints one "Label: Value" line per field (e.g. "Password expires: never") - splits each line on the first colon only, since some values (e.g. dates like "Aug 05, 2026") contain no colon but labels always do. .PARAMETER Lines Raw stdout lines captured from the guest command. .OUTPUTS [PSCustomObject[]] one entry per parsed line, each with Name and Value properties. #> [CmdletBinding()] [OutputType([Object[]])] Param ( [Parameter(Mandatory = $true)] [AllowNull()] [String[]]$Lines ) $fields = [System.Collections.Generic.List[PSCustomObject]]::new() foreach ($line in @($Lines)) { $parts = $line -split ':', 2 if ($parts.Count -eq 2) { $fields.Add([PSCustomObject]@{ Name = $parts[0].Trim(); Value = $parts[1].Trim() }) } } return @($fields) } function Test-VcfCheckAriaNodePasswordExpiration { <# .SYNOPSIS Runs and evaluates `chage -l` for a single Aria appliance node's guest OS credential. .DESCRIPTION Helper for Test-VcfVrslcmRootPasswordExpiration. Resolves the appliance's short VM name from -Fqdn, runs `chage -l <Username>` via Invoke-VcfApplianceCommand (guest operations through vCenter, not a direct SSH session), and parses the result with ConvertFrom-VcfCheckChageOutput. A finite "Maximum number of days between password change" (chage's default fresh-appliance value is 99999, effectively "never") or a "Password expires" date means the account is configured to expire. A parseable "Password expires" date in the past is a Fail (root is already locked out); a date less than 30 days away is a Warning; any other finite expiration is also a Warning. Aria appliances are not expected to rotate their guest OS root password on a schedule - an expired root account can silently block SSH-based maintenance and LCM operations. .PARAMETER VCenterFqdn FQDN of the already-connected vCenter that manages the appliance VM. .PARAMETER Fqdn Appliance FQDN as registered in SDDC Manager. Its hostname label is used as the vCenter VM name. .PARAMETER Product Friendly product name (e.g. "Aria Operations") to include in Detail messages. .PARAMETER Username Guest OS account to check (the SSH credential's username, typically "root"). .PARAMETER Credential Guest OS credential for -Username. .OUTPUTS [PSCustomObject] with Fqdn, Username, Status ('Pass'/'Warning'/'Fail'/'Error'), Detail, ExpirationDate, DaysRemaining, and Fields (the parsed chage output, empty on failure) properties. #> [CmdletBinding()] [OutputType([PSCustomObject])] Param ( [Parameter(Mandatory = $true)] [ValidateNotNullOrEmpty()] [String]$VCenterFqdn, [Parameter(Mandatory = $true)] [ValidateNotNullOrEmpty()] [String]$Fqdn, [Parameter(Mandatory = $true)] [ValidateNotNullOrEmpty()] [String]$Product, [Parameter(Mandatory = $true)] [ValidateNotNullOrEmpty()] [String]$Username, [Parameter(Mandatory = $true)] [PSCredential]$Credential ) $vmName = ($Fqdn -split '\.')[0] $commandResult = Invoke-VcfApplianceCommand -VmName $vmName -Server $VCenterFqdn -Fqdn $Fqdn ` -Credential $Credential -ScriptText "chage -l $Username" if (-not $commandResult.Success) { return [PSCustomObject]@{ Fqdn = $Fqdn Username = $Username Status = 'Error' Detail = "Unable to retrieve password expiration data from `"$Fqdn`": $($commandResult.ErrorMessage)" ExpirationDate = 'Unknown' DaysRemaining = 'Unknown' Fields = @() } } $lines = @($commandResult.ScriptOutput -split "`r?`n") | Where-Object { $_ -ne '' } if ($lines.Count -eq 0) { return [PSCustomObject]@{ Fqdn = $Fqdn Username = $Username Status = 'Error' Detail = "`"chage -l $Username`" on `"$Fqdn`" returned no output." ExpirationDate = 'Unknown' DaysRemaining = 'Unknown' Fields = @() } } $fields = ConvertFrom-VcfCheckChageOutput -Lines $lines if ($fields.Count -eq 0) { return [PSCustomObject]@{ Fqdn = $Fqdn Username = $Username Status = 'Error' Detail = "`"chage -l $Username`" on `"$Fqdn`" returned no parseable output." ExpirationDate = 'Unknown' DaysRemaining = 'Unknown' Fields = @() } } $maxDaysField = $fields | Where-Object { $_.Name -eq 'Maximum number of days between password change' } | Select-Object -First 1 $expiresField = $fields | Where-Object { $_.Name -eq 'Password expires' } | Select-Object -First 1 $maxDays = 0 $expiresDate = Get-Date $expirationDate = 'Never' $daysRemainingDisplay = 'N/A' if ($expiresField -and $expiresField.Value -ne 'never' -and [DateTime]::TryParse($expiresField.Value, [ref]$expiresDate)) { $expirationDate = $expiresField.Value $daysRemaining = [Math]::Ceiling(($expiresDate - (Get-Date)).TotalDays) $daysRemainingDisplay = [Math]::Max(0, $daysRemaining) if ($daysRemaining -lt 0) { $status = 'Fail' $detail = "Root password for $Product on `"$Fqdn`" expired on $($expiresField.Value)." } elseif ($daysRemaining -lt 30) { $status = 'Warning' $detail = "Root password for $Product on `"$Fqdn`" expires on $($expiresField.Value) ($daysRemaining day(s) remaining)." } else { $status = 'Pass' $detail = "Root password for $Product on `"$Fqdn`" expires on $($expiresField.Value) ($daysRemaining day(s) remaining) - more than 30 days away." } } elseif ($maxDaysField -and [Int32]::TryParse($maxDaysField.Value, [ref]$maxDays) -and $maxDays -lt 99999) { $expirationDate = "$maxDays day(s) after last change" $daysRemainingDisplay = $maxDays if ($maxDays -lt 30) { $status = 'Warning' $detail = "Root password for $Product on `"$Fqdn`" is set to expire after $maxDays day(s)." } else { $status = 'Pass' $detail = "Root password for $Product on `"$Fqdn`" is set to expire after $maxDays day(s) - more than 30 days away." } } elseif ($expiresField -and $expiresField.Value -ne 'never') { $status = 'Warning' $expirationDate = $expiresField.Value $detail = "Root password for $Product on `"$Fqdn`" expires on $($expiresField.Value)." } else { $status = 'Pass' $detail = "Root password for $Product on `"$Fqdn`" does not expire." } return [PSCustomObject]@{ Fqdn = $Fqdn Username = $Username Status = $status Detail = $detail ExpirationDate = $expirationDate DaysRemaining = $daysRemainingDisplay Fields = $fields } } function Test-VcfVrslcmRootPasswordExpiration { <# .SYNOPSIS Checks root password expiration settings on every deployed Aria Suite appliance node. .DESCRIPTION Enumerates every Aria Suite product with credentials registered in SDDC Manager (vRSLM, VRLI, VROPS, VRA, WSA) via Invoke-VcfGetCredentials, then for each SSH-credentialed node runs Test-VcfCheckAriaNodePasswordExpiration - which executes `chage -l <user>` on the appliance through vCenter guest operations (Invoke-VcfApplianceCommand). Reports per-node sub-progress via Write-VcfCheckSubProgress as each appliance node is checked to ensure real-time status updates during execution. Outcome behavior: - Skipped: Returns 'Skipped' if Aria Suite Lifecycle Manager is not deployed in the environment. - Pass: Returns 'Pass' if every registered node's root password is set to never expire. - Warning: Returns 'Warning' if one or more nodes has a finite password expiration configured (including one expiring in fewer than 30 days), or a node's expiration data could not be retrieved, and no node's password has already expired. - Fail: Returns 'Fail' if one or more nodes' root password has already expired. - Error: Returns 'Error' if SDDC Manager's credential inventory or the management vCenter connection cannot be established at all. Constructs a breakdown table ('Rows') of Product, Fqdn, Username, ExpirationDate, DaysRemaining, and Status per node. .PARAMETER Context The VcfCheck.Context object. Must already be connected to SDDC Manager. .PARAMETER DisplayName Optional friendly display name for the check result. .OUTPUTS [PSObject] A single VcfCheck.Result object. #> [CmdletBinding()] [OutputType([PSObject])] Param ( [Parameter(Mandatory = $true)] [PSObject]$Context, [Parameter(Mandatory = $false)] [String]$DisplayName = '' ) $startedAt = Get-Date $checkId = 'vrslcm_root_password_expiration' $products = @('VRSLCM', 'VRLI', 'VROPS', 'VRA', 'WSA') $productFriendlyNames = @{ 'VRSLCM' = 'Aria Suite Lifecycle Manager' 'VRLI' = 'Aria Operations for Logs' 'VROPS' = 'Aria Operations' 'VRA' = 'Aria Automation' 'WSA' = 'Workspace ONE Access' } 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 { $vcenterFqdn = Get-VcfCheckManagementVCenterFqdn -Context $Context Connect-VcfCheckVCenter -Context $Context -Fqdn $vcenterFqdn } catch { return New-VcfCheckResult -CheckId $checkId -Status Error ` -TargetComponent $connection.Fqdn -Exception $_.Exception.Message ` -StartedAt $startedAt -CompletedAt (Get-Date) -DisplayName $DisplayName } $nodeTasks = [System.Collections.Generic.List[PSCustomObject]]::new() foreach ($product in $products) { try { $response = Invoke-VcfGetCredentials -ResourceType $product -ErrorAction Stop } catch { continue } $friendlyProduct = if ($productFriendlyNames.ContainsKey($product)) { $productFriendlyNames[$product] } else { $product } $entries = @($response.Elements) | Where-Object { $_.CredentialType -eq 'SSH' } foreach ($entry in $entries) { $nodeTasks.Add([PSCustomObject]@{ FriendlyProduct = $friendlyProduct; Entry = $entry }) } } $rows = [System.Collections.Generic.List[PSCustomObject]]::new() $failures = [System.Collections.Generic.List[String]]::new() $warnings = [System.Collections.Generic.List[String]]::new() $nodeIndex = 0 foreach ($nodeTask in $nodeTasks) { $nodeIndex++ $entry = $nodeTask.Entry $nodeFqdn = $entry.Resource.ResourceName Write-VcfCheckSubProgress -Context $Context -Current $nodeIndex -Total $nodeTasks.Count ` -Label $nodeFqdn -Unit 'appliance nodes' $secure = ConvertTo-SecureStringForCredential -PlainText $entry.Password $nodeCredential = [PSCredential]::new($entry.Username, $secure) $outcome = Test-VcfCheckAriaNodePasswordExpiration -VCenterFqdn $vcenterFqdn ` -Fqdn $nodeFqdn -Product $nodeTask.FriendlyProduct -Username $entry.Username -Credential $nodeCredential Remove-Variable -Name nodeCredential, secure -ErrorAction SilentlyContinue if ($outcome.Status -eq 'Fail') { $failures.Add($outcome.Detail) } elseif ($outcome.Status -ne 'Pass') { $warnings.Add($outcome.Detail) } $rows.Add([PSCustomObject]@{ Product = $nodeTask.FriendlyProduct Fqdn = $outcome.Fqdn Username = $outcome.Username ExpirationDate = $outcome.ExpirationDate DaysRemaining = $outcome.DaysRemaining Status = $outcome.Status }) } $rows = [System.Collections.Generic.List[PSCustomObject]]($rows | Sort-Object -Property Product) if ($rows.Count -eq 0) { return New-VcfCheckResult -CheckId $checkId -Status Pass ` -TargetComponent $connection.Fqdn ` -Detail 'No Aria Suite appliance nodes with SSH credentials were found in SDDC Manager.' ` -StartedAt $startedAt -CompletedAt (Get-Date) -DisplayName $DisplayName } if ($failures.Count -gt 0) { $status = 'Fail' $detail = "One or more Aria Suite appliance nodes have an expired root password: $($failures -join '; ')" if ($warnings.Count -gt 0) { $detail += " Additional nodes have a root password expiration configured or could not be checked: $($warnings -join '; ')" } } elseif ($warnings.Count -gt 0) { $status = 'Warning' $detail = "One or more Aria Suite appliance nodes have a root password expiration configured or could not be checked: $($warnings -join '; ')" } else { $status = 'Pass' $expiringCount = @($rows | Where-Object { $_.ExpirationDate -ne 'Never' }).Count if ($expiringCount -gt 0) { $detail = "Root password on every Aria Suite appliance node is at least 30 days from expiration ($expiringCount node(s) have a configured expiration)." } else { $detail = 'Root password on every Aria Suite appliance node is set to never expire.' } } return New-VcfCheckResult -CheckId $checkId -Status $status ` -TargetComponent $connection.Fqdn -Detail $detail -Rows @($rows) ` -StartedAt $startedAt -CompletedAt (Get-Date) -DisplayName $DisplayName } #endregion |