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-VcfCheckApplianceProductIdentity { <# .SYNOPSIS Confirms an Aria appliance's guest OS identifies as the expected product. .DESCRIPTION Helper for Test-VcfCheckAriaNodePasswordExpiration. Runs `egrep "<GrepPattern>" <FilePath>` via Invoke-VcfApplianceCommand to confirm the VM registered in SDDC Manager as -Product is actually running that product's appliance image, rather than trusting the registered FQDN alone - a stale or mismatched SDDC Manager inventory entry could otherwise point this check at the wrong VM. .PARAMETER VmName Short VM name to pass to Invoke-VcfApplianceCommand. .PARAMETER VCenterFqdn FQDN of the already-connected vCenter that manages the appliance VM. .PARAMETER Fqdn Appliance FQDN as registered in SDDC Manager. .PARAMETER Credential Guest OS credential for the appliance. .PARAMETER FilePath Guest OS file to grep (e.g. "/etc/vmware-prelude/va-release.conf" or "/opt/vmware/etc/ovfEnv.xml"). .PARAMETER GrepPattern Full egrep pattern expected to match in -FilePath (e.g. "va.name.*Automation" or "VMware.*Operations"). .PARAMETER Description Friendly product identity fragment (e.g. "Automation") to include in the Detail message. .OUTPUTS [PSCustomObject] with Confirmed (bool) and Detail (string, populated only when not confirmed). #> [CmdletBinding()] [OutputType([PSCustomObject])] Param ( [Parameter(Mandatory = $true)] [ValidateNotNullOrEmpty()] [String]$VmName, [Parameter(Mandatory = $true)] [ValidateNotNullOrEmpty()] [String]$VCenterFqdn, [Parameter(Mandatory = $true)] [ValidateNotNullOrEmpty()] [String]$Fqdn, [Parameter(Mandatory = $true)] [PSCredential]$Credential, [Parameter(Mandatory = $true)] [ValidateNotNullOrEmpty()] [String]$FilePath, [Parameter(Mandatory = $true)] [ValidateNotNullOrEmpty()] [String]$GrepPattern, [Parameter(Mandatory = $true)] [ValidateNotNullOrEmpty()] [String]$Description ) $commandResult = Invoke-VcfApplianceCommand -VmName $VmName -Server $VCenterFqdn -Fqdn $Fqdn ` -Credential $Credential -ScriptText "egrep `"$GrepPattern`" $FilePath" if (-not $commandResult.Success -or [String]::IsNullOrWhiteSpace($commandResult.ScriptOutput)) { return [PSCustomObject]@{ Confirmed = $false Detail = "`"$Fqdn`" does not identify as a `"$Description`" appliance in $FilePath; skipping root password check to avoid checking the wrong VM." } } return [PSCustomObject]@{ Confirmed = $true; Detail = '' } } 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. If -IdentityCheck is supplied, first confirms the node's guest OS identity via Test-VcfCheckApplianceProductIdentity (matching -IdentityCheck.GrepPattern against -IdentityCheck.FilePath) and returns an Error result without running `chage` if the identity does not match - a registered FQDN that no longer points at the expected appliance should not be trusted for a password-expiration verdict. 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. .PARAMETER IdentityCheck Optional [PSCustomObject] with FilePath, GrepPattern, and Description properties. When supplied, the node's identity is confirmed via Test-VcfCheckApplianceProductIdentity before the password check runs. .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, [Parameter(Mandatory = $false)] [PSCustomObject]$IdentityCheck = $null ) $vmName = ($Fqdn -split '\.')[0] if ($IdentityCheck) { $identity = Test-VcfCheckApplianceProductIdentity -VmName $vmName -VCenterFqdn $VCenterFqdn ` -Fqdn $Fqdn -Credential $Credential -FilePath $IdentityCheck.FilePath ` -GrepPattern $IdentityCheck.GrepPattern -Description $IdentityCheck.Description if (-not $identity.Confirmed) { return [PSCustomObject]@{ Fqdn = $Fqdn Username = $Username Status = 'Error' Detail = $identity.Detail ExpirationDate = 'Unknown' DaysRemaining = 'Unknown' Fields = @() } } } $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). For VRA and VROPS nodes, the guest OS identity is confirmed first (va-release.conf and ovfEnv.xml respectively) before `chage` runs. 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' } $productIdentityChecks = @{ 'VRA' = [PSCustomObject]@{ FilePath = '/etc/vmware-prelude/va-release.conf'; GrepPattern = 'va.name.*Automation'; Description = 'Automation' } 'VROPS' = [PSCustomObject]@{ FilePath = '/opt/vmware/etc/ovfEnv.xml'; GrepPattern = 'VMware.*Operations'; Description = 'Operations' } 'VRLI' = [PSCustomObject]@{ FilePath = '/etc/vmware/.buildInfo'; GrepPattern = 'Operations for Logs'; Description = 'Operations for Logs' } } 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' } $identityCheck = if ($productIdentityChecks.ContainsKey($product)) { $productIdentityChecks[$product] } else { $null } foreach ($entry in $entries) { $nodeTasks.Add([PSCustomObject]@{ FriendlyProduct = $friendlyProduct; Entry = $entry; IdentityCheck = $identityCheck }) } } $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 ` -IdentityCheck $nodeTask.IdentityCheck 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 |