Src/Private/Helpers.ps1
|
function Test-UserPrincipalName { <# .SYNOPSIS Validates that a string is a properly formatted User Principal Name. #> [CmdletBinding()] [OutputType([bool])] param ( [Parameter(Mandatory)] [string]$UserPrincipalName ) return ($UserPrincipalName -match '^[^@\s]+@[^@\s]+\.[^@\s]+$') } function Write-TranscriptLog { <# .SYNOPSIS Writes a structured log entry to the host and optionally to a transcript file. Level values: DEBUG | INFO | SUCCESS | WARNING | ERROR #> [CmdletBinding()] param ( [Parameter(Mandatory, Position = 0)] [string]$Message, [Parameter(Position = 1)] [ValidateSet('DEBUG', 'INFO', 'SUCCESS', 'WARNING', 'ERROR')] [string]$Level = 'INFO', [Parameter(Position = 2)] [string]$Category = 'GENERAL' ) $Timestamp = Get-Date -Format 'yyyy-MM-dd HH:mm:ss' $Entry = "[$Timestamp] [$Level] [$Category] $Message" $null = switch ($Level) { 'DEBUG' { Write-PScriboMessage -Message $Entry } 'INFO' { Write-Host " $Entry" } 'SUCCESS' { Write-Host " $Entry" -ForegroundColor Green } 'WARNING' { Write-PScriboMessage -IsWarning -Message $Entry } 'ERROR' { Write-Host " $Entry" -ForegroundColor Red } } if ($script:TranscriptLogPath) { try { Add-Content -Path $script:TranscriptLogPath -Value $Entry -ErrorAction SilentlyContinue | Out-Null } catch { } } } function Invoke-WithRetry { <# .SYNOPSIS Executes a ScriptBlock with configurable retry logic and exponential back-off. #> [CmdletBinding()] param ( [Parameter(Mandatory)] [scriptblock]$ScriptBlock, [Parameter(Mandatory)] [string]$OperationName, [int]$MaxAttempts = 3, [int]$DelaySeconds = 5 ) $Attempt = 0 $CurrentDelay = $DelaySeconds while ($Attempt -lt $MaxAttempts) { $Attempt++ try { Write-TranscriptLog "Attempt ${Attempt}/${MaxAttempts}: $OperationName" 'DEBUG' 'RETRY' $null = (& $ScriptBlock) Write-TranscriptLog "Attempt ${Attempt}/${MaxAttempts} succeeded: $OperationName" 'DEBUG' 'RETRY' return } catch { if ($Attempt -lt $MaxAttempts) { Write-TranscriptLog "Attempt ${Attempt}/${MaxAttempts} failed for '$OperationName'. Retrying in ${CurrentDelay}s... Error: $($_.Exception.Message)" 'WARNING' 'RETRY' Start-Sleep -Seconds $CurrentDelay $CurrentDelay = $CurrentDelay * 2 } else { Write-TranscriptLog "All ${MaxAttempts} attempts failed for '$OperationName'. Error: $($_.Exception.Message)" 'ERROR' 'RETRY' throw } } } } function Show-AbrDebugExecutionTime { <# .SYNOPSIS Tracks and displays execution time for report sections. #> [CmdletBinding()] param ( [switch]$Start, [switch]$End, [string]$TitleMessage ) if ($Start) { $script:SectionTimers[$TitleMessage] = [System.Diagnostics.Stopwatch]::StartNew() Write-TranscriptLog "Starting section: $TitleMessage" 'DEBUG' 'TIMER' } if ($End) { if ($script:SectionTimers -and $script:SectionTimers.ContainsKey($TitleMessage)) { $script:SectionTimers[$TitleMessage].Stop() $Elapsed = $script:SectionTimers[$TitleMessage].Elapsed.TotalSeconds Write-TranscriptLog "Completed section: $TitleMessage (${Elapsed}s)" 'DEBUG' 'TIMER' $null = $script:SectionTimers.Remove($TitleMessage) } } } function ConvertTo-HashToYN { <# .SYNOPSIS Converts boolean values in a hashtable/ordered-dictionary to Yes/No strings. #> [CmdletBinding()] param ( [Parameter(Mandatory, ValueFromPipeline)] [System.Collections.IDictionary]$Hash ) process { $Result = [ordered]@{} foreach ($Key in $Hash.Keys) { $Value = $Hash[$Key] if ($Value -is [bool]) { $Result[$Key] = if ($Value) { 'Yes' } else { 'No' } } elseif ($null -eq $Value -or $Value -eq '') { $Result[$Key] = '--' } else { $Result[$Key] = $Value } } return $Result } } function Export-ExoToExcel { <# .SYNOPSIS Exports one or more arrays of PSCustomObjects to a multi-sheet Excel workbook. .PARAMETER Sheets Ordered hashtable of SheetName => array of PSCustomObjects. .PARAMETER Path Full file path for the output .xlsx file. .PARAMETER TenantId Tenant name used in the title row of each sheet. #> [CmdletBinding()] param ( [Parameter(Mandatory)] [System.Collections.Specialized.OrderedDictionary]$Sheets, [Parameter(Mandatory)] [string]$Path, [string]$TenantId = 'Unknown Tenant' ) if (-not (Get-Module -ListAvailable -Name ImportExcel -ErrorAction SilentlyContinue)) { Write-Warning "ImportExcel module is not installed. Run 'Install-Module -Name ImportExcel -Force'." Write-Warning "Falling back to CSV export..." foreach ($SheetName in $Sheets.Keys) { $CsvPath = $Path -replace '\.xlsx$', "_$($SheetName -replace '\s','_').csv" $Sheets[$SheetName] | Export-Csv -Path $CsvPath -NoTypeInformation -Encoding UTF8 Write-Host " - CSV exported: $CsvPath" -ForegroundColor Yellow } return } Import-Module ImportExcel -ErrorAction SilentlyContinue $FirstSheet = $true foreach ($SheetName in $Sheets.Keys) { $Data = $Sheets[$SheetName] if (-not $Data -or @($Data).Count -eq 0) { continue } $ExcelParams = @{ Path = $Path WorksheetName = $SheetName AutoSize = $true AutoFilter = $true FreezeTopRow = $true BoldTopRow = $true PassThru = $false } if ($FirstSheet) { $ExcelParams['ClearSheet'] = $true } try { $Data | Export-Excel @ExcelParams $FirstSheet = $false Write-Host " - Sheet exported: $SheetName ($(@($Data).Count) rows)" -ForegroundColor Cyan } catch { Write-Warning " - Failed to export sheet '$SheetName': $($_.Exception.Message)" } } if (Test-Path $Path) { Write-Host " - Excel workbook saved: $Path" -ForegroundColor Green } } function Write-AbrDebugLog { <# .SYNOPSIS Writes a structured debug log entry (module-scoped wrapper). #> param ( [string]$Message, [ValidateSet('INFO', 'WARN', 'ERROR', 'DEBUG', 'SUCCESS')][string]$Level = 'INFO', [string]$Section = 'GENERAL' ) $Entry = "[$(Get-Date -Format 'yyyy-MM-dd HH:mm:ss.fff')] [$Level] [$Section] $Message" if ($script:DebugLogEnabled) { $null = $script:DebugLogEntries.Add($Entry) } $null = switch ($Level) { 'ERROR' { Write-Host " [DBG-ERR] $Entry" -ForegroundColor Red } 'WARN' { Write-Host " [DBG-WRN] $Entry" -ForegroundColor Yellow } 'SUCCESS' { if ($script:DebugLogEnabled) { Write-Host " [DBG] $Entry" -ForegroundColor Green }; $null } default { if ($script:DebugLogEnabled) { Write-Host " [DBG] $Entry" -ForegroundColor DarkGray }; $null } } } function Write-ExoError { param([string]$Section, [string]$Message) $Entry = "[$(Get-Date -Format 'yyyy-MM-dd HH:mm:ss')] [ERROR] [$Section] $Message" $null = $script:ErrorLog.Add($Entry) Write-Host " [ERR] $Entry" -ForegroundColor Red if ($script:ErrorLogPath) { try { Add-Content -Path $script:ErrorLogPath -Value $Entry -ErrorAction SilentlyContinue } catch {} } } |