Tools/Invoke-VCFPatchScanner.ps1
|
<# .SYNOPSIS Command-line entry point for the VcfPatchScanner module, invoked by Start-VCFPatchScannerServer.py. .DESCRIPTION Accepts environment and credential parameters from the Python web UI server, initializes the VcfPatchScanner module, and dispatches to one of four operating modes: - Discovery (DiscoverSddcManagers / DiscoverFleetManager): outputs JSON to stdout, exits. - Credential validation (ValidateCredentialsOnly): outputs JSON to stdout, exits. - Full vulnerability scan: runs Invoke-VCFPatchScanner, writes findings JSON, exits. Credentials are never passed as CLI arguments. They are read from environment variables set by the Python server from the allowlist-filtered subprocess environment: SDDC_MANAGER_PASSWORD, VCF_OPS_PASSWORD, VCF_FM_PASSWORD, VCENTER_PASSWORD, NSX_MANAGER_PASSWORD (vsphere8/vvf9 only — vcf5 retrieves it via SDDC Manager API), VRSLCM_PASSWORD. .PARAMETER VcfMajorVersion Environment type. One of: vcf5, vcf9, vsphere8, vvf9. .PARAMETER LogLevel PowerShell log level forwarded to Initialize-PatchScanLogging. Default: INFO. .PARAMETER LogDirectory Absolute path to the log directory. When empty the module uses its default. .PARAMETER SecurityAdvisoryFile Path to the security advisory reference JSON. Default: Data/securityAdvisory.json. .PARAMETER FindingsOutputPath Absolute path where the findings JSON file is written by the scan. .PARAMETER ConnectionTimeoutSeconds Per-endpoint connection timeout in seconds. Range 1-900. Default: 30. .PARAMETER DiscoverFleetManager When set, discovers the Fleet Manager FQDN from VCF Operations and exits. .PARAMETER DiscoverVrslcm When set, queries SDDC Manager GET /v1/vrslcms for a registered vRSLCM instance and outputs JSON to stdout, then exits. VCF 5.x only. .PARAMETER VcfOpsVersion VCF Operations version string (e.g. "VCF Operations 9.1.0.0"). When provided, the Fleet Manager discovery selects the version-appropriate API: 9.1+ uses the Suite API internal components endpoint; 9.0 uses the CASA capabilities endpoint. .PARAMETER FetchSddcCredential When set, retrieves the SDDC Manager username and password from the Fleet Manager locker (VCF 9.0 LCops Fleet Manager only) and outputs JSON to stdout, then exits. .PARAMETER DiscoverSddcManagers When set, discovers SDDC Manager FQDNs from VCF Operations and exits. .PARAMETER EnvironmentDisplayName Human-readable label for this environment. ANSI escape codes and control characters are stripped before use to prevent log injection. .PARAMETER FailedEndpointFqdns JSON array string of FQDNs to re-inventory (retry-failed-only mode). Only valid RFC 1123 hostnames are accepted; invalid entries are silently dropped. .PARAMETER IgnoreInvalidCertificate When set, TLS certificate validation is skipped for all endpoint connections. .PARAMETER RetryFailedEndpointsOnly When set with FailedEndpointFqdns, restricts inventory to the listed endpoints. .PARAMETER ValidateCredentialsOnly When set, tests credentials for all configured endpoints and outputs JSON, then exits. .PARAMETER SddcManagerInstanceName Human-readable VCF 9 instance name (e.g. "San Francisco") discovered from VCF Operations. Stamped onto all VCF 9 inventory items so findings can be grouped by instance. .PARAMETER SddcManagerServer SDDC Manager FQDN or IP. Required for vcf5 and vcf9. .PARAMETER SddcManagerUser SDDC Manager username. Required for vcf5 and vcf9. .PARAMETER VrslcmServer vRealize Suite Lifecycle Manager FQDN. Optional for vcf5. .PARAMETER VrslcmUser vRealize Suite Lifecycle Manager username. Optional for vcf5. .PARAMETER VcfOpsServer VCF Operations FQDN. Required for vcf9. .PARAMETER VcfOpsUser VCF Operations username. Required for vcf9. .PARAMETER VcfFMServer VCF Fleet Manager / Fleet Lifecycle Manager FQDN. Required for vcf9. .PARAMETER VcfFMUser VCF Fleet Manager / Fleet Lifecycle Manager username. Required for vcf9. .PARAMETER VcfMinorVersion VCF minor version string (e.g. "9.1"). Optional; used to label Fleet Manager endpoints correctly in validation results when the auth path cannot determine the version automatically. .PARAMETER VcenterServer vCenter Server FQDN. Required for vsphere8 and vvf9. .PARAMETER VcenterUser vCenter username. Required for vsphere8 and vvf9. .PARAMETER NsxManagerServer NSX Manager FQDN. Required for vvf9; optional for vsphere8. .PARAMETER NsxManagerUser NSX Manager username. Required when NsxManagerServer is configured. .EXAMPLE pwsh -NonInteractive -File Invoke-VCFPatchScanner.ps1 -VcfMajorVersion vcf9 -SddcManagerServer sddc.corp.local -SddcManagerUser administrator@vsphere.local .EXAMPLE pwsh -NonInteractive -File Invoke-VCFPatchScanner.ps1 -DiscoverSddcManagers -VcfOpsServer ops.corp.local -VcfOpsUser admin@local -LogLevel WARNING .NOTES This script is the contract boundary between Start-VCFPatchScannerServer.py and the VcfPatchScanner PowerShell module. Changes to parameter names must be reflected in the Python server's _ENV_TYPE_FIELDS and _build_ps_args dictionaries. #> [CmdletBinding()] Param ( [Parameter(Mandatory = $false)] [ValidateRange(1, 900)] [Int]$ConnectionTimeoutSeconds = 30, [Parameter(Mandatory = $false)] [Switch]$DiscoverFleetManager, [Parameter(Mandatory = $false)] [Switch]$DiscoverSddcManagers, [Parameter(Mandatory = $false)] [Switch]$DiscoverVrslcm, [Parameter(Mandatory = $false)] [ValidateNotNullOrEmpty()] [String]$EnvironmentDisplayName, [Parameter(Mandatory = $false)] [ValidateNotNullOrEmpty()] [String]$FailedEndpointFqdns, [Parameter(Mandatory = $false)] [Switch]$FetchSddcCredential, [Parameter(Mandatory = $false)] [ValidateNotNullOrEmpty()] [String]$FindingsOutputPath, [Parameter(Mandatory = $false)] [Switch]$IgnoreInvalidCertificate, [Parameter(Mandatory = $false)] [AllowEmptyString()] [ValidateNotNull()] [String]$LogDirectory = '', [Parameter(Mandatory = $false)] [ValidateNotNullOrEmpty()] [String]$LogLevel = 'INFO', [Parameter(Mandatory = $false)] [ValidateNotNullOrEmpty()] [String]$NsxManagerServer, [Parameter(Mandatory = $false)] [ValidateNotNullOrEmpty()] [String]$NsxManagerUser, [Parameter(Mandatory = $false)] [Switch]$RetryFailedEndpointsOnly, [Parameter(Mandatory = $false)] [AllowEmptyString()] [ValidateNotNull()] [String]$SddcManagerInstanceName = '', [Parameter(Mandatory = $false)] [ValidateNotNullOrEmpty()] [String]$SddcManagerServer, [Parameter(Mandatory = $false)] [ValidateNotNullOrEmpty()] [String]$SddcManagerUser, [Parameter(Mandatory = $false)] [ValidateNotNullOrEmpty()] [String]$SecurityAdvisoryFile = 'Data/securityAdvisory.json', [Parameter(Mandatory = $false)] [Switch]$ValidateCredentialsOnly, [Parameter(Mandatory = $false)] [ValidateNotNullOrEmpty()] [String]$VcenterBuildMapFile, [Parameter(Mandatory = $false)] [ValidateNotNullOrEmpty()] [String]$VcenterServer, [Parameter(Mandatory = $false)] [ValidateNotNullOrEmpty()] [String]$VcenterUser, [Parameter(Mandatory = $false)] [ValidateNotNullOrEmpty()] [String]$VcfFMServer, [Parameter(Mandatory = $false)] [ValidateNotNullOrEmpty()] [String]$VcfFMUser, [Parameter(Mandatory = $false)] [ValidateSet('vcf5', 'vcf9', 'vsphere8', 'vvf9')] [String]$VcfMajorVersion, [Parameter(Mandatory = $false)] [ValidateNotNullOrEmpty()] [String]$VcfMinorVersion, [Parameter(Mandatory = $false)] [ValidateNotNullOrEmpty()] [String]$VcfOpsServer, [Parameter(Mandatory = $false)] [ValidateNotNullOrEmpty()] [String]$VcfOpsUser, [Parameter(Mandatory = $false)] [ValidateNotNullOrEmpty()] [String]$VcfOpsVersion, [Parameter(Mandatory = $false)] [ValidateNotNullOrEmpty()] [String]$VrslcmServer, [Parameter(Mandatory = $false)] [ValidateNotNullOrEmpty()] [String]$VrslcmUser ) # Suppress PSStyle ANSI escape codes so stderr captured by the server is plain text. # $PSStyle is available in PowerShell 7.2+; the guard makes this a no-op on older versions. if ($null -ne $PSStyle) { $PSStyle.OutputRendering = 'PlainText' } # Validate required environment variable before importing the module — fail fast with a # clear setup message rather than allowing the module to silently write to a null location. if ([String]::IsNullOrWhiteSpace($env:VcfPatchScannerBaseDirectory)) { Write-Host "ERROR: VcfPatchScannerBaseDirectory is not set. Run Initialize-VcfPatchScanner before using the scanner." -ForegroundColor Red exit 1 } if (-not (Test-Path -LiteralPath $env:VcfPatchScannerBaseDirectory.Trim() -PathType Container)) { Write-Host "ERROR: VcfPatchScannerBaseDirectory points to a path that does not exist: '$($env:VcfPatchScannerBaseDirectory.Trim())'. Re-run Initialize-VcfPatchScanner." -ForegroundColor Red exit 1 } # Import the module. # Prefer the path injected by Start-VCFPatchScannerServer.py (VCFPATCHSCANNER_MODULE_PSD1), which is # always correct regardless of where the Tools directory is deployed. Fall back to the path # relative to this script for direct invocation (e.g. development or testing). $envModulePsd1 = ([String]$env:VCFPATCHSCANNER_MODULE_PSD1).Trim() if (-not [String]::IsNullOrWhiteSpace($envModulePsd1) -and (Test-Path -LiteralPath $envModulePsd1 -PathType Leaf)) { $modulePath = $envModulePsd1 } else { $modulePath = Join-Path -Path (Split-Path -Path $PSScriptRoot -Parent) -ChildPath 'VcfPatchScanner.psd1' } try { Import-Module -Name $modulePath -Force -ErrorAction Stop } catch { if ($ValidateCredentialsOnly -or $DiscoverSddcManagers -or $DiscoverFleetManager -or $DiscoverVrslcm -or $FetchSddcCredential) { # Write-Output (stdout) rather than Write-Host (information stream) so the Python # server can read this message; stdout=PIPE does not capture Write-Host output. @{ instances = @(); opsVersion = ""; vcenterFqdns = @(); error = "The server could not load its module. Run Initialize-VcfPatchScanner and restart the server. (Detail: $($_.Exception.Message))" } | ConvertTo-Json -Compress exit 1 } throw } # Guard against an older module version that pre-dates functions required by this script. # When VCFPATCHSCANNER_MODULE_PSD1 points to a stale PSModulePath installation the module # loads successfully (no Import-Module error) but the function is absent — producing an # opaque CommandNotFoundException in the UI. Detect the mismatch here and emit a clear, # actionable JSON error instead. $requiredDiscoveryFunctions = @( 'Get-SddcManagerListFromVcfOps' 'Get-FleetManagerFromVcfOps' 'Get-SddcCredentialFromFleetManager' 'Initialize-PatchScanLogging' ) $missingFunctions = $requiredDiscoveryFunctions | Where-Object { -not (Get-Command -Name $_ -ErrorAction SilentlyContinue) } if ($missingFunctions) { $missingList = $missingFunctions -join ', ' $outOfDateMsg = "The server installation is outdated. Please run Initialize-VcfPatchScanner to update, then restart the server." if ($ValidateCredentialsOnly -or $DiscoverSddcManagers -or $DiscoverFleetManager -or $DiscoverVrslcm -or $FetchSddcCredential) { @{ instances = @(); opsVersion = ""; vcenterFqdns = @(); error = $outOfDateMsg } | ConvertTo-Json -Compress exit 1 } throw [System.InvalidOperationException]::new($outOfDateMsg) } # Strip ANSI CSI sequences, OSC sequences, null bytes, carriage returns, and JSON-unsafe # characters from the display name before it reaches log lines, findings JSON, or filenames. if ([String]::IsNullOrWhiteSpace($EnvironmentDisplayName)) { $sanitizedDisplayName = "Scan-$(Get-Date -Format 'yyyyMMdd_HHmmss')" } else { $sanitizedDisplayName = $EnvironmentDisplayName ` -replace '\x1b\[[0-9;]*[a-zA-Z]', '' ` -replace '\x1b\][^\x07\x1b]*(\x07|\x1b\\)', '' ` -replace '[\x00\r]', '' ` -replace '[\\"]', '' $sanitizedDisplayName = $sanitizedDisplayName.Trim() if ([String]::IsNullOrWhiteSpace($sanitizedDisplayName)) { $sanitizedDisplayName = "Scan-$(Get-Date -Format 'yyyyMMdd_HHmmss')" } } # Discovery-only switches exit before anything needs $envConfig, so skip building it. # ValidateCredentialsOnly and scan paths always receive -VcfMajorVersion from the Python server. $isDiscoveryOnly = $DiscoverSddcManagers -or $DiscoverFleetManager -or $DiscoverVrslcm -or $FetchSddcCredential if (-not $isDiscoveryOnly) { if ([String]::IsNullOrWhiteSpace($VcfMajorVersion)) { Write-Host "ERROR: -VcfMajorVersion is required for scan and credential-validation operations. Valid values: vcf5, vcf9, vsphere8, vvf9." -ForegroundColor Red exit 1 } $configParams = @{ Name = $sanitizedDisplayName Type = $VcfMajorVersion } # Add VCF 5/9 parameters if (-not [String]::IsNullOrWhiteSpace($SddcManagerInstanceName)) { $configParams['SddcManagerInstanceName'] = $SddcManagerInstanceName } if ($SddcManagerServer) { $configParams['SddcManagerServer'] = $SddcManagerServer } if ($SddcManagerUser) { $configParams['SddcManagerUser'] = $SddcManagerUser } # Add VCF 5.x optional parameters if ($VrslcmServer) { $configParams['VrslcmServer'] = $VrslcmServer } if ($VrslcmUser) { $configParams['VrslcmUser'] = $VrslcmUser } # Add VCF 9 parameters if ($VcfOpsServer) { $configParams['VcfOpsServer'] = $VcfOpsServer } if ($VcfOpsUser) { $configParams['VcfOpsUser'] = $VcfOpsUser } if ($VcfFMServer) { $configParams['VcfFMServer'] = $VcfFMServer } if ($VcfFMUser) { $configParams['VcfFMUser'] = $VcfFMUser } # Add vSphere/VVF parameters if ($VcenterServer) { $configParams['VcenterServer'] = $VcenterServer } if ($VcenterUser) { $configParams['VcenterUser'] = $VcenterUser } if ($NsxManagerServer) { $configParams['NsxManagerServer'] = $NsxManagerServer } if ($NsxManagerUser) { $configParams['NsxManagerUser'] = $NsxManagerUser } try { $envConfig = New-PatchScanEnvironment @configParams } catch { # A ParameterBindingException here means Invoke-VCFPatchScanner.ps1 passed a parameter # that New-PatchScanEnvironment does not declare — a code defect, not a user error. Write-Host "ERROR: Environment configuration failed: $($_.Exception.Message)" -ForegroundColor Red exit 1 } } # Discover SDDC Manager FQDNs via VCF Operations and output JSON to stdout. # $InformationPreference silences Write-Host (which Write-LogMessage uses) so that # stdout contains only the JSON result line. Diagnostics still go to the log file. if ($DiscoverSddcManagers) { $InformationPreference = 'SilentlyContinue' Initialize-PatchScanLogging -LogLevel $LogLevel -LogDirectory $LogDirectory | Out-Null try { $discoveryResult = Get-SddcManagerListFromVcfOps -VcfOpsServer $VcfOpsServer -VcfOpsUser $VcfOpsUser -TimeoutSeconds $ConnectionTimeoutSeconds $instanceList = @($discoveryResult.Instances | ForEach-Object { @{ fqdn = $_.Fqdn; instanceName = $_.InstanceName; sddcUsername = $_.SddcUsername } }) @{ instances = $instanceList opsVersion = $discoveryResult.OpsVersion vcenterFqdns = @($discoveryResult.VcenterFqdns) error = $null } | ConvertTo-Json -Compress -Depth 3 exit 0 } catch { @{ instances = @(); opsVersion = ""; vcenterFqdns = @(); error = $_.Exception.Message } | ConvertTo-Json -Compress exit 1 } } # Retrieve SDDC Manager credential from the Fleet Manager locker (VCF 9.0 only). if ($FetchSddcCredential) { $InformationPreference = 'SilentlyContinue' Initialize-PatchScanLogging -LogLevel $LogLevel -LogDirectory $LogDirectory | Out-Null try { $result = Get-SddcCredentialFromFleetManager -FmServer $VcfFMServer -TimeoutSeconds $ConnectionTimeoutSeconds @{ sddcUsername = $result.SddcUsername; sddcPassword = $result.SddcPassword; error = $null } | ConvertTo-Json -Compress exit 0 } catch { @{ sddcUsername = $null; sddcPassword = $null; error = $_.Exception.Message } | ConvertTo-Json -Compress exit 1 } } # Discover Fleet Manager FQDN from VCF Operations 9.1 and output JSON to stdout. # Available on VCF Operations 9.1+; returns an error for 9.0 (no VSP component registered). if ($DiscoverFleetManager) { $InformationPreference = 'SilentlyContinue' Initialize-PatchScanLogging -LogLevel $LogLevel -LogDirectory $LogDirectory | Out-Null try { $fmResult = Get-FleetManagerFromVcfOps -VcfOpsServer $VcfOpsServer -VcfOpsUser $VcfOpsUser ` -TimeoutSeconds $ConnectionTimeoutSeconds ` -VcfOpsVersion ($VcfOpsVersion ?? '') @{ fleetFqdn = $fmResult.FleetFqdn; vcfFMUser = $fmResult.VcfFMUser; error = $null } | ConvertTo-Json -Compress exit 0 } catch { @{ fleetFqdn = $null; vcfFMUser = $null; error = $_.Exception.Message } | ConvertTo-Json -Compress exit 1 } } # Discover vRSLCM FQDN registered with SDDC Manager (VCF 5.x only) and output JSON to stdout. if ($DiscoverVrslcm) { $InformationPreference = 'SilentlyContinue' Initialize-PatchScanLogging -LogLevel $LogLevel -LogDirectory $LogDirectory | Out-Null try { $vrslcmResult = Get-VrslcmFromSddcManager -Server $SddcManagerServer -User $SddcManagerUser ` -TimeoutSeconds $ConnectionTimeoutSeconds @{ vrslcmFqdn = $vrslcmResult.VrslcmFqdn vrslcmVersion = $vrslcmResult.VrslcmVersion error = $vrslcmResult.Error } | ConvertTo-Json -Compress exit 0 } catch { @{ vrslcmFqdn = $null; vrslcmVersion = ""; error = $_.Exception.Message } | ConvertTo-Json -Compress exit 1 } } # Validate credentials for all configured endpoints and exit with 0 (success) or 1 (failure). # Suppress Write-Host output so stdout carries only the JSON result line (same pattern as DiscoverSddcManagers). if ($ValidateCredentialsOnly) { $InformationPreference = 'SilentlyContinue' Initialize-PatchScanLogging -LogLevel $LogLevel -LogDirectory $LogDirectory | Out-Null Write-LogMessage -Type INFO -Message "Running in validation-only mode" try { $connParams = @{ EnvironmentType = $VcfMajorVersion; TimeoutSeconds = $ConnectionTimeoutSeconds } if ($SddcManagerServer) { $connParams['SddcManagerServer'] = $SddcManagerServer } if ($SddcManagerUser) { $connParams['SddcManagerUser'] = $SddcManagerUser } if ($VrslcmServer) { $connParams['VrslcmServer'] = $VrslcmServer } if ($VrslcmUser) { $connParams['VrslcmUser'] = $VrslcmUser } if ($VcfOpsServer) { $connParams['VcfOpsServer'] = $VcfOpsServer } if ($VcfOpsUser) { $connParams['VcfOpsUser'] = $VcfOpsUser } if ($VcfFMServer) { $connParams['VcfFMServer'] = $VcfFMServer } if ($VcfFMUser) { $connParams['VcfFMUser'] = $VcfFMUser } if ($VcfMinorVersion) { $connParams['VcfMinorVersion'] = $VcfMinorVersion } if ($VcenterServer) { $connParams['VcenterServer'] = $VcenterServer } if ($VcenterUser) { $connParams['VcenterUser'] = $VcenterUser } if ($NsxManagerServer) { $connParams['NsxManagerServer'] = $NsxManagerServer } if ($NsxManagerUser) { $connParams['NsxManagerUser'] = $NsxManagerUser } $connResult = Test-PatchScanConnection @connParams $connResult | ConvertTo-Json -Depth 3 -Compress exit ([Int](-not $connResult.Success)) } catch { Write-LogMessage -Type ERROR -Message "Validation error: $($_.Exception.Message)" Write-LogMessage -Type DEBUG -Message "Exception type: $($_.Exception.GetType().FullName)" exit 1 } } # Run the vulnerability scan Initialize-PatchScanLogging -LogLevel $LogLevel -LogDirectory $LogDirectory | Out-Null $scanParams = @{ AdvisoryPath = $SecurityAdvisoryFile EnvironmentConfig = [PSCustomObject]$envConfig EnvironmentType = $VcfMajorVersion TimeoutSeconds = $ConnectionTimeoutSeconds UseLiveInventory = $true } if (-not [String]::IsNullOrWhiteSpace($VcenterBuildMapFile)) { $scanParams['VcenterBuildMapFile'] = $VcenterBuildMapFile } # Use provided findings path or default if (-not [String]::IsNullOrWhiteSpace($FindingsOutputPath)) { $scanParams['FindingsOutputPath'] = $FindingsOutputPath } # Retry-failed-only: restrict inventory to the previously failed FQDNs. if ($RetryFailedEndpointsOnly -and -not [String]::IsNullOrWhiteSpace($FailedEndpointFqdns)) { try { $parsedFqdns = @($FailedEndpointFqdns | ConvertFrom-Json) # Accept only valid RFC 1123 hostnames to prevent an adversary-crafted findings # file from injecting arbitrary connection targets into the retry list. $fqdnPattern = '^[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])?)*$' $fqdnArray = @($parsedFqdns | Where-Object { $_ -match $fqdnPattern }) if ($fqdnArray.Count -gt 0) { $scanParams['IncludeOnlyFqdns'] = [String[]]$fqdnArray Write-LogMessage -Type INFO -Message "Retry-failed-only mode: scanning $($fqdnArray.Count) endpoint(s): $($fqdnArray -join ', ')" } } catch { Write-LogMessage -Type WARNING -Message "Could not parse FailedEndpointFqdns JSON; running full scan instead: $($_.Exception.Message)" } } $result = Invoke-VCFPatchScanner @scanParams # Exit with the result code exit $result.ExitCode |