LibreDevOpsHelpers.Utils/LibreDevOpsHelpers.Utils.psm1
|
Set-StrictMode -Version Latest function Get-LdoSecureRandomInt { # Internal. Returns a cryptographically strong integer in [0, Maximum). [CmdletBinding()] [OutputType([int])] param([Parameter(Mandatory)][int]$Maximum) return [System.Security.Cryptography.RandomNumberGenerator]::GetInt32($Maximum) } function Test-LdoPath { <# .SYNOPSIS Tests that one or more paths exist. .DESCRIPTION Returns $true only when every supplied path exists. Missing paths are logged as warnings, found paths as debug. Useful as a precondition guard. .PARAMETER Path One or more paths to test. .EXAMPLE if (-not (Test-LdoPath -Path './main.tf', './variables.tf')) { throw 'Missing files' } .OUTPUTS System.Boolean #> [CmdletBinding()] [OutputType([bool])] param( [Parameter(Mandatory)] [ValidateNotNullOrEmpty()] [string[]]$Path ) $allExist = $true foreach ($item in $Path) { if (Test-Path -Path $item) { Write-LdoLog -Level DEBUG -Message "Found path: $item" } else { Write-LdoLog -Level WARN -Message "Path not found: $item" $allExist = $false } } return $allExist } function Assert-LdoCommand { <# .SYNOPSIS Asserts that one or more commands are available on PATH. .DESCRIPTION Throws when any of the named commands cannot be resolved. Use before shelling out to an external CLI so the failure is clear rather than a cryptic execution error. .PARAMETER Name One or more command or executable names to check. .EXAMPLE Assert-LdoCommand -Name 'az', 'terraform' .OUTPUTS None #> [CmdletBinding()] [OutputType([void])] param( [Parameter(Mandatory)] [ValidateNotNullOrEmpty()] [string[]]$Name ) $missing = @() foreach ($command in $Name) { if (Get-Command -Name $command -ErrorAction SilentlyContinue) { Write-LdoLog -Level DEBUG -Message "Found command: $command" } else { $missing += $command } } if ($missing.Count -gt 0) { $message = "Required command(s) not found on PATH: $($missing -join ', ')" Write-LdoLog -Level ERROR -Message $message throw $message } } function Assert-LdoEnvironmentVariable { <# .SYNOPSIS Asserts that one or more environment variables are set. .DESCRIPTION Throws when any named environment variable is missing or empty. Values are never logged. .PARAMETER Name One or more environment variable names to check. .EXAMPLE Assert-LdoEnvironmentVariable -Name 'ARM_CLIENT_ID', 'ARM_TENANT_ID' .OUTPUTS None #> [CmdletBinding()] [OutputType([void])] param( [Parameter(Mandatory)] [ValidateNotNullOrEmpty()] [string[]]$Name ) $missing = @() foreach ($variable in $Name) { $value = [System.Environment]::GetEnvironmentVariable($variable) if ([string]::IsNullOrWhiteSpace($value)) { $missing += $variable } else { Write-LdoLog -Level DEBUG -Message "Environment variable present: $variable" } } if ($missing.Count -gt 0) { $message = "Missing environment variable(s): $($missing -join ', ')" Write-LdoLog -Level ERROR -Message $message throw $message } } function New-LdoRandomSequence { <# .SYNOPSIS Generates a random character sequence from an alphabet. .DESCRIPTION Uses a cryptographically strong random number generator to pick characters from the supplied alphabet. .PARAMETER Length Number of characters to generate. .PARAMETER Alphabet The set of characters to draw from. .EXAMPLE New-LdoRandomSequence -Length 16 -Alphabet 'abcdef0123456789' .OUTPUTS System.String #> [CmdletBinding()] [OutputType([string])] param( [Parameter(Mandatory)] [ValidateRange(1, 4096)] [int]$Length, [Parameter(Mandatory)] [ValidateNotNullOrEmpty()] [string]$Alphabet ) $builder = [System.Text.StringBuilder]::new($Length) for ($i = 0; $i -lt $Length; $i++) { $null = $builder.Append($Alphabet[(Get-LdoSecureRandomInt -Maximum $Alphabet.Length)]) } return $builder.ToString() } function New-LdoPassword { <# .SYNOPSIS Generates a strong random password. .DESCRIPTION Produces a password of the requested length using a cryptographically strong random number generator, guaranteeing at least one uppercase, lowercase, digit and special character. The final order is shuffled so the guaranteed characters are not positionally predictable. .PARAMETER Length Total password length. Minimum 8. Defaults to 24. .PARAMETER AsSecureString Return the password as a SecureString instead of plaintext. .EXAMPLE New-LdoPassword -Length 32 .EXAMPLE $secret = New-LdoPassword -AsSecureString .OUTPUTS System.String or System.Security.SecureString #> [CmdletBinding()] [OutputType([string], [System.Security.SecureString])] param( [ValidateRange(8, 256)] [int]$Length = 24, [switch]$AsSecureString ) $upper = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ' $lower = 'abcdefghijklmnopqrstuvwxyz' $digit = '0123456789' $special = '!@#$%^&*()-_=+[]{}' $all = $upper + $lower + $digit + $special $chars = [System.Collections.Generic.List[char]]::new() $chars.Add($upper[(Get-LdoSecureRandomInt -Maximum $upper.Length)]) $chars.Add($lower[(Get-LdoSecureRandomInt -Maximum $lower.Length)]) $chars.Add($digit[(Get-LdoSecureRandomInt -Maximum $digit.Length)]) $chars.Add($special[(Get-LdoSecureRandomInt -Maximum $special.Length)]) for ($i = $chars.Count; $i -lt $Length; $i++) { $chars.Add($all[(Get-LdoSecureRandomInt -Maximum $all.Length)]) } # Fisher-Yates shuffle so the guaranteed characters are not always at the front. for ($i = $chars.Count - 1; $i -gt 0; $i--) { $j = Get-LdoSecureRandomInt -Maximum ($i + 1) $tmp = $chars[$i]; $chars[$i] = $chars[$j]; $chars[$j] = $tmp } Write-LdoLog -Level DEBUG -Message "Generated a password of length $Length." if ($AsSecureString) { # Build the SecureString character by character so the password is never held # in an interned plaintext string. $secure = [System.Security.SecureString]::new() foreach ($char in $chars) { $secure.AppendChar($char) } $secure.MakeReadOnly() return $secure } return (-join $chars) } function ConvertTo-LdoBoolean { <# .SYNOPSIS Converts a string to a boolean safely. .DESCRIPTION Accepts true/false, 1/0, yes/no and y/n (case-insensitive). Empty or whitespace becomes $false. Anything else throws, so a malformed value never silently maps to the wrong boolean (unlike a plain [bool] cast where any non-empty string is $true). .PARAMETER Value The string to convert. .EXAMPLE ConvertTo-LdoBoolean -Value $env:ENABLE_FEATURE .OUTPUTS System.Boolean #> [CmdletBinding()] [OutputType([bool])] param( [AllowEmptyString()] [AllowNull()] [string]$Value ) if ([string]::IsNullOrWhiteSpace($Value)) { return $false } switch ($Value.Trim().ToLowerInvariant()) { { $_ -in 'true', '1', 'yes', 'y' } { return $true } { $_ -in 'false', '0', 'no', 'n' } { return $false } default { $message = "Cannot convert '$Value' to a boolean. Expected true/false, 1/0, yes/no." Write-LdoLog -Level ERROR -Message $message throw $message } } } function ConvertTo-LdoNull { <# .SYNOPSIS Normalises empty or quote-only strings to $null. .DESCRIPTION Returns $null when the value is null, whitespace, or just a pair of empty quotes ('' or ""). Otherwise returns the value unchanged. Handy for cleaning values passed through shells and pipelines. .PARAMETER Value The value to normalise. .EXAMPLE ConvertTo-LdoNull -Value $env:OPTIONAL_SETTING .OUTPUTS System.String #> [CmdletBinding()] [OutputType([string])] param( [AllowEmptyString()] [AllowNull()] [string]$Value ) if ([string]::IsNullOrWhiteSpace($Value) -or $Value -eq "''" -or $Value -eq '""') { return $null } return $Value } function Get-LdoOperatingSystem { <# .SYNOPSIS Returns the current operating system family. .DESCRIPTION Returns one of 'Linux', 'Windows' or 'macOS'. Throws if the platform cannot be determined. .EXAMPLE switch (Get-LdoOperatingSystem) { 'Linux' { ... } 'Windows' { ... } } .OUTPUTS System.String #> [CmdletBinding()] [OutputType([string])] param() $os = if ($IsLinux) { 'Linux' } elseif ($IsWindows) { 'Windows' } elseif ($IsMacOS) { 'macOS' } else { $null } if (-not $os) { $message = 'Unable to determine the operating system.' Write-LdoLog -Level ERROR -Message $message throw $message } Write-LdoLog -Level DEBUG -Message "Operating system detected: $os" return $os } function Assert-LdoLastExitCode { <# .SYNOPSIS Throws when the last native command exited non-zero. .DESCRIPTION Checks $LASTEXITCODE and throws a descriptive error naming the operation when it is not zero. Call immediately after invoking an external CLI so failures surface clearly. .PARAMETER Operation Description of the command that ran, used in the error message. .EXAMPLE az group create --name rg --location uksouth Assert-LdoLastExitCode -Operation 'az group create' .OUTPUTS None #> [CmdletBinding()] [OutputType([void])] param( [Parameter(Mandatory)][ValidateNotNullOrEmpty()][string]$Operation ) if ($LASTEXITCODE -ne 0) { throw "$Operation failed with exit code $LASTEXITCODE." } } function Get-LdoPublicIpAddress { <# .SYNOPSIS Returns the caller's public IPv4 address. .DESCRIPTION Queries a public IP echo service and returns the trimmed address. Throws when no address can be determined. .PARAMETER Uri The IP echo endpoint. Defaults to https://checkip.amazonaws.com. .EXAMPLE $ip = Get-LdoPublicIpAddress .OUTPUTS System.String #> [CmdletBinding()] [OutputType([string])] param( [ValidateNotNullOrEmpty()][string]$Uri = 'https://checkip.amazonaws.com' ) $ip = (Invoke-RestMethod -Uri $Uri -ErrorAction Stop).Trim() if ([string]::IsNullOrWhiteSpace($ip)) { throw 'Failed to determine the public IP address.' } return $ip } Export-ModuleMember -Function ` Test-LdoPath, ` Assert-LdoCommand, ` Assert-LdoEnvironmentVariable, ` New-LdoRandomSequence, ` New-LdoPassword, ` ConvertTo-LdoBoolean, ` ConvertTo-LdoNull, ` Get-LdoOperatingSystem, ` Assert-LdoLastExitCode, ` Get-LdoPublicIpAddress |