PSGerickeUtil.psm1
<#
.SYNOPSIS Sets the log path for the script. .DESCRIPTION The Set-LogPath function sets the path where logs will be stored. If the specified folder path does not exist, it will be created. .PARAMETER Path The full path where logs will be stored. This parameter is mandatory. .PARAMETER WhatIf Shows what would happen if the cmdlet runs. The cmdlet is not run. .PARAMETER Confirm Prompts you for confirmation before running the cmdlet. .EXAMPLE Set-LogPath -Path "C:\Logs\MyLog.txt" This example sets the log path to "C:\Logs\MyLog.txt". If the "C:\Logs" folder does not exist, it will be created. .NOTES Author: Stefan Gericke Date: 2024-11-21 #> function Set-LogPath { [CmdletBinding(SupportsShouldProcess = $true)] param( [Parameter(Mandatory = $true)] [String] $Path ) trap { throw "Error setting log path: $($_.Exception.Message)" } $folderPath = Split-Path -Path $Path if (-not (Test-Path -Path $folderPath)) { New-Item -Path $folderPath -ItemType Directory -Force -ErrorAction Stop } if ($PSCmdlet.ShouldProcess("script variable", "Set path of the logfile")) { $script:logPath = $Path } } <# .SYNOPSIS Sets the Pushover api key for REST API calls. .DESCRIPTION The Set-RestApiKeyForPushoverApi function sets a secure Pushover api key for use in REST API calls. The user key is stored in a script-scoped variable for later use. .PARAMETER ApiKey The secure string representing the Pushover user key. This parameter is mandatory. .PARAMETER WhatIf Shows what would happen if the cmdlet runs. The cmdlet is not run. .PARAMETER Confirm Prompts you for confirmation before running the cmdlet. .EXAMPLE PS C:\> $secureUserKey = Read-Host "Enter Pushover User Key" -AsSecureString PS C:\> Set-RestApiKeyForPushoverApi -UserKey $secureUserKey This example prompts the user to enter a Pushover user key securely and then sets it using the Set-RestApiKeyForPushoverUser function. .NOTES Author: Stefan Gericke Date: 2024-11-21 #> function Set-RestApiKeyForPushoverApi { [CmdletBinding(SupportsShouldProcess = $true)] param( [Parameter(Mandatory = $true)] [securestring] $ApiKey ) if ($PSCmdlet.ShouldProcess("script variable", "Set api key")) { $script:pushoverApiKey = $ApiKey } } <# .SYNOPSIS Sets the Pushover user key for REST API calls. .DESCRIPTION The Set-RestApiKeyForPushoverUser function sets a secure Pushover user key for use in REST API calls. The user key is stored in a script-scoped variable for later use. .PARAMETER UserKey The secure string representing the Pushover user key. This parameter is mandatory. .PARAMETER WhatIf Shows what would happen if the cmdlet runs. The cmdlet is not run. .PARAMETER Confirm Prompts you for confirmation before running the cmdlet. .EXAMPLE PS C:\> $secureUserKey = Read-Host "Enter Pushover User Key" -AsSecureString PS C:\> Set-RestApiKeyForPushoverUser -UserKey $secureUserKey This example prompts the user to enter a Pushover user key securely and then sets it using the Set-RestApiKeyForPushoverUser function. .NOTES Author: Stefan Gericke Date: 2024-11-21 #> function Set-RestApiKeyForPushoverUser { [CmdletBinding(SupportsShouldProcess = $true)] param( [Parameter(Mandatory = $true)] [securestring] $UserKey ) if ($PSCmdlet.ShouldProcess("script variable", "Set user key")) { $script:pushoverUserKey = $UserKey } } <# .SYNOPSIS Sets a script-level variable to indicate whether UTC time should be used. .DESCRIPTION The Set-Utc function sets a script-level variable `$script:utc` to the value of the `$Utc` parameter. This function supports the ShouldProcess feature, allowing for confirmation before making changes. .PARAMETER Utc A mandatory boolean parameter that specifies whether to set the script-level variable to use UTC time. .PARAMETER WhatIf Shows what would happen if the cmdlet runs. The cmdlet is not run. .PARAMETER Confirm Prompts you for confirmation before running the cmdlet. .EXAMPLE Set-Utc -Utc $true This example sets the script-level variable to use UTC time. .EXAMPLE Set-Utc -Utc $false This example sets the script-level variable to not use UTC time. .INPUTS [bool] The function accepts a boolean value as input. .OUTPUTS None. The function does not produce any output. .NOTES Author: Stefan Gericke Date: 2025-01-07 #> function Set-Utc { [CmdletBinding(SupportsShouldProcess = $true)] param( [Parameter(Mandatory = $true)] [bool] $Utc ) if ($PSCmdlet.ShouldProcess("script variable", "Set utc time")) { $script:utc = $Utc } } <# .SYNOPSIS Sends a push notification using the Pushover service. .DESCRIPTION This function sends a push notification to a specified device using the Pushover service. It requires a user key and an API key for authentication. .PARAMETER UserKey The user key for the Pushover service. This is a secure string. .PARAMETER ApiKey The API key for the Pushover service. This is a secure string. .PARAMETER Message The message to be sent. This parameter is mandatory. .PARAMETER Device The device to which the message should be sent. Valid values are "iPadPro2020", "iPhone13Pro", and "iPhoneBI". .PARAMETER Title The title of the message. .PARAMETER Url A URL to be included with the message. .PARAMETER UrlTitle The title of the URL. .PARAMETER Priority The priority of the message. Valid values are "Lowest", "Low", "Normal", "High", and "Emergency". .PARAMETER Sound The sound to be played with the message. Valid values are "pushover", "bike", "bugle", "cashregister", "classical", "cosmic", "falling", "gamelan", "incoming", "intermission", "magic", "mechanical", "pianobar", "siren", "spacealarm", "tugboat", "alien", "climb", "persistent", "echo", "updown", and "none". .PARAMETER WhatIf Shows what would happen if the command runs. The command is not executed. .PARAMETER Confirm Prompts you for confirmation before running the command. .EXAMPLE Send-Pushover -UserKey $userKey -ApiKey $apiKey -Message "Test message" -Device "iPhone13Pro" -Priority "Normal" -Sound "pushover" This example sends a test message to the device "iPhone13Pro" with normal priority and the default "pushover" sound. .EXAMPLE Send-Pushover -UserKey $userKey -ApiKey $apiKey -Message "Urgent message" -Priority "Emergency" -Sound "siren" This example sends an urgent message with emergency priority and the "siren" sound. .NOTES Author: Stefan Gericke Date: 2024-11-06 #> function Send-Pushover { [CmdletBinding(SupportsShouldProcess = $true)] Param( [securestring] $UserKey, [securestring] $ApiKey, [Parameter(Mandatory = $true)] [string] $Message, [ValidateSet("iPadPro2020", "iPhone13Pro", "iPhoneBI")] [string] $Device, [string] $Title, [string] $Url, [string] $UrlTitle, [ValidateSet("Lowest", "Low", "Normal", "High", "Emergency")] [string] $Priority, [ValidateSet("pushover", "bike", "bugle", "cashregister", "classical", "cosmic", "falling", "gamelan", "incoming", "intermission", "magic", "mechanical", "pianobar", "siren", "spacealarm", "tugboat", "alien", "climb", "persistent", "echo", "updown", "none")] [string] $Sound ) # Check if user key and/or API key is available for doing web request if ([string]::IsNullOrEmpty($script:pushoverUserKey) -or $script:pushoverUserKey -ne $UserKey) { if ([string]::IsNullOrEmpty($UserKey) -and [string]::IsNullOrEmpty($script:pushoverUserKey)) { Write-Error "User key is mandatory to send a push notification!" -Category NotSpecified } elseif (![string]::IsNullOrEmpty($UserKey)) { Set-RestApiKeyForPushoverUser -UserKey $UserKey } } if ([string]::IsNullOrEmpty($script:pushoverApiKey) -or $script:pushoverApiKey -ne $ApiKey) { if ([string]::IsNullOrEmpty($ApiKey) -and [string]::IsNullOrEmpty($script:pushoverApiKey)) { Write-Error "Api key is mandatory to send a push notification!" -Category NotSpecified } elseif (![string]::IsNullOrEmpty($ApiKey)) { Set-RestApiKeyForPushoverApi -ApiKey $ApiKey } } # Map priority string to integer value switch ($Priority) { "Lowest" { $iPriority = -2 } "Low" { $iPriority = -1 } "Normal" { $iPriority = 0 } "High" { $iPriority = 1 } "Emergency" { $iPriority = 2 } Default { $iPriority = 0 } } # Prepare data for the request $data = @{ token = [Runtime.InteropServices.Marshal]::PtrToStringAuto([Runtime.InteropServices.Marshal]::SecureStringToBSTR($script:pushoverApiKey)) user = [Runtime.InteropServices.Marshal]::PtrToStringAuto([Runtime.InteropServices.Marshal]::SecureStringToBSTR($script:pushoverUserKey)) message = $Message } # Check if token and user are available if ([string]::IsNullOrEmpty($data["token"]) -or [string]::IsNullOrEmpty($data["user"])) { Write-Error -Exception ([System.ArgumentException]::new("App and User key are mandatory to send a push notification!")) -ErrorAction Continue return $null } if ($Device) { $data.Add("device", $Device) } if ($Title) { $data.Add("title", $Title) } if ($Url) { $data.Add("url", $Url) } if ($UrlTitle) { $data.Add("url_title", $UrlTitle) } if ($Sound) { $data.Add("sound", $Sound) } if ($Priority) { $data.Add("priority", $iPriority) if ($iPriority -eq 2) { $data.Add("retry", 30) $data.Add("expire", 1800) } } # Debug output if enabled if ($DebugPreference) { foreach ($key in $data.Keys) { if ($key -eq "token" -or $key -eq "user") { Write-Debug "Parameter: $key, Value: $(Get-MaskString -InputString $data[$key] -VisibleLength 3)" } else { Write-Debug "Parameter: $key, Value: $($data[$key])" } } } try { $uri = "https://api.pushover.net/1/messages.json" Write-Debug "Uri: $($uri)" if ($PSCmdlet.ShouldProcess("Message: $($Message)", "Send notification to API")) { $null = Invoke-RestMethod -Method Post -Uri $uri -Body $data } } catch { Write-Error -Exception ([System.Net.WebException]::new("Web request not successful: $($_.ErrorDetails)")) -ErrorAction Continue return $null } } <# .SYNOPSIS Retrieves information about users with Enterprise Voice enabled in Microsoft Teams. .DESCRIPTION This function checks if a user or all users in the tenant have Enterprise Voice enabled in Microsoft Teams. It can either check a specific user by their UserPrincipalName or export the information for the whole tenant to a CSV file. .PARAMETER UserPrincipalName The UserPrincipalName of the user to check. This parameter is mandatory if you are checking a specific user. .PARAMETER ExportCSV A switch to indicate if the results for the whole tenant should be exported to a CSV file. .PARAMETER ExportPath The path where the CSV file should be saved. If not specified, a default path in the TEMP directory will be used. .EXAMPLE Get-TeamsPSTNEnterpriseVoiceEnabled -UserPrincipalName "user@example.com" Retrieves information about the specified user. .EXAMPLE Get-TeamsPSTNEnterpriseVoiceEnabled -ExportCSV Retrieves information about all users in the tenant and exports it to a CSV file in the TEMP directory. .EXAMPLE Get-TeamsPSTNEnterpriseVoiceEnabled -ExportCSV -ExportPath "C:\Exports\EV-enabled.csv" Retrieves information about all users in the tenant and exports it to the specified CSV file. .NOTES Author: Stefan Gericke Date: 2024-11-06 #> function Get-TeamsPSTNEnterpriseVoiceEnabled { [CmdletBinding(DefaultParameterSetName = 'WholeTenant')] Param( [Parameter(ParameterSetName = 'UserPrincipalName', Mandatory = $true, Position = 0)] [ValidateNotNullOrEmpty()] [string[]] $UserPrincipalName, [Parameter(ParameterSetName = 'WholeTenant', Position = 0)] [switch] $ExportCSV, [Parameter(ParameterSetName = 'WholeTenant', Position = 1)] [ValidateNotNullOrEmpty()] [string] $ExportPath = (Join-Path -Path $Env:TEMP -ChildPath "$(Get-Date -Format yyyyMMdd_HHmm)_EV-enabled.csv") ) # Check if you already have a connection to Microsoft Teams try { $null = Get-CsTenant Write-Debug "Connection to Microsoft Teams already established." } catch { Write-Warning "You are not connected to Microsoft Teams!" Write-Warning "Please log in with your credentials and the MFA token in your browser." try { Connect-MicrosoftTeams $null = Get-CsTenant Write-Debug "You are successfully logged in to Microsoft Teams." } catch { Write-Error -Exception ([System.Net.WebException]::new("The log in to Microsoft Teams was not successful. The script will stop here!")) return $null } } # Check the parameter set and collect the results switch ($PSCmdlet.ParameterSetName) { # Collect the information for the specified user 'UserPrincipalName' { $results = New-Object -TypeName "System.Collections.ArrayList" foreach ($upn in $UserPrincipalName) { try { Write-Debug "Request for $($upn) ..." $response = Get-CsOnlineUser -Identity $upn -ErrorAction Continue if ($null -eq $response) { Write-Error "UPN $($upn) can't be found" continue } else { $results.Add([PSCustomObject]@{"UserPrincipalName" = $response.UserPrincipalName; "EnterpriseVoiceEnabled" = $response.EnterpriseVoiceEnabled }) } } catch { Write-Error $message.Exception.Message } } } 'WholeTenant' { # Collect all users with Enterprise Voice enabled in the tenant Write-Verbose "Get all users on the tenant with Enterprise Voice flag enabled. This will take some time. Please wait ..." try { if ($DebugPreference) { $debugNoOfUpns = 10 Write-Debug "Get only the first $($debugNoOfUpns) as result!" $results = Get-CsOnlineUser -Filter { EnterpriseVoiceEnabled -eq "true" } -ResultSize $debugNoOfUpns -ErrorAction Stop | Select-Object -Property UserPrincipalName, Alias, EnterpriseVoiceEnabled } else { $results = Get-CsOnlineUser -Filter { EnterpriseVoiceEnabled -eq "true" } -ErrorAction Stop | Select-Object -Property UserPrincipalName, Alias, EnterpriseVoiceEnabled } if ($ExportCSV -or !([string]::IsNullOrEmpty($ExportPath))) { $results | Export-Csv -Path $ExportPath -NoTypeInformation } } catch { $errorMessage = $_.Exception.Message if ($errorMessage -eq "Access Denied.") { Write-Error -Exception ([System.UnauthorizedAccessException]::new("No Permission to run this command!")) } Disconnect-MicrosoftTeams return $null } } } return $results } <# .SYNOPSIS Creates a symbolic link, hard link, or junction. .DESCRIPTION This script creates a symbolic link, hard link, or junction at the specified path. Administrator permissions are required to create symbolic links and junctions. .PARAMETER LinkType The type of link to create. Valid values are 'SymbolicLink', 'HardLink', and 'Junction'. .PARAMETER TargetPath The target path for the link. .PARAMETER LinkPath The path where the link will be created. .EXAMPLE Add-LinkOfFileFolder -LinkType SymbolicLink -TargetPath "C:\Target" -LinkPath "C:\Link" Creates a symbolic link from "C:\Link" to "C:\Target". .EXAMPLE Add-LinkOfFileFolder -LinkType HardLink -TargetPath "C:\Target" -LinkPath "C:\Link" Creates a hard link from "C:\Link" to "C:\Target". .EXAMPLE Add-LinkOfFileFolder -LinkType Junction -TargetPath "C:\Target" -LinkPath "C:\Link" Creates a junction from "C:\Link" to "C:\Target". .NOTES Author: Stefan Gericke Date: 2024-11-07 #> function Add-LinkOfFileFolder { [CmdletBinding()] param ( [Parameter(Mandatory = $true)] [ValidateNotNullOrEmpty()] [ValidateSet('SymbolicLink', 'HardLink', 'Junction')] [string] $LinkType, [Parameter(Mandatory = $true)] [ValidateNotNullOrEmpty()] [string] $TargetPath, [Parameter(Mandatory = $true)] [ValidateNotNullOrEmpty()] [string] $LinkPath ) function Test-Administrator { $currentUser = New-Object Security.Principal.WindowsPrincipal([Security.Principal.WindowsIdentity]::GetCurrent()) return $currentUser.IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator) } # Check if the target path exists if (!(Test-Path -Path $TargetPath)) { Write-Error -Exception ([System.IO.FileNotFoundException]::new("The target path $TargetPath does not exist.")) return $null } # CHeck if the link type is symbolic link or junction and if the user is an administrator if (($LinkType -eq 'SymbolicLink' -or $LinkType -eq 'Junction') -and !(Test-Administrator)) { Write-Error -Exception ([System.UnauthorizedAccessException]::new("Administrator permissions are required to create a symbolic link or junction.")) return $null } try { switch ($LinkType) { 'SymbolicLink' { $result = New-Item -ItemType SymbolicLink -Path $LinkPath -Value $TargetPath -ErrorAction Stop } 'HardLink' { $result = New-Item -ItemType HardLink -Path $LinkPath -Value $TargetPath -ErrorAction Stop } 'Junction' { $result = New-Item -ItemType Junction -Path $LinkPath -Value $TargetPath -ErrorAction Stop } } # Check if the link was created successfully if (!(Test-Path -Path $LinkPath)) { Write-Error -Exception ([System.IO.IOException]::new("The $LinkType was not created successfully.")) return $null } Write-Verbose "$LinkType created successfully from $LinkPath to $TargetPath" return $result } catch { Write-Error $_.Exception.Message return $null } } <# .SYNOPSIS Retrieves credentials for an M365 tenant from either the clipboard, a file, or a secure string. .DESCRIPTION The Get-CredentialForM365Tenant function retrieves the client secret, client ID, tenant domain, and tenant ID for an M365 tenant. The credentials can be retrieved from the clipboard, a specified file, or a secure string. The function validates the retrieved values to ensure they are in the correct format. .PARAMETER FromClipboard Specifies that the credentials should be retrieved from the clipboard. .PARAMETER FilePath Specifies the path to the file from which the credentials should be retrieved. .PARAMETER SecureString Specifies the secure string from which the credentials should be retrieved. .EXAMPLE Get-CredentialForM365Tenant -FromClipboard Retrieves the credentials from the clipboard. .EXAMPLE Get-CredentialForM365Tenant -FilePath "C:\path\to\credentials.txt" Retrieves the credentials from the specified file. .EXAMPLE $secureString = ConvertTo-SecureString "client_secret client_id tenant_domain tenant_id" -AsPlainText -Force Get-CredentialForM365Tenant -SecureString $secureString Retrieves the credentials from the provided secure string. .NOTES The credentials should be stored in the clipboard, file, or secure string in the following format: <client_secret> <client_id> <tenant_domain> <tenant_id> The client ID and tenant ID must be valid GUIDs. The tenant domain must be a valid domain name. #> function Get-CredentialForM365Tenant { [CmdletBinding(DefaultParameterSetName = 'FromSecurestring')] [OutputType([System.Collections.Hashtable])] param ( [Parameter(ParameterSetName = 'FromClipboard', Mandatory = $true, Position = 0)] [switch] $FromClipboard, [Parameter(ParameterSetName = 'FromFile', Mandatory = $true, Position = 0)] [string] $FilePath, [Parameter(ParameterSetName = 'FromSecurestring', Mandatory = $true, Position = 0)] [securestring] $SecureString ) switch ($PSCmdlet.ParameterSetName) { 'FromClipboard' { # Retrieve the clipboard content if ($FromClipboard) { # Condition created just for Pester } Write-Debug "Retrieving the encrypted credential from the clipboard." try { $secret = Get-Clipboard | ConvertTo-SecureString -ErrorAction Stop Write-Debug "Successfully retrieved the encrypted credential from the clipboard." } catch { throw [System.ArgumentException]::new("Failed to retrieve the encrypted credential from the clipboard.") } } 'FromFile' { # Check if the file exists Write-Debug "Retrieving the encrypted credential from the file: $FilePath" if (-not (Test-Path -Path $FilePath)) { throw [System.IO.FileNotFoundException]::new() } # Retrieve the encrypted credential from the file try { $secret = Get-Content -Path $FilePath | ConvertTo-SecureString -ErrorAction Stop Write-Debug "Successfully retrieved the encrypted credential from the file: $FilePath" } catch { throw [System.UnauthorizedAccessException]::new("Failed to retrieve the encrypted credential from the file: $FilePath") } } 'FromSecurestring' { # Retrieve the credential from the secure string Write-Debug "Retrieving the credential from the secure string." $secret = $SecureString } } # Check if the secure string contains the client secret, client ID, tenant domain, and tenant ID if ((([Runtime.InteropServices.Marshal]::PtrToStringAuto([Runtime.InteropServices.Marshal]::SecureStringToBSTR($secret))) -split (" ")).Length -ne 4) { throw [System.FormatException]::new("The secure string must contain the client secret, client ID, tenant domain, and tenant ID. Make sure your encryoed string is in the format: <client_secret> <client_id> <tenant_domain> <tenant_id>") } $clientSecret = ConvertTo-SecureString -String ((([Runtime.InteropServices.Marshal]::PtrToStringAuto([Runtime.InteropServices.Marshal]::SecureStringToBSTR($secret))) -split (" "))[0]) -AsPlainText -Force $clientID = (([Runtime.InteropServices.Marshal]::PtrToStringAuto([Runtime.InteropServices.Marshal]::SecureStringToBSTR((($Secret))))) -split (" "))[1] $tenantdomain = (([Runtime.InteropServices.Marshal]::PtrToStringAuto([Runtime.InteropServices.Marshal]::SecureStringToBSTR((($Secret))))) -split (" "))[2] $tenantId = (([Runtime.InteropServices.Marshal]::PtrToStringAuto([Runtime.InteropServices.Marshal]::SecureStringToBSTR((($Secret))))) -split (" "))[3] # Check if clientSecret is a valid secure string if ($clientSecret.Length -eq 0) { throw [System.FormatException]::new("The ClientSecret must be a secure string.") } Write-Debug "ClientSecret is a valid secure string: $(Get-MaskString -InputString ((([Runtime.InteropServices.Marshal]::PtrToStringAuto([Runtime.InteropServices.Marshal]::SecureStringToBSTR($secret))) -split (" "))[0]) -VisibleLength 3)" # Check if clientID is a valid GUID $regexGuid = "^[{]?[0-9a-fA-F]{8}-([0-9a-fA-F]{4}-){3}[0-9a-fA-F]{12}[}]?$" if (-not ($ClientID -match $regexGuid)) { throw [System.FormatException]::new("The ClientID must be a valid GUID: $clientID") } Write-Debug "ClientID is a valid GUID: $clientID" # Check if TenantDomain is a valid domain $regexDomain = "^(?!:\/\/)([a-zA-Z0-9-_]+\.)*[a-zA-Z0-9][a-zA-Z0-9-_]+\.[a-zA-Z]{2,11}?$" if (-not ($tenantdomain -match $regexDomain)) { throw throw [System.FormatException]::new("The TenantDomain must be a valid domain: $tenantdomain") } Write-Debug "TenantDomain is a valid domain: $tenantdomain" # Check if TenantId is a valid GUID if (-not ($tenantId -match $regexGuid)) { throw throw [System.FormatException]::new("The TenantId must be a valid GUID.") } Write-Debug "TenantId is a valid GUID: $tenantId" return @{ ClientSecret = New-Object -TypeName System.Management.Automation.PSCredential -ArgumentList $clientID, $clientSecret TenantDomain = $tenantdomain TenantId = $tenantId } } <# .SYNOPSIS Formats a JSON file by reading its content, converting it to a PowerShell object, and then writing it back to a specified target file. .DESCRIPTION The Get-JsonFormatter function reads the content of a JSON file from a specified source file path, converts it to a PowerShell object, and then writes the formatted JSON content to a specified target file path. If the target file already exists, the function can overwrite it if the -Force switch is used. .PARAMETER SourceFilePath The path to the source JSON file that needs to be formatted. This parameter is mandatory. .PARAMETER TargetFilePath The path to the target file where the formatted JSON content will be written. This parameter is mandatory. .PARAMETER Depth Specifies how many levels of contained objects are included in the JSON representation. The default value is 99. .PARAMETER Compress A switch parameter that, if specified, compresses the JSON output by removing unnecessary white spaces. This parameter is optional. .PARAMETER Force A switch parameter that, if specified, forces the function to overwrite the target file if it already exists. This parameter is optional. .EXAMPLE Get-JsonFormatter -SourceFilePath "C:\path\to\source.json" -TargetFilePath "C:\path\to\target.json" This example reads the JSON content from "C:\path\to\source.json", formats it, and writes it to "C:\path\to\target.json". .EXAMPLE Get-JsonFormatter -SourceFilePath "C:\path\to\source.json" -TargetFilePath "C:\path\to\target.json" -Force This example reads the JSON content from "C:\path\to\source.json", formats it, and writes it to "C:\path\to\target.json", overwriting the target file if it already exists. .EXAMPLE Get-JsonFormatter -SourceFilePath "C:\path\to\source.json" -TargetFilePath "C:\path\to\target.json" -Depth 5 This example reads the JSON content from "C:\path\to\source.json", formats it with a depth of 5, and writes it to "C:\path\to\target.json". .EXAMPLE Get-JsonFormatter -SourceFilePath "C:\path\to\source.json" -TargetFilePath "C:\path\to\target.json" -Compress This example reads the JSON content from "C:\path\to\source.json", formats it, compresses the JSON output, and writes it to "C:\path\to\target.json". .NOTES - The SourceFilePath and TargetFilePath cannot be the same. - If the SourceFilePath does not exist, the function will throw an error. - If the TargetFilePath exists and the -Force switch is not used, the function will prompt the user to confirm overwriting the file. #> function Get-JsonFormatter { [CmdletBinding()] param ( [Parameter(Mandatory = $true)] [string] $SourceFilePath, [Parameter(Mandatory = $true)] [string] $TargetFilePath, [Parameter(Mandatory = $false)] [int] $Depth = 99, [Parameter(Mandatory = $false)] [switch] $Compress, [Parameter(Mandatory = $false)] [switch] $Force ) # Check if SourceFilePath and TargetFilePath are not the same if ($SourceFilePath -eq $TargetFilePath) { throw "SourceFilePath and TargetFilePath cannot be the same." } # Check if the SourceFilePath not exists if (-not (Test-Path -Path $SourceFilePath)) { throw [System.IO.FileNotFoundException]::new("The file '$SourceFilePath' does not exist.") } # Check if the TargetFilePath exists if (Test-Path -Path $TargetFilePath) { if (-not $Force) { Write-Error "A file '$TargetFilePath' already exists. Use -Force to overwrite the file." -Category ResourceExists # Ask the user if they want to overwrite the file and repeat the process until the user type 'Y' or 'N' do { $response = Read-Host "Do you want to overwrite the file '$TargetFilePath'? (Y/N)" if ($response -eq 'N') { Write-Error "The file '$TargetFilePath' already exists." return $null } } while ($response -ne 'Y') } Write-Verbose "Overwriting the file '$TargetFilePath'." } # Get the content of the JSON file $json = Get-Content -Path $SourceFilePath -Raw try { # Convert the JSON content to a PowerShell object $jsonObject = $json | ConvertFrom-Json } catch { Write-Error $_.Exception.Message -Category InvalidData } # Convert the PowerShell object back to JSON and write it to the target file $jsonObject | ConvertTo-Json -Depth $Depth -Compress:$Compress | Set-Content -Path $TargetFilePath } <# .SYNOPSIS Masks a string or secure string, showing only a specified number of visible characters. .DESCRIPTION The Get-MaskString function takes an input string or secure string and masks it, showing only a specified number of visible characters at the beginning of the string. The rest of the string is replaced with asterisks (*). .PARAMETER InputString The input string to be masked. This parameter is mandatory when using the 'String' parameter set. .PARAMETER SecureString The secure string to be masked. This parameter is mandatory when using the 'Secure String' parameter set. .PARAMETER VisibleLength The number of characters to remain visible at the beginning of the string. This parameter is mandatory for both 'String' and 'Secure String' parameter sets. .EXAMPLE PS C:\> Get-MaskString -InputString "SensitiveData" -VisibleLength 4 Sens******** .EXAMPLE PS C:\> $secureString = ConvertTo-SecureString "SensitiveData" -AsPlainText -Force PS C:\> Get-MaskString -SecureString $secureString -VisibleLength 4 Sens******** .NOTES Author: Stefan Gericke Date: 2025-01-09 #> function Get-MaskString { [CmdletBinding(DefaultParameterSetName = 'Secure String')] param ( [Parameter(ParameterSetName = 'String', Mandatory = $true, Position = 0)] [string] $InputString, [Parameter(ParameterSetName = 'Secure String', Mandatory = $true, Position = 0)] [securestring] $SecureString, [Parameter(ParameterSetName = 'String', Mandatory = $true, Position = 1)] [Parameter(ParameterSetName = 'Secure String', Mandatory = $true, Position = 1)] [int] $VisibleLength ) switch ($PSCmdlet.ParameterSetName) { 'Secure String' { Write-Debug "Secure string entered." # Convert the secure string to a plain text string $InputString = [Runtime.InteropServices.Marshal]::PtrToStringAuto([Runtime.InteropServices.Marshal]::SecureStringToBSTR($SecureString)) } } # Check if the input string is shorter than the visible length Write-Debug "Length of the String: $($InputString.Length); Visible Length: $VisibleLength" if (($InputString.Length - 3) -le $VisibleLength) { Write-Error "The visible length must be less than the length of the input string. A minimum of 3 characters must be visible." -ErrorAction SilentlyContinue return $null } # Mask the input string $visiblePart = $InputString.Substring(0, $VisibleLength) $maskedPart = '*' * ($InputString.Length - $VisibleLength) $output = $visiblePart + $maskedPart return $output } <# .SYNOPSIS Creates a new credential for an M365 tenant. .DESCRIPTION This function generates a new credential for an M365 tenant by taking the ClientID, ClientSecret, TenantDomain, and TenantId as inputs. It validates these inputs and then converts them into a secure string which can be either copied to the clipboard, saved to a file, or returned as output. .PARAMETER ClientID The ClientID of the M365 tenant. It must be a valid GUID. .PARAMETER ClientSecret The ClientSecret of the M365 tenant. It must be a secure string. .PARAMETER TenantDomain The domain of the M365 tenant. It must be a valid domain from the format <domain>.onmicrosoft.com. .PARAMETER TenantId The TenantId of the M365 tenant. It must be a valid GUID. .PARAMETER ToClipboard Switch to indicate that the encrypted credential should be copied to the clipboard. .PARAMETER FilePath The file path where the encrypted credential should be saved. .PARAMETER NoOutput Indicates that the encrypted credential should be returned as output without any additional actions. .PARAMETER WhatIf Shows what would happen if the command runs. The command is not executed. .PARAMETER Confirm Prompts you for confirmation before running the command. .EXAMPLE PS> New-CredentialForM365Tenant -ClientID "your-client-id" -ClientSecret (ConvertTo-SecureString "your-client-secret" -AsPlainText -Force) -TenantDomain "your-tenant-domain" -TenantId "your-tenant-id" -ToClipboard This example creates a new credential for an M365 tenant and copies the encrypted credential to the clipboard. .EXAMPLE PS> New-CredentialForM365Tenant -ClientID "your-client-id" -ClientSecret (ConvertTo-SecureString "your-client-secret" -AsPlainText -Force) -TenantDomain "your-tenant-domain" -TenantId "your-tenant-id" -FilePath "C:\path\to\file.txt" This example creates a new credential for an M365 tenant and saves the encrypted credential to a specified file. .EXAMPLE PS> New-CredentialForM365Tenant -ClientID "your-client-id" -ClientSecret (ConvertTo-SecureString "your-client-secret" -AsPlainText -Force) -TenantDomain "your-tenant-domain" -TenantId "your-tenant-id" -NoOutput This example creates a new credential for an M365 tenant and returns the encrypted credential as output. .NOTES Ensure that the ClientID and TenantId are valid GUIDs, the ClientSecret is a secure string, and the TenantDomain is a valid domain. #> function New-CredentialForM365Tenant { [CmdletBinding(DefaultParameterSetName = 'NoOutput', SupportsShouldProcess = $true)] param ( [Parameter(ParameterSetName = 'NoOutput', Mandatory = $true)] [Parameter(ParameterSetName = 'ToClipboard', Mandatory = $true)] [Parameter(ParameterSetName = 'ToFile', Mandatory = $true)] [string] $ClientID, [Parameter(ParameterSetName = 'NoOutput', Mandatory = $true)] [Parameter(ParameterSetName = 'ToClipboard', Mandatory = $true)] [Parameter(ParameterSetName = 'ToFile', Mandatory = $true)] [securestring] $ClientSecret, [Parameter(ParameterSetName = 'NoOutput', Mandatory = $true)] [Parameter(ParameterSetName = 'ToClipboard', Mandatory = $true)] [Parameter(ParameterSetName = 'ToFile', Mandatory = $true)] [string] $TenantDomain, [Parameter(ParameterSetName = 'NoOutput', Mandatory = $true)] [Parameter(ParameterSetName = 'ToClipboard', Mandatory = $true)] [Parameter(ParameterSetName = 'ToFile', Mandatory = $true)] [string] $TenantId, [Parameter(ParameterSetName = 'ToClipboard', Mandatory = $true)] [switch] $ToClipboard, [Parameter(ParameterSetName = 'ToFile', Mandatory = $true)] [string] $FilePath ) # Check if ClientID is a valid GUID $regexGuid = "^[{]?[0-9a-fA-F]{8}-([0-9a-fA-F]{4}-){3}[0-9a-fA-F]{12}[}]?$" if (-not ($ClientID -match $regexGuid)) { throw [System.ArgumentException]::new("The ClientID must be a valid GUID.") } Write-Debug "ClientID is a valid GUID: $ClientID" # Check if ClientSecret is a valid secure string if ($ClientSecret.Length -eq 0) { throw [System.ArgumentException]::new("The ClientSecret must be a secure string.") } Write-Debug "Secure string entered: $(Get-MaskString -InputString ([Runtime.InteropServices.Marshal]::PtrToStringAuto([Runtime.InteropServices.Marshal]::SecureStringToBSTR($ClientSecret))) -VisibleLength 3)" # Check if TenantDomain is a valid domain $regexDomain = "^[a-zA-Z0-9-]+\.onmicrosoft\.com$" if (-not ($TenantDomain -match $regexDomain)) { throw [System.ArgumentException]::new("The TenantDomain must be a valid domain.") } Write-Debug "TenantDomain is a valid domain: $TenantDomain" # Check if TenantId is a valid GUID if (-not ($TenantId -match $regexGuid)) { throw [System.ArgumentException]::new("The TenantId must be a valid GUID.") } Write-Debug "TenantId is a valid GUID: $TenantId" # Convert the parameters to a single string and encrypt it $secureString = ConvertTo-SecureString (([System.Runtime.InteropServices.Marshal]::PtrToStringAuto([System.Runtime.InteropServices.Marshal]::SecureStringToBSTR($ClientSecret))) + " " + $ClientID + " " + $TenantDomain + " " + $TenantId) -AsPlainText -Force $encryptedText = ConvertFrom-SecureString $secureString switch ($PSCmdlet.ParameterSetName) { 'ToClipboard' { if ($ToClipboard) { if ($PSCmdlet.ShouldProcess("clipboard", "Set encrypted string")) { $encryptedText | Set-Clipboard } return $encryptedText } return $null } 'ToFile' { $directory = Split-Path -Path $FilePath -Parent if (Test-Path -Path $directory) { # Check if the directory exists if (Test-Path -Path $FilePath) { # Check if the file exists Write-Error -Exception ([System.IO.IOException]::new("The file ($($FilePath)) already exists and won't be overwritten.")) } else { if ($PSCmdlet.ShouldProcess("File: $($FilePath)", "Create new file")) { $encryptedText | Out-File -FilePath $FilePath } } } else { Write-Error "The directory ($($directory)) does not exist and the file cannot be created." } return $encryptedText } 'NoOutput' { return $encryptedText } } } <# .SYNOPSIS Removes old versions of PowerShell modules, keeping only the latest version. .DESCRIPTION The Remove-OldModuleVersions function scans the module paths defined in the $env:PSModulePath environment variable and removes all but the latest version of each module. This helps in cleaning up disk space by removing outdated module versions. .PARAMETER None This function does not take any parameters. .PARAMETER WhatIf Shows what would happen if the command runs. The command is not executed. .PARAMETER Confirm Prompts you for confirmation before running the command. .EXAMPLE Remove-OldModuleVersions This command will remove old versions of PowerShell modules, keeping only the latest version in each module path. .NOTES - This script requires PowerShell 7.0 or higher. - The script supports macOS and Windows operating systems. - On Windows, the system module path "C:\Windows\system32\WindowsPowerShell\v1.0\Modules" is ignored. #> function Remove-OldModuleVersion { [CmdletBinding(SupportsShouldProcess = $true)] param() # Script will run only with PowerShell 7.0 or higher Write-Debug "Checking PowerShell version..." if ($PSVersionTable.PSVersion.Major -lt 7) { Write-Error "This script requires PowerShell 7.0 or higher" return $null } # Define delimiter of $env:PSModulePath variable based on OS if ($IsMacOS) { $delimiter = ':' Write-Debug "Detected macOS, using ':' as delimiter" } elseif ($IsWindows) { $delimiter = ';' Write-Debug "Detected Windows, using ';' as delimiter" # Ignore the system module path on Windows $ignoreWindowsSystemPath = @("C:\Windows\system32\WindowsPowerShell\v1.0\Modules") # Administrative rights are required to remove modules on Windows Write-Debug "Checking if running as administrator..." if (-not ([Security.Principal.WindowsPrincipal][Security.Principal.WindowsIdentity]::GetCurrent()).IsInRole([Security.Principal.WindowsBuiltInRole] "Administrator")) { Write-Error "This script requires administrative rights to remove modules" -Category PermissionDenied return $null } } else { Write-Error "Not detected macOS or Windows. No support!" -Category DeviceError return $null } # Get all module paths from $env:PSModulePath $modulePaths = $env:PSModulePath.split($delimiter) | Where-Object { $_ -notin $ignoreWindowsSystemPath } Write-Verbose "Module paths: $($modulePaths -join ', ')" $amountOfData = 0 # Loop through each module path foreach ($modulePath in $modulePaths) { # Get all modules in the module path $modules = Get-ChildItem -Path $modulePath -Directory -ErrorAction SilentlyContinue # Loop through each module foreach ($module in $modules) { Write-Verbose "Checking module '$($module.FullName)'..." $moduleVersions = Get-ChildItem -Path $module.FullName -Directory # Get the latest version of the module $latestVersion = $moduleVersions | Sort-Object Name -Descending | Select-Object -First 1 # Remove all versions except the latest version $moduleVersions | Where-Object { $_.Name -ne $latestVersion.Name } | ForEach-Object { Write-Verbose "Removing version '$($_.Name)'..." # Count amount of data to be removed $amountOfData += (Get-ChildItem -Path $_.FullName -Recurse | Measure-Object -Property Length -Sum).Sum if ($PSCmdlet.ShouldProcess($_.FullName, "Remove-Item")) { Remove-Item -Path $_.FullName -Recurse -Force Write-Verbose "Removed version '$($_.Name)'" } } } } # Calculate in MB $amountOfData = [math]::Round($amountOfData / 1MB, 2) Write-Verbose "Removed $amountOfData MB of data" return $null } <# .SYNOPSIS Encrypts a secure string and sets it to the clipboard. .DESCRIPTION This function prompts the user to enter a secure string, encrypts it, and then sets the encrypted string to the clipboard. .PARAMETER None This function does not take any parameters. .PARAMETER WhatIf Shows what would happen if the command runs. The command is not executed. .PARAMETER Confirm Prompts you for confirmation before running the command. .EXAMPLE Set-ClipboardWithEncryptedString Prompts the user to enter a secure string, encrypts it, and sets the encrypted string to the clipboard. .NOTES Author: Stefan Gericke Date: 2024-11-06 #> function Set-ClipboardWithEncryptedString { [CmdletBinding(SupportsShouldProcess = $true)] Param() # Prompt the user to enter a secure string $secureString = Read-Host -AsSecureString -Prompt "Enter the secure string you need in the clipboard" Write-Debug "Secure string entered: $(Get-MaskString -InputString ([Runtime.InteropServices.Marshal]::PtrToStringAuto([Runtime.InteropServices.Marshal]::SecureStringToBSTR($secureString))) -VisibleLength 3)" # Convert the secure string to an encrypted standard string $encryptedString = $secureString | ConvertFrom-SecureString # Set the encrypted string to the clipboard if ($PSCmdlet.ShouldProcess("clipboard", "Set encrypted string")) { $encryptedString | Set-Clipboard } } <# .SYNOPSIS Writes a log message to a specified log file with a given log level. .DESCRIPTION The Write-Log function writes a log message to a specified log file with a given log level. It supports logging messages with different levels such as Error, Warn, Info, Ok, Failed, and Success. The function can also log the date and time in UTC or local time based on the provided parameters. .PARAMETER LogMessage The message to be logged. This parameter is mandatory. .PARAMETER LogLevel The level of the log message. This parameter is mandatory and must be one of the following values: Error, Warn, Info, Ok, Failed, Success. .PARAMETER LogFile The path to the log file where the message will be written. If not specified, the function will use the script variable $script:logPath. .PARAMETER UTC A boolean parameter that indicates whether to log the date and time in UTC. If not specified, the function will use local time. .EXAMPLE Write-Log -LogMessage "This is an informational message" -LogLevel "Info" -LogFile "C:\Logs\logfile.txt" -UTC $true This example writes an informational message to the specified log file in UTC time. .EXAMPLE Write-Log -LogMessage "An error occurred" -LogLevel "Error" This example writes an error message to the default log file in local time. .NOTES The function requires the Set-LogPath and Set-Utc functions to be defined in the script or module. #> function Write-Log { [CmdletBinding()] param( [Parameter(Mandatory = $true)] [string] $LogMessage, [Parameter(Mandatory = $true)] [ValidateNotNullOrEmpty()] [ValidateSet("Error", "Warn", "Info", "Ok", "Failed", "Success")] [string] $LogLevel, [string] $LogFile, [bool] $UTC ) # Check the LogFile variable is existing in script variable or parameter if ($script:logPath -ne $LogFile) { if (![string]::IsNullOrEmpty($LogFile)) { Set-LogPath -Path $LogFile } } Write-Debug "Log Path: $script:logPath" # Check the Utc variable is used as a parameter and set the Utc variable $utcUsed = $PSBoundParameters.ContainsKey('UTC') if ($utcUsed) { if ($UTC) { Set-Utc -Utc $true } else { Set-Utc -Utc $false } } Write-Debug "UTC: $script:utc" # Set Date/Time if ($script:utc) { # Get the current date and time in UTC $dateTime = Get-Date -Format "yyyy-MM-dd HH:mm:ss" -AsUTC } else { # Get the current date and time in local time $dateTime = Get-Date -Format "yyyy-MM-dd HH:mm:ss" } # Set log level $strLength = $MyInvocation.MyCommand.Parameters.LogLevel.Attributes.ValidValues | Sort-Object -Property Length -Descending | Select-Object -First 1 -ExpandProperty Length $countSpaces = $strLength + 3 - $LogLevel.Length $line = $dateTime + " " + $LogLevel.ToUpper() + (" " * $countSpaces ) + $LogMessage try { $line | Out-File -FilePath $script:logPath -Append # Write the line on the terminal Write-Verbose $line return $line } catch { $message = $_ Write-Error -Exception ([System.IO.IOException]::new("Message ($line) not added to log file: $message")) -ErrorAction Continue return $line } } # Utilities/Write-Log.ps1 $script:logPath = Join-Path -Path $Env:TEMP -ChildPath "$(Get-Date -Format yyyyMMdd_HHmm)_Script.log" $script:utc = $false # Rest API/Send-Pushover.ps1 $script:pushoverUserKey = New-Object -TypeName System.Security.SecureString $script:pushoverApiKey = New-Object -TypeName System.Security.SecureString |