validation_utils.ps1
|
$PUBLIC_KEY = ('{0}/ZertoPublicKey.pem' -f $psScriptRoot) # Password requirements for 10.8 https://help.zerto.com/bundle/Linux.ZVM.HTML.10.8/page/ZVM_Appliance_Reset_Password.htm $MIN_PASSWORD_LEN = 14 $PASSWORD_REGEX = [regex]"^(?=.*[A-Z])(?=.*[^A-Za-z])(?=.*\d)(?=.*[\W_]).{$MIN_PASSWORD_LEN,}$" $MAX_CONSECUTIVE_CHARACTERS_IN_PASSWORD = 3 # Password requirements for 10u5 https://help.zerto.com/bundle/Linux.ZVM.HTML.10.0_U5/page/ZVM_Appliance_Reset_Password.htm and against injection attacks $NOT_ALLOWED_CHARACTERS_IN_PASSWORD_REGEX = [regex]'[\$"''\s]' # The $ " ' or whitespace is not allowed $ALLOWED_FIRST_CHARACTER_IN_PASSWORD_REGEX = [regex]'[a-zA-Z0-9]' # The non-alphanumeric first character is not allowed function Test-HasConsecutiveCharacters { <# .SYNOPSIS Tests if a string contains consecutive (ascending/descending) or repeating characters. #> param( [string]$str ) $consecutiveThreshold = $MAX_CONSECUTIVE_CHARACTERS_IN_PASSWORD + 1 # Looking for 3+ consecutive characters # Iterate through each possible starting position for ($i = 0; $i -le ($str.Length - $consecutiveThreshold); $i++) { $isAscending = $true $isDescending = $true $isRepeating = $true # Check block of 4 characters starting at position i for ($j = 1; $j -lt $consecutiveThreshold; $j++) { $currentCharCode = [int]$str[$i + $j - 1] $nextCharCode = [int]$str[$i + $j] # Check ascending sequence (each char is +1 from previous) if ($nextCharCode -ne $currentCharCode + 1) { $isAscending = $false } # Check descending sequence (each char is -1 from previous) if ($nextCharCode -ne $currentCharCode - 1) { $isDescending = $false } # Check repeating sequence (each char is same as previous) if ($nextCharCode -ne $currentCharCode) { $isRepeating = $false } # Early exit if neither sequence type is possible if ((-not $isAscending) -and (-not $isDescending) -and (-not $isRepeating)) { break } } if ($isAscending -or $isDescending -or $isRepeating) { Write-Host "Password contains $($isAscending ? 'ascending' : '')$($isDescending ? 'descending' : '')$($isRepeating ? 'repeating' : '') characters." return $true } } return $false } function Validate-ZertoPasswordCompliance { <# .SYNOPSIS Checks whether password meets Zerto password requirements .DESCRIPTION The ZVMA password policies have evolved over time. It is possible that the users current Keycloak password remains valid but no longer complies with the updated requirements. In such case, this validator will fail, and users should update their Keycloak password to meet the latest policy requirements. #> param ( [SecureString]$Password ) process { $pass = ConvertFrom-SecureString -SecureString $Password -AsPlainText if ($pass -notmatch $PASSWORD_REGEX) { throw "Zerto password requirements are not met. Password should contain at least one uppercase letter, one digit, one non-alphanumeric character, and be at least $MIN_PASSWORD_LEN characters long." } if (Test-HasConsecutiveCharacters -str $pass) { throw "Zerto password requirements are not met. Password should not contain more than $MAX_CONSECUTIVE_CHARACTERS_IN_PASSWORD sequential or repeating characters." } if ($pass -match $NOT_ALLOWED_CHARACTERS_IN_PASSWORD_REGEX) { throw 'Zerto password requirements in AVS are not met. Password should not contain $ (dollar sign), " (double quote), '' (single quote), or whitespace characters.' } if ($pass[0] -notmatch $ALLOWED_FIRST_CHARACTER_IN_PASSWORD_REGEX) { throw "Zerto password requirements in AVS are not met. Password should begin with an alphanumeric character (a-z, A-Z, or 0-9)." } } } function Validate-HostsForNonVaio { param( [string]$HostName ) Write-Host "Starting $($MyInvocation.MyCommand)..." $vmHosts = if ($HostName) { Get-VMHost -Name $HostName } else { Get-VMHost } $AV64_HOST_MANUFACTURER = "Microsoft" $av64Hosts = $vmHosts | Where-Object { $_.Manufacturer -eq $AV64_HOST_MANUFACTURER } if ($av64Hosts) { $hostNames = $av64Hosts.Name -join ", " throw "Non-VAIO ZVMA cannot operate with AV64 hosts. Found AV64 host(s): $hostNames." } } #TODO Remove function because VAIO is the only supported deployment method # function Validate-ZertoParams { # param ( # [Parameter(Mandatory = $true)] # [SecureString]$ZertoAdminPassword, # [Parameter(Mandatory = $true)] # [bool]$IsVaio # ) # process { # Validate-ZertoPasswordCompliance -Password $ZertoAdminPassword # if (-not $IsVaio) { # Validate-HostsForNonVaio # } # } # } function Validate-FileBySignature { param ( [Parameter(Mandatory = $true, HelpMessage = "File to verify")] [string]$FilePath, [Parameter(Mandatory = $true, HelpMessage = "Signature file to verify")] [string]$SignatureFilePath ) process { Write-Host "Verifying signature for $FilePath" Write-Host "The verification process might take a while, please wait..." $isVerified = (openssl dgst -sha256 -verify $PUBLIC_KEY -signature $SignatureFilePath $FilePath 2>&1) -join ";" if ($isVerified -eq "Verified OK") { Write-Host "File signature was verified successfully for $FilePath by $SignatureFilePath" return $true } else { Write-Host "Could not verify $FilePath signature by $SignatureFilePath" return $false } } } function Validate-VcEnvParams { param( [ValidateNotNullOrEmpty()] [string]$DatastoreName, [ValidateNotNullOrEmpty()] [string]$NetworkName, [ValidateNotNullOrEmpty()] [string]$ApplianceIp, [ValidateNotNullOrEmpty()] [string]$SubnetMask, [ValidateNotNullOrEmpty()] [string]$DefaultGateway, [ValidateNotNullOrEmpty()] [string]$DNS ) process { Write-Host "Starting $($MyInvocation.MyCommand)..." if (-not (Validate-DatastoreName -DatastoreName $DatastoreName)) { throw "Datastore '$DatastoreName' validation failed." } if (-not (Validate-NetworkName -NetworkName $NetworkName)) { throw "Network '$NetworkName' validation failed." } Validate-NetworkSettings -ApplianceIp $ApplianceIp -SubnetMask $SubnetMask -DefaultGateway $DefaultGateway -DNS $DNS } } function Validate-NetworkSettings { param ( [ValidateNotNullOrEmpty()] [string]$ApplianceIp, [ValidateNotNullOrEmpty()] [string]$SubnetMask, [ValidateNotNullOrEmpty()] [string]$DefaultGateway, [ValidateNotNullOrEmpty()] [string]$DNS ) Write-Host "Starting $($MyInvocation.MyCommand)..." # Validate the correct format of the IP addresses xxx.xxx.xxx.xxx try { $ipBytes = [System.Net.IPAddress]::Parse($ApplianceIp).GetAddressBytes() } catch { throw "The provided ZVMA IP '$ApplianceIp' format is invalid." } try { $maskBytes = [System.Net.IPAddress]::Parse($SubnetMask).GetAddressBytes() } catch { throw "The provided Subnet Mask '$SubnetMask' format is invalid." } try { $gwBytes = [System.Net.IPAddress]::Parse($DefaultGateway).GetAddressBytes() } catch { throw "The provided Default Gateway '$DefaultGateway' format is invalid." } try { [System.Net.IPAddress]::Parse($DNS).GetAddressBytes() | Out-Null } catch { throw "The provided DNS '$DNS' format is invalid." } # Validate the subnet mask, by ensuring contiguous 1s followed by 0s $binaryMask = -join ($maskBytes | ForEach-Object { [Convert]::ToString($_, 2).PadLeft(8, '0') }) $isContiguous = $binaryMask -match '^1+0+$' if (-not $isContiguous) { throw "The provided Subnet Mask '$SubnetMask' is invalid." } # Validate the IP and Gateway are in the same subnet, by ensuring the network parts of the IP and Gateway are the same for ($i = 0; $i -lt $maskBytes.Length; $i++) { if (($ipBytes[$i] -band $maskBytes[$i]) -ne ($gwBytes[$i] -band $maskBytes[$i])) { throw "The provided ZVMA IP '$ApplianceIp' and Default Gateway '$DefaultGateway' are not in the same subnet." } } } function Get-ValidatedHostName ($HostName, $NetworkName, $DatastoreName) { Write-Host "Starting $($MyInvocation.MyCommand)..." $allValidMatchingHostsNames = Select-ValidMatchingHostsNames -NetworkName $NetworkName -DatastoreName $DatastoreName if ($HostName) { if ($allValidMatchingHostsNames -contains $HostName) { Write-Host "Host provided by the user is valid: $HostName" return $HostName } else { throw "Host provided by the user is not valid or does not match the specified network and datastore: $HostName" } } else { $validHostName = $allValidMatchingHostsNames | Select-Object -First 1 Write-Host "No host provided by the user. A host selected automatically: $validHostName" return $validHostName } } function Select-ValidMatchingHostsNames ($NetworkName, $DatastoreName) { Write-Host "Starting $($MyInvocation.MyCommand)..." $datastore = Get-Datastore -Name $DatastoreName -ErrorAction SilentlyContinue # When $DatastoreName is wrong, Get-Datastore fails with 'Datastore was not found using the specified filter(s).' error. $network = Get-View -ViewType Network -Property Name -Filter @{'Name' = "^$NetworkName$" } # Without the ^ and $, the filter will match networks that contain the specified name, rather than exactly match it. $hosts = Get-VMHost | Where-Object { # Select hot state ($_.ConnectionState -eq "Connected" -and $_.PowerState -eq "PoweredOn") -and # Check if the host has access to the datastore ($null -ne $datastore) -and ($_.ExtensionData.Datastore -contains $datastore.ExtensionData.MoRef) -and # Check if the host has access to the network ($null -ne $network) -and ($_.ExtensionData.Network -contains $network.MoRef) } if (@($hosts).Count -eq 0) { throw "No powered-on hosts with access to both datastore '$DatastoreName' and network '$NetworkName' were found." } Write-Host "Total number of hosts with access to both datastore '$DatastoreName' and network '$NetworkName': $(@($hosts).Count)" $hostsNames = $hosts | Sort-Object -Property Name | Select-Object -ExpandProperty Name return $hostsNames } function Test-VmExists { param( [Parameter(Mandatory = $true, HelpMessage = "VM name pattern")] [string]$VmName ) process { Write-Host "Starting $($MyInvocation.MyCommand)..." $vm = Get-VM -Name $VmName -ErrorAction SilentlyContinue | Select-Object -First 1 if ($null -eq $vm) { Write-Host "'$VmName' VM does not exist" return $false } else { Write-Host "'$($vm.Name)' VM exists" return $true } } } function Validate-BiosUUID { param( [ValidateNotNullOrEmpty()] [string]$DatastoreName, [ValidateNotNullOrEmpty()] [string]$BiosUuid # The parameter expects <BIOS UUID without hyphens>_<Host name> format. Host BIOS UUID is in MOB -> Property Path: host.hardware.systemInfo.uuid" ) process { Write-Host "Starting $($MyInvocation.MyCommand)..." $Datastore = Get-Datastore -Name $DatastoreName | Select-Object -first 1 $TEMP_DRIVE = "TEMP_DRIVE" New-PSDrive -Name $TEMP_DRIVE -Location $Datastore -PSProvider VimDatastore -Root '/' | Out-Null $exists = Test-Path "$($TEMP_DRIVE):/zagentid/$BiosUuid" Remove-PSDrive -Name $TEMP_DRIVE | Out-Null if ($exists) { Write-Host "BiosUuid '$BiosUuid' exists. Validation successful." return $true } else { Write-Host "BiosUuid '$BiosUuid' does not exist. Validation failed." return $false } } } function Validate-DigitsOnly { param( [Parameter(Mandatory = $true, HelpMessage = "Input string to validate all the characters are numeric")] [string]$InputString ) process { Write-Host "Starting $($MyInvocation.MyCommand)..." if ($InputString -match "^\d+$") { Write-Host "InputString=$InputString contains digits only" return $true } Write-Error "Validation failed. InputString=$InputString contains non-numeric characters" return $false } } function Validate-DatastoreName { param( [ValidateNotNullOrEmpty()] [string]$DatastoreName ) process { Write-Host "Starting $($MyInvocation.MyCommand)..." $datastore = Get-Datastore -Name $DatastoreName -ErrorAction SilentlyContinue | Select-Object -first 1 if ($null -eq $datastore) { Write-Host "Datastore '$DatastoreName' does not exist. Validation failed." return $false } Write-Host "Datastore '$DatastoreName' exists. Validation successful." return $true } } function Validate-NetworkName { param( [ValidateNotNullOrEmpty()] [string]$NetworkName ) process { Write-Host "Starting $($MyInvocation.MyCommand)..." $network = Get-VirtualNetwork -Name $NetworkName -ErrorAction SilentlyContinue | Select-Object -first 1 if ($null -eq $network) { Write-Host "Network '$NetworkName' does not exist. Validation failed." return $false } Write-Host "Network '$NetworkName' exists. Validation successful." return $true } } function Validate-AvsParams { param ( [string]$TenantId, [string]$ClientId, [SecureString]$ClientSecret, [string]$SubscriptionId, [string]$ResourceGroupName, [string]$AvsCloudName ) Write-Host "Starting $($MyInvocation.MyCommand)" $authUri = "https://login.microsoftonline.com/$TenantId/oauth2/token" $authBody = @{ 'client_id' = $ClientId 'client_secret' = (ConvertFrom-SecureString -SecureString $ClientSecret -AsPlainText) 'grant_type' = "client_credentials" 'resource' = "https://management.core.windows.net/" } try { $authResponse = Invoke-RestMethod -Uri $authUri -Method Post -Body $authBody -ContentType "application/x-www-form-urlencoded" } catch { try { $errMsg = ($_ | ConvertFrom-Json).error_description Write-Host "Authentication error details: $errMsg" } catch { Write-Host "Authentication error details cannot be parsed." } throw "Authentication failed for Azure. Please check the values of the TenantID-ClientID-ClientSecret combination." } $token = $authResponse.access_token $subscriptionUri = "https://management.azure.com/subscriptions/$SubscriptionId/?api-version=2020-01-01" try { [void] (Invoke-RestMethod -Uri $subscriptionUri -Method Get -Headers @{ Authorization = "Bearer $token" }) } catch { try { $errMsg = ($_ | ConvertFrom-Json).error.message Write-Host "The subscription error details: $errMsg" } catch { Write-Host "The subscription error details cannot be parsed." } throw "The subscription '$SubscriptionId' was not found for the tenant '$TenantId'." } $avsUri = "https://management.azure.com/subscriptions/$SubscriptionId/resourceGroups/$ResourceGroupName/providers/Microsoft.AVS/privateClouds/$AvsCloudName/?api-version=2020-03-20" try { [void] (Invoke-RestMethod -Uri $avsUri -Method Get -Headers @{ Authorization = "Bearer $token" }) } catch { try { $errMsg = ($_ | ConvertFrom-Json).error.message Write-Host "The private cloud error details: $errMsg" } catch { Write-Host "The private cloud error details cannot be parsed." } throw "The private cloud '$AvsCloudName' was not found under resource group '$ResourceGroupName' for the tenant '$TenantId'. Or the client (Application) '$ClientId' does not have authorization to perform action on '$ResourceGroupName'." } Write-Host "AVS parameters are valid." } function Assert-ReconfigurationToken ($Token) { Write-Host "Starting $($MyInvocation.MyCommand)" try { $Url = "https://www.zerto.com/myzerto/wp-json/services/zerto/s3-ova-employee?key=" + $Token $response = Invoke-WebRequest -Uri $Url -ErrorAction Stop -TimeoutSec 1800 $content = $response.Content if ($content -ne '{"success":true}') { throw "Reconfiguration token is invalid." } Write-Host "Reconfiguration token is valid" } catch { throw "Reconfiguration token is invalid." } } |