Private/Logging.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 Logging function Protect-VcfCheckLogMessage { <# .SYNOPSIS Redacts known secret-bearing patterns from a message before it is logged or reported. .DESCRIPTION A denylist of known secret-bearing patterns (HTTP Authorization headers, Basic/Bearer auth schemes, JSON password/token fields). This is a defense-in-depth backstop, not the primary control—the primary control is that this module never builds a command string or log message containing a plaintext secret in the first place (guest-ops credentials are passed via -GuestCredential parameter binding, never embedded in ScriptText). Applied to every message Write-LogMessage writes, and to Exception text before it is placed in a VcfCheck.Result. .PARAMETER Message The raw message text. .OUTPUTS [String] the redacted message. #> [CmdletBinding()] [OutputType([String])] Param ( [Parameter(Mandatory = $true)] [AllowEmptyString()] [String]$Message ) $redacted = $Message $redacted = $redacted -replace '(?i)Authorization:\s*[^\r\n]+', 'Authorization: [REDACTED]' $redacted = $redacted -replace '(?i)Basic\s+[A-Za-z0-9+/=]{8,}', 'Basic [REDACTED]' $redacted = $redacted -replace '(?i)Bearer\s+[A-Za-z0-9\-._~+/]{8,}=*', 'Bearer [REDACTED]' $redacted = $redacted -replace '(?i)("(?:password|token|secret|pwd)"\s*:\s*)"[^"]*"', '$1"[REDACTED]"' $redacted = $redacted -replace '(?i)(-w\s+|--password[= ]|-GuestPassword\s+)\S+', '$1[REDACTED]' return $redacted } function Write-LogMessage { <# .SYNOPSIS Writes a timestamped, type-prefixed log message to console and/or log file. .DESCRIPTION Screen output is filtered by the configured log level threshold (set via Initialize-VcfCheckLogging). Only messages at or above the configured level are displayed on the console. All messages are always written to the log file regardless of level, so DEBUG context needed to diagnose a run is never silently discarded. When the orchestrator is executing a check, it sets $Script:VcfCheckCurrentCheckId to that check's ID, and every message logged during execution is tagged with "[<CheckId>]" so a given log line can be traced back to the check that produced it. .PARAMETER Type Message type: DEBUG, INFO, WARNING, ERROR. .PARAMETER Message The message text. Never pass a plaintext secret. .EXAMPLE Write-LogMessage -Type INFO -Message "Connected to SDDC Manager vcf01-sddcmgr01." .EXAMPLE Write-LogMessage -Type ERROR -Message "Check sddc_lock_table failed: $($_.Exception.Message)" #> [CmdletBinding()] Param ( [Parameter(Mandatory = $true)] [ValidateSet('DEBUG', 'INFO', 'WARNING', 'ERROR')] [String]$Type, [Parameter(Mandatory = $true)] [ValidateNotNullOrEmpty()] [String]$Message, [Parameter(Mandatory = $false)] [Switch]$NoNewline ) $levelOrder = @{ 'DEBUG' = 0; 'INFO' = 1; 'WARNING' = 2; 'ERROR' = 3 } $configuredLevel = if ($Script:VcfCheckLogLevel) { $Script:VcfCheckLogLevel } else { 'INFO' } $aboveScreenThreshold = $levelOrder[$Type] -ge $levelOrder[$configuredLevel] $sanitizedMessage = Protect-VcfCheckLogMessage -Message $Message $timestamp = (Get-Date).ToString('yyyy-MM-dd HH:mm:ss.fff') $checkTag = if ($Script:VcfCheckCurrentCheckId) { " [$($Script:VcfCheckCurrentCheckId)]" } else { '' } $formattedMessage = "[$timestamp] [$Type]$checkTag $sanitizedMessage" if ($aboveScreenThreshold) { switch ($Type) { 'DEBUG' { Write-Host $formattedMessage -ForegroundColor Gray -NoNewline:$NoNewline } 'INFO' { Write-Host $formattedMessage -ForegroundColor White -NoNewline:$NoNewline } 'WARNING' { Write-Host $formattedMessage -ForegroundColor Yellow -NoNewline:$NoNewline } 'ERROR' { Write-Host $formattedMessage -ForegroundColor Red -NoNewline:$NoNewline } } } if ($Script:VcfCheckLogFilePath) { try { $fileExists = Test-Path -LiteralPath $Script:VcfCheckLogFilePath Add-Content -LiteralPath $Script:VcfCheckLogFilePath -Value $formattedMessage -ErrorAction Stop if (-not $fileExists -and $PSVersionTable.Platform -ne 'Win32NT') { & chmod 600 $Script:VcfCheckLogFilePath 2>$null } } catch { Write-Host "Warning: Could not write to log file: $($_.Exception.Message)" -ForegroundColor Yellow } } } function Get-VcfCheckLogDirectory { <# .SYNOPSIS Returns the directory where VcfCheck logs are written. .DESCRIPTION Returns the configured log directory if logging has been initialized, otherwise the default Logs/ directory relative to the module installation directory. Useful for the bundled Python report server, which tails the same log directory. .OUTPUTS [String] Fully qualified path to the log directory. #> [CmdletBinding()] [OutputType([String])] Param () if ($Script:VcfCheckLogDirectory) { return $Script:VcfCheckLogDirectory } return Join-Path -Path (Split-Path -Parent $PSScriptRoot) -ChildPath 'Logs' } function Initialize-VcfCheckLogging { <# .SYNOPSIS Initializes logging for a VcfCheck run. .DESCRIPTION Resolves the log directory (explicit param > $env:VcfCheckBaseDirectory\Logs), creates it with owner-only permissions if missing, and opens a new dated log file. All severities are always written to the file; only messages at or above LogLevel are echoed to the console. .PARAMETER LogDirectory Absolute or relative path to the log directory. When omitted, resolved from $env:VcfCheckBaseDirectory. Throws if neither is available. .PARAMETER LogLevel Minimum log level to display on console: DEBUG, INFO, WARNING, ERROR. Default INFO. .OUTPUTS [String] Absolute path to the active log file. #> [CmdletBinding()] [OutputType([String])] Param ( [Parameter(Mandatory = $false)] [AllowEmptyString()] [String]$LogDirectory = '', [Parameter(Mandatory = $false)] [ValidateSet('DEBUG', 'INFO', 'WARNING', 'ERROR')] [String]$LogLevel = 'INFO' ) $Script:VcfCheckLogLevel = $LogLevel if ([String]::IsNullOrWhiteSpace($LogDirectory)) { if ([String]::IsNullOrWhiteSpace($env:VcfCheckBaseDirectory)) { throw [System.InvalidOperationException]::new( "`$env:$($Script:VCF_CHECK_ENV_VAR) is not set. Run Initialize-VcfCheck before starting a precheck run, or pass -LogDirectory explicitly." ) } $Script:VcfCheckLogDirectory = Join-Path -Path $env:VcfCheckBaseDirectory.Trim() -ChildPath $Script:CHECK_LOGS_DIR_NAME } elseif ([System.IO.Path]::IsPathRooted($LogDirectory)) { $Script:VcfCheckLogDirectory = $LogDirectory } else { $Script:VcfCheckLogDirectory = Join-Path -Path (Split-Path -Parent $PSScriptRoot) -ChildPath $LogDirectory } if (-not (Test-Path -LiteralPath $Script:VcfCheckLogDirectory -PathType Container)) { $null = New-Item -ItemType Directory -Path $Script:VcfCheckLogDirectory -Force if ($PSVersionTable.Platform -ne 'Win32NT') { & chmod 700 $Script:VcfCheckLogDirectory 2>$null } } # Naming uses a PascalCase prefix, ISO-like hyphenated date, one file per calendar day, # and no per-run ID in the name. $fileTimeStamp = Get-Date -Format 'yyyy-MM-dd' $Script:VcfCheckLogFilePath = Join-Path -Path $Script:VcfCheckLogDirectory -ChildPath "VcfCheckEngine-$fileTimeStamp.log" if (-not (Test-Path -LiteralPath $Script:VcfCheckLogFilePath)) { New-Item -ItemType File -Path $Script:VcfCheckLogFilePath -Force | Out-Null } return $Script:VcfCheckLogFilePath } function Write-VcfCheckRuntimeInfo { <# .SYNOPSIS Logs PowerShell, PowerCLI, VcfCheck module, and environment information to the log. .DESCRIPTION Writes a single INFO log line with PowerShell version, VMware PowerCLI version, VcfCheck module version, Python version, and OS. Called once per precheck run immediately after Initialize-VcfCheckLogging. Detects Python version from the environment variable if set, otherwise attempts to query python3/python executable. Non-fatal — continues even if detection fails. .EXAMPLE Initialize-VcfCheckLogging -LogDirectory $LogDirectory | Out-Null Write-VcfCheckRuntimeInfo #> [CmdletBinding()] Param () try { $pcliMod = Get-Module -Name 'VCF.PowerCLI' -ListAvailable -ErrorAction SilentlyContinue | Sort-Object { [Version]$_.Version } -Descending | Select-Object -First 1 $checkMod = Get-Module -Name 'VcfCheck' -ErrorAction SilentlyContinue $pcliVer = if ($pcliMod) { $pcliMod.Version.ToString() } else { 'not loaded' } $checkVer = if ($checkMod) { $checkMod.Version.ToString() } else { 'unknown' } # Detect Python version from environment variable or by querying executable $pyVer = 'unknown' if ($env:VCF_CHECK_PYTHON_VERSION) { $pyVer = $env:VCF_CHECK_PYTHON_VERSION } else { try { # Try python3 first, then python - suppress errors if not found $pythonExe = @('python3', 'python') | Where-Object { $null -ne (Get-Command $_ -ErrorAction SilentlyContinue) } | Select-Object -First 1 if ($pythonExe) { $pyVersionOutput = & $pythonExe --version 2>&1 if ($pyVersionOutput -match '(\d+\.\d+(?:\.\d+)?)') { $pyVer = $matches[1] } } } catch { # Silently ignore Python detection errors - logging still succeeds with 'unknown' } } Write-LogMessage -Type INFO -Message "Runtime: PowerShell=$($PSVersionTable.PSVersion) | VCF.PowerCLI=$pcliVer | VcfCheck=v$checkVer | Python=$pyVer | OS=$($PSVersionTable.OS)" } catch { # Non-fatal: still log even if something fails above Write-LogMessage -Type INFO -Message "Runtime: PowerShell=$($PSVersionTable.PSVersion) | VCF.PowerCLI=unknown | VcfCheck=unknown | Python=unknown | OS=$($PSVersionTable.OS)" } } #endregion Logging |