AzStackHci.DiagnosticSettings.psm1

##########################################################################################################
<#
.SYNOPSIS
    Author: Neil Bird, MSFT
    Version: 0.4.9
    Created: July 16th 2024
    Updated: March 28th 2025
 
.DESCRIPTION
 
    This module includes functions that automate configuring diagnostics settings and/or validation for Azure Local clusters. This includes
    a minimum size for the page file (if the node has low disk space) and Kernel memory dump settings. It also includes functions to test
    connectivity between nodes and Azure, and tests to detect if SSL inspection is present.
 
    # Memory Dump Settings references:
        # https://blogs.msdn.microsoft.com/clustering/2015/05/18/windows-server-2016-failover-cluster-troubleshooting-enhancements-active-dump/
        # https://blogs.msdn.microsoft.com/clustering/2016/03/02/troubleshooting-hangs-using-live-dump/
        # https://blogs.msdn.microsoft.com/ntdebugging/2010/04/02/how-to-use-the-dedicateddumpfile-registry-value-to-overcome-space-limitations-on-the-system-drive-when-capturing-a-system-memory-dump/
        # https://support.microsoft.com/en-us/help/949052/kernel-memory-dump-files-may-not-be-generated-on-windows-server-2008-b
        # https://learn.microsoft.com/en-us/windows/win32/wer/wer-settings
        # https://learn.microsoft.com/en-us/troubleshoot/windows-server/performance/memory-dump-file-options
        # https://learn.microsoft.com/en-us/windows-server/administration/server-core/server-core-memory-dump
        # https://learn.microsoft.com/en-us/azure/azure-local/concepts/firewall-requirements
 
.EXAMPLE
 
    This module contains the following exported functions:
 
        1. Get-AzStackHciMemoryDumpSettings
        2. Set-AzStackHciMemoryDumpSettings
        3. Restore-AzStackHciMemoryDumpSettings
        4. Get-AzStackHciPageFileSettings
        5. Restore-AzStackHciPageFileSettings
        6. Set-AzStackHciPageFileMinimumSettings
        7. Set-AzStackHciUserModeCrashDumpSettings
        8. Test-AzStackHciSSLInspection
        9. Send-ClusterPerformanceHistory
        10. Test-AzureLocalConnectivity
        11. Test-Layer7Connectivity
        12. Test-TCPConnectivity
     
.NOTES
    THIS CODE-SAMPLE IS PROVIDED "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED
    OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE IMPLIED WARRANTIES OF MERCHANTABILITY AND/OR
    FITNESS FOR A PARTICULAR PURPOSE.
 
    This sample is not supported under any Microsoft standard support program or service.
    The script is provided AS IS without warranty of any kind. Microsoft further disclaims all
    implied warranties including, without limitation, any implied warranties of merchantability
    or of fitness for a particular purpose. The entire risk arising out of the use or performance
    of the sample and documentation remains with you. In no event shall Microsoft, its authors,
    or anyone else involved in the creation, production, or delivery of the script be liable for
    any damages whatsoever (including, without limitation, damages for loss of business profits,
    business interruption, loss of business information, or other pecuniary loss) arising out of
    the use of or inability to use the sample or documentation, even if Microsoft has been advised
    of the possibility of such damages, rising out of the use of or inability to use the sample script,
    even if Microsoft has been advised of the possibility of such damages.
 
#>

##########################################################################################################

#Requires -Version 5.0

# ///////////////////////////////////////////////////////////////////
# SetDedicatedDumpFileSize Function
# Used to calculate size of dedicated dump file, based on the
# physical node memory size.
# ///////////////////////////////////////////////////////////////////
Function SetDedicatedDumpFileSize {
    <#
    .SYNOPSIS
 
    Sets the size of the dedicated dump file
 
    .DESCRIPTION
 
    Sets the size of the dedicated dump file, based on the physical node memory size.
 
    #>


    # Requires administrator permissions to set dedicated dump file size and check system memory
    if(-NOT ([Security.Principal.WindowsPrincipal][Security.Principal.WindowsIdentity]::GetCurrent()).IsInRole([Security.Principal.WindowsBuiltInRole] "Administrator")){
        # If not running as an Administrator, throw an error and exit
        Write-Error "This script must be run as an Administrator."
        return
    }

    # Set default minimum dedicated dump file size, for systems with less than 768 GiB of memory
    [uint32]$SetDedicatedDumpFileSize = 65536 # 64GB
    try
    {
        $totalPhysicalMemory = (Get-CimInstance -ClassName Win32_PhysicalMemory | Measure-Object -Property Capacity -Sum).Sum
        Write-Verbose "System memory size == $([math]::round($totalPhysicalMemory/1Gb,2)) GiB" -Verbose
        if ($totalPhysicalMemory -ge 768GB)
        {
            # large memory systems, require 128 GiB dedicated dump file size
            $SetDedicatedDumpFileSize = 131072 # 128 GiB
        }
    }
    catch
    {
        # If we fail to get total physical memory, just use the default size.
        Write-Verbose "Failed getting total physical memory. Error: $($_.Exception.Message)" -Verbose
        Throw "Failed getting total physical memory. Error: $($_.Exception.Message)"
    }
    Write-Verbose "Dedicated Dump File Size set to '$SetDedicatedDumpFileSize' MiB" -Verbose

    return $SetDedicatedDumpFileSize

} # End of SetDedicatedDumpFileSize

# ///////////////////////////////////////////////////////////////////
# SetDedicatedDumpFilePath Function
# Used to define the path for the dedicated dump file, using
# the 'DedicatedDumpFileDriveLetter' input parameter
# ///////////////////////////////////////////////////////////////////
Function SetDedicatedDumpFilePath {
    <#
    .SYNOPSIS
 
    Sets the path for the dedicated dump file
 
    .DESCRIPTION
 
    Sets the path for the dedicated dump file, using the 'DedicatedDumpFileDriveLetter' input parameter.
    Checks free disks space on the target drive, and ensures there is sufficient space for the dedicated dump file.
 
    #>


    [CmdletBinding()]
    param (
        # Path to drive letter for DedicatedDumpFile
        [Parameter(Mandatory=$true,Position=1)]
        [ValidateScript({Test-Path $_})]
        [String]
        [ValidateLength(2,3)]$DedicatedDumpFileDriveLetter, # Drive letter for the dedicated dump file, must be a valid drive letter

        # Optional switch, used to ignore disk space check for the dedicated dump file
        [Parameter(Mandatory=$false,Position=2)]
        [Switch]
        $IgnoreDiskSpaceCheck
    )

    # Requires administrator permissions to set dedicated dump file path and check disk space
    if(-NOT ([Security.Principal.WindowsPrincipal][Security.Principal.WindowsIdentity]::GetCurrent()).IsInRole([Security.Principal.WindowsBuiltInRole] "Administrator")){
        # If not running as an Administrator, throw an error and exit
        Write-Error "This script must be run as an Administrator."
        return
    }

    # if the drive letter is only 2 characters (C:), add a backslash to the end (C:\)
    if($DedicatedDumpFileDriveLetter.Length -eq 2){
        $DedicatedDumpFileDriveLetter = $($DedicatedDumpFileDriveLetter + "\")
    }

    # Create the path for the dedicated dump file using the drive letter
    [string]$DedicatedDumpFilePath = "$($DedicatedDumpFileDriveLetter)DedicatedDumpFile.sys"
    try
    {
        # Check path and disk space
        $Disk = Get-PSDrive -ErrorAction Stop | Where-Object { $PSItem.Root -eq $DedicatedDumpFileDriveLetter }
        Write-Verbose "Current Disk Free Size = $([math]::round($Disk.Free / 1GB,2)) GiB" -Verbose
        Write-Verbose "Dedicated Dump File size required for node = $($script:DedicatedDumpFileSize*1Mb/1Gb) GiB" -Verbose
        # Add the minimum disk space required for the dedicated dump file, plus 10% of the total disk space
        $script:MinimumRequiredDiskSpace = [math]::round($script:DedicatedDumpFileSize*1Mb/1Gb + ((($disk.Free + $disk.Used)/1Gb) * 0.1),2)
        Write-Verbose "Minimum Required Disk Space, including 10% additional free disk space = $script:MinimumRequiredDiskSpace GiB" -Verbose
        if (($Disk.Free/1Gb -lt $script:MinimumRequiredDiskSpace) -and (-not($IgnoreDiskSpaceCheck.IsPresent)))
        {
            Write-Verbose "Node physical disk free space is below minimum size for large host dump settings + 10% reserve, so settings can not be applied." -Verbose
            throw "Node physical disk free space is below minimum size for large host dump settings + 10% reserve, so settings can not be applied."
        } else {
            if($IgnoreDiskSpaceCheck.IsPresent){
                Write-Verbose "*** Free disk space checks have been ignored, settings can be applied.... ***" -Verbose
            } else { 
                Write-Verbose "Node physical disk free space is above minimum required for dedicated dump settings + 10% reserve. Settings can be applied." -Verbose
                Write-Verbose "Sufficient disk space on $DedicatedDumpFileDriveLetter for a dedicated dump file of $([math]::round($DedicatedDumpFileSize*1MB/1Gb)) GiB." -Verbose
            }
        }
    } catch{
        # If we fail to get free disk space on target drive, error
        Write-Verbose "Failed getting free disk space on $DedicatedDumpFileDriveLetter. Error: $($_.Exception.Message)" -Verbose
        Throw "Failed getting free disk space on $DedicatedDumpFileDriveLetter. Error: $($_.Exception.Message)"
    }
    Write-Verbose "Dedicate Dump File Path = '$DedicatedDumpFilePath' and there is sufficient free space on the target drive."

    return $DedicatedDumpFilePath

} # End of SetDedicatedDumpFilePath

# ///////////////////////////////////////////////////////////////////
# Get-AzStackHciPageFileSettings Function
# Used to get information about the current page file settings
# on the system.
# ///////////////////////////////////////////////////////////////////
Function Get-AzStackHciPageFileSettings {
    <#
    .SYNOPSIS
 
    Gets current page file settings
 
    .DESCRIPTION
 
    Queries the system for current page file settings
 
    #>


    # Requires administrator permissions to get page file settings and write to log file to disk
    if(-NOT ([Security.Principal.WindowsPrincipal][Security.Principal.WindowsIdentity]::GetCurrent()).IsInRole([Security.Principal.WindowsBuiltInRole] "Administrator")){
        # If not running as an Administrator, throw an error and exit
        Write-Error "This script must be run as an Administrator."
        return
    }

    $DateFormatted = Get-Date -f "yyyyMMdd"
    if(-not(Test-Path "C:\ProgramData\AzStackHci.DiagnosticSettings\"))
    {
        New-Item "C:\ProgramData\AzStackHci.DiagnosticSettings\" -ItemType Directory -ErrorAction SilentlyContinue | Out-Null
    }
    
    try {
        # Get current page file settings
        [bool]$script:PageFileAutoManaged = (Get-CimInstance Win32_ComputerSystem).AutomaticManagedPagefile
    } catch {
        Write-Verbose "Failed to get current page file settings. Error: $($_.Exception.Message)" -Verbose
        Throw "Failed to get current page file settings. Error: $($_.Exception.Message)"
    }
    
    "$(Get-Date -Format "yyyy-MM-dd HH:mm:ss")"  | Out-File $("C:\ProgramData\AzStackHci.DiagnosticSettings\PageFile_Settings_$DateFormatted.txt") -ErrorAction Stop

    "Saving Page File configuration for node $($env:computername)" | Out-File $("C:\ProgramData\AzStackHci.DiagnosticSettings\PageFile_Settings_$DateFormatted.txt") -Append -ErrorAction Stop

    if($script:PageFileAutoManaged -eq $False){
        # Page File manual settings
        Write-Verbose "Current Settings: Page File automatic management is Disabled" -Verbose
        "Current Settings: Page File automatic management is Disabled`n" | Out-File $("C:\ProgramData\AzStackHci.DiagnosticSettings\PageFile_Settings_$DateFormatted.txt") -Append -ErrorAction Stop
        Write-Verbose "Current Page File Configuration:" -Verbose
        "Current Page File Configuration:" | Out-File $("C:\ProgramData\AzStackHci.DiagnosticSettings\PageFile_Settings_$DateFormatted.txt") -Append -ErrorAction Stop
        # Note: if Page File Automatic Manager = True, the PageFileSettings is Null
        try {
            $script:PageFileConfiguration = Get-WmiObject -ClassName Win32_PageFileSetting -EnableAllPrivileges
        } catch {
            Write-Verbose "Failed to get current page file settings. Error: $($_.Exception.Message)" -Verbose
            Throw "Failed to get current page file settings. Error: $($_.Exception.Message)"
        }
        $PageFileSettings = ("Caption", "Description", "InitialSize", "MaximumSize" , "Name", "SettingID")
        foreach($PageFileSetting in $PageFileSettings){
            Write-Verbose "`t$PageFileSetting = $($PageFileConfiguration.$PageFileSetting)" -Verbose
            "`t$PageFileSetting = $($PageFileConfiguration.$PageFileSetting)" | Out-File $("C:\ProgramData\AzStackHci.DiagnosticSettings\PageFile_Settings_$DateFormatted.txt") -Append -ErrorAction Stop
        }
        Write-Verbose "`n" -Verbose
        
    } elseif($script:PageFileAutoManaged -eq $True){
        Write-Verbose "Current Settings: Page File automatic management is Enabled" -Verbose
        "Current Settings: Page File automatic management is Enabled" | Out-File $("C:\ProgramData\AzStackHci.DiagnosticSettings\PageFile_Settings_$DateFormatted.txt") -Append -ErrorAction Stop
    
    } else {
        Write-Error "Current Settings: Page File automatic management is Unknown" -Verbose
        "Current Settings: Page File automatic management is Unknown" | Out-File $("C:\ProgramData\AzStackHci.DiagnosticSettings\PageFile_Settings_$DateFormatted.txt") -Append -ErrorAction Stop
    }
        
    # Page File usage: (info only, not always accurate)
    try {
        $script:PageFileUsage = Get-CimInstance Win32_PageFileUsage
    } catch {
        Write-Verbose "Failed to get page file usage. Error: $($_.Exception.Message)" -Verbose
        Throw "Failed to get page file usage. Error: $($_.Exception.Message)"
    }
    $PageFileUsageAttributes = ("AllocatedBaseSize", "Caption", "CurrentUsage", "Description" , "InstallDate", "Name", "PeakUsage", "Status", "TempPageFile")
    Write-Verbose "Page File usage:" -Verbose
    "Page File usage:" | Out-File $("C:\ProgramData\AzStackHci.DiagnosticSettings\PageFile_Settings_$DateFormatted.txt") -Append -ErrorAction Stop
    foreach($PageFileUsageAttribute in $PageFileUsageAttributes){
        Write-Verbose "`t$PageFileUsageAttribute = $($PageFileUsage.$PageFileUsageAttribute)" -Verbose
        "`t$PageFileUsageAttribute = $($PageFileUsage.$PageFileUsageAttribute)" | Out-File $("C:\ProgramData\AzStackHci.DiagnosticSettings\PageFile_Settings_$DateFormatted.txt") -Append -ErrorAction Stop
    }
    $script:PageFileAllocatedBaseSize = $PageFileUsage.AllocatedBaseSize
    $script:PageFileCurrentUsage = $PageFileUsage.CurrentUsage
    $script:PageFilePeakUsage = $PageFileUsage.PeakUsage

    Write-Verbose "Current Page File settings exported to 'C:\ProgramData\AzStackHci.DiagnosticSettings\PageFile_Settings_$DateFormatted.txt'" -Verbose

    Write-Host `n

} # End of Get-AzStackHciPageFileSettings

# ///////////////////////////////////////////////////////////////////
# Set-AzStackHciPageFileMinimumSettings Function
# Used to set a static 4GB page file on the system, target drive is
# the only parameter.
# ///////////////////////////////////////////////////////////////////
Function Set-AzStackHciPageFileMinimumSettings {
    <#
    .SYNOPSIS
 
    Sets page file settings to minimum 4GB fixed size
 
    .DESCRIPTION
 
    Queries the system for current page file settings
 
    #>


    [CmdletBinding()]
    param (
        # Path to drive letter for PageFile
        [Parameter(Mandatory=$true,Position=1)]
        [ValidateScript({Test-Path $_})]
        [String]
        [ValidateLength(2,3)]$PageFileFileDriveLetter # Drive letter for the page file, must be a valid drive letter
    )

    # Requires administrator permissions to set page file settings
    if(-NOT ([Security.Principal.WindowsPrincipal][Security.Principal.WindowsIdentity]::GetCurrent()).IsInRole([Security.Principal.WindowsBuiltInRole] "Administrator")){
        # If not running as an Administrator, throw an error and exit
        Write-Error "This script must be run as an Administrator."
        return
    }

    # Call function to get current Page File settings and save to backup log file
    Get-AzStackHciPageFileSettings

    # if the drive letter is only 2 characters (C:), add a backslash to the end (C:\)
    if($PageFileFileDriveLetter.Length -eq 2){
        $PageFileFileDriveLetter = $($PageFileFileDriveLetter + "\")
    }
    try {
        # Get current page file settings
        [bool]$script:PageFileAutoManaged = (Get-CimInstance Win32_ComputerSystem).AutomaticManagedPagefile
    } catch {
        Write-Verbose "Failed to get current page file settings. Error: $($_.Exception.Message)" -Verbose
        Throw "Failed to get current page file settings. Error: $($_.Exception.Message)"
    }
    
    # If page file is NOT automatically managed
    if($script:PageFileAutoManaged -eq $False){
        # Page File manual settings
        Write-Host "Automatic management of page file already disabled"
        Write-Host "Configuring a fixed page file of 4GB size using file: $($PageFileFileDriveLetter)pagefile.sys`n"
        try {
            $script:PageFileConfiguration = Get-WmiObject -ClassName Win32_PageFileSetting -EnableAllPrivileges
        } catch {
            Write-Verbose "Failed to get existing page file settings. Error: $($_.Exception.Message)" -Verbose
            Throw "Error: Failed to get existing page file settings. Error: $($_.Exception.Message)"
        }
        # Remove existing manual page file settings
        if($PageFileConfiguration){
            if(($PageFileConfiguration | Measure-Object).Count -eq 1){
                try {
                    # Delete existing page file settings
                    $PageFileConfiguration.Delete()
                } catch {
                    Write-Verbose "Failed to delete existing page file settings. Error: $($_.Exception.Message)" -Verbose
                    Throw "Error: Failed to delete existing page file settings. Error: $($_.Exception.Message)"
                }
                Write-Verbose "Existing page file settings removed." -Verbose
            } else {
                Write-Verbose "Unexpected configuration, more than one page file is currently configured! Expected 1, but found $(($PageFileConfiguration | Measure-Object).Count) x page files." -Verbose
                Throw "Error: Unexpected configuration, more than one page file currently configured! Expected 1, but found $(($PageFileConfiguration | Measure-Object).Count) x page files."
            }
        } else {
            Write-Verbose "Existing page file settings not found." -Verbose
        }
        # Set a New page file, and set the size to 4GB fixed
        try 
        {
            # Use New-CimInstance to create a new page file setting, due to "Set-WmiInstance : Generic failure" error, if no existing page file settings are configured (as deleted).
            $PageFile = New-CimInstance -ClassName Win32_PageFileSetting -Property @{ Name= "$($PageFileFileDriveLetter)pagefile.sys" }
            $PageFile | Set-CimInstance -Property @{ InitialSize = 4096; MaximumSize = 4096 }
        } catch {
            Write-Verbose "Failed to set minimum page file settings. Error: $($_.Exception.Message)" -Verbose
            Throw "Failed to set minimum page file settings. Error: $($_.Exception.Message)"
        }
        if($PageFile){
            Write-Verbose "Page File settings updated successfully to a fixed size of 4GB" -Verbose
            Write-Verbose "Page File: = '$($pagefile.Description)'" -Verbose
        } else {
            Write-Verbose "Failed to configure minimum page file settings." -Verbose
            Throw "Error: Failed to configure minimum page file settings."
        }

    } elseif($script:PageFileAutoManaged -eq $True){
        # If the page file is currently automatically managed
        Write-Host "Disabling automatic management of page file"
        # Remove System Managed:
        try {
            $ComputerSystem = Get-WmiObject -ClassName Win32_ComputerSystem -EnableAllPrivileges    
        }
        catch {
            Write-Verbose "Failed to get existing automatic management settings from WMI. Error: $($_.Exception.Message)" -Verbose
            Throw "Error: Failed to get existing automatic management settings from WMI. Error: $($_.Exception.Message)"
        }
        try {
            $ComputerSystem.AutomaticManagedPagefile = $false
            $ComputerSystem.Put() | Out-Null
        }
        catch {
            Write-Verbose "Failed to disable automatic management of page file. Error: $($_.Exception.Message)" -Verbose
            Throw "Error: Failed to disable automatic management of page file. Error: $($_.Exception.Message)"
        }
        Write-Verbose "Automatic management of page file disabled" -Verbose

        try {
            $script:PageFileConfiguration = Get-WmiObject -ClassName Win32_PageFileSetting -EnableAllPrivileges
        } catch {
            Write-Verbose "Failed to get existing page file settings. Error: $($_.Exception.Message)" -Verbose
            Throw "Error: Failed to get existing page file settings. Error: $($_.Exception.Message)"
        }

        Write-Host "Configuring a fixed page file of 4GB size using file: $($PageFileFileDriveLetter)pagefile.sys`n"
        # Remove existing page file setting and set a new page file, and set the size to 4GB fixed
        if($PageFileConfiguration){
            if(($PageFileConfiguration | Measure-Object).Count -eq 1){
                try {
                    # Delete existing page file settings
                    $PageFileConfiguration.Delete()
                } catch {
                    Write-Verbose "Failed to delete existing page file settings. Error: $($_.Exception.Message)" -Verbose
                    Throw "Error: Failed to delete existing page file settings. Error: $($_.Exception.Message)"
                }
                Write-Verbose "Existing page file settings removed." -Verbose
            } else {
                Write-Verbose "Unexpected configuration, more than one page file is currently configured! Expected 1, but found $(($PageFileConfiguration | Measure-Object).Count) x page files." -Verbose
                Throw "Error: Unexpected configuration, more than one page file currently configured! Expected 1, but found $(($PageFileConfiguration | Measure-Object).Count) x page files."
            }

            # Set a New page file, and set the size to 4GB fixed
            try 
            {
                # Use New-CimInstance to create a new page file setting, due to "Set-WmiInstance : Generic failure" error, if no existing page file settings are configured (as deleted).
                $PageFile = New-CimInstance -ClassName Win32_PageFileSetting -Property @{ Name= "$($PageFileFileDriveLetter)pagefile.sys" }
                $PageFile | Set-CimInstance -Property @{ InitialSize = 4096; MaximumSize = 4096 }
            } catch {
                Write-Verbose "Failed to set new page file settings. Error: $($_.Exception.Message)" -Verbose
                Throw "Failed to set new page file settings. Error: $($_.Exception.Message)"
            }
            if($PageFile){
                Write-Verbose "Page File settings updated successfully to a fixed size of 4GB" -Verbose
                Write-Verbose "Page File: = '$($pagefile.Description)'" -Verbose
            } else {
                Write-Verbose "Failed to configure minimum page file settings." -Verbose
                Throw "Error: Failed to configure minimum page file settings."
            }
            Write-Verbose "Page File settings updated to minimum recommended configuration, (static size of 4GB)." -Verbose
        }
        
        Write-Verbose "A system restart is required for Page File changes to take effect.`n`n" -Verbose

    } else {
        Write-Error "Current Settings: Page File automatic management is Unknown (not enabled or disabled)." -Verbose
        Throw "Error: Current Settings: Page File automatic management is Unknown (not enabled or disabled)."
    } # End of If-Else

} # End of Set-AzStackHciPageFileMinimumSettings

# ///////////////////////////////////////////////////////////////////
# Set-AzStackHciMemoryDumpSettings Main function
# Used to set the registry settings required to obtain a kernel memory dump
# ///////////////////////////////////////////////////////////////////
Function Set-AzStackHciMemoryDumpSettings {
    <#
    .SYNOPSIS
 
    Sets the registry settings required to obtain a kernel memory dump
 
    .DESCRIPTION
 
    Sets the fourteen recommended registry settings required for the system to generate a kernel memory dump.
    Enabling memory dumps on the nodes with large memory size, the size of the dedicated dump file is
    calculated based on the system memory present in the node.
    #>


    [CmdletBinding()]
    param (
        # Path to drive letter for DedicatedDumpFile
        [Parameter(Mandatory=$true,Position=1)]
        [ValidateScript({Test-Path $_})]
        [String]
        [ValidateLength(2,3)]$DedicatedDumpFileDriveLetter, # Drive letter for the dedicated dump file, must be a valid drive letter

        # Optional switch, used to minimize the page file size if required to save space
        [Parameter(Mandatory=$false,Position=2)]
        [Switch]
        $ConfigureMinimumPageFile,

        # Optional switch, used to ignore disk space check for the dedicated dump file
        [Parameter(Mandatory=$false,Position=3)]
        [Switch]
        $IgnoreDiskSpaceCheck
    )

    # Requires administrator permissions to set memory dump settings in the registry
    if(-NOT ([Security.Principal.WindowsPrincipal][Security.Principal.WindowsIdentity]::GetCurrent()).IsInRole([Security.Principal.WindowsBuiltInRole] "Administrator")){
        # If not running as an Administrator, throw an error and exit
        Write-Error "This script must be run as an Administrator."
        return
    }

    # if the drive letter is only 2 characters (C:), add a backslash to the end (C:\)
    if($DedicatedDumpFileDriveLetter.Length -eq 2){
        $DedicatedDumpFileDriveLetter = $($DedicatedDumpFileDriveLetter + "\")
    }

    # Call function to Set the Size of the DedicateDumpFile based on node memory size
    [uint32]$script:DedicatedDumpFileSize = SetDedicatedDumpFileSize

    # Call function to Set the Path of the DedicateDumpFile and validate there is sufficient space
    if($IgnoreDiskSpaceCheck.IsPresent){
        # Call function to Set the Path of the DedicateDumpFile and ignore disk space check
        [string]$script:DedicatedDumpFilePath = SetDedicatedDumpFilePath $DedicatedDumpFileDriveLetter -IgnoreDiskSpaceCheck
    } else {
        # Call function to Set the Path of the DedicateDumpFile and validate there is sufficient space
        [string]$script:DedicatedDumpFilePath = SetDedicatedDumpFilePath $DedicatedDumpFileDriveLetter
    }

    if($ConfigureMinimumPageFile.IsPresent){
        # Call function to Set Page File to minimum 4GB settings
        Set-AzStackHciPageFileMinimumSettings -PageFileFileDriveLetter $DedicatedDumpFileDriveLetter
    }

    # Call function to Get and Save current page file settings
    Get-AzStackHciMemoryDumpSettings

    $HKLMCrashControl = "HKLM:\System\CurrentControlSet\Control\CrashControl"
    try { 
        Set-ItemProperty -Path $HKLMCrashControl -Name AutoReboot -Type DWord -Value 1 -ErrorAction Stop # Automatic reboot
        Set-ItemProperty -Path $HKLMCrashControl -Name DisableEmoticon -Type DWord -Value 1 -ErrorAction Stop # Disable emoticons (display bug check error information on console)
        Set-ItemProperty -Path $HKLMCrashControl -Name CrashDumpEnabled -Type DWord -Value 2 -ErrorAction Stop # Kernel memory dump
        Set-ItemProperty -Path $HKLMCrashControl -Name FilterPages -Type DWord -Value 1 -ErrorAction Stop # Filter pages, used for active memory dump, but still set it for kernel memory dump
        Set-ItemProperty -Path $HKLMCrashControl -Name NMICrashDump -Type DWord -Value 1 -ErrorAction Stop # Support NMI crashes
        Set-ItemProperty -Path $HKLMCrashControl -Name DedicatedDumpFile -Type String -Value $DedicatedDumpFilePath -ErrorAction Stop # Dedicated Dump File Path
        Set-ItemProperty -Path $HKLMCrashControl -Name DumpFileSize -Type DWord -Value $DedicatedDumpFileSize -ErrorAction Stop # Dedicated Dump File size
        Set-ItemProperty -Path $HKLMCrashControl -Name IgnorePagefileSize -Type DWord -Value 1 -ErrorAction Stop # Required for large memory systems
        Set-ItemProperty -Path $HKLMCrashControl -Name AlwaysKeepMemoryDump -Type DWord -Value 1 -ErrorAction Stop # Prevents automatic deletion of memory dump files
        Set-ItemProperty -Path $HKLMCrashControl -Name LogEvent -Type DWord -Value 1 -ErrorAction Stop # Log event 1001 in System log
    } catch {
        Write-Verbose "Failed to set CrashControl registry settings. Error: $($_.Exception.Message)" -Verbose
        Throw "Failed to set CrashControl registry settings. Error: $($_.Exception.Message)"
    }
    
    # Memory dump file, should be the same drive as the dedicated dump file, so that the dedicated dump file is renamed to memory.dmp on system restart.
    try {
        Set-ItemProperty -Path $HKLMCrashControl -Name DumpFile -Type ExpandString -Value "$($DedicatedDumpFileDriveLetter)memory.dmp" -ErrorAction Stop
    } catch {
        Write-Verbose "Failed to set DumpFile registry settings. Error: $($_.Exception.Message)" -Verbose
        Throw "Failed to set DumpFile registry settings. Error: $($_.Exception.Message)"
    }

    try {
        #<!-- MinidumpsCount, Overwrite -->
        Set-ItemProperty -Path $HKLMCrashControl -Name MinidumpsCount -Type DWord -Value 1 -ErrorAction Stop
        Set-ItemProperty -Path $HKLMCrashControl -Name Overwrite -Type DWord -Value 1 -ErrorAction Stop
        #<!-- Minidump Dirrectory -->
        if(-not(test-path "C:\Windows\Minidump")) { New-Item "C:\Windows\Minidump" -ItemType Directory -ErrorAction SilentlyContinue | Out-Null }
        Set-ItemProperty -Path $HKLMCrashControl -Name MinidumpDir -Type ExpandString -Value "C:\Windows\Minidump\" -ErrorAction Stop
    } catch { 
        Write-Verbose "Failed to set Minidump registry settings. Error: $($_.Exception.Message)" -Verbose
        Throw "Failed to set Minidump registry settings. Error: $($_.Exception.Message)"
    }

    Write-Verbose "System memory dump settings have been configured for a Kernel Memory Dump to be written to '$($DedicatedDumpFileDriveLetter)memory.dmp'" -Verbose
    Write-Verbose "Dedicated Dump File configured to '$DedicatedDumpFilePath', with a size of $DedicatedDumpFileSize MiB ($([math]::round($DedicatedDumpFileSize*1MB/1Gb)) GiB).`n`n" -Verbose
    Write-Verbose "A system restart is required for the Memory Dump changes to take effect." -Verbose

} # End of Set-AzStackHciMemoryDumpSettings

# ///////////////////////////////////////////////////////////////////
# Restore-AzStackHciMemoryDumpSettings function
# Used to restore or roll back crash control registry settings
# ///////////////////////////////////////////////////////////////////
Function Restore-AzStackHciMemoryDumpSettings
{
    <#
    .SYNOPSIS
 
    Restores registry values for crash control / memory dump settings
 
    .DESCRIPTION
 
    Restores the fourteen recommended registry settings from an earlier time, using a backup file as input.
    Requires administrator permissions to restore memory dump settings in the registry
    #>


    [CmdletBinding()]
    param (
        # File path to backup file
        [Parameter(Mandatory=$true,Position=0)]
        [ValidateScript({Test-Path $_})]
        [String]$MemoryDumpSettingsFilePath # File used to restore settings from, must be a valid path

    )

    # Requires administrator permissions to set memory dump settings in the registry
    if(-NOT ([Security.Principal.WindowsPrincipal][Security.Principal.WindowsIdentity]::GetCurrent()).IsInRole([Security.Principal.WindowsBuiltInRole] "Administrator")){
        # If not running as an Administrator, throw an error and exit
        Write-Error "This script must be run as an Administrator."
        return
    }

    # Read the memory dump settings from the backup file into an array
    if($MemoryDumpSettingsFilePath -like "C:\ProgramData\AzStackHci.DiagnosticSettings\MemoryDump_Settings_*"){
        Write-Verbose "Reading Memory Dump settings from backup file: $MemoryDumpSettingsFilePath" -Verbose
    } else {
        Write-Verbose "Memory Dump settings backup file path must start with 'C:\ProgramData\AzStackHci.DiagnosticSettings\MemoryDump_Settings_'" -Verbose
        throw "Error: Memory Dump settings backup file path must start with 'C:\ProgramData\AzStackHci.DiagnosticSettings\MemoryDump_Settings_'"
    }
    [string[]]$RestoreMemoryDumpSettings = Get-Content $MemoryDumpSettingsFilePath -ErrorAction Stop

    $HKLMCrashControl = "HKLM:\System\CurrentControlSet\Control\CrashControl"

    # Date time the Memory Dump settings were saved should be the first line in the file
    try {
        [datetime]$DateTimeMemoryDumpSettings = $RestoreMemoryDumpSettings[0]    
    }
    catch {
        Write-Verbose "Failed to get the date and time the Memory Dump settings were saved. Error: $($_.Exception.Message)" -Verbose
        throw "Error: Failed to get the date and time the Memory Dump settings were saved. Error: $($_.Exception.Message)"
    }
    
    # Confirm with user before restoring the memory dump settings
    Write-Verbose "Restoring Memory Dump settings from $MemoryDumpSettingsFilePath, saved on $DateTimeMemoryDumpSettings" -Verbose
    $RestoreConfirmation = Read-Host "Do you want to restore the memory dump settings from $MemoryDumpSettingsFilePath? (Y/N)" -ErrorAction Stop
    if($RestoreConfirmation -ne "Y"){
        Write-Verbose "Memory Dump settings restore cancelled by user." -Verbose
        return
    }
    
    # Restore the memory dump settings, the first six lines (0-5) are the date and time, and the next 14 lines are the settings
    # i = 6 is the seventh line in the file, which is the first setting
    for($i=6; $i -lt ($RestoreMemoryDumpSettings.Count -2); $i++)
    {
        $Setting = $RestoreMemoryDumpSettings[$i]
        # Debugging output, when running with -Debug
        Write-Debug "Backup file input: $Setting"
        # Split the setting into the name and value
        $SettingName = $Setting.Split(" ",[System.StringSplitOptions]::RemoveEmptyEntries)[0].Trim()
        $SettingValue = $Setting.Split(" ",[System.StringSplitOptions]::RemoveEmptyEntries)[1].Trim()
        # Check if the setting value is a comment, as these are not set in the registry
        if($SettingValue -eq "//Setting"){ 
            $SettingValue = $Setting.Split("//",[System.StringSplitOptions]::RemoveEmptyEntries)[1].Trim()
        }
        # Check if the setting was not configured, so will need deleting to restore the settings
        if($SettingValue -eq "Setting not configured") {
            # Check if the setting is present in the registry
            Remove-Variable TestValue -ErrorAction SilentlyContinue
            try {
                Get-ItemProperty -Path $HKLMCrashControl -Name $SettingName -ErrorAction SilentlyContinue -ErrorVariable TestValue | Out-Null
            } catch {
                Write-Verbose "Setting '$SettingName' not present in the registry." -Verbose
            }
            # Execute the setting removal if it was not configured and is present in the registry
            if(-not($TestValue)) { 
                # Value exists, remove the setting if it was not configured in the backup file
                Write-Verbose "Removing setting '$SettingName' from the registry." -Verbose
                Remove-ItemProperty -Path $HKLMCrashControl -Name $SettingName -Force -ErrorAction Stop | Out-Null
            } else {
                Write-Verbose "Info: Expected setting '$SettingName' to be configured in the registry, but is not." -Verbose
            }
        # Setting is configured still, restore the setting value from the backup file
        } else {
            # Get the current setting in the registry, to get the type of the value
            Remove-Variable TestValue -ErrorAction SilentlyContinue
            try {
                $RegValue = Get-ItemProperty -Path $HKLMCrashControl -Name $SettingName -ErrorAction SilentlyContinue -ErrorVariable TestValue
            } catch {
                Write-Error "Setting '$SettingName' not present in the registry." -Verbose
            }
            if(-not($TestValue)) { 
                # Get the type of the registry value
                $RegValueType = (($RegValue | Get-Member | Where-Object{$_.Name -eq $SettingName}).Definition -split " ")[0]
                if($RegValueType -eq "int") {
                    # Convert the registry value type to DWord
                    $RegValueType = "DWord"
                } elseif($RegValueType -eq "string") {
                    # Convert the registry value type to ExpandString
                    $RegValueType = "ExpandString"
                }
                # Restore the registry value to match the backup file setting
                try {
                    Write-Verbose "Restoring setting '$SettingName' to value '$SettingValue', type = '$RegValueType'." -Verbose
                    Set-ItemProperty -Path $HKLMCrashControl -Name $SettingName -Type $RegValueType -Value $SettingValue -ErrorAction Stop
                }
                catch {
                    Write-Error "Failed to restore setting '$SettingName' to value '$SettingValue'. Error: $($_.Exception.Message)"
                }
            # Error, the setting is not present in the registry!
            } else {
                throw "Expected setting '$SettingName' to be configured in the registry, but is not."
            }
        }

    }
    Write-Verbose "Memory dump settings have been restored from backup file." -Verbose
    Write-Verbose "A system restart is required for the Memory Dump changes to take effect." -Verbose

} # End of Restore-AzStackHciMemoryDumpSettings

# ///////////////////////////////////////////////////////////////////
# Restore-AzStackHciPageFileSettings function
# Used to restore or roll back page file settings
# ///////////////////////////////////////////////////////////////////
Function Restore-AzStackHciPageFileSettings
{
    <#
    .SYNOPSIS
 
    Restores page file settings from a backup file
 
    .DESCRIPTION
 
    Restores the page file settings from a backup file, using the 'PageFile_Settings_YYYYMMDD.txt' file.
    Requires administrator permissions to restore page file settings
     
    #>


    [CmdletBinding()]
    param (
        # File path to backup file
        [Parameter(Mandatory=$true,Position=0)]
        [ValidateScript({Test-Path $_})]
        [String]$PageFileSettingsFilePath # File used to restore settings from, must be a valid path

    )

    # Requires administrator permissions to set page file settings
    if(-NOT ([Security.Principal.WindowsPrincipal][Security.Principal.WindowsIdentity]::GetCurrent()).IsInRole([Security.Principal.WindowsBuiltInRole] "Administrator")){
        # If not running as an Administrator, throw an error and exit
        Write-Error "This script must be run as an Administrator."
        return
    }

    # Read the memory dump settings from the backup file into an array
    if($PageFileSettingsFilePath -like "C:\ProgramData\AzStackHci.DiagnosticSettings\PageFile_Settings_*"){
        Write-Verbose "Reading Page File settings from backup file: $PageFileSettingsFilePath" -Verbose
    } else {
        Write-Verbose "Page File settings backup file path must start with 'C:\ProgramData\AzStackHci.DiagnosticSettings\PageFile_Settings_'" -Verbose
        throw "Error: Page File settings backup file path must start with 'C:\ProgramData\AzStackHci.DiagnosticSettings\PageFile_Settings_'"
    }
    # Read the page file settings from the backup file into an array
    [string[]]$RestorePageFileSettings = Get-Content $PageFileSettingsFilePath -ErrorAction Stop

    try {
        # Get current page file settings
        [bool]$script:PageFileAutoManaged = (Get-CimInstance Win32_ComputerSystem).AutomaticManagedPagefile
    } catch {
        Write-Verbose "Failed to get current page file settings. Error: $($_.Exception.Message)" -Verbose
        Throw "Failed to get current page file settings. Error: $($_.Exception.Message)"
    }

    # Date time thePage File settings were saved should be the first line in the file
    try {
        [datetime]$DateTimePageFileSettings = $RestorePageFileSettings[0]    
    }
    catch {
        throw "Failed to get the date and time the Page File settings were saved. Error: $($_.Exception.Message)"
    }
    
    # Confirm with user before restoring the page file settings
    Write-Verbose "Restoring Page File settings from $PageFileSettingsFilePath, saved on $DateTimePageFileSettings" -Verbose
    $RestoreConfirmation = Read-Host "Do you want to restore the page file settings from $PageFileSettingsFilePath? (Y/N)" -ErrorAction Stop
    if($RestoreConfirmation -ne "Y"){
        Write-Verbose "Page File settings restore cancelled by user." -Verbose
        return
    }
    
    [int]$RestoreVariableCounter = 0
    # Restore the page file settings, the first two lines (0-1) are the date and time and machine name. The required information starts at line three.
    # i = 2 is the third line in the file, which is the first setting (Enabled or Disabled for automatic page file management)
    # Loop through the settings, and restore the page file settings
    for($i=2; $i -lt ($RestorePageFileSettings.Count -3); $i++)
    {
        # Get the setting from the backup file array on each iteration
        $Setting = $RestorePageFileSettings[$i]

        if(($i -eq 2) -and ($Setting -eq "Current Settings: Page File automatic management is Disabled")){
            # Page File settings are manually configured
            [bool]$RestorePageFileAutoManaged = $False
            Continue
        } elseif(($i -eq 2) -and ($Setting -eq "Current Settings: Page File automatic management is Enabled")){
            # Page File settings are automatically managed
            [bool]$RestorePageFileAutoManaged = $True
            Continue
        } else {
            if($i -eq 2){
                Write-Verbose "Error: Expected setting 'Current Settings: Page File automatic management is Disabled' or 'Current Settings: Page File automatic management is Enabled', but got '$Setting'." -Verbose
                throw "Error: Expected setting 'Current Settings: Page File automatic management is Disabled' or 'Current Settings: Page File automatic management is Enabled', but got '$Setting'."
            }
        }
        
        if(($i -eq 3) -and ($RestorePageFileAutoManaged)){
            if($Setting -eq "Page File usage:"){
                # Skip the line that says "Page File usage:"
                Continue
            }
        } elseif(($i -eq 3) -and (-not($RestorePageFileAutoManaged))){
            if($Setting -eq "Current Page File Configuration:"){
                # Skip the line that says "Current Page File Configuration:"
                Continue
            }
        }

        if([string]::IsNullOrWhiteSpace($Setting)){
            # Skip blank lines, such as line 4
            Continue
        }
        # Debugging output, when running with -Debug
        Write-Debug "Backup file input: $Setting"
        # Split the setting into the name and value
        $SettingName = $Setting.Split("=",[System.StringSplitOptions]::RemoveEmptyEntries)[0].Trim()
        if(-not($SettingName -in ("Page File usage:","Current Page File Configuration:"))){
            # Check if the setting value is a comment, as won't have a value in the backup file
            $SettingValue = $Setting.Split("=",[System.StringSplitOptions]::RemoveEmptyEntries)[1].Trim()
        }

        # If the page file was automatically managed in the backup file
        if($RestorePageFileAutoManaged){
            # We us the "Name" setting to restore the page file settings, as this is the path to the Page file.
            if($SettingName -eq "Name"){
                # Revert Page File to System Managed:
                # Check if the page file is already automatically managed, if not, revert to automatically managed
                if($PageFileAutoManaged){
                    # Page File is already automatically managed
                    Write-Verbose "Restore Page File settings: Unexpected configuration - Page File is already automatically managed, no action required." -Verbose
                    throw "Restore Page File settings: Unexpected configuration - Page File is already configured to be automatically managed, no action required."
                
                } else { # Page File is manually configured (expected with 4GB fixed size), so revert to automatically managed
                    
                    Write-Verbose "Restore Page File: Reverting Page File to automatically managed." -Verbose
                    # Remove manual page file, using the current settings
                    try {
                        $script:PageFileConfiguration = Get-WmiObject -ClassName Win32_PageFileSetting -EnableAllPrivileges
                    } catch {
                        Write-Verbose "Failed to get existing page file settings. Error: $($_.Exception.Message)" -Verbose
                        Throw "Failed to get existing page file settings. Error: $($_.Exception.Message)"
                    }
                    # Check there is only one page file configured
                    if(($script:PageFileConfiguration | Measure-Object).Count -eq 1) {
                        # Remove existing manual page file settings
                        try {
                            # Remove existing manual page file settings
                            $PageFileConfiguration.Delete()
                        } catch {
                            Write-Verbose "Failed to delete existing page file settings. Error: $($_.Exception.Message)" -Verbose
                            Throw "Failed to delete existing page file settings. Error: $($_.Exception.Message)"
                        }
                        Write-Verbose "Existing manual page file settings removed." -Verbose
                    } else {
                        Write-Verbose "Error: Unexpected configuration - Expected only 1 page file to be configured, but found $(($script:PageFileConfiguration | Measure-Object).Count)" -Verbose
                        throw "Error: Unexpected configuration - Expected only 1 page file to be configured, but found $(($script:PageFileConfiguration | Measure-Object).Count)"
                    }
                    
                    # New Page File, using the file path from the backup file:
                    try {
                        Write-Verbose "Setting new page file settings to automatically managed." -Verbose
                        $RestorePageFile = New-CimInstance -ClassName Win32_PageFileSetting -Property @{ Name= $SettingValue }
                        $RestorePageFile | Set-CimInstance -Property @{ InitialSize = 0; MaximumSize = 0 }
                    } catch {
                        Write-Verbose "Error: Failed to restore system managed page file settings. Error: $($_.Exception.Message)" -Verbose
                        Throw "Error: Failed to restore system managed page file settings. Error: $($_.Exception.Message)"
                    }
                    if($RestorePageFile){
                        Write-Verbose "System managed page file configured successfully: '$($RestorePageFile.Description)'" -Verbose
                    } else {
                        Write-Verbose "Failed to restore system managed page file settings." -Verbose
                        Throw "Error: Failed to restore system managed page file settings."
                    }

                    # Enable Automatic Management on all drives:
                    try {
                        $ComputerSystem = Get-WmiObject -ClassName Win32_ComputerSystem -EnableAllPrivileges    
                    }
                    catch {
                        Write-Verbose "Failed to get existing automatic management settings from WMI. Error: $($_.Exception.Message)" -Verbose
                        Throw "Error: Failed to get existing automatic management settings from WMI. Error: $($_.Exception.Message)"
                    }
                    try {
                        $ComputerSystem.AutomaticManagedPagefile = $true
                        $ComputerSystem.Put() | Out-Null
                        Write-Verbose "Automatic management of page file enabled" -Verbose
                    }
                    catch {
                        Write-Verbose "Failed to enable automatic management of page file. Error: $($_.Exception.Message)" -Verbose
                        Throw "Error: Failed to enable automatic management of page file. Error: $($_.Exception.Message)"
                    }

                    Write-Verbose "Page File settings reverted to automatically managed." -Verbose
                    Write-Verbose "A system restart is required for the Page File changes to take effect." -Verbose
                    Return $True
                }
            }

        # If the page file was manually configured in the backup file
        } elseif(-not($RestorePageFileAutoManaged)){
            
            # Get the six manual Page File settings from input backup file...
            if($SettingName -in ("Caption", "Description", "InitialSize", "MaximumSize" , "Name", "SettingID")){
                # Increment the counter for the number of settings restored
                $RestoreVariableCounter++
                if($RestoreVariableCounter -le 6){ # Only create variables for first six settings that match above.
                    # Set the Page File settings from the backup file
                    New-Variable -Name RestorePageFile$SettingName -Value $SettingValue
                } else {
                    Write-Verbose "Error: Unexpected configuration - Restoring multiple manual page files." -Verbose
                    throw "Unexpected configuration - Restoring multiple manual page files"
                }
            }
            
            # Restore the Page File settings from the backup file
            if($SettingName -eq "Page File usage:"){
                # Ready to restore the manual page file settings, as we don't use the "Page File usage:" line or anything after it.
                # Get the current page file settings to check if the page file is the same as the backup file
                try {
                    $script:PageFileConfiguration = Get-WmiObject -ClassName Win32_PageFileSetting -EnableAllPrivileges
                } catch {
                    Write-Verbose "Failed to get existing page file settings. Error: $($_.Exception.Message)" -Verbose
                    Throw "Failed to get existing page file settings. Error: $($_.Exception.Message)"
                }
                # Ensure there is only one page file currently configured (expected with 4GB fixed size)
                if(($script:PageFileConfiguration | Measure-Object).Count -eq 1) {
                    # Check if the page file is the same as the backup file
                    if($PageFileConfiguration.Caption -eq $RestorePageFileCaption){
                        # Remove existing manual page file settings
                        try {
                            # Remove existing manual page file settings
                            $PageFileConfiguration.Delete()
                        } catch {
                            Write-Verbose "Failed to delete existing page file settings. Error: $($_.Exception.Message)" -Verbose
                            Throw "Failed to delete existing page file settings. Error: $($_.Exception.Message)"
                        }
                        Write-Verbose "Existing manual page file settings removed." -Verbose
                    }
                } else {
                    Write-Verbose "Error: Unexpected configuration - Expected only 1 page file to be configured, but found $(($script:PageFileConfiguration | Measure-Object).Count)" -Verbose
                    throw "Error: Unexpected configuration - Expected only 1 page file to be configured, but found $(($script:PageFileConfiguration | Measure-Object).Count)"
                }

                # Restore page file, creating a new page file configuration, setting the size values from the backup file
                try {
                    Write-Verbose "Restoring page file settings from backup file." -Verbose
                    Write-Debug "{Name = $RestorePageFileName; InitialSize = $RestorePageFileInitialSize; MaximumSize = $RestorePageFileMaximumSize"
                    $RestorePageFile = New-CimInstance -ClassName Win32_PageFileSetting -Property @{ Name= $RestorePageFileName }
                    $RestorePageFile | Set-CimInstance -Property @{ InitialSize = $RestorePageFileInitialSize; MaximumSize = $RestorePageFileMaximumSize }
                } catch {
                    Write-Verbose "Failed to restore static page file settings. Error: $($_.Exception.Message)" -Verbose
                    Throw "Error: Failed to restore static page file settings. Error: $($_.Exception.Message)"
                }
                if($RestorePageFile){
                    Write-Verbose "Static page file configuration restored successfully: '$($RestorePageFile.Description)'" -Verbose
                } else {
                    Write-Verbose "Failed to restore static page file settings." -Verbose
                    Throw "Error: Failed to restore static page file settings."
                }
                Write-Verbose "Page File configured with InitialSize = $RestorePageFileInitialSize and MaximumSize = $RestorePageFileMaximumSize" -Verbose

                Write-Verbose "Page File settings have been successfully restored from backup file." -Verbose
                Write-Verbose "A system restart is required for the Page File changes to take effect." -Verbose
                Return $True
            }
        
        } else {
            Write-Verbose "Error: Unexpected configuration - Page File settings were not automatically managed or manually configured." -Verbose
            throw "Unexpected configuration - Page File settings are not automatically managed or manually configured."
        }

    } # End of for loop

} # End of Restore-AzStackHciPageFileSettings function

# ///////////////////////////////////////////////////////////////////
# Set-AzStackHciUserModeCrashDumpSettings function
# Used to set the registry settings for user mode crash dumps
# ///////////////////////////////////////////////////////////////////
Function Set-AzStackHciUserModeCrashDumpSettings {
    <#
    .SYNOPSIS
 
    Sets the registry settings for user mode crash dumps
 
    .DESCRIPTION
 
    Sets the registry settings for user mode crash dumps, enabling Windows Error Reporting to create user mode dumps on the host.
 
    #>


    # Requires administrator permissions to set user mode crash dump settings
    if(-NOT ([Security.Principal.WindowsPrincipal][Security.Principal.WindowsIdentity]::GetCurrent()).IsInRole([Security.Principal.WindowsBuiltInRole] "Administrator")){
        # If not running as an Administrator, throw an error and exit
        Write-Error "This script must be run as an Administrator."
        return
    }

    # Enabling Windows Error Reporting to create user mode dumps on Host
    $HKLMWERLocalDumps = "HKLM:\SOFTWARE\Microsoft\Windows\Windows Error Reporting\LocalDumps"
    New-Item $HKLMWERLocalDumps -ErrorAction SilentlyContinue | Out-Null
    
    # Host_Application_LocalDump_DumpType
    Set-ItemProperty -Path $HKLMWERLocalDumps -Name DumpType -Type DWord -Value 1 -ErrorAction Stop

    # Use path "C:\Windows\CrashDumps" and compress the folder
    if(-not(test-path "C:\Windows\CrashDumps")) { New-Item "C:\Windows\CrashDumps" -ItemType Directory -ErrorAction SilentlyContinue | Out-Null }
    Write-Verbose "Compressing the 'C:\Windows\CrashDumps' folder..." -Verbose
    Compact /i /c /a /s:C:\Windows\CrashDumps

    # Host_Application_LocalDump_DumpFolder
    Set-ItemProperty -Path $HKLMWERLocalDumps -Name DumpFolder -Type ExpandString -Value "C:\Windows\CrashDumps" -ErrorAction Stop

    # Host_Application_LocalDump_DumpCount, only keep one dump per process to limit disk space usage
    Set-ItemProperty -Path $HKLMWERLocalDumps -Name DumpCount -Type DWord -Value 1 -ErrorAction Stop
    
} # End of Set-AzStackHciUserModeCrashDumpSettings function

# ///////////////////////////////////////////////////////////////////
# Get-AzStackHciMemoryDumpSettings function
# Used to get the current memory dump settings on the system
# ///////////////////////////////////////////////////////////////////
Function Get-AzStackHciMemoryDumpSettings {
    <#
    .SYNOPSIS
 
    Gets current crash dump settings
 
    .DESCRIPTION
 
    Queries the registry for current crash dump settings
 
    #>

    
    # Requires administrator permissions to get crash dump settings and write to log file to disk
    if(-NOT ([Security.Principal.WindowsPrincipal][Security.Principal.WindowsIdentity]::GetCurrent()).IsInRole([Security.Principal.WindowsBuiltInRole] "Administrator")){
        # If not running as an Administrator, throw an error and exit
        Write-Error "This script must be run as an Administrator."
        return
    }

    # Call function, to Set the Size of the DedicateDumpFile based on node memory size
    [uint32]$script:DedicatedDumpFileSize = SetDedicatedDumpFileSize

    $HKLMCrashControl = "HKLM:\System\CurrentControlSet\Control\CrashControl"
    $CrashControlSettings = Get-Item -Path $HKLMCrashControl -ErrorAction Stop | Get-ItemProperty -ErrorAction Stop

    $script:CurrentDumpFile = $CrashControlSettings.DumpFile
    
    $HKLMCrashControl = "HKLM:\System\CurrentControlSet\Control\CrashControl"
    $CrashControlProperties = "AutoReboot","DisableEmoticon","LogEvent","CrashDumpEnabled","FilterPages","NMICrashDump","DedicatedDumpFile","DumpFileSize","IgnorePagefileSize","AlwaysKeepMemoryDump","DumpFile","MinidumpsCount","Overwrite","MinidumpDir"

    $script:CurrentSettings = @{} # Create a hashtable to store the current settings properties
    foreach($PropertyName in $CrashControlProperties)
    {
        # Get-ItemPropertyValue throws even with -ErrorAction SilentlyContinue
        $CurrentSetting = Get-ItemProperty $HKLMCrashControl -Name $PropertyName -ErrorAction SilentlyContinue
        if ($CurrentSetting)
        {
            # Add description
            if($PropertyName -eq "CrashDumpEnabled"){
                $CurrentCrashDumpMode = switch ($CurrentSetting.CrashDumpEnabled) {
                    1 { if ($CurrentSetting.FilterPages) { "CrashDumpEnabled = 1 (Active Memory Dump)" } else { "CrashDumpEnabled = 1 (Complete Memory Dump)" } }
                    2 {"CrashDumpEnabled = 2 (Kernel Memory Dump)"}
                    3 {"CrashDumpEnabled = 3 (Small Memory Dump)"}
                    7 {"CrashDumpEnabled = 7 (Automatic Memory Dump)"}
                    default {"Unknown"}
                }
            }
            # Add actual configured value
            $CurrentSettings.$PropertyName = $CurrentSetting.$PropertyName
        } else {
            $CurrentSettings.$PropertyName = "//Setting not configured"
        }
    }
    # Output the current settings to the console
    Write-Verbose "Current Memory Dump settings:`n" -Verbose
    $CurrentSettings | Out-String | Write-Verbose -Verbose
    $DateFormatted = Get-Date -f "yyyyMMdd"
    if(-not(Test-Path "C:\ProgramData\AzStackHci.DiagnosticSettings\"))
    {
        New-Item "C:\ProgramData\AzStackHci.DiagnosticSettings\" -ItemType Directory -ErrorAction SilentlyContinue | Out-Null
    }
    "$(Get-Date -Format "yyyy-MM-dd HH:mm:ss")" | Out-File $("C:\ProgramData\AzStackHci.DiagnosticSettings\MemoryDump_Settings_$DateFormatted.txt") -ErrorAction Stop
    "Backup of Crash Dump registry settings on $env:COMPUTERNAME - (current configuration = $CurrentCrashDumpMode)`n" | Out-File $("C:\ProgramData\AzStackHci.DiagnosticSettings\MemoryDump_Settings_$DateFormatted.txt") -Append -ErrorAction Stop
    $CurrentSettings | Out-File $("C:\ProgramData\AzStackHci.DiagnosticSettings\MemoryDump_Settings_$DateFormatted.txt") -Append -ErrorAction Stop
    Write-Verbose "Current Memory Dump settings exported to 'C:\ProgramData\AzStackHci.DiagnosticSettings\MemoryDump_Settings_$DateFormatted.txt'" -Verbose

} # End of Get-AzStackHciMemoryDumpSettings function

# ///////////////////////////////////////////////////////////////////
# Test-AzStackHciSSLInspection function
# Used to check for the presence of SSL Inspection on traffic sent to a specified URL
# ///////////////////////////////////////////////////////////////////
function Test-AzStackHciSSLInspection {
    <#
    .SYNOPSIS
    Function to check for the presence of SSL Inspection on traffic sent to a specified URL.
 
    .PARAMETER url
    The URL to test for SSL Inspection
 
    .DESCRIPTION
    Expects Microsoft or DigiCert certificates to be used for SSL/TLS connections to the specified URL.
    If a different certificate is detected, the script will report that SSL Inspection is present.
    Script checks for redirects and follows them to test further URLs if required.
    Returns $true if SSL Inspection is detected, otherwise $false.
    #>


    [CmdletBinding()]
    param (
        [parameter(Mandatory=$true,HelpMessage="The URL to test for SSL Inspection",Position=0)]
        [System.Uri]$url
    )

    $ErrorActionPreference = "Stop"

    Write-Host "`n`t///////////////////////////////////////////////"
    Write-Host "`t Basic SSL Inspection Test for Azure Stack HCI"
    Write-Host "`t///////////////////////////////////////////////`n"

    [bool]$RedirectsComplete = $false
    [bool]$SSLInspectionDetected = $false

    Write-Host "Starting SSL Inspection Tests`n"

    Write-Host "Date/Time = $(Get-Date -Format 'yyyy-MM-dd HH:mm:ss')"
    Write-Host "Performing test from hostname: $($env:COMPUTERNAME)`n"

    do {
        $Request = $null
        $Response = $null
        $Request = [System.Net.HttpWebRequest]::Create($url)
        $Request.Method = "GET"
        $Request.AllowAutoRedirect = $False
        $Request.Proxy = [System.Net.WebRequest]::DefaultWebProxy
            # Proxy credentials are not required for this test, if using a non-transparent proxy, uncomment the line below, and/or setup proxy authentication as required
            # $Request.Proxy.Credentials = [System.Net.CredentialCache]::DefaultCredentials
        Write-Host "Testing SSL/TLS Certificate for endpoint: '$($Request.Address.AbsoluteUri)'"
        try {
            [System.Net.HttpWebResponse]$Response = $Request.GetResponse()    
        }
        catch {
            Write-Host "Error: $($_.Exception.Message)"
        }
        # Check if the certificate subject contains "O=Microsoft" as most SSL inspection appliances will replace the certificate with their own
        if(($Request.ServicePoint.Certificate.Subject).Contains("O=Microsoft")){
            Write-Host -ForegroundColor Green "Expected Certificate Subject Found: `nSubject = $($Request.ServicePoint.Certificate.Subject)"
        } else {
            $SSLInspectionDetected = $true
            Write-Host -ForegroundColor Red "UNKNOWN Certificate Subject Found: `nSubject = '$($Request.ServicePoint.Certificate.Subject)'"
            Write-Host -ForegroundColor Yellow "`tNote: Expected Certificate Contains Subject = 'O=Microsoft'"
        }
        # Check if the certificate issuer contains "O=Microsoft Corporation" or "O=DigiCert Inc" as most SSL inspection appliances will replace the certificate with their own
        if(($Request.ServicePoint.Certificate.Issuer).Contains("O=Microsoft Corporation") -or (($Request.ServicePoint.Certificate.Issuer).Contains("O=DigiCert Inc"))){
            Write-Host -ForegroundColor Green "Expected Certificate Issuer Found: `nCertificate Issuer = $($Request.ServicePoint.Certificate.Issuer)"
        } else {
            $SSLInspectionDetected = $true
            Write-Host -ForegroundColor Red "UNKNOWN Certificate Issuer Found: `nCertificate Issuer = '$($Request.ServicePoint.Certificate.Issuer)'"
            Write-Host -ForegroundColor Yellow "`tNote: Expected Certificate Contains Issuer = 'O=Microsoft Corporation' or 'O=DigiCert Inc'"
        }
        # If $Response exists, check if for any redirects to further test required URLs
        if($Response){
            if(-not([string]::IsNullOrWhiteSpace($Response.Headers["Location"]))){
                Write-Host "Checking Redirected URL, as 'HTTP StatusCode = $($Response.StatusCode)'"
                $url = $Response.Headers["Location"]
            } else {
                # No redirects found
                $RedirectsComplete = $true
            }
            $Response.Close()
        } else {
            # No response found, unable to determine if redirects are required
            $RedirectsComplete = $true
        }
        write-host ""

    } while (
        $RedirectsComplete -ne $true
    )

    
    # Note: this is crude test, matching only the certificate subject and isser, is it not an exhaustive test, such as matching X509 certificate thumb prints.
    Write-Host -ForegroundColor Yellow "Note: This is a simple / crude test, using logic to match the Subject and Isser certificates by text strings only."
    Write-Host -ForegroundColor Yellow "It is not intended to be an exhaustive certificate validity test, such as using X509 certificate thumbprints.`n"

    if($SSLInspectionDetected -eq $true){
        Write-Host -ForegroundColor Red "SSL Inspection Detected!`nCheck your network / proxy server configuration for SSL Inspection - https://learn.microsoft.com/en-us/azure-stack/hci/concepts/firewall-requirements`n"
    } else {
        Write-Host -ForegroundColor Green "No SSL Inspection Detected :-)`n"
    }

    Write-Host "SSL Inspection Tests Complete`n"
    return $SSLInspectionDetected

} # End of Test-AzStackHciSSLInspection function

# ///////////////////////////////////////////////////////////////////
# Send-ClusterPerformanceHistory function
# Used to collect and send cluster performance history to Microsoft.
# Uses Get-SddcDiagnosticInfo and Send-DiagnosticInformation functions
# ///////////////////////////////////////////////////////////////////
function Send-ClusterPerformanceHistory {
<#
    .SYNOPSIS
    Collects the SDDC diagnostic information, including the cluster performance history, and sends the diagnostic information to Microsoft.
 
    .DESCRIPTION
    This function collects the SDDC diagnostic information, including the cluster performance history, and sends the diagnostic information to Microsoft.
    The function requires the name of the cluster to collect the SDDC and Cluster Performance History.
    The function also requires the time frame for the performance history, the root path to store the output files.
    There is an optional switch to ignore disk space check for log collection and an optional switch to exclude cluster performance history from the diagnostic information.
     
    .PARAMETER ClusterName
    The name of the cluster to collect SDDC and Cluster Performance History.
 
    .PARAMETER PerformanceHistoryTimeFrame
    The time frame for the performance history. The default is LastDay.
 
    .PARAMETER OutputRootPath
    The root path to store the output files. The default is C:\Temp, if not specified.
 
    .PARAMETER IgnoreDiskSpaceCheck
    Optional switch, used to ignore disk space check for log collection.
 
    .PARAMETER ExcludeClusterPerformanceHistory
    Optional switch, to exclude cluster performance history from the diagnostic information.
 
    .EXAMPLE
    Send-ClusterPerformanceHistory -ClusterName "Cluster-01" -OutputRootPath "C:\Temp"
 
    This example collects the SDDC diagnostic information, including the cluster performance history, for the cluster named "Cluster-01".
    The performance history time frame defaults to "LastDay", as the parameter was not specified.
    The output files are stored in the "C:\Temp" folder. The disk space check is checked, to ensure there is 10GB + 5% free space before starting the log collection.
#>


    [CmdletBinding()]
    param (
        # The name of the cluster to collect SDDC and Cluster Performance History
        [Parameter(Mandatory = $true, Position=0, HelpMessage="Enter the Cluster Name")]
        [ValidateScript({Get-Cluster -Name $_})]
        [string]$ClusterName,

        # The time frame for the performance history
        [Parameter(Mandatory = $false,Position=1)]
        [ValidateSet("LastHour","LastDay","LastWeek","LastMonth","LastYear")]
        # Default is LastDay
        [string]$PerformanceHistoryTimeFrame = "LastDay",

        # The root path to store the output files
        [Parameter(Mandatory = $false,Position=2)]
        [ValidateScript({Test-Path -Path ($_.Substring(0,3))})] # Must be drive letter, not UNC path
        # Default is C:\Temp, if not specified
        [string]$OutputRootPath = "C:\Temp",

        # Optional switch, used to ignore disk space check for log collection
        [Parameter(Mandatory=$false,Position=3)]
        [Switch]$IgnoreDiskSpaceCheck,

        [Parameter(Mandatory = $false,Position=4)]
        [switch]$ExcludeClusterPerformanceHistory # optional switch, to exclude cluster performance history from the diagnostic information
    )

    # Requires administrator permissions to run this function
    if(-NOT ([Security.Principal.WindowsPrincipal][Security.Principal.WindowsIdentity]::GetCurrent()).IsInRole([Security.Principal.WindowsBuiltInRole] "Administrator")){
        # If not running as an Administrator, throw an error and exit
        Write-Error "This function must be run as an Administrator."
        return
    }

    # Check path and disk space
    [uint32]$LogCollectionFileSize = 10240 # 10GB, approx log collection size
    $Disk = Get-PSDrive -ErrorAction Stop | Where-Object { $PSItem.Root -eq $OutputRootPath.Substring(0,3) }
    Write-Output "Target disk free space = $([math]::round($Disk.Free / 1GB,2)) GiB"
    # Add the minimum / expected disk space required for log collection, plus 5% of the total disk space
    $MinimumRequiredDiskSpace = [math]::round($LogCollectionFileSize*1Mb/1Gb + ((($disk.Free + $disk.Used)/1Gb) * 0.05),2)
    if (($Disk.Free/1Gb -lt $MinimumRequiredDiskSpace) -and (-not($IgnoreDiskSpaceCheck.IsPresent)))
    {
        Write-Output "$($OutputRootPath.Substring(0,3)) disk free space is below minimum recommended size for log collection, plus 5% of total space as reserve"
        Throw "Insufficient disk space on the drive '$($OutputRootPath.Substring(0,3))' to store the output log files. Minimum recommended disk space is $MinimumRequiredDiskSpace GiB, increase space or consider use of '-IgnoreDiskSpaceCheck' switch."
    } else {
        if($IgnoreDiskSpaceCheck.IsPresent){
            Write-Output "*** Free disk space checks have been ignored, proceeding with log collection ***"
        } else { 
            Write-Output "Info: $($OutputRootPath.Substring(0,3)) has $([math]::round($Disk.Free/1Gb,2)) free disk space, this is above minimum recommended for log collection + reserve of 5% of total disk size ($MinimumRequiredDiskSpace GiB), performing log collection...."
        }
    }    
    # Get the current date in the format yyyyMMdd, used for the output folders
    $DateFormatted = Get-Date -f "yyyyMMdd"

    if(-not(Test-Path -Path $OutputRootPath)){
        Write-Output "Creating '$OutputRootPath\ output folder"
        try {
            New-Item -Path "$OutputRootPath" -ItemType Directory -Force | Out-Null
        } catch {  
            Throw "Failed to create '$OutputRootPath' output folder $($_.Exception.Message)"
        }        
    }

    # Create a folder to store the output files
    if(-not(Test-Path -Path "$OutputRootPath\Logs")){
        Write-Output "Creating '$OutputRootPath\SDDC-Logs\$($DateFormatted)-ClusterPerf' temporary output folder"
        try {
            New-Item -Path "$OutputRootPath\SDDC-Logs\$($DateFormatted)-ClusterPerf" -ItemType Directory -Force | Out-Null
        } catch {  
            Throw "Failed to create '$OutputRootPath\SDDC-Logs\$($DateFormatted)-ClusterPerf' output folder $($_.Exception.Message)"
        }
        
        Write-Output "Creating '$OutputRootPath\SDDC\$($DateFormatted)-ClusterPerf' target folder for the output zip file"
        try {
            New-Item -Path "$OutputRootPath\SDDC\$($DateFormatted)-ClusterPerf" -ItemType Directory -Force | Out-Null
        } catch {  
            Throw "Failed to create '$OutputRootPath\SDDC\$($DateFormatted)-ClusterPerf' output folder $($_.Exception.Message)"
        }
    }

    # Get the SDDC diagnostic information, including the cluster performance history
    Write-Output "Getting the SDDC diagnostic information for cluster '$ClusterName'..."

    if($ExcludeClusterPerformanceHistory.IsPresent){ # Exclude cluster performance history from the diagnostic information

        # Exclude cluster performance history from the diagnostic information
        Write-Output "Excluding cluster performance history from the diagnostic information"
        # Get the SDDC diagnostic information, including the cluster performance history
        Get-SddcDiagnosticInfo -WriteToPath "$OutputRootPath\SDDC-Logs\$($DateFormatted)-ClusterPerf" `
        -ClusterName "$ClusterName" `
        -IncludeGetNetView `
        -ZipPrefix "$OutputRootPath\SDDC\$($DateFormatted)-ClusterPerf" `
        -IncludePerformance $true `
        -DaysOfArchive 0 `
        -ErrorAction Continue

    } else { # Include cluster performance history from the diagnostic information

        # Include cluster performance history from the diagnostic information
        Get-SddcDiagnosticInfo -WriteToPath "$OutputRootPath\SDDC-Logs\$($DateFormatted)-ClusterPerf" `
        -ClusterName "$ClusterName" `
        -IncludeGetNetView `
        -ZipPrefix "$OutputRootPath\SDDC\$($DateFormatted)-ClusterPerf" `
        -IncludeClusterPerformanceHistory `
        -PerformanceHistoryTimeFrame $PerformanceHistoryTimeFrame `
        -IncludePerformance $true `
        -DaysOfArchive 0 `
        -ErrorAction Continue

    }
    
    # Confirm with user before deleting the output zip file:
    try {
        $ZipFile = Get-Item -Path "$OutputRootPath\SDDC\$($DateFormatted)-ClusterPerf*.zip"
    } catch {
        Throw "Failed to get the SDDC Zip File $($_.Exception.Message)"
    }
    # Ensure only a single zip file is found:
    if($ZipFile.count -eq 1){
        Write-Output "SDDC Zip file = '$($ZipFile.FullName)'"
    } else {
        Throw "Found more than 1 x zip file using this wildcard path: '$OutputRootPath\SDDC\$($DateFormatted)-ClusterPerf*.zip'"
    }

    # Extract the zip file to the target "SDDC" folder for sending the diagnostic information to Microsoft, as this folder is shown in the ingestion process
    Write-Output "Extracting the zip file '$($ZipFile.FullName)' to '$OutputRootPath\SDDC\$($DateFormatted)-ClusterPerf\SDDC' folder"
    New-Item -ItemType Directory -Path "$OutputRootPath\SDDC\$($DateFormatted)-ClusterPerf\SDDC" -Force | Out-Null
    try {
        Expand-Archive -Path $ZipFile.FullName -DestinationPath "$OutputRootPath\SDDC\$($DateFormatted)-ClusterPerf\SDDC" -Force
    } catch {
        Throw "Failed to extract the zip file $($_.Exception.Message)"
    }
    
    # Send the diagnostic information to Microsoft, use "-CollectSddc $false" parameter, as SDDC event logs are collected as part of Get-SddcDiagnosticInfo
    try {
        Write-Output "Sending the diagnostic information to Microsoft..."
        Send-DiagnosticData -CollectSddc $false -SupplementaryLogs "$OutputRootPath\SDDC\$($DateFormatted)-ClusterPerf\SDDC" -Verbose -ErrorAction Continue
    } catch {
        Throw "Failed to send the diagnostic information to Microsoft using Send-DiagnosticData $($_.Exception.Message)"
    }

    # Remove the SDDC folder, after sending the diagnostic information
    try {
        Remove-Item -Path "$OutputRootPath\SDDC\$($DateFormatted)-ClusterPerf\SDDC" -Recurse
    } catch {
        Throw "Failed to remove the '$OutputRootPath\SDDC\$($DateFormatted)-ClusterPerf\SDDC' folder $($_.Exception.Message)"
    }

    # Confirm with user before deleting the output zip file:
    $DeleteConfirmation = Read-Host "Do you want to DELETE the zip file '$($ZipFile.Name)', - ($([math]::round($zipfile.Length/1MB,2)) MiB) ? (Y/N)" -ErrorAction Stop
    if($DeleteConfirmation -ne "Y"){
        Write-Output "Delete zip file cancelled by user."
        return
    } else {
        Write-Output "Deleting zip file '$($ZipFile.FullName)'"
        Remove-Item -Path $ZipFile.FullName -ErrorAction Stop -Verbose
    }

} # End of Send-ClusterPerformanceHistory


# ////////////////////////////////////////////////////////////////////////
# Connectivity Test Functions below
# ////////////////////////////////////////////////////////////////////////















# ////////////////////////////////////////////////////////////////////////////
# Function to test version of PowerShell
# This function checks the version of PowerShell and returns an error if the version is greater than 5.1.x
Function Test-PowerShellVersion5 {
    # Check if the PowerShell version is 5.1.x
    $versionMaximum = [version]'5.1.99999.999'
    # Get the PowerShell version from the PSVersionTable
    $PowerShellVersion = [version]$PSVersionTable.PSVersion
    # Compare the PowerShell version with the maximum version
    if ($PowerShellVersion -gt $versionMaximum) {
        # PowerShell version is greater than 5.1.x, return an error
        Write-Verbose "PowerShell version $($PSVersionTable.PSVersion.ToString()) detected."
        # return $false
        Return $false
    } else {
        # PowerShell version is 5.1.x or lower, continue
        Write-Verbose "PowerShell version $($PSVersionTable.PSVersion.ToString()) detected."
        # return $true
        Return $true
    }
}


# ////////////////////////////////////////////////////////////////////////////
# This function converts a hashtable to a string representation
Function Convert-HashTableToString {

    param
    (
        [Parameter(Mandatory = $true)]
        [System.Collections.Hashtable]
        $HashTable
    )
    $HashString = "@{"
    $Keys = $HashTable.keys
    foreach ($Key in $Keys)
    {
        $v = $HashTable[$key]
        if ($key -match "\s")
        {
            $HashString += "`"$key`"" + "=" + "`"$v`"" + ";"
        }
        else
        {
            $HashString += $key + "=" + "`"$v`"" + ";"
        }
    }
    $HashString += "}"
    return $HashString

}


# ////////////////////////////////////////////////////////////////////////////
# Function to extract domains from URLs
Function Get-DomainFromURL {
    param (
        [string]$url
    )
    # Remove the protocol (http:// or https://) and extract the domain and port
    $url = $url -replace "^https?://", ""
    # Check if the URL contains a port number
    # If the URL contains a port number, extract it
    if ($url -match ":(\d+)$") {
        $port = [int]($url -replace ".*:(\d+)$", '$1')
        $url = $url -replace ":(\d+)$", ""
    } else {
        $port = $null
    }
    $domain = $url -split '/' | Select-Object -First 1
    return @{ Domain = $domain; Port = $port }
}

# ////////////////////////////////////////////////////////////////////////////
# Function to test connectivity using Layer 7 (HTTP/HTTPS) requests
# This function checks for SSL inspection, redirects, and other Layer 7 connectivity issues
Function Test-Layer7Connectivity {
    param (
        [Parameter(Mandatory=$true, Position=0, HelpMessage="Endpoint URL, do NOT include the protocol, example: 'login.microsoftonline.com'")]
        [ValidateNotNullOrEmpty()]
        [string]$url,

        [Parameter(Mandatory=$true, Position=1, HelpMessage="The endpoint port, for example: 80, or 443")]
        [ValidateRange(1,65535)]
        [int]$port
    )

    # Set the default Verbose and debug preferences, to 'Continue' if the Verbose or Debug switches are passed
    # This allows the script to run in Verbose and Debug mode, if the switches are passed
    if($PSBoundParameters['Debug']) {
        $DebugPreference = 'Continue'
    }
    if($PSBoundParameters['Verbose']) {
        $VerbosePreference = 'Continue'
    }

    # Check if the PowerShell version is 5.1.x
    # This is required for the script to run, as it uses features deprecated in later versions, such as v7.x.x
    if(-not(Test-PowerShellVersion5)){
        # PowerShell version is greater than 5.1.x, return an error
        Write-Host "'Test-AzureLocalConnectivity' function requires PowerShell version 5.1.x" -ForegroundColor Red
        Write-Host "PowerShell version $($PSVersionTable.PSVersion.ToString()) detected." -ForegroundColor Red
        Throw "Unexpected PowerShell version '$($PSVersionTable.PSVersion.ToString())' detected. Please run command again using PowerShell version 5.1.x"
    }

    # Initialize variables
    [string]$OriginalURL = $url
    [int]$OriginalPort = $port

    # Check if the port is 80 for HTTP or port 443, 8084 or 8443 for HTTPS and update the URL accordingly
    if ($port -eq 443) {
        if($url -notmatch "^https://") {
            # If the URL does not start with https://, add it
            $url = "https://$url"
        }
    } elseif ($port -eq 8084 -or $port -eq 8443) {
        if($url -notmatch "^https://") {
            # If the URL does not start with https://, add it
            $url = "https://$url"
        }
    } elseif($port -eq 80) {
        if($url -notmatch "^http://") {
            # If the URL does not start with http://, add it
            $url = "http://$url"
        }
    } else {
        # Not port 80, 443, 8084 or 8443, check if the URL is HTTP or HTTPS
        # Check if the URL does NOT match HTTP and HTTPS, if not and port number is unexpected, return an error message
        if(($url -notmatch "^http://") -and ($url -notmatch "^https://")){
            # Invalid port specified, return an error message
            # This is a valid port, but not for HTTP or HTTPS
            Write-Host "Invalid port specified: $port for url $url"
            $Layer7Status = "Invalid port: $port"
            $Layer7Response = "Invalid port: $port"
            return $Layer7Status, $Layer7Response
        }
    }

    # Check if using a Proxy, this should be set in the script scope, but this function can be called independently
    # Check if the proxy is set using the Get-Proxy function
    if(-not($Proxy)){
        $script:Proxy = Get-Proxy
    }

    # Remove variables
    Remove-Variable ReturnLayer7Response -ErrorAction SilentlyContinue
    Remove-Variable ReturnCertIssuer -ErrorAction SilentlyContinue
    Remove-Variable RedirectsComplete -ErrorAction SilentlyContinue
    Remove-Variable SSLInspectionDetected -ErrorAction SilentlyContinue
    Remove-Variable RedirectedURL -ErrorAction SilentlyContinue
    Remove-Variable Layer7ResponseTime -ErrorAction SilentlyContinue
    Remove-Variable RedirectCount -ErrorAction SilentlyContinue
    Remove-Variable Layer7status -ErrorAction SilentlyContinue

    # Ignore certificate validation and use TLS 1.2
    if (-not ([System.Management.Automation.PSTypeName]'ServerCertificateValidationCallback').Type)
    {
        # Ignore SSL certificate validation
        Write-Debug "Ignoring SSL certificate validation"
        # Add the ServerCertificateValidationCallback class to ignore SSL certificate validation
        # This is required for testing, as some devices may have self-signed certificates
        # and we do not want to fail the test due to SSL inspection
$certCallback = @"
    using System;
    using System.Net;
    using System.Net.Security;
    using System.Security.Cryptography.X509Certificates;
    public class ServerCertificateValidationCallback
    {
        public static void Ignore()
        {
            if(ServicePointManager.ServerCertificateValidationCallback == null)
            {
                ServicePointManager.ServerCertificateValidationCallback +=
                    delegate
                    (
                        Object obj,
                        X509Certificate certificate,
                        X509Chain chain,
                        SslPolicyErrors errors
                    )
                    {
                        return true;
                    };
            }
        }
    }
"@

    $null = Add-Type $certCallback
    # Ignore SSL certificate validation
    $null = [ServerCertificateValidationCallback]::Ignore()
    # Set the security protocol to TLS 1.2
    $null = [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12
    }

    # Initialize variables
    [bool]$RedirectsComplete = $false
    [int]$RedirectCount = 0
    
    # //////////////////////////////////////////////////
    # loop to handle redirects from web server response
    # //////////////////////////////////////////////////
    do {
        # Initialize variables
        $stopwatch =  [system.diagnostics.stopwatch]::StartNew()
        # counter to determine if the loop should continue
        ++$RedirectCount
        
        # If this is a redirect, add the port based on the URL
        if($RedirectCount -gt 1){
            # $RedirectCount is greater than 1, so we are in a redirect loop
            # Determine the port based on the URL for redirects only, as the port is not specified in the URL
            if($url -match "https://"){
                $port = 443
            } elseif($url -match "http://"){
                $port = 80
            } else {
                Write-Debug "Invalid URL specified: $url"
                $Layer7Status = "Failed"
                $ReturnLayer7Response = "Invalid URL: $url"
                return $Layer7Status, $ReturnLayer7Response
            }
        # Check if the redirect counter is greater than 9, to prevent infinite loops
        }

        # Invoke-WebRequest parameters
        $iwrParams =@{
            Uri = $url
            UseBasicParsing = $true
            TimeoutSec = 60
            MaximumRedirection = 0
            ErrorAction = 'SilentlyContinue'
            ErrorVariable = 'IwrError'
        }

        # If Proxy is enabled, add the proxy server to the parameters
        #if($Proxy.Enabled){
            #if ($proxy) {
                #$iwrParams.Add('Proxy', $Proxy.Server)
                #$iwrParams.Add('ProxyUseDefaultCredentials', $true)
            #}
        #}

        Write-Debug "RedirectCount = $RedirectCount, URL = $url, Port = $port"
        # Check if the redirect count is equal to 10, to prevent infinite loops
        if($RedirectCount -eq 10){
            # $RedirectCount is equal to 10, so we are in a redirect loop
            Write-Warning "Redirection loop detected for URL: $url, please check the URL and try again"
            $Layer7Status = "Failed"
            $ReturnLayer7Response = "Redirection loop detected"
            $RedirectsComplete = $true
        }

        # Initialize / remove variables, as we are in a loop
        Remove-Variable Layer7Response -ErrorAction SilentlyContinue
        Remove-Variable Layer7Status -ErrorAction SilentlyContinue
        Remove-Variable IwrResult -ErrorAction SilentlyContinue
        
        Write-Verbose "Connecting to endpoint: '$($url)' on port $port"

        try {
            # Invoke-WebRequest to the URL, with the specified parameters
            # Use the -UseBasicParsing switch to avoid using Internet Explorer for parsing
            # Use the -TimeoutSec switch to set the timeout for the request
            # Set the -MaximumRedirection switch to zero, for the maximum number of redirects to follow (0 = no redirects), then handle redirects in the loop
            # Use the -ErrorAction switch to set the error action to SilentlyContinue
            # Use the -ErrorVariable switch to set the error variable to IwrError
            # Use the -Proxy switch to set the proxy server to use
            # Use the -ProxyUseDefaultCredentials switch to use the default credentials for the proxy server

            Write-Debug "Calling Invoke-WebRequest with hashtable parameters: $(Convert-HashTableToString $iwrParams)"
            # Set the progress preference to SilentlyContinue to suppress progress messages
            # This speeds up the request, as it does not display progress messages
            $ProgressPreference = 'SilentlyContinue'

            # Invoke-WebRequest to the URL, splatting the specified parameters
            $IwrResult = Invoke-WebRequest @iwrParams

        } catch {
            # Catch the exception from Invoke-WebRequest, using $error variable
            Write-Debug "Error returned from Invoke-WebRequest $url : $($_.Exception.Message)"
            # Check if the error message contains "The remote server returned an error: "
            [string]$ParseError = 'The remote server returned an error: '
            if ($_.Exception.Message.ToString() -like "The remote server returned an error: *") {
                # Extract the error message
                $Layer7Response = $_.Exception.Message.ToString().SubString($_.Exception.Message.ToString().IndexOf($ParseError)+$ParseError.Length,$_.Exception.Message.Length-$_.Exception.Message.IndexOf($ParseError)-$ParseError.Length-1)
            } else {
                $Layer7Response = $_.Exception.Message
            }
            # The output of Invoke-WebRequest is not a valid response, so set the Layer7Status to "Failed"
            $Layer7Status = "Failed"
            # //// Post processing of the Web Server Status Code ($_.Exception.Message) will take place lower down in the code
        }

        # //// Code path when the request was successful
        if($IwrResult) {
            
            # Success, we have a response:
            $Layer7Status = "Success"
            # Request was successful
            $Layer7Response = "($($IwrResult.StatusCode)) $($IwrResult.StatusDescription)"

            # Check if the response contains a certificate using function Get-SslCertificateChain
            # This function will check for SSL inspection, and return the certificate issuer
            Remove-Variable Certificates -ErrorAction SilentlyContinue
            Remove-Variable NoCertificatesRequired -ErrorAction SilentlyContinue

            # For SSL inspection, check if the certificate details for the first endpoint only
            if($RedirectCount -eq 1){
                # Return the Layer7Response for the first endpoint only
                $ReturnLayer7Response = $Layer7Response
                # Check if the URL is HTTPS
                if($url -match "https://"){
                    # Get the certificate chain for the URL
                    # The function will check for SSL inspection, and return the certificate issuer
                    # The function will return the certificate issuer, thumbprint, and subject
                    $Certificates = Get-SslCertificateChain -url $url -AllowAutoRedirect $false -ErrorAction SilentlyContinue
                    # Check if the certificate chain is not null
                    if($Certificates -and $Certificates.ChainElements.Certificate.Count -gt 0){
                        Write-Verbose "Certificates Chain found with $(($Certificates.ChainElements.Certificate).count) parts"
                    } else {
                        # No certificate found
                        Write-Host "Error: No SSL Certificates found" -ForegroundColor Red
                    }
                } else {
                    # No certificate found
                    $NoCertificatesRequired = $true
                    Write-Verbose "Port 80 - SSL Not Required"
                }

                 # Get Certificate details for successful request response
                # Check if the certificate chain is not null
                if($Certificates){
                    # ////// Check the endpoint's certificate:
                    if(-not([string]::IsNullOrWhiteSpace($certificates.ChainElements.Certificate[0].Subject))){
                        # Set the certificate subject to the variable
                        $ReturnCertSubject = $certificates.ChainElements.Certificate[0].Subject
                        # Check the certificate using Test-Certificate, to check if the certificate is trusted for SSL
                        # Check if the certificate is trusted for SSL
                        if(Test-Certificate -Cert $certificates.ChainElements.Certificate[0] -Policy SSL -ErrorAction SilentlyContinue -ErrorVariable certError){
                            # Certificate is valid for SSL
                            Write-Verbose "Certificate is trusted for SSL: $ReturnCertSubject"
                            # Check if the certificate is valid for the endpoint, using the domain name
                                # Check if the certificate is valid for the endpoint
                                if(Test-Certificate -Cert $certificates.ChainElements.Certificate[0] -Policy SSL -DNSName (Get-DomainFromURL -url $url).Domain -ErrorAction SilentlyContinue -ErrorVariable certError){
                                    # Certificate is a valid for URL
                                    Write-Verbose "Certificate is a valid for endpoint: $url"
                                } else {
                                    # Certificate is a valid for URL
                                    Write-Host "Certificate is not valid for endpoint: $url" -ForegroundColor Red
                                    Write-Host "Possible SSL Inspection detected, certificate is not trusted." -ForegroundColor Red
                                    $Layer7Status = "Failed"
                                    $ReturnLayer7Response = $ReturnLayer7Response + " - SSL Inspection detected"
                                    $script:SSLInspectionDetected = $true
                                    $script:SSLInspectedURLs += $url
                                }
                        } else {
                            # Certificate is not valid for SSL
                            Write-Host "Certificate is not trusted for SSL: $ReturnCertSubject" -ForegroundColor Red
                            # SSL inspection detected
                            Write-Host "Possible SSL Inspection detected, certificate is not trusted." -ForegroundColor Red
                            $Layer7Status = "Failed"
                            $ReturnLayer7Response = $ReturnLayer7Response + " - SSL Inspection detected"
                            $script:SSLInspectionDetected = $true
                            $script:SSLInspectedURLs += $url
                        }
                    } else {
                        # No certificate subject found
                        $ReturnCertSubject = "No certificate common name found"
                    }
                    # Check the endpoint's certificate issuer is not null
                    if(-not([string]::IsNullOrWhiteSpace($certificates.ChainElements.Certificate[0].Issuer))){
                        # Set the certificate issuer to the variable
                        $ReturnCertIssuer = $certificates.ChainElements.Certificate[0].Issuer
                    } else {
                        # No certificate issuer found
                        $ReturnCertIssuer = "No certificate issuer found"
                    }
                    # Check the endpoint's certificate thumbprint is not null
                    if(-not([string]::IsNullOrWhiteSpace($certificates.ChainElements.Certificate[0].Thumbprint))){
                        # Set the certificate thumbprint to the variable
                        $ReturnCertThumbprint = $certificates.ChainElements.Certificate[0].Thumbprint
                    } else {
                        # No certificate thumbprint found
                        $ReturnCertThumbprint = "No certificate thumbprint found"
                    }

                    # ////// Intermediate certificate details
                    # Check the endpoint's Intermediate certificate Subject is not null
                    if(-not([string]::IsNullOrWhiteSpace($certificates.ChainElements.Certificate[1].Subject))){
                        # Set the certificate issuer to the variable
                        $ReturnCertIntSubject = $certificates.ChainElements.Certificate[1].Subject
                    } else {
                        # No intermediate certificate Subject found
                        $ReturnCertIntSubject = "No intermediate certificate common name found"
                    }
                    # Check the endpoint's Intermediate certificate subject is not null
                    if(-not([string]::IsNullOrWhiteSpace($certificates.ChainElements.Certificate[1].Issuer))){
                        # Set the certificate issuer to the variable
                        $ReturnCertIntIssuer = $certificates.ChainElements.Certificate[1].Issuer
                    } else {
                        # No intermediate certificate issuer found
                        $ReturnCertIntIssuer = "No intermediate certificate issuer found"
                    }
                    # Check the endpoint's Intermediate certificate thumbprint is not null
                    if(-not([string]::IsNullOrWhiteSpace($certificates.ChainElements.Certificate[1].Thumbprint))){
                        # Set the certificate thumbprint to the variable
                        $ReturnCertIntThumbprint = $certificates.ChainElements.Certificate[1].Thumbprint
                    } else {
                        # No intermediate certificate thumbprint found
                        $ReturnCertIntThumbprint = "No intermediate certificate thumbprint found"
                    }

                    # ////// Root certificate details
                    # Check the endpoint's Root certificate Subject is not null
                    if(-not([string]::IsNullOrWhiteSpace($certificates.ChainElements.Certificate[2].Subject))){
                        # Set the certificate issuer to the variable
                        $ReturnCertRootSubject = $certificates.ChainElements.Certificate[2].Subject
                    } else {
                        # No root certificate Subject found
                        $ReturnCertRootSubject = "No root certificate common name found"
                    }
                    # Check the endpoint's Root certificate issuer is not null
                    if(-not([string]::IsNullOrWhiteSpace($certificates.ChainElements.Certificate[2].Issuer))){
                        # Set the certificate issuer to the variable
                        $ReturnCertRootIssuer = $certificates.ChainElements.Certificate[2].Issuer
                    } else {
                        # No root certificate issuer found
                        $ReturnCertRootIssuer = "No root certificate issuer found"
                        Write-Host "ERROR: Root certificate issuer not found, this may be due to SSL inspection!?" -ForegroundColor Red
                    }
                    # Check the endpoint's Root certificate thumbprint is not null
                    if(-not([string]::IsNullOrWhiteSpace($certificates.ChainElements.Certificate[2].Thumbprint))){
                        # Set the certificate thumbprint to the variable
                        $ReturnCertRootThumbprint = $certificates.ChainElements.Certificate[2].Thumbprint
                    } else {
                        # No root certificate thumbprint found
                        $ReturnCertRootThumbprint = "No root certificate thumbprint found"
                    }
                } elseif($NoCertificatesRequired){
                    # No certificate found, as the port is 80
                    $ReturnCertIssuer = "Port 80 - SSL not required"
                    $ReturnCertSubject = "Port 80 - SSL not required"
                    $ReturnCertThumbprint = "Port 80 - SSL not required"
                    $ReturnCertIntIssuer = "Port 80 - SSL not required"
                    $ReturnCertIntSubject = "Port 80 - SSL not required"
                    $ReturnCertIntThumbprint = "Port 80 - SSL not required"
                    $ReturnCertRootIssuer = "Port 80 - SSL not required"
                    $ReturnCertRootSubject = "Port 80 - SSL not required"
                    $ReturnCertRootThumbprint = "Port 80 - SSL not required"
                } else {
                    # No certificate found, but expected
                    $ReturnCertIssuer = "Error retrieving certificates"
                    $ReturnCertSubject = "Error retrieving certificates"
                    $ReturnCertThumbprint = "Error retrieving certificates"
                    $ReturnCertIntIssuer = "Error retrieving certificates"
                    $ReturnCertIntSubject = "Error retrieving certificates"
                    $ReturnCertIntThumbprint = "Error retrieving certificates"
                    $ReturnCertRootIssuer = "Error retrieving certificates"
                    $ReturnCertRootSubject = "Error retrieving certificates"
                    $ReturnCertRootThumbprint = "Error retrieving certificates"
                    Write-Host "Error retrieving certificates..." -ForegroundColor Red
                }
            }

            # //////// Redirection Handling ////////
            # Check if the request was redirected
            if(($IwrResult.StatusCode -ge 300 -and $IwrResult.StatusCode -le 399) -or ($IwrResult.StatusCode -like "Moved*")){
                # Web endpoint request was redirected

                # We have a response, but need to check for redirects
                # Check for redirects
                if(-not([string]::IsNullOrWhiteSpace($IwrResult.Headers["Location"]))){
                    # Redirect found
                    Write-Host "Redirect: 'HTTP StatusCode = $($IwrResult.StatusCode)'"
                    Write-Verbose ("Web request took {0} seconds to complete" -f ($stopwatch.ElapsedMilliseconds/1000))
                    # Check if the URL is relative or absolute
                    if($IwrResult.Headers["Location"].ToString().Substring(0,1) -eq "/"){
                        # Relative URL, append to the original URL
                        $url = "$($url.ToString().SubString(0,$url.IndexOf('://')))://$((Get-DomainFromURL -url $($IwrResult.Headers.[Location])).Domain)$($IwrResult.Headers["Location"])"
                    } else {
                        # Absolute URL, use the redirected URL
                        [string]$url = $IwrResult.Headers["Location"]
                    }
                }

                if($RedirectCount -eq 1){
                    # First response, return the response, we do not want the status response from any redirects (if present)
                    $ReturnLayer7Response = $Layer7Response
                } else {
                    # Do nothing....
                    # Greater than 1 redirect, so we do not want subsequent responses for the initial status code response.
                }
                
                if($url -match "https://"){
                    $DestinationPort = 443
                } elseif($url -match "http://"){
                    $DestinationPort = 80
                } else {
                    Write-Warning "Invalid URL specified: '$url', redirected url from $($IwrResult.Headers.[Location])"
                    $Layer7Status = "Invalid URL: $url"
                    return $Layer7Status, "N/A"
                }

                # //// Add to the RedirectedResults array
                # Get the domain from the URL
                $RedirectedURL = (Get-DomainFromURL -url $url).Domain
                Write-Host "Redirecting to '$url'" -ForegroundColor Yellow
                # Update the Note, to show the redirect target:
                $ResultsToUpdate = $script:Results | Where-Object { $_.url -eq $OriginalURL -and ($_.Port -eq $OriginalPort) }
                ForEach($ResultToUpdate in $ResultsToUpdate){
                    if($ResultToUpdate.Note){
                        # Update the note to show the redirect target, if it exists (might not if function called independently)
                        $ResultToUpdate.Note = "Redirects to '$($RedirectedURL)' - $($ResultToUpdate.Note)"
                    }
                }
                # Add the redirected URL to the results
                if(-not([string]::IsNullOrWhiteSpace($RedirectedURL))){

                    # Additional check for the URL to not already exist in the Results array
                    [bool]$RedirectExistsInResults = $false
                    ForEach($result in $script:Results){
                        if((Get-DomainFromURL -url $result.url).Domain -eq $RedirectedURL){
                            Write-Debug "Redirected URL already exists in Results array, skipping"
                            $RedirectExistsInResults = $true
                            break
                        }
                    }
                    # Add the redirected URL to the RedirectedResults array
                    if($script:RedirectedResults.count -gt 0){
                        # Check if the URL already exists in the RedirectedResults array, and that it does not exist in the Results array, only add new ones
                        if(($script:RedirectedResults.url -notcontains $RedirectedURL) -and (-not($RedirectExistsInResults))){
                            # URL does not exist in the RedirectedResults AND Results array, add it
                                [array]$script:RedirectedResults += [PSCustomObject]@{
                                RowID = 0
                                url = $RedirectedURL
                                redirect = $url
                                Port = $DestinationPort
                                ArcGateway = $false
                                IsWildcard = $false
                                Source = "Redirect for $(($script:Results | Where-Object { ((Get-DomainFromURL -url $_.URL).Domain -eq (Get-DomainFromURL -url $OriginalURL).Domain) -and ($_.Port -eq $OriginalPort) } | Select-Object -First 1).Source)"
                                Note = "Redirected URL from $($OriginalURL) - $(($script:Results | Where-Object { ((Get-DomainFromURL -url $_.URL).Domain -eq (Get-DomainFromURL -url $OriginalURL).Domain) -and ($_.Port -eq $OriginalPort) } | Select-Object -First 1).Note)"
                                TCPStatus = ""
                                IPAddress = ""
                                Layer7Status = ""
                                Layer7Response = ""
                                Layer7ResponseTime = ""
                                CertificateSubject = ""
                                CertificateIssuer = ""
                                CertificateThumbprint = ""
                                IntermediateCertificateIssuer = ""
                                IntermediateCertificateSubject = ""
                                IntermediateCertificateThumbprint = ""
                                RootCertificateIssuer = ""
                                RootCertificateSubject = ""
                                RootCertificateThumbprint = ""
                            }
                            Write-Debug "Added $($script:RedirectedResults[-1]) to RedirectedResults array"
                        } else {
                            # URL already exists in the RedirectedResults array, do not add it again
                            Write-Debug "Redirected URL already exists in RedirectedResults Array AND Results array, skipping"
                        }
                    } else {
                        # First entry in RedirectedResults array, this stops the "-notcontains" check above failing when the array is empty
                        # Check that the URL does not already exist in the Results array, only add new ones
                        if(-not($RedirectExistsInResults)){
                            # First entry in RedirectedResults array, this stops the "-notcontains" check above failing when the array is empty
                            [array]$script:RedirectedResults = [PSCustomObject]@{
                                RowID = 0
                                url = $RedirectedURL
                                redirect = $url
                                Port = $DestinationPort
                                ArcGateway = $false
                                IsWildcard = $false
                                Source = "Redirect for $(($script:Results | Where-Object { ((Get-DomainFromURL -url $_.URL).Domain -eq (Get-DomainFromURL -url $OriginalURL).Domain) -and ($_.Port -eq $OriginalPort) } | Select-Object -First 1).Source)"
                                Note = "Redirected URL from $($OriginalURL) - $(($script:Results | Where-Object { ((Get-DomainFromURL -url $_.URL).Domain -eq (Get-DomainFromURL -url $OriginalURL).Domain) -and ($_.Port -eq $OriginalPort) } | Select-Object -First 1).Note)"
                                TCPStatus = ""
                                IPAddress = ""
                                Layer7Status = ""
                                Layer7Response = ""
                                Layer7ResponseTime = ""
                                CertificateSubject = ""
                                CertificateIssuer = ""
                                CertificateThumbprint = ""
                                IntermediateCertificateIssuer = ""
                                IntermediateCertificateSubject = ""
                                IntermediateCertificateThumbprint = ""
                                RootCertificateIssuer = ""
                                RootCertificateSubject = ""
                                RootCertificateThumbprint = ""
                            }
                            Write-Debug "Added $($script:RedirectedResults[-1]) to RedirectedResults array"
                        } else {
                            # URL already exists in the RedirectedResults array, do not add it again
                            Write-Debug "Redirected URL $RedirectedURL already exists in Results array, skipping"
                        }
                    }
                } else {
                    Write-Warning "Redirected URL is null for $url, skipping"
                }

            } else {
                
                # No (more) redirects found
                # Set the Layer7ResponseTime to the elapsed time
                $Layer7ResponseTime = $stopwatch.ElapsedMilliseconds/1000
                $RedirectsComplete = $true

            }
            
        # ///// Handling of exception web responses
        } else {

            # Response was not successful, but might be expected
            # Check if the response is a known value, or if the response is from a proxy or firewall responding
            # Documentation: https://developer.mozilla.org/en-US/docs/Web/HTTP/Status
            $ReturnLayer7Response = $Layer7Response
            
            # Check if the response is a known value
            if($Layer7Response -eq "Unable to connect to the remote server"){
                # Overwrite the Layer7Status to "Failed", when when $Request.HaveResponse is true.
                $Layer7Status = "Failed"

            } elseif($Layer7Response -eq "The operation has timed out"){
                # Overwrite the Layer7Status to "Failed", when when $Request.HaveResponse is true.
                $Layer7Status = "Failed"

            } elseif($Layer7Response -like "The remote name could not be resolved*"){
                # Overwrite the Layer7Status to "Failed", when when $Request.HaveResponse is true.
                $Layer7Status = "Failed"

            } elseif($Layer7Response -eq "(400) Bad Request"){
                # Request was successful, but it was a bad request
                $Layer7Status = "Success"

            } elseif($Layer7Response -eq "(401) Unauthorized"){
                # Request was successful, but the request was not authorized
                $Layer7Status = "Success"

            } elseif($Layer7Response -eq "(404) Not Found"){
                # Request was successful, but the URL was not found
                $Layer7Status = "Success"

            # Exception handling for 403 Forbidden responses
            } elseif($Layer7Response -eq "(403) Forbidden"){
                
                # Additional connectivity test for 403 Forbidden, to check if the URL is accessible
                if($($IwrError.ErrorRecord.Exception.Response.Server) -like "Microsoft-IIS*") {
                        # Microsoft IIS Server detected, valid connection
                        $Layer7Status = "Success"

                } elseif($($IwrError.ErrorRecord.Exception.Response.Server) -like "AkamaiGHost*") {
                    # AkamaiGHost detected, valid connection
                    $Layer7Status = "Success"

                } elseif($($IwrError.ErrorRecord.Exception.Response.Server) -like "Qwilt*") {
                    # Qwilt detected, valid connection
                    $Layer7Status = "Success"

                } elseif($($IwrError.ErrorRecord.Exception.Response.Server) -like "AzureContainerRegistry") {
                    # AzureContainerRegistry detected, valid connection
                    $Layer7Status = "Success"
                    
                } elseif($($IwrError.ErrorRecord.Exception.Response.StatusDescription) -eq "Forbidden - unexpected URL format") {
                    # "Forbidden - unexpected URL format" detected, valid connection
                    # Example URL: tlu.dl.delivery.mp.microsoft.com on port 80, which intermittently changes between "AkamaiGHost" and a null value for "$_.Exception.Response.Server"
                    $Layer7Status = "Success"

                } elseif($($IwrError.ErrorRecord.Exception.Response.Headers) -contains "x-azure-ref") {
                    # Azure Front Door response detected, valid connection
                    $Layer7Status = "Success"

                } elseif($($IwrError.ErrorRecord.Exception.Response.Headers) -contains "X-MSEdge-Ref") {
                    # Microsoft Edge response detected, valid connection
                    $Layer7Status = "Success"

                } elseif($($IwrError.ErrorRecord.Exception.Response.Headers) -contains "x-ms-request-id") {
                    # Microsoft Edge response detected, valid connection
                    $Layer7Status = "Success"

                } elseif($($IwrError.ErrorRecord.Exception.Response.Server) -like "Zscaler*") {
                    
                    # Known firewall / proxy device intercepting request
                    $Layer7Status = "Failed"
    
                } elseif($IwrError.Message.ToString().Contains("You do not have permission to view this directory or page using the credentials that you supplied.")){
                        # Expected response from a couple of endpoints
                        # Response: "403 - Forbidden: Access is denied."
                        # Server Error
                        # Server example: 'https://azurewatsonanalysis-prod.core.windows.net' and keyvaults
                        # Response: "403 - Forbidden: Access is denied. You do not have permission to view this directory or page using the credentials that you supplied."
                        # You do not have permission to view this directory or page using the credentials that you supplied.
                        $Layer7Status = "Success"

                } else {
                # ////// All other 403 Forbidden responses ///////
                # Unknown, set Layer7Status to "Failed"
                $Layer7Status = "Failed"

                }

            } elseif($Layer7Response -eq "(408) Request Timeout"){
                # Overwrite the Layer7Status to "Failed", when when $Request.HaveResponse is true.
                # https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/408
                $Layer7Status = "Failed"

            } elseif($Layer7Response -eq "(429) Too Many Requests"){
                # Overwrite the Layer7Status to "Failed", when when $Request.HaveResponse is true.
                # https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/429
                $Layer7Status = "Failed"

            } elseif($Layer7Response -eq "(500) Internal Server Error"){
                # Overwrite the Layer7Status to "Failed", when when $Request.HaveResponse is true.
                # https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/500
                $Layer7Status = "Success"

            } elseif($Layer7Response -eq "(502) Bad Gateway"){
                # Overwrite the Layer7Status to "Failed", another device can respond, which means $Request.HaveResponse is true.
                # https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/502
                $Layer7Status = "Failed"

                # Additional check for 502 Bad Gateway, to check for "Microsoft-Azure-Application-Gateway/v2" in the response
                if($($IwrError.ErrorRecord.Exception.Response.Server) -like 'Microsoft-Azure-Application-Gateway*') {
                    Write-Verbose "Azure Application Gateway response detected, valid connection"
                    $Layer7Status = "Success"

                # Additional check for arc gateway response
                } elseif($IwrError.Message.ToString().Contains("Our services aren't available right now") -and (($OriginalURL -like "*.gw.arc.azure.com") -or ($OriginalURL -eq "dp.stackhci.azure.com"))) {
                    Write-Verbose "Azure Front Door response detected, valid connection"
                    $Layer7Status = "Success"
    
                }

            # Could not 'Could not establish trust relationship for the SSL/TLS secure channel'
            } elseif($Layer7Response -like "*Could not establish trust relationship for the SSL/TLS secure channel"){
                # Overwrite the Layer7Status to "Failed"
                $Layer7Status = "Failed"
                
            # ////// All other / unhandled exceptions treat as Failed //////
            } else {
                # Overwrite the Layer7Status to "Failed"
                $Layer7Status = "Failed"

            }

            # Write debug information for failed requests
            if($IwrError){
                Write-Debug "Invoke-WebRequest: Exception Response StatusCode = $($IwrError.ErrorRecord.Exception.Response.StatusCode)"
                Write-Debug "Invoke-WebRequest: Exception Response Server = $($IwrError.ErrorRecord.Exception.Response.Server)"
                Write-Debug "Invoke-WebRequest: Command line: $($IwrError.ErrorRecord.InvocationInfo.Line)"
                Write-Debug "Invoke-WebRequest: Exception Status = $($IwrError.ErrorRecord.Exception.Status)"
                Write-Debug "Invoke-WebRequest: FullyQualifiedErrorId = $($IwrError.ErrorRecord.FullyQualifiedErrorId)"
                Write-Debug "Invoke-WebRequest: Exception HResult = $($IwrError.HResult)"
                ForEach ($response in ($IwrError.ErrorRecord.Exception.Response | Select-Object -Property *)) {
                    ForEach ($property in ($response.PSObject.Properties)) {
                        if($($property.Value).Count -eq 1) {
                            Write-Debug "$($property.Name) = $($property.Value)"
                        } elseif($($property.Value).Count -gt 1){
                            Write-Debug "$($property.Name) items: = $($property.Value.Count)"
                            ForEach($item in $property.Value) {
                                Write-Debug "$($property.Name).$($item) = $($IwrError.ErrorRecord.Exception.Response.$($property.Name)["$item"])"
                            }
                        } else {
                            # Do not output empty values
                        }
                    }
                }
                Write-Debug "Invoke-WebRequest: ErrorRecord Exception Message = $($IwrError.ErrorRecord.Exception.Message)"
                Write-Debug "Invoke-WebRequest: Full Exception Message = $($IwrError.Message)"


            }

            # Check Certificate details for exception responses only
            # Check if the URL is HTTPS
            Remove-Variable Certificates -ErrorAction SilentlyContinue
            Remove-Variable NoCertificatesRequired -ErrorAction SilentlyContinue
            if($Layer7Status -eq "Success"){
                if($url -match "https://"){
                    # Get the certificate chain for the URL
                    # The function will check for SSL inspection, and return the certificate issuer
                    # The function will return the certificate issuer, thumbprint, and subject
                    $Certificates = Get-SslCertificateChain -url $url -AllowAutoRedirect $false -ErrorAction SilentlyContinue
                } else {
                    # No certificate found
                    $NoCertificatesRequired = $true
                    Write-Verbose "Port 80 - SSL Not Required"
                }

                # Get Certificate details for exception responses
                if($Certificates){
                    # Check if the certificate chain is not null
                    if($Certificates -and $Certificates.ChainElements.Certificate.Count -gt 0){
                        Write-Verbose "Certificates Chain found with $(($Certificates.ChainElements.Certificate).count) parts"
                    } else {
                        # No certificate found
                        Write-Host "Error: No SSL Certificates found" -ForegroundColor Red
                    }
                    # Check if the certificate subject is not null
                    if(-not([string]::IsNullOrWhiteSpace($certificates.ChainElements.Certificate[0].Subject))){
                        # Set the certificate subject to the variable
                        $ReturnCertSubject = $certificates.ChainElements.Certificate[0].Subject 
                        # Check the certificate using Test-Certificate, to check if the certificate is trusted for SSL
                        # Check if the certificate is trusted for SSL
                        if(Test-Certificate -Cert $certificates.ChainElements.Certificate[0] -Policy SSL -ErrorAction SilentlyContinue -ErrorVariable certError){
                            # Certificate is valid for SSL
                            Write-Verbose "Certificate is trusted for SSL: $ReturnCertSubject"
                            # Check if the certificate is valid for the URL
                            if(Test-Certificate -Cert $certificates.ChainElements.Certificate[0] -Policy SSL -DNSName (Get-DomainFromURL -url $url).Domain -ErrorAction SilentlyContinue -ErrorVariable certError){
                                # Certificate is a valid for URL
                                Write-Verbose "Certificate is a valid for endpoint: $url"
                            } else {
                                # Certificate is a valid for URL
                                Write-Host "Certificate is not valid for endpoint: $url" -ForegroundColor Red
                                Write-Host "Possible SSL Inspection detected, certificate is not trusted." -ForegroundColor Red
                                $Layer7Status = "Failed"
                                $ReturnLayer7Response = $ReturnLayer7Response + " - SSL Inspection detected"
                                $script:SSLInspectionDetected = $true
                                $script:SSLInspectedURLs += $url
                            }
                        } else {
                            # Certificate is not valid for SSL
                            Write-Host "Certificate is not trusted for SSL: $ReturnCertSubject" -ForegroundColor Red
                            # SSL inspection detected
                            Write-Host "Possible SSL Inspection detected, certificate is not trusted." -ForegroundColor Red
                            $Layer7Status = "Failed"
                            $ReturnLayer7Response = $ReturnLayer7Response + " - SSL Inspection detected"
                            $script:SSLInspectionDetected = $true
                            $script:SSLInspectedURLs += $url
                        }
                    } else {
                        # No certificate subject found
                        $ReturnCertSubject = "No certificate common name found"
                    }
                    # Check if the certificate issuer is not null
                    if(-not([string]::IsNullOrWhiteSpace($certificates.ChainElements.Certificate[0].Issuer))){
                        # Set the certificate issuer to the variable
                        $ReturnCertIssuer = $certificates.ChainElements.Certificate[0].Issuer
                    } else {
                        # No certificate issuer found
                        $ReturnCertIssuer = "No certificate issuer found"
                    }
                    # Check if the certificate thumbprint is not null
                    if(-not([string]::IsNullOrWhiteSpace($certificates.ChainElements.Certificate[0].Thumbprint))){
                        # Set the certificate thumbprint to the variable
                        $ReturnCertThumbprint = $certificates.ChainElements.Certificate[0].Thumbprint
                    } else {
                        # No certificate thumbprint found
                        $ReturnCertThumbprint = "No certificate thumbprint found"
                    }
                    # Check if the intermediate certificate subject is not null
                    if(-not([string]::IsNullOrWhiteSpace($certificates.ChainElements.Certificate[1].Subject))){
                        # Set the certificate issuer to the variable
                        $ReturnCertIntSubject = $certificates.ChainElements.Certificate[1].Subject
                    } else {
                        # No intermediate certificate Subject found
                        $ReturnCertIntSubject = "No intermediate certificate common name found"
                    }
                    # Check if the intermediate certificate issuer is not null
                    if(-not([string]::IsNullOrWhiteSpace($certificates.ChainElements.Certificate[1].Issuer))){
                        # Set the certificate issuer to the variable
                        $ReturnCertIntIssuer = $certificates.ChainElements.Certificate[1].Issuer
                    } else {
                        # No intermediate certificate issuer found
                        $ReturnCertIntIssuer = "No intermediate certificate issuer found"
                    }
                    # Check if the intermediate certificate thumbprint is not null
                    if(-not([string]::IsNullOrWhiteSpace($certificates.ChainElements.Certificate[1].Thumbprint))){
                        # Set the certificate thumbprint to the variable
                        $ReturnCertIntThumbprint = $certificates.ChainElements.Certificate[1].Thumbprint
                    } else {
                        # No intermediate certificate thumbprint found
                        $ReturnCertIntThumbprint = "No intermediate certificate thumbprint found"
                    }
                    # Check if the root certificate subject is not null
                    if(-not([string]::IsNullOrWhiteSpace($certificates.ChainElements.Certificate[2].Subject))){
                        # Set the certificate issuer to the variable
                        $ReturnCertRootSubject = $certificates.ChainElements.Certificate[2].Subject
                    } else {
                        # No root certificate Subject found
                        $ReturnCertRootSubject = "No root certificate common name found"
                    }
                    # Check if the root certificate issuer is not null
                    if(-not([string]::IsNullOrWhiteSpace($certificates.ChainElements.Certificate[2].Issuer))){
                        # Set the certificate issuer to the variable
                        $ReturnCertRootIssuer = $certificates.ChainElements.Certificate[2].Issuer
                    } else {
                        # No root certificate issuer found
                        $ReturnCertRootIssuer = "No root certificate issuer found"
                    }
                    # Check if the root certificate thumbprint is not null
                    if(-not([string]::IsNullOrWhiteSpace($certificates.ChainElements.Certificate[2].Thumbprint))){
                        # Set the certificate thumbprint to the variable
                        $ReturnCertRootThumbprint = $certificates.ChainElements.Certificate[2].Thumbprint
                    } else {
                        # No root certificate thumbprint found
                        $ReturnCertRootThumbprint = "No root certificate thumbprint found"
                    }
                } elseif($NoCertificatesRequired){
                    # No certificate found, as the port is 80
                    $ReturnCertIssuer = "Port 80 - SSL not required"
                    $ReturnCertSubject = "Port 80 - SSL not required"
                    $ReturnCertThumbprint = "Port 80 - SSL not required"
                    $ReturnCertIntIssuer = "Port 80 - SSL not required"
                    $ReturnCertIntSubject = "Port 80 - SSL not required"
                    $ReturnCertIntThumbprint = "Port 80 - SSL not required"
                    $ReturnCertRootIssuer = "Port 80 - SSL not required"
                    $ReturnCertRootSubject = "Port 80 - SSL not required"
                    $ReturnCertRootThumbprint = "Port 80 - SSL not required"
                } else {
                    # No certificate found, but expected
                    $ReturnCertIssuer = "Error retrieving certificates"
                    $ReturnCertSubject = "Error retrieving certificates"
                    $ReturnCertThumbprint = "Error retrieving certificates"
                    $ReturnCertIntIssuer = "Error retrieving certificates"
                    $ReturnCertIntSubject = "Error retrieving certificates"
                    $ReturnCertIntThumbprint = "Error retrieving certificates"
                    $ReturnCertRootIssuer = "Error retrieving certificates"
                    $ReturnCertRootSubject = "Error retrieving certificates"
                    $ReturnCertRootThumbprint = "Error retrieving certificates"               
                }

            } # End of certificate check
            
            $Layer7ResponseTime = $stopwatch.ElapsedMilliseconds/1000
            # End loop, no response to check for redirects
            $RedirectsComplete = $true
        
        } # End of IwrResult check
         
    } while (
        # Continue the loop until the RedirectsComplete variable is set to True
        ($RedirectsComplete -ne $true)
    )

    # Return the Layer 7 status and Response from the Function
    Write-Debug "Layer7Status = $Layer7Status, Layer7Response = $ReturnLayer7Response"
    Write-Debug "Certificate Issuer = $ReturnCertIssuer"

    # Time taken to complete the web request
    Write-Verbose ("Web request took {0} seconds to complete" -f $Layer7ResponseTime)

    if($Layer7Status -eq "Success"){
        # Check if the Layer 7 status is a success
        Write-Host "Result: $Layer7Status" -ForegroundColor Green
    } else {
        # Check if the Layer 7 status is a failure
        Write-Host "Result: $Layer7Status" -ForegroundColor Red
    }
    
    # Remove the variables from the session
    Remove-Variable ReturnVariable -ErrorAction SilentlyContinue
    # Create a new PSObject to return the results
    $ReturnVariable = [PsCustomObject][Ordered]@{
        # Layer7Status
        Layer7Status = $Layer7Status
        # Layer7Response
        Layer7Response = $ReturnLayer7Response
        # Layer7ResponseTime
        Layer7ResponseTime = $Layer7ResponseTime
        # Certificate Issuer
        CertificateIssuer = $ReturnCertIssuer
        # Certificate Subject
        CertificateSubject = $ReturnCertSubject
        # Certificate Thumbprint
        CertificateThumbprint = $ReturnCertThumbprint
        # Intermediate Certificate Issuer
        CertificateIntermediateIssuer = $ReturnCertIntIssuer
        # Intermediate Certificate Subject
        CertificateIntermediateSubject = $ReturnCertIntSubject
        # Intermediate Certificate Thumbprint
        CertificateIntermediateThumbprint = $ReturnCertIntThumbprint
        # Root Certificate Issuer
        CertificateRootIssuer = $ReturnCertRootIssuer
        # Root Certificate Subject
        CertificateRootSubject = $ReturnCertRootSubject
        # Root Certificate Thumbprint
        CertificateRootThumbprint = $ReturnCertRootThumbprint
    }
    # Return the output
    Write-Debug "ReturnVariable = $ReturnVariable"
    return $ReturnVariable
}

# ////////////////////////////////////////////////////////////////////////////
# Function to test TCP connectivity using Test-NetConnection cmdlet
Function Test-TCPConnectivity {
    param (
        [ValidateLength(1, 255)]
        [string]$url,
        [ValidateRange(1, 65535)]
        [int]$port
    )
    try {
        # Test the TCP connectivity to the URL and Port, with default timeout of 5 seconds, WarningAction SilentlyContinue, and ErrorAction Stop
        $testResult = Test-NetConnection -ComputerName $url -Port $port -WarningAction SilentlyContinue -ErrorAction Stop
    } catch {
        Write-Host "Error: testing TCP connectivity to $url on port $port"
        Write-Error "Error: $($_.Exception.Message)"
        return "Failed", ""
    }
    $TcpStatus = if ($testResult.TcpTestSucceeded) { "Success" } else { "Failed" }
    $ipAddress = $testResult.RemoteAddress
    return $TcpStatus, $ipAddress
}

# ////////////////////////////////////////////////////////////////////////////
# Function to perform NTP test for time.windows.com or custom NTP server
Function Test-NTPConnectivity {
    param (
        [string]$ntpServer
    )
    Write-Host "Testing NTP connectivity to '$ntpServer' on UDP port 123"
    # Run the w32tm command to test NTP connectivity, with 2 samples
    $ntpResult = w32tm /stripchart /computer:$ntpServer /dataonly /samples:2
    # Check if the result contains "error:"
    if ($ntpResult -match "error:") {
        Write-Host "NTP test failed" -ForegroundColor Red
        # Write the error message, first item, as there could be two lines
        Write-Host "Diagnostic info: $($ntpResult | Where-Object { $_ -like '*error*'} | Select-Object -First 1)" -ForegroundColor Red
        $status = "Failed"
        $ipAddress = ""
    } else {
        Write-Host "NTP test successful" -ForegroundColor Green
        $status = "Success"
        if ($ntpResult -match "\[(.*?)\]") {
            # find the IP address in the square brackets of the result, first row, zero
            $ipAddress = $ntpResult[0].Substring($ntpResult[0].IndexOf("[")+1,$ntpResult[0].IndexOf("]")-$ntpResult[0].IndexOf("[")-1)
        } else {
            $ipAddress = ""
        }
    }
    # Return the status and IP address
    return $status, $ipAddress
}

# ////////////////////////////////////////////////////////////////////////////
# Enhanced Function to expand wildcard URLs dynamically using pattern matching *.domain.com
# This function identifies any specific URLs (non-wildcards) that match a wildcard URL entry and tests them
Function Expand-WildcardUrlsDynamically {

    # Split into wildcard and non-wildcard results
    [array]$wildcardResults = $script:Results | Where-Object { $_.IsWildcard -eq $true }
    [array]$nonWildcardResults = $script:Results | Where-Object { $_.IsWildcard -eq $false }

    [int]$wildcardCount = 0

    ForEach ($wildcard in $wildcardResults) {

        $wildcardCount++

        # Create a regex pattern from the wildcard URL
        $wildcardPattern = $wildcard.URL -replace "\*", ".*"

        # Find matching non-wildcard URLs, for each wildcard URL that exists
        [array]$matchingUrls = @()
        $matchingUrls = $nonWildcardResults | Where-Object { $_.URL -match "^$wildcardPattern$" }

        Write-Host "Expanding wildcard URL $wildcardCount of $($wildcardResults.Count): $($wildcard.URL)"

        if($matchingUrls.Count -eq 0) {
            Write-Host "Info: No matched for $($wildcard.source), $($wildcard.URL)`n" -ForegroundColor Yellow

        } elseif ($matchingUrls.Count -gt 0) {
            # Matches found, expand the wildcard entry
            # Incremental counter for matching URLs
            [int]$counter = 0
            # Loop for each matching URL
            foreach ($match in $matchingUrls) {
                $counter++
                Write-Host "Processing Wildcard URL $counter of $($matchingUrls.count): $($match.URL)"
                # Add URL based on test for either HTTP or HTTPS
                if($match.Port -is [int]){
                    # If the URL already exists in the results, update the note
                    if($script:Results.URL -contains $match.URL){
                        # URL already exists in the results, skip
                        Write-Verbose "Info: Url $($match.URL) already exists in the results, skipping"
                        if($match.Note -notlike "URL matches wildcard from *"){
                            # Update the note to include the matching URL
                           ($script:Results | Where-Object { $PSItem.URL -eq $match.URL } | Select-Object -First 1).Note = "URL matches wildcard from $($wildcard.Source), to test URL $($wildcard.URL) - $($match.Note)"
                        }
 
                    }
                    # If the URL is not present, add the matching URL to the results
                    if($script:Results.URL -notcontains $match.URL){
                        # Specific URL matched to Wildcard URL does not exist in the results, add it
                        $newEntry = $wildcard.PSObject.Copy()
                        $newEntry.URL = $match.URL
                        $newEntry.Port = $match.Port
                        $newEntry.ArcGateway = $match.ArcGateway
                        $newEntry.Source = "$($match.Source)"
                        $newEntry.Note = "URL matches wildcard from $($match.Source), to test URL $($wildcard.URL) - $($match.Note)"
                        # Set IsWildcard to false, as this is no longer a wildcard URL, it is a specific URL to test a wildcard URL
                        $newEntry.IsWildcard = $false
                        if($ArcGatewayDeployment.IsPresent){
                            # Skip URLs with ArcGateway -eq $True
                            if($match.ArcGateway){
                                # Skip URLs that support Arc Gateway
                                Write-Host "Skipped URL: '$($newEntry.URL)' as supported by Arc Gateway" -ForegroundColor Yellow
                                $newEntry.TCPStatus = "Skipped"
                                $newEntry.Note = "Skipped, URL is supported by Arc Gateway - $($match.Note)"
                                $newEntry.IPAddress = "N/A"
                                $newEntry.Layer7Status = "Skipped"
                                $newEntry.Layer7Response = "N/A"
                                $newEntry.Layer7ResponseTime = "N/A"
                                $newEntry.CertificateIssuer = "N/A"
                                $newEntry.CertificateSubject = "N/A"
                                $newEntry.CertificateThumbprint = "N/A"
                                $newEntry.IntermediateCertificateIssuer = "N/A"
                                $newEntry.IntermediateCertificateSubject = "N/A"
                                $newEntry.IntermediateCertificateThumbprint = "N/A"
                                $newEntry.RootCertificateIssuer = "N/A"
                                $newEntry.RootCertificateSubject = "N/A"
                                $newEntry.RootCertificateThumbprint = "N/A"
                            } else {
                                # Does not support Arc Gateway
                                # Check if the domain name exists in DNS
                                $DNSCheck = Get-DnsRecord -url (Get-DomainFromURL -url $match.URL).Domain
                                if($DNSCheck.DNSExists){
                                    if($IncludeTCPConnectivityTests.IsPresent){
                                        $newEntry.TCPStatus, $newEntry.IPAddress = Test-TCPConnectivity -url $match.URL -port $match.Port
                                    } else {
                                        $newEntry.TCPStatus = "N/A"
                                        $newEntry.IPAddress = $DNSCheck.IpAddress
                                    }
                                    # Layer 7 connectivity test
                                    $Layer7Results = Test-Layer7Connectivity -url $match.URL -port $match.Port
                                    $newEntry.Layer7Status = $Layer7Results.Layer7Status
                                    $newEntry.Layer7Response = $Layer7Results.Layer7Response
                                    $newEntry.Layer7ResponseTime = $Layer7Results.Layer7ResponseTime
                                    $newEntry.CertificateIssuer = $Layer7Results.CertificateIssuer
                                    $newEntry.CertificateSubject = $Layer7Results.CertificateSubject
                                    $newEntry.CertificateThumbprint = $Layer7Results.CertificateThumbprint
                                    $newEntry.IntermediateCertificateIssuer = $Layer7Results.CertificateIntermediateIssuer
                                    $newEntry.IntermediateCertificateSubject = $Layer7Results.CertificateIntermediateSubject
                                    $newEntry.IntermediateCertificateThumbprint = $Layer7Results.CertificateIntermediateThumbprint
                                    $newEntry.RootCertificateIssuer = $Layer7Results.CertificateRootIssuer
                                    $newEntry.RootCertificateSubject = $Layer7Results.CertificateRootSubject
                                    $newEntry.RootCertificateThumbprint = $Layer7Results.CertificateRootThumbprint
                                } else {
                                    # DNS name does not exist
                                    if($IncludeTCPConnectivityTests.IsPresent){
                                        $newEntry.TCPStatus = "Failed"
                                    } else {
                                        $newEntry.TCPStatus = "N/A"
                                    }
                                    $newEntry.IPAddress = $DNSCheck.IpAddress
                                    $newEntry.Layer7Status = "Failed" # Layer 7 status failed, as DNS name does not exist
                                    $newEntry.Layer7Response = "N/A"
                                    $newEntry.Layer7ResponseTime = "N/A"
                                    $newEntry.CertificateIssuer = "N/A"
                                    $newEntry.CertificateSubject = "N/A"
                                    $newEntry.CertificateThumbprint = "N/A"
                                    $newEntry.IntermediateCertificateIssuer = "N/A"
                                    $newEntry.IntermediateCertificateSubject = "N/A"
                                    $newEntry.IntermediateCertificateThumbprint = "N/A"
                                    $newEntry.RootCertificateIssuer = "N/A"
                                    $newEntry.RootCertificateSubject = "N/A"
                                    $newEntry.RootCertificateThumbprint = "N/A"
                                }
                            }
                        } else {
                            # Else process URLs with ArcGateway -eq $False
                            # Check if the domain name exists in DNS
                            $DNSCheck = Get-DnsRecord -url (Get-DomainFromURL -url $match.URL).Domain
                            if($DNSCheck.DNSExists){
                                if($IncludeTCPConnectivityTests.IsPresent){
                                    $newEntry.TCPStatus, $newEntry.IPAddress = Test-TCPConnectivity -url $match.URL -port $match.Port
                                } else {
                                    $newEntry.TCPStatus = "N/A"
                                    $newEntry.IPAddress = $DNSCheck.IpAddress
                                }
                                # Layer 7 connectivity test
                                $Layer7Results = Test-Layer7Connectivity -url $match.URL -port $match.Port
                                $newEntry.Layer7Status = $Layer7Results.Layer7Status
                                $newEntry.Layer7Response = $Layer7Results.Layer7Response
                                $newEntry.Layer7ResponseTime = $Layer7Results.Layer7ResponseTime
                                $newEntry.CertificateIssuer = $Layer7Results.CertificateIssuer
                                $newEntry.CertificateSubject = $Layer7Results.CertificateSubject
                                $newEntry.CertificateThumbprint = $Layer7Results.CertificateThumbprint
                                $newEntry.IntermediateCertificateIssuer = $Layer7Results.CertificateIntermediateIssuer
                                $newEntry.IntermediateCertificateSubject = $Layer7Results.CertificateIntermediateSubject
                                $newEntry.IntermediateCertificateThumbprint = $Layer7Results.CertificateIntermediateThumbprint
                                $newEntry.RootCertificateIssuer = $Layer7Results.CertificateRootIssuer
                                $newEntry.RootCertificateSubject = $Layer7Results.CertificateRootSubject
                                $newEntry.RootCertificateThumbprint = $Layer7Results.CertificateRootThumbprint
                            
                            } else {
                                # DNS name does not exist
                                if($IncludeTCPConnectivityTests.IsPresent){
                                    $newEntry.TCPStatus = "Failed"
                                } else {
                                    $newEntry.TCPStatus = "N/A"
                                }
                                $newEntry.IPAddress = $DNSCheck.IpAddress
                                $newEntry.Layer7Status = "Failed" # Layer 7 status failed, as DNS name does not exist
                                $newEntry.Layer7Response = "N/A"
                                $newEntry.Layer7ResponseTime = "N/A"
                                $newEntry.CertificateIssuer = "N/A"
                                $newEntry.CertificateSubject = "N/A"
                                $newEntry.CertificateThumbprint = "N/A"
                                $newEntry.IntermediateCertificateIssuer = "N/A"
                                $newEntry.IntermediateCertificateSubject = "N/A"
                                $newEntry.IntermediateCertificateThumbprint = "N/A"
                                $newEntry.RootCertificateIssuer = "N/A"
                                $newEntry.RootCertificateSubject = "N/A"
                                $newEntry.RootCertificateThumbprint = "N/A"
                            }
                        }
                        # Add the new match to the expanded results
                        $script:Results += $wildcard
                    } else {
                        # URL already exists in the results, skip
                        Write-Host "Info: Url $($wildcard.URL) already exists in the results, skipping" -ForegroundColor Yellow
                        if($match.Note -notlike "URL matches wildcard from *"){
                            # Update the note to include the matching URL
                            $match.Note = "URL matches wildcard from $($match.Source), to test URL $($wildcard.URL) - $($match.Note)"
                        }

                    }
                    
                    # If wildcard URL is not in the results, we need to add it
                    if($script:Results.URL -notcontains $wildcard.URL){
                        # Specific URL matched to Wildcard URL does not exist in the results, add it
                        $newEntry = $wildcard.PSObject.Copy()
                        $newEntry.URL = $wildcard.URL
                        $newEntry.Port = $wildcard.Port
                        $newEntry.ArcGateway = $wildcard.ArcGateway
                        $newEntry.IsWildcard = $true
                        $newEntry.Source = "$($wildcard.Source)"
                        $newEntry.TCPStatus = "N/A"
                        $newEntry.IPAddress = "N/A"
                        $newEntry.Layer7Status = "N/A"
                        $newEntry.Layer7Response = "N/A"
                        $newEntry.Layer7ResponseTime = "N/A"
                        $newEntry.CertificateIssuer = "N/A"
                        $newEntry.CertificateSubject = "N/A"
                        $newEntry.CertificateThumbprint = "N/A"
                        $newEntry.IntermediateCertificateIssuer = "N/A"
                        $newEntry.IntermediateCertificateSubject = "N/A"
                        $newEntry.IntermediateCertificateThumbprint = "N/A"
                        $newEntry.RootCertificateIssuer = "N/A"
                        $newEntry.RootCertificateSubject = "N/A"
                        $newEntry.RootCertificateThumbprint = "N/A"
                        $newEntry.Note = "Wildcard URL, connectivity tested by URL $($match.URL) - $($wildcard.Note)"

                        # Add the new match to the expanded results
                        $script:Results += $wildcard
                    } else {
                        # URL already exists in the results, skip
                        Write-Verbose "Info: Url $($wildcard.URL) already exists in the results, skipping"
                        if($wildcard.Note -notlike "Wildcard URL, tested by *"){
                            # Update the note to include the matching URL
                            $WildcardMatches = $script:Results | Where-Object { $PSItem.URL -eq $wildcard.URL }
                            ForEach($WildcardMatch in $WildcardMatches) {
                                # Update the note to include the matching URL
                                if($WildcardMatch.Note -notlike "Wildcard URL, tested by *"){
                                    # Update the note to include the matching URL
                                    $WildcardMatch.Note = "Wildcard URL, tested by $($match.URL) from $($match.Source) - $($wildcard.Note)"
                                }
                            }
                        }
                    }

                } else {
                    Write-Warning "Port not valid for url: $($match.URL), port: $($match.Port), note: $($match.Source) - skipping"
                }
            Write-Host ""
            }
        } else {
            Write-Warning "Unexpected: Wildcard URL $($wildcard.URL) matches status unknown, skipping"
        }
    }

}

# ////////////////////////////////////////////////////////////////////////////
# Check DNS to for domain name, and return IP address if found
Function Get-DnsRecord {
    param (
        [ValidateLength(1, 255)]
        [string]$url
    )

    # Initialize variables
    [bool]$dnsExists = $false
    Write-Debug "Checking to see if $($url) returns an IP address from DNS"
    
    # Call Resolve-DnsName twice, to check if the domain name exists, as for some subdomains, the first call may return success without an IP address
    # But the second call to DNS correctly returns the error
    # Loop twice:
    For ($i=1; $i -le 2; $i++) {
    
        # Remove variables
        Remove-Variable DNSCheckError -ErrorAction SilentlyContinue
        # Initialize variables
        $DNSCheck = @()
        # Check if the domain name exists in DNS
        Write-Debug "Checking DNS for $url - Attempt $i"

        try {
     
            # Check if the domain name exists in DNS using Resolve-DnsName
            $DNSCheck = Resolve-DnsName -Name $url -Type A -DnsOnly -ErrorAction Stop -ErrorVariable DNSCheckError

        # /////////////////////
        # Error handling logic
        # /////////////////////
        } catch [System.Management.Automation.CommandNotFoundException] { # Catch if Resolve-DnsName is not found, not expected
            Write-Host "Resolve-DnsName is not found, please run this script on a Windows 8/Server 2012 or later machine" -ForegroundColor "Red"
            Exit

        } catch { 
            # Catch DNS Errors
            # Check if the error message contains 'DNS name does not exist'
            if($_.Exception.Message.ToString().Contains('DNS name does not exist')) {
                Write-Debug "DNS Error for '$url' Exception: $($_.Exception.Message)"
            } else {
                # All other DNS errors
                if($i -eq 2){
                    Write-Host "`tUnknown DNS Error for '$url' - Exception Message: $($_.Exception.Message)" -ForegroundColor "Red"
                }
            }

        } Finally {
            # If no DNS errors, set the ipAddress variable to IP address returned from DNS
            if(-not($DNSCheckError)) {
                # Check if the DNS name exists
                if($DNSCheck){
                    if(($DNSCheck.IPAddress).count -gt 1){
                        # Use first IP address returned from DNS
                        $ipAddress = ($DNSCheck.IPAddress)[0]
                    } else {
                        # Only one IP address returned from DNS
                        $ipAddress = $DNSCheck.IPAddress
                    }
                    # Output message on second attempt
                    if($i -eq 2){
                        # DNS successful, return IP address
                        Write-Verbose "DNS successful for $url, returned IP Address: $ipAddress"
                        $dnsExists = $True
                    }
                } else {
                    # No IP address returned from DNS, but record exists
                    $ipAddress = "No Type A record found in DNS"
                    if($i -eq 2){
                        Write-Host "DNS failed for $url - $ipAddress" -ForegroundColor Red
                    }
                    $dnsExists = $False
                }
            
            } else {
                # DNS Error variable exists, set IP address to "DNS Lookup Failed"
                $ipAddress = "DNS name does not exist"
                if($i -eq 2){
                    Write-Host "DNS Lookup Failed for $url" -ForegroundColor "Red"
                }
                $dnsExists = $False
            }
        } # End of Finally block

    } # End of For loop

    # Return True/False and IP Address output as a PSObject
    $DNSReturnVariable = New-Object PsObject -Property @{
        # True/False
        DNSExists = $dnsExists
        # IP Address, or "DNS Lookup Failed"
        IPAddress = $ipAddress
    }
    return $DNSReturnVariable
}

# ////////////////////////////////////////////////////////////////////////////
# Function to manually test known subdomains for wildcard URLs
Function Test-ManuallyDefinedSubdomains {

    # Define manually known subdomains to test for each wildcard
    $manualSubdomains = @(
        @{ Wildcard = "*.blob.storage.azure.net"; Subdomains = @("mystorageaccount.blob.core.windows.net","eus2azreplstore214.blob.core.windows.net") },
        @{ Wildcard = "*.download.windowsupdate.com"; Subdomains = @("1a.au.download.windowsupdate.com") },
        @{ Wildcard = "*.update.microsoft.com"; Subdomains = @("fe2.update.microsoft.com") },
        @{ Wildcard = "*.windowsupdate.com"; Subdomains = @("ctldl.windowsupdate.com") },
        @{ Wildcard = "*.endpoint.security.microsoft.com"; Subdomains = @("edr-neu3.eu.endpoint.security.microsoft.com") },
        @{ Wildcard = "*.prod.hot.ingest.monitor.core.windows.net"; Subdomains = @("prod5.prod.hot.ingest.monitor.core.windows.net") },
        @{ Wildcard = "*.servicebus.windows.net"; Subdomains = @("azgn-southcentralus-public-1p-sn-vazr0002.servicebus.windows.net") }
    )

    # Incremental counter for subdomains
    [int]$subdomainCount = 0
    
    # Test each subdomain for connectivity
    ForEach ($entry in $manualSubdomains) {
        $wildcard = $entry.Wildcard
        $subdomains = $entry.Subdomains

        # Check if the wildcard URL is supported by Arc Gateway, if so, skip the wildcard URL
        if($ArcGatewayDeployment.IsPresent -and ($PreArcGatewayRemoval | Where-Object { ($_.URL -eq $wildcard) -and ($_.ArcGateway -eq $true) })) {
            # Skip wildcard URLs that have already been tested
            Write-Host "Info: Wildcard Url $wildcard, supports the Arc Gateway, skipping" -ForegroundColor Yellow
            Continue
        }
        Write-Host "`nTesting manually defined subdomains for wildcard $wildcard"
        ForEach ($subdomain in $subdomains) {
            # Test each subdomain for TCP and Layer 7 connectivity
            $subdomainCount++
            Write-Host "Processing subdomain $subdomainCount of $($manualSubdomains.subdomains.count): $subdomain"
            # Test port 80 and 443 for each subdomain
            ForEach ($port in @(80, 443)) {
                
                # Exceptions to some URLs, that do not support port 80 or 443
                # Skip port 443 for ctldl.windowsupdate.com, download.windowsupdate.com, fe2.update.microsoft.com and 1a.au.download.windowsupdate.com
                if(($subdomain -in ("ctldl.windowsupdate.com","download.windowsupdate.com","fe2.update.microsoft.com","1a.au.download.windowsupdate.com")) -and ($port -eq 443)){
                    # Skip port 443 for ctldl.windowsupdate.com, download.windowsupdate.com, fe2.update.microsoft.com and 1a.au.download.windowsupdate.com
                    Write-Host "Skipping port 443 for $subdomain" -ForegroundColor Yellow
                    Continue
                }
                # Skip port 80 for prod5.prod.hot.ingest.monitor.core.windows.net and edr-neu3.eu.endpoint.security.microsoft.com
                if(($subdomain -in ("prod5.prod.hot.ingest.monitor.core.windows.net","edr-neu3.eu.endpoint.security.microsoft.com")) -and ($port -eq 80)){
                    # Skip port 80 for prod5.prod.hot.ingest.monitor.core.windows.net and edr-neu3.eu.endpoint.security.microsoft.com
                    Write-Host "Skipping port 80 for $subdomain" -ForegroundColor Yellow
                    Continue
                }
                
                # Check if the domain name exists in DNS
                $DNSCheck = Get-DnsRecord -url (Get-DomainFromURL -url $subdomain).Domain
                if($DNSCheck.DNSExists){
                    if($IncludeTCPConnectivityTests.IsPresent){
                        # Test TCP and Layer 7 connectivity
                        $TCPstatus, $ipAddress = Test-TCPConnectivity -url $subdomain -port $port
                    } else {
                        $TCPstatus = "N/A"
                        $ipAddress = $DNSCheck.IpAddress
                    }
                    # Test Layer 7 connectivity using Test-Layer7Connectivity function
                    $Layer7Results = Test-Layer7Connectivity -url $subdomain -port $port
                    # Assign Layer 7 results to variables
                    $Layer7Status = $Layer7Results.Layer7Status
                    $Layer7Response = $Layer7Results.Layer7Response
                    $Layer7ResponseTime = $Layer7Results.Layer7ResponseTime
                    $CertificateIssuer = $Layer7Results.CertificateIssuer
                    $CertificateSubject = $Layer7Results.CertificateSubject
                    $CertificateThumbprint = $Layer7Results.CertificateThumbprint
                    $CertificateIntermediateIssuer = $Layer7Results.CertificateIntermediateIssuer
                    $CertificateIntermediateSubject = $Layer7Results.CertificateIntermediateSubject
                    $CertificateIntermediateThumbprint = $Layer7Results.CertificateIntermediateThumbprint
                    $CertificateRootIssuer = $Layer7Results.CertificateRootIssuer
                    $CertificateRootSubject = $Layer7Results.CertificateRootSubject
                    $CertificateRootThumbprint = $Layer7Results.CertificateRootThumbprint

                } else {
                    # DNS name does not exist
                    if($IncludeTCPConnectivityTests.IsPresent){
                        $TCPstatus = "Failed"
                    } else {
                        $TCPstatus = "N/A"
                    }
                    $ipAddress = "N/A"
                    $Layer7Status = "Failed"
                    $Layer7Response = "N/A"
                    $Layer7ResponseTime = "N/A"
                    $CertificateIssuer = "N/A"
                    $CertificateSubject = "N/A"
                    $CertificateThumbprint = "N/A"
                    $CertificateIntermediateIssuer = "N/A"
                    $CertificateIntermediateSubject = "N/A"
                    $CertificateIntermediateThumbprint = "N/A"
                    $CertificateRootIssuer = "N/A"
                    $CertificateRootSubject = "N/A"
                    $CertificateRootThumbprint = "N/A"
                }
                
                # Check if the URL already exists in the "allTestedUrls" results, only add new ones
                if($script:Results.URL -notcontains $subdomain) {
                    # Array to store the subdomain entry
                    $subdomainEntry = @()
                    # ArcGateway = $true, as all wildcards are "microsoft.com" or "windows.net", so should support ArcGateway
                    $subdomainEntry += [PSCustomObject]@{
                        RowID = 0
                        URL = $subdomain
                        Port = $port
                        ArcGateway = $true
                        IsWildcard = $false
                        Source = "Test for $(($script:Results | Where-Object { ($_.URL -eq $wildcard) } | Select-Object -First 1).Source)"
                        Note = "Manually defined URL to test Wildcard GitHub URL $wildcard"
                        TCPStatus = $TCPstatus
                        IPAddress = $ipAddress
                        Layer7Status = $Layer7Status
                        Layer7Response = $Layer7Response
                        Layer7ResponseTime = $Layer7ResponseTime
                        CertificateIssuer = $CertificateIssuer
                        CertificateSubject = $CertificateSubject
                        CertificateThumbprint = $CertificateThumbprint
                        IntermediateCertificateIssuer = $CertificateIntermediateIssuer
                        IntermediateCertificateSubject = $CertificateIntermediateSubject
                        IntermediateCertificateThumbprint = $CertificateIntermediateThumbprint
                        RootCertificateIssuer = $CertificateRootIssuer
                        RootCertificateSubject = $CertificateRootSubject
                        RootCertificateThumbprint = $CertificateRootThumbprint
                    }
                    # Add the subdomain entry to the results
                    $script:Results += $subdomainEntry
                    $MatchedWildcardURLs = ($script:Results | Where-Object { $PSItem.URL -eq $wildcard } )
                    ForEach($MatchedWildcardURL in $MatchedWildcardURLs) {
                        $MatchedWildcardURL.Note = "Wildcard URL, tested by manually defined URL $($subdomain) - $($MatchedWildcardURL.Note)"
                    }
                } else {
                    Write-Verbose "Info: Url $($subdomain) already exists in the results, skipping"
                    $MatchedWildcardURLs = ($script:Results | Where-Object { $PSItem.URL -eq $wildcard } )
                    ForEach($MatchedWildcardURL in $MatchedWildcardURLs) {
                        $MatchedWildcardURL.Note = "Wildcard URL, tested by manually defined URL $($subdomain) - $($MatchedWildcardURL.Note)"
                    }
                }

            }
        } # End of ForEach per $port

    } # End of ForEach $subdomain

}

# ////////////////////////////////////////////////////////////////////////////
# Function to process results
Function Publish-Results {
    param (
        [array]$results
    )

    # Assign Row IDs and reorder columns
    $rowIdCounter = 1
    foreach ($result in $script:Results) {
        $result.RowID = $rowIdCounter
        $rowIdCounter++
    }

    $script:Results = $script:Results | Select-Object RowID, URL, Port, ArcGateway, IsWildcard, Source, IPAddress, Layer7Status, Layer7Response, Layer7ResponseTime, Note, TCPStatus, CertificateIssuer, CertificateSubject, CertificateThumbprint, IntermediateCertificateIssuer, IntermediateCertificateSubject, IntermediateCertificateThumbprint, RootCertificateIssuer, RootCertificateSubject, RootCertificateThumbprint

    # Sort the array by the properties Layer7Status, Source, Url
    $script:Results = $script:Results | Sort-Object -Property Layer7Status, Source, Url

    # Export results to CSV
    # Add computer name in the output file name
    $script:csvFile = "$script:OutputFolderPath\AzureLocal_ConnectivityTest_$($env:COMPUTERNAME)_$DateFormatted.csv"
    try {
        $script:Results | Export-Csv -Path $script:csvFile -NoTypeInformation
    } catch {
        Write-Host "Failed to save test results to $script:csvFile"
        Write-Error "Error: $_.Exception.Message"
    }

    # // Calculate the number of successful, failed, and skipped URLs
    # Use TCP Status, if using TCP Connectivity Test switch
    [array]$successResults = @()
    if($IncludeTCPConnectivityTests.IsPresent){
        # Use TCPStatus for successful results
        [array]$successResults = $script:Results | Where-Object { $_.TCPStatus -eq "Success" }
    } else {
        # Otherwise default to Layer7Status
        [array]$successResults = $script:Results | Where-Object { $_.Layer7Status -eq "Success" }
    }
    # Failed URLs results
    [array]$failedResults = @()
    [array]$failedResults = $script:Results | Where-Object { $_.Layer7Status -eq "Failed" }
    # Skipped URLs results
    [array]$skippedResults = @()
    [array]$skippedResults = $script:Results | Where-Object { $_.TCPStatus -like "Skipped*" -or $_.Layer7Status -eq "Skipped" }

    # If the PassThru switch is not present, display the results
    if(-not($PassThru.IsPresent)){

        if($failedResults.Count -gt 0) {
            Write-Host "`nThe following URLs failed:" -ForegroundColor Red
            if($IncludeTCPConnectivityTests.IsPresent){
                $failedResults | Format-Table -Property RowID, Source, URL, Port, TCPStatus, IpAddress, Layer7Response -AutoSize
            } else {
                $failedResults | Format-Table -Property RowID, Source, URL, Port, IpAddress, Layer7Response -AutoSize
            }

        } else {
            Write-Host "`nNo URLs failed.`n" -ForegroundColor Green
        }

        if($successResults.Count -gt 0) {
            Write-Host "The following URLs were successful:" -ForegroundColor Green
            if($IncludeTCPConnectivityTests.IsPresent){
                $successResults | Format-Table -Property RowID, Source, URL, Port, TCPStatus, IpAddress, Layer7Response -AutoSize
            } else {
                $successResults | Format-Table -Property RowID, Source, URL, Port, IpAddress, Layer7Response -AutoSize
            }
        } else {
            Write-Host "No URLs were successful.`n"
        }

        if($skippedResults.Count -gt 0) {
            Write-Host "The following URLs were skipped:"
            $skippedResults | Format-Table -Property RowID, Source, URL, Port, Layer7Status, Note -AutoSize
        } else {
            Write-Host "No URLs were skipped.`n" -ForegroundColor Green
        }

        # Display test results summary
        Write-Host "`nTest results summary:"
        Write-Host "---------------------------------`n"

        Write-Host "Total URLs tested: $($script:Results.Count)"
        Write-Host "Successful URLs: $($successResults.Count)" -ForegroundColor Green
        if($failedResults.Count -gt 0){
            Write-Host "Failed URLs: $($failedResults.Count)" -ForegroundColor Red
        } else {
            Write-Host "Failed URLs: $($failedResults.Count)"
        }
        Write-Host "Skipped URLs: $($skippedResults.Count)`n" -ForegroundColor Yellow

        Write-Host "The test result for each endpoint is shown above. For detailed output, including certificate information review the CSV file listed below."
        
    } # End of if -not($PassThru.IsPresent)

    # Return the results array if the PassThru switch is present
    if($PassThru.IsPresent){
        return $script:Results
    }

    Write-Host "`nIMPORTANT: Only URLs with a Source of 'GitHub URL' and 'Environment Checker URL' are required on firewall / proxy outbound allow rules." -ForegroundColor Yellow -NoNewline
    Write-Host " Any URLs with a Source of 'Redirect for' or 'Test for Wildcard' are only used for testing connectivity to the required endpoints using the automation in this module." -ForegroundColor Yellow

    Write-Host "`nAzure Local product documentation for firewall requirements can be accessed using this URL from a device with a browser:`n`n`tMicrosoft documentation: 'https://learn.microsoft.com/azure/azure-local/concepts/firewall-requirements'`n" -ForegroundColor Green

} # End of Publish-Results

# ////////////////////////////////////////////////////////////////////////////
# Main function to test connectivity
Function Test-AzureLocalConnectivity {

    [CmdletBinding()]
    param (
        [Parameter(Mandatory=$false, Position=0, HelpMessage="Enter the Azure region: (EastUS, WestEurope, AustraliaEast, CanadaCentral, CentralIndia, JapanEast, SouthCentral, SouthEastAsia)")]
        # Not case sensitive, use the full region name, no spaces
        [ValidateSet("EastUS", "WestEurope", "AustraliaEast", "CanadaCentral", "CentralIndia", "JapanEast", "SouthCentral", "SouthEastAsia")]
        [string]$AzureRegion,

        [Parameter(Mandatory=$false, Position=1, HelpMessage="Optional parameter to specify a custom KeyVault URL to test connectivity, including the https:// prefix, e.g. https://yourhcikeyvaultname.vault.azure.net")]
        [ValidateScript({if($_.ToString().Length -lt 25 -or $_.ToString().Length -gt 255) {
            # 25 allows for https:// and the domain name
            # 255 is the maximum length for a KeyVault URL
            # Have to use .ToString() to get the length of the URL, as $_ is a Uri object
            # Invalid length, show example using custom error message
            Throw "'$($_.ToString())' is not valid KeyVault URL endpoint, length = $($_.ToString().Length). Expected length is between 25 to 255 characters. Example parameter for Arc Gateway URL: 'https://yourhcikeyvaultname.vault.azure.net'"
        } else {
            # Valid URL length, return $True
            $True
        }})]
        [System.Uri]$KeyVaultURL,

        [Parameter(Mandatory=$false, Position=2, HelpMessage="Optional switch to ONLY test URLs that do NOT support Arc Gateway, default is to test all URLs")]
        [switch]$ArcGatewayDeployment,

        [Parameter(Mandatory=$false, Position=3, HelpMessage="Optional parameter to specify a custom Arc Gateway URL to test connectivity, including the https:// prefix, for example: 'e.g. 'https://1be59945-12c0-4cda-9580-84a66a1120a0.gw.arc.azure.com'")]
        [ValidateScript({if($_.ToString().Length -lt 60 -or $_.ToString().Length -gt 62) {
            # 60 allows for http:// and 62 allows for https:// and a trailing slash
            # Have to use .ToString() to get the length of the URL, as $_ is a Uri object
            # 60-62 characters is the expected length for an Arc Gateway URL
            # Invalid length, show example using custom error message
            Throw "'$($_.ToString())' is not valid Arc Gateway URL endpoint, length = $($_.ToString().Length). Expected length is between 60 to 62 characters. Example parameter for Arc Gateway URL: 'https://1be59945-12c0-4cda-9580-84a66a1120a0.gw.arc.azure.com'"
        } else {
            # Valid URL length, return $True
            $True
        }})]
        [System.Uri]$ArcGatewayURL,

        [Parameter(Mandatory=$false, Position=4, HelpMessage="Optional parameter to specify a custom DNS Name for the NTP Time Server, this should NOT include a http:// or https:// prefix, e.g. 'yourtimeserver.fqdn'")]
        [ValidateLength(1, 255)]
        [string]$NTPTimeServer,

        [Parameter(Mandatory=$false, Position=5, HelpMessage="Optional switch to include tests for TCP Connectivity, for scenarios such as not using a Proxy.")]
        [switch]$IncludeTCPConnectivityTests,

        [Parameter(Mandatory=$false, Position=6, HelpMessage="Optional switch to exclude testing Redirected URLs.")]
        [switch]$ExcludeRedirectedUrls,

        [Parameter(Mandatory=$false, Position=7, HelpMessage="Optional switch to exclude testing manually defined subdomains for Wildcard endpoints.")]
        [switch]$ExcludeWildcardTests,

        [Parameter(Mandatory=$false, Position=8, HelpMessage="Optional parameter to include URLs from hardware OEM partners.")]
        [ValidateSet("DellEMC")]
        [string]$IncludeOEMUrls,

        [Parameter(Mandatory=$false, Position=9, HelpMessage="Optional parameter to return '`$Results' array object in PowerShell, for further processing.")]
        [switch]$PassThru

    )

    # ////////////////////////////////////////////////////////////////////////////
    # Set the default Verbose and debug preferences, to 'Continue' if the Verbose or Debug switches are passed
    # This allows the script to run in Verbose and Debug mode, if the switches are passed
    if($PSBoundParameters['Debug']) {
        $DebugPreference = 'Continue'
    }
    if($PSBoundParameters['Verbose']) {
        $VerbosePreference = 'Continue'
    }

    # ////////////////////////////////////////////////////////////////////////////
    # Check if the script is running as an administrator
    if (-not(Test-Elevation)) {
        Write-Host "This function must be run as an administrator" -ForegroundColor Red
        Return
    }
    
    # Check if the PowerShell version is 5.1.x
    # This is required for the script to run, as it uses features deprecated in later versions, such as v7.x.x
    if(-not(Test-PowerShellVersion5)){
        # PowerShell version is greater than 5.1.x, return an error
        Write-Host "'Test-AzureLocalConnectivity' function requires PowerShell version 5.1.x" -ForegroundColor Red
        Write-Host "PowerShell version $($PSVersionTable.PSVersion.ToString()) detected." -ForegroundColor Red
        Throw "Unexpected PowerShell version '$($PSVersionTable.PSVersion.ToString())' detected. Please run command again using PowerShell version 5.1.x"
    }

    # ////////////////////////////////////////////////////////////////////////////

    # Setup the output folder name and path
    $script:DateFormatted = Get-Date -f "yyyy-MM-dd-HH-mm-ss"
    [string]$script:OutputFolderPath = "C:\ProgramData\AzStackHci.DiagnosticSettings"
    if(-not(Test-Path $script:OutputFolderPath)) {
        New-Item $script:OutputFolderPath -ItemType Directory -ErrorAction SilentlyContinue | Out-Null
    }
    
    # Start the PowerShell Transcript
    [string]$script:TranscriptFile = "$script:OutputFolderPath\Transcript_AzureLocal_ConnectivityTest_$DateFormatted.log"
    Write-Host "Starting PowerShell transcript...."
    Start-Transcript -Path $script:TranscriptFile -ErrorAction SilentlyContinue

    # Call function to show the ASCII art banner
    Show-ASCIIArtBanner

    # Check and update the module using latest version in PowerShell Gallery, returns true if module has been updated
    if(Update-ModuleVersion -ModuleName "AzStackHci.DiagnosticSettings") {
        # Stop the Transcript block
        Stop-Transcript -ErrorAction SilentlyContinue
        # Call function to redact user name from the transcript file
        Remove-PIIFromTranscriptFile -TranscriptFilePath $script:TranscriptFile
        # Exit function, so that it can be reloaded with the updated version
        Return
    } else {
        # No nothing, continue with the script, returned false
    }

    # ////////////////////////////////////////////////////////////////////////////
    # Get the Azure region from the user if not specified
    if(-not($AzureRegion)){
        $AzureRegion = Read-Host "Enter your Azure region: (EastUS, WestEurope, AustraliaEast, CanadaCentral, CentralIndia, JapanEast, SouthCentral, SouthEastAsia)"
        # Check if the Azure region is valid
        if(-not($regionUrls.ContainsKey($AzureRegion))){
            Write-Host "Invalid Azure region specified!" -ForegroundColor "Red"
            Write-Host "Valid Azure regions, which support for Azure Local deployments: EastUS, WestEurope, AustraliaEast, CanadaCentral, CentralIndia, JapanEast, SouthCentral, SouthEastAsia" -ForegroundColor "Yellow"
            Return
        }
    }

    # ////////////////////////////////////////////////////////////////////////////
    # Initialize the arrays with zero length
    # Initialize the results array
    [System.Collections.ArrayList]$script:results = @()
    # Initialize the RedirectedResults array
    [System.Collections.ArrayList]$script:RedirectedResults = @()
    # Initialize the SSLInspectedURLs array
    [System.Collections.ArrayList]$script:SSLInspectedURLs = @()

    # ////////////////////////////////////////////////////////////////////////////
    Write-Host "`nPerforming connectivity tests for $AzureRegion from host: $($env:COMPUTERNAME) on $(Get-Date -f "yyyy-MM-dd") at $(Get-Date -f "HH:mm:ss")`n" -ForegroundColor Cyan

    # User-defined keyvault URL replacement (modify if needed)
    if([string]::IsNullOrWhiteSpace($KeyVaultURL)) {
        [string]$script:UpdatedKeyVaultURL = "https://yourhcikeyvaultname.vault.azure.net"
        Write-Host "Parameter '-KeyVaultURL' was not specified, using default value of $script:UpdatedKeyVaultURL" -ForegroundColor Yellow
    } else {
        [string]$script:UpdatedKeyVaultURL = $KeyVaultURL # Set the user-defined KeyVault URL
        Write-Debug "Variable 'UpdatedKeyVaultURL' set to $script:UpdatedKeyVaultURL"
    }

    # User-defined ArcGateway URL replacement (modify if needed)
    if($ArcGatewayDeployment.IsPresent) {
        Write-Host "`nSwitch '-ArcGatewayDeployment' was specified as a parameter, only testing URLs that do NOT support Azure Arc Gateway`n" -ForegroundColor Green
        if([string]::IsNullOrWhiteSpace($ArcGatewayURL)) {
            Write-Host "`tWARNING: -ArcGatewayDeployment switch present, but -ArcGatewayURL parameter was not specified, using default value of 'https://yourarcgatewayendpointid.gw.arc.azure.com'`n" -ForegroundColor Yellow
            [string]$script:UpdatedArcGatewayURL = "https://yourarcgatewayendpointid.gw.arc.azure.com"
            Write-Debug "Parameter '-ArcGatewayURL' was not specified, using default value of $script:UpdatedArcGatewayURL"
            
        } else {
            [string]$script:UpdatedArcGatewayURL = $ArcGatewayURL # Set the user-defined ArcGateway URL
            Write-Debug "Variable '`$UpdatedArcGatewayURL' set to $script:UpdatedArcGatewayURL"
        }
    } else {
        # Not using the ArcGatewayDeployment switch
        if([string]::IsNullOrWhiteSpace($ArcGatewayURL)) {
            [string]$script:UpdatedArcGatewayURL = "https://yourarcgatewayendpointid.gw.arc.azure.com"
            Write-Debug "Parameter '-ArcGatewayURL' was not specified, using default value of $script:UpdatedArcGatewayURL"
            
        } else {
            Write-Host "`tUnexpected: -ArcGatewayDeployment switch NOT passed, but an ArcGatewayURL was specified as a parameter?!" -ForegroundColor Magenta
            [string]$script:UpdatedArcGatewayURL = $ArcGatewayURL # Set the user-defined ArcGateway URL
            Write-Debug "Parameter '`$UpdatedArcGatewayURL' set to $script:UpdatedArcGatewayURL"
        }
    }

    # User-defined NTP Time Server replacement (modify if needed)
    if([string]::IsNullOrWhiteSpace($NTPTimeServer)) {
        Write-Host "Parameter '-NTPTimeServer' was not specified, using default NTP server: 'time.windows.com'" -ForegroundColor Yellow
    } else {
        Write-Debug "Using user-defined NTP time server: '$NTPTimeServer'"
    }

    # ////////////////////////////////////////////////////////////////////////////
    # Check if a Proxy is configured using the function Get-Proxy
    $script:Proxy = Get-Proxy

    # ////////////////////////////////////////////////////////////////////////////

    # ////////////////////////////////////////////
    # Load URLs from Environment Checker module
    # ////////////////////////////////////////////

    if(-not($PSBoundParameters['Verbose'])){
        Write-Host "`t[Optional] Use -Verbose for additional details of endpoint tests."
    }
    if(-not($PSBoundParameters['Debug'])){
        Write-Host "`t[Optional] Use -Debug for full diagnostic level output of endpoint tests, (can be used with -Verbose).`n"
    }

    # Call the function to check the environment checker module is installed and running latest version
    $Location = Get-AzStackHciEnvironmentCheckerModule
    
    # Load URLs from online web calls or local Targets.json files
    [array]$OnlineUrlTargets = @()
    try {
        $OnlineUrlTargets = Get-AzStackHciConnectivityTarget -ErrorAction SilentlyContinue
    } catch {  
        Write-Host "Failed to download endpoint URLs from Environment Checker Online endpoint" -ForegroundColor Yellow
    }
    
    # ////////////////////////////////////////////////////////////////////////////
    # Process the URLs from the Environment Checker module
    # If no online URLs are found, use the local Targets.json files
    if(-not($OnlineUrlTargets -or ($OnlineUrlTargets.Count -eq 0))){
        Write-Host "No online URLs found, using local Targets.json files for EnvChecker endpoint urls list" -ForegroundColor Yellow
        $Files = Get-ChildItem -Recurse -Path $Location | Where-Object Name -like "*Targets.json"
        # Load URLs from local Targets.json files
        foreach ($File in $Files) {
            $content = Get-Content -Path $File.FullName
            $object = $content | ConvertFrom-Json
            foreach ($item in $Object) {
                foreach ($endpoint in $Item.Endpoint) {
                    $domain = Get-DomainFromURL -url $endpoint
                    # $url = $domain.Domain
                    $url = $endpoint
                    $port = if ($domain.Port) { $domain.Port } else { if ($item.Protocol -eq 'https') { 443 } else { 80 } }
                    [bool]$wildcard = if($endpoint -match [regex]::Escape('*')){$true} else {$false}
                    # check if already in the list with exact URL and Port
                    if($script:Results | Where-Object { ((Get-DomainFromURL -url $_.URL).Domain -eq (Get-DomainFromURL -url $url).Domain) -and ($_.Port -eq $port) }) {
                        Write-Debug "Environment Checker endpoints: URL '$url' with port '$port' already exists in the results array, skipping"
                        Continue
                    } else {
                        Write-Debug "Adding URL '$url' with port '$port' to results array for processing"
                        $script:Results += [PSCustomObject]@{
                            RowID = 0
                            URL = $url
                            Port = $port
                            ArcGateway = ($item.ArcGateway)
                            IsWildcard = $wildcard
                            Source = if($wildcard){"Wildcard Environment Checker URL"} else {"Environment Checker URL"}
                            Note = ($item.Description).Trim()
                            TCPStatus = ""
                            IPAddress = ""
                            Layer7Status = ""
                            Layer7Response = ""
                            Layer7ResponseTime = ""
                            CertificateIssuer = ""
                            CertificateSubject = ""
                            CertificateThumbprint = ""
                            IntermediateCertificateIssuer = ""
                            IntermediateCertificateSubject = ""
                            IntermediateCertificateThumbprint = ""
                            RootCertificateIssuer = ""
                            RootCertificateSubject = ""
                            RootCertificateThumbprint = ""
                        }
                    }
                }
            }
        } # End of foreach $File
    } else {
        # Online URLs found, process the URLs from the online Environment Checker targets
        Write-Host "Online URLs found, using online for EnvChecker endpoint urls list" -ForegroundColor Green
        # Load URLs from online Targets.json files
        foreach ($item in $OnlineUrlTargets) {
            foreach ($endpoint in $Item.Endpoint) {
                $domain = Get-DomainFromURL -url $endpoint
                # $url = $domain.domain
                $url = $endpoint
                $port = if ($domain.Port) { $domain.Port } else { if ($item.Protocol -eq 'https') { 443 } else { 80 } }
                # check if already in the list with same Domain Name from URL and exact Port
                if($script:Results | Where-Object { ((Get-DomainFromURL -url $_.URL).Domain -eq (Get-DomainFromURL -url $url).Domain) -and ($_.Port -eq $port) }) {
                    Write-Debug "Environment Checker endpoints: URL '$url' with port '$port' already exists in the results array, skipping"
                    Continue
                } else {
                    $script:Results += [PSCustomObject]@{
                        RowID = 0
                        URL = $url
                        Port = $port
                        ArcGateway = ($item.ArcGateway)
                        IsWildcard = if($endpoint -contains "*"){$true} else {$false}
                        Source = "Environment Checker URL"
                        Note = ($item.Description).Trim()
                        TCPStatus = ""
                        IPAddress = ""
                        Layer7Status = ""
                        Layer7Response = ""
                        Layer7ResponseTime = ""
                        CertificateIssuer = ""
                        CertificateSubject = ""
                        CertificateThumbprint = ""
                        IntermediateCertificateIssuer = ""
                        IntermediateCertificateSubject = ""
                        IntermediateCertificateThumbprint = ""
                        RootCertificateIssuer = ""
                        RootCertificateSubject = ""
                        RootCertificateThumbprint = ""
                    }
                }
            }
        }
    }


    # ////////////////////////////////////////////////////////////////////////////
    # Check if the results array and store it in a variable
    # Count the number of URLs added
    [int]$EnvCheckerURLs = ($script:Results.Count)
    Write-Host "Environment Checker: Added $EnvCheckerURLs endpoint urls to perform connectivity tests`n" -ForegroundColor Green

    # ////////////////////////////////////////////////////////////////////////////
    # Load URLs from GitHub pages
    $regionUrls = @{
        "EastUS" = "https://raw.githubusercontent.com/Azure/AzureStack-Tools/master/HCI/EastUSendpoints/eastus-hci-endpoints.md"
        "WestEurope" = "https://raw.githubusercontent.com/Azure/AzureStack-Tools/master/HCI/WestEuropeendpoints/westeurope-hci-endpoints.md"
        "AustraliaEast" = "https://raw.githubusercontent.com/Azure/AzureStack-Tools/master/HCI/AustraliaEastendpoints/AustraliaEast-hci-endpoints.md"
        "CanadaCentral" = "https://raw.githubusercontent.com/Azure/AzureStack-Tools/master/HCI/CanadaCentralEndpoints/canadacentral-hci-endpoints.md"
        "CentralIndia" = "https://raw.githubusercontent.com/Azure/AzureStack-Tools/refs/heads/master/HCI/IndiaCentralEndpoints/IndiaCentral-hci-endpoints.md"
        "JapanEast" = "https://raw.githubusercontent.com/Azure/AzureStack-Tools/refs/heads/master/HCI/JapanEastEndpoints/japaneast-hci-endpoints.md"
        "SouthCentral" = "https://raw.githubusercontent.com/Azure/AzureStack-Tools/refs/heads/master/HCI/SouthCentralUSEndpoints/southcentralus-hci-endpoints.md"
        "SouthEastAsia" = "https://raw.githubusercontent.com/Azure/AzureStack-Tools/refs/heads/master/HCI/SouthEastAsiaEndpoints/southeastasia-hci-endpoints.md"
    }

    # ////////////////////////////////////////////////////////////////////////////

    # ////////////////////////////////////////////
    # Download and parse URLs from GitHub pages
    # ////////////////////////////////////////////
    if ($regionUrls.ContainsKey($AzureRegion)) {
        $endpointUrl = $regionUrls[$AzureRegion]
        try {
            $endpointsContent = Invoke-WebRequest -Uri $endpointUrl -UseBasicParsing -ErrorAction SilentlyContinue -ErrorVariable GitHubURLDownloadError
        } catch {
            Write-Host "Failed to download endpoint URLs from GitHub page: $endpointUrl - Error: $($_.Exception.Message)" -ForegroundColor Yellow
        }
        # If download fails, use cached URLs from module base path
        if($GitHubURLDownloadError) {

            # Load the cached URLs from the module
            Write-Host "Using cached GitHub URLs from module" -ForegroundColor Yellow
            # Check if the module is installed
            # Get the latest cached URL file, using the module base path and GitHub-URI-Cache_* folder to find the latest cached URLs
            [string]$UrlCacheFolderPatternMatch = "GitHub-URI-Cache_*"
            # Get the latest cached URL file using module base path and GitHub-URI-Cache_* folder wildcard
            [string]$DiagnosticSettingsBasePath = (Get-Module -Name "AzStackHci.DiagnosticSettings" -ListAvailable | Sort-Object Version -Descending | Select-Object -First 1).ModuleBase
            [System.IO.DirectoryInfo]$UrlCacheLocation = Get-ChildItem -Path "$DiagnosticSettingsBasePath\$UrlCacheFolderPatternMatch" | Sort-Object LastWriteTime -Descending | Select-Object -First 1
            # Check if the UrlCacheLocation variable is empty
            if(-not($UrlCacheLocation)){
                Write-Error "No cached folder found for GitHub URL endpoints within in module installation folder: '$DiagnosticSettingsBasePath', please check the module is installed correctly?"
                Return
            } else {
                # Found cached URLs
                Write-Host "Using module cached versions of GitHub URL endpoints from date: '$($UrlCacheLocation.Name.Split('_')[-1])' for Azure region: $AzureRegion" -ForegroundColor Yellow
            }
            # Get the cached URL file name, using the region name from the GitHub URL
            $UrlCacheFileContents = Get-Content -Path "$($UrlCacheLocation.FullName)\$($regionUrls.$AzureRegion.Split("/")[-1])"
            
            if($null -eq $UrlCacheFileContents){
                Write-Error "No cached file contents found for GitHub URL endpoints file '$($UrlCacheLocation.FullName)\$($regionUrls.$AzureRegion.Split("/")[-1])'"
                Return
            } else {
                # Split the cached file contents into a string array of lines
                $lines = $UrlCacheFileContents -split "`n"
            }

        } else {
            # Download successful
            Write-Host "Successfully downloaded endpoint URLs from GitHub page: $endpointUrl" -ForegroundColor Green
            # Split the downloaded file contents into a string array of lines
            $lines = $endpointsContent.Content -split "`n"
        }

        # Parse the URLs from the GitHub page, creating a list of URLs to test in the results array
        ForEach ($line in $lines) {
            if ($line -match "^\|\s*(\d+)\s*\|") {
                # $rowId = [int]($line -replace "^\|\s*(\d+)\s*\|.*", '$1')
                $columns = $line -split "\|"
                $url = $columns[3].Trim()
                $ports = $columns[4].Trim() -split ','
                $note = $columns[5].Trim()
                $ArcGatewayText = $columns[6].Trim()
                if(($ArcGatewayText.Length -ge 2) -and ($ArcGatewayText.Substring(0,2) -eq "No")){
                    # "No"
                    [bool]$ArcGateway = $false
                } elseif(($ArcGatewayText.Length -ge 3) -and ($ArcGatewayText.Substring(0,3) -eq "Yes")){
                    # "Yes"
                    [bool]$ArcGateway = $true
                } else {
                    # Unknown
                    Write-Warning "Unknown ArcGateway status for url: '$url' : $ArcGatewayText"
                    [bool]$ArcGateway = $false
                }
                # Use only the domain name from the URL, remove any absolute path from the endpoint URL
                $url = (Get-DomainFromURL -url $url).Domain
                foreach ($port in $ports) {
                    $isWildcard = $url.Contains("*")
                    if($script:Results | Where-Object { ((Get-DomainFromURL -url $_.URL).Domain -eq $url) -and ($_.Port -eq $port) }) {
                        Write-Debug "GitHub endpoints: URL '$url' with port '$port' already exists in the results array, skipping"
                        # If the ArcGateway value is different, update it
                        $script:Results | Where-Object { ((Get-DomainFromURL -url $_.URL).Domain -like $url) -and ($_.Port -eq $port) } | ForEach-Object {
                            $_.ArcGateway = $ArcGateway
                        }
                        Continue
                    } else {
                        Write-Debug "GitHub endpoints: Adding URL '$url' with port '$port' to results array for processing"
                        $script:Results += [PSCustomObject]@{
                            RowID = 0
                            URL = $url
                            Port = [int]$port
                            ArcGateway = $ArcGateway
                            IsWildcard = $isWildcard
                            Source = if ($isWildcard) { "Wildcard GitHub URL" } else { "GitHub URL" }
                            Note = $note
                            TCPStatus = ""
                            IPAddress = ""
                            Layer7Status = ""
                            Layer7Response = ""
                            Layer7ResponseTime = ""
                            CertificateIssuer = ""
                            CertificateSubject = ""
                            CertificateThumbprint = ""
                            IntermediateCertificateIssuer = ""
                            IntermediateCertificateSubject = ""
                            IntermediateCertificateThumbprint = ""
                            RootCertificateIssuer = ""
                            RootCertificateSubject = ""
                            RootCertificateThumbprint = ""
                        }
                    }
                }
            }
            
        }
    } else {
        # Invalid Azure region specified
        # Not expected to reach this point, as the Azure region is validated above
        Throw "Invalid Azure region specified!"
    }

    # ////////////////////////////////////////////////////////////////////////////
    # Calculate the number of URLs added from GitHub, by subtracting the number of URLs from the Environment Checker
    # Count the number of URLs added from GitHub
    [int]$GitHubURLs = ($script:Results.Count - $EnvCheckerURLs)
    # Output the number of URLs added from GitHub
    if(-not($GitHubURLDownloadError)){
        Write-Host "GitHub: Using online update, added $GitHubURLs endpoint urls to perform connectivity tests" -ForegroundColor Green
    } else {
        Write-Host "GitHub: Using cached markdown file, added $GitHubURLs endpoint urls to perform connectivity tests" -ForegroundColor Yellow
    }

    # ////////////////////////////////////////////////////////////////////////////
    # Add Dell URLs if switch is used
    if($IncludeOEMUrls -eq "DellEMC") {
        $additionalUrls = @(
            [PSCustomObject]@{ RowID = 0; URL = "downloads.emc.com"; Port = 443; ArcGateway = $false; IsWildcard = $false; Source = "OEM DellEMC"; Note = "DellEMC URL for APEX Cloud Platform solution"; TCPStatus = ""; IPAddress = ""; Layer7Status = ""; Layer7Response = ""; Layer7ResponseTime = ""; CertificateIssuer = "" },
            [PSCustomObject]@{ RowID = 0; URL = "dl.dell.com"; Port = 443; ArcGateway = $false; IsWildcard = $false; Source = "OEM DellEMC"; Note = "DellEMC URL for APEX Cloud Platform solution"; TCPStatus = ""; IPAddress = ""; Layer7Status = ""; Layer7Response = ""; Layer7ResponseTime = ""; CertificateIssuer = "" },
            [PSCustomObject]@{ RowID = 0; URL = "esrs3-core.emc.com"; Port = 443; ArcGateway = $false; IsWildcard = $false; Source = "OEM DellEMC"; Note = "DellEMC URL for APEX Cloud Platform solution"; TCPStatus = ""; IPAddress = ""; Layer7Status = ""; Layer7Response = ""; Layer7ResponseTime = ""; CertificateIssuer = ""},
            [PSCustomObject]@{ RowID = 0; URL = "esrs3-core.emc.com"; Port = 8443; ArcGateway = $false; IsWildcard = $false; Source = "OEM DellEMC"; Note = "DellEMC URL for APEX Cloud Platform solution"; TCPStatus = ""; IPAddress = ""; Layer7Status = ""; Layer7Response = ""; Layer7ResponseTime = ""; CertificateIssuer = "" },
            [PSCustomObject]@{ RowID = 0; URL = "esrs3-coredr.emc.com"; Port = 443; ArcGateway = $false; IsWildcard = $false; Source = "OEM DellEMC"; Note = "DellEMC URL for APEX Cloud Platform solution"; TCPStatus = ""; IPAddress = ""; Layer7Status = ""; Layer7Response = ""; Layer7ResponseTime = ""; CertificateIssuer = "" },
            [PSCustomObject]@{ RowID = 0; URL = "esrs3-coredr.emc.com"; Port = 8443; ArcGateway = $false; IsWildcard = $false; Source = "OEM DellEMC"; Note = "DellEMC URL for APEX Cloud Platform solution"; TCPStatus = ""; IPAddress = ""; Layer7Status = ""; Layer7Response = ""; Layer7ResponseTime = ""; CertificateIssuer = "" },
            [PSCustomObject]@{ RowID = 0; URL = "colu.dell.com"; Port = 443; ArcGateway = $false; IsWildcard = $false; Source = "OEM DellEMC"; Note = "DellEMC URL for APEX Cloud Platform solution"; TCPStatus = ""; IPAddress = ""; Layer7Status = ""; Layer7Response = ""; Layer7ResponseTime = ""; CertificateIssuer = "" }
        )
        # Add DellEMC URLs to the results array for processing
        $script:Results += $additionalUrls
    }

    # ////////////////////////////////////////////////////////////////////////////
    # Deduplicate URLs, in case Environment Checker and GitHub URLs overlap using Sort-Object -Unique with Url and Port
    Write-Host "Deduplicating $($script:Results.count) x URL endpoints..." -ForegroundColor Green
    $script:Results = $script:Results | Sort-Object -Property URL, Port -Unique

    # Count the number of URLs added
    Write-Host "Total URL endpoints to test: $($script:Results.Count)`n" -ForegroundColor Green

    # ////////////////////////////////////////////////////////////////////////////
    # Skip URLs that are not required to be tested
    $skipUrls = @(
        [PSCustomObject]@{ URL = "*.waconazure.com"; ArcGateway = $true; IsWildcard = $true; Note = "Wildcard URL, unable to test directly - For Windows Admin Center management after deployment."; TCPStatus = ""; IPAddress = ""; Layer7Status = "" },
        [PSCustomObject]@{ URL = "wustat.windows.com"; ArcGateway = $false; IsWildcard = $false; Note = "For Microsoft Update, allowing the OS to receive updates."; TCPStatus = ""; IPAddress = ""; Layer7Status = "" }
    )

  
    # ////////////////////////////////////////////////////////////////////////////
    # Check if the ArcGatewayDeployment switch is used, remove URLs that support Arc Gateway
    if($ArcGatewayDeployment.IsPresent){
        Write-Host "Removing URLs that support Arc Gateway from the list..." -ForegroundColor Yellow
        [int]$PreCount = $script:Results.Count
        # Create a copy of the results array, to store the initial results for checking Arc Gateway support later
        [array]$script:PreArcGatewayRemoval = $script:Results
        $script:Results = $script:Results | Where-Object { $_.ArcGateway -eq $false }
        [int]$PostCount = $script:Results.Count
        Write-Host "Removed $($PreCount - $PostCount) URLs that support Arc Gateway from the list.`n" -ForegroundColor Yellow
        Write-Host "Total URLs to test (excluding Arc Gateway supported URLs): $($script:Results.Count)`n" -ForegroundColor Green
    }


    # ////////////////////////////////////////////
    # ///// Main processing loop /////
    # ////////////////////////////////////////////

    # Initialize URL counter
    [int]$urlCount = 0

    # ////////////////////////////////////////////////////////////////////////////
    # Process each URL using the ForEach loop
    ForEach ($urlObj in $script:Results) {

        # Increment the URL counter
        $urlCount++
        # Output the URL being processed and count
        Write-Host "Processing endpoint: $urlCount of $($script:Results.Count)`nTesting: $($urlObj.URL)"
        
        # Remove DNSCheck variable if it exists, on each loop
        Remove-Variable DNSCheck -ErrorAction SilentlyContinue
        
        Write-Debug "Arc Gateway boolean for url '$($urlObj.url)' from source $($urlObj.Source) = $($urlObj.ArcGateway)"
        # If the ArcGatewayDeployment switch is used, only test URLs that do not support Arc Gateway
        if($ArcGatewayDeployment.IsPresent){
            # Skip URLs with ArcGateway -eq $True
            if($urlObj.ArcGateway){
                # Skip URLs that support Arc Gateway
                Write-Host "Skipped URL: '$($urlObj.URL)' as supported by Arc Gateway" -ForegroundColor Yellow
                $urlObj.TCPStatus = "Skipped"
                $urlObj.Note = "Skipped, URL is supported by Arc Gateway - $($urlObj.Note)"
                $urlObj.IPAddress = "N/A"
                $urlObj.Layer7Status = "Skipped"
                $urlObj.Layer7Response = "N/A"
                $urlObj.Layer7ResponseTime = "N/A"
                $urlObj.CertificateIssuer = "N/A"
                $urlObj.CertificateSubject = "N/A"
                $urlObj.CertificateThumbprint = "N/A"
                $urlObj.IntermediateCertificateIssuer = "N/A"
                $urlObj.IntermediateCertificateSubject = "N/A"
                $urlObj.IntermediateCertificateThumbprint = "N/A"
                $urlObj.RootCertificateIssuer = "N/A"
                $urlObj.RootCertificateSubject = "N/A"
                $urlObj.RootCertificateThumbprint = "N/A"
                # Add line return after each URL
                Write-Host ""
                Continue
            }
        }
        # Process URLs that require special handling
        if($urlObj.URL -in ("yourarcgatewayendpointid.gw.arc.azure.com","yourarcgatewayendpointid.gw.arc.azure.net")) {
            if($ArcGatewayDeployment.IsPresent){
                # Replace the URL with the user-defined Arc Gateway URL, if specified
                $urlObj.URL = (Get-DomainFromURL -url $script:UpdatedArcGatewayURL).Domain
                # Call function to check DNS entry exists for the URL
                $DNSCheck = Get-DnsRecord -url (Get-DomainFromURL -url $urlObj.URL).Domain
                if($DNSCheck.DNSExists){
                    if($IncludeTCPConnectivityTests.IsPresent){
                        # Test TCP and Layer 7 connectivity
                        $urlObj.TCPStatus, $urlObj.IPAddress = Test-TCPConnectivity -url $urlObj.URL -port $urlObj.Port
                    } else {
                        $urlObj.TCPStatus = "N/A"
                        $urlObj.IPAddress = $DNSCheck.IpAddress
                    }
                    # Test Layer 7 connectivity
                    $Layer7Results = Test-Layer7Connectivity -url $urlObj.URL -port $urlObj.Port
                    $urlObj.Layer7Status = $Layer7Results.Layer7Status
                    $urlObj.Layer7Response = $Layer7Results.Layer7Response
                    $urlObj.Layer7ResponseTime = $Layer7Results.Layer7ResponseTime
                    $urlObj.CertificateIssuer = $Layer7Results.CertificateIssuer
                    $urlObj.CertificateSubject = $Layer7Results.CertificateSubject
                    $urlObj.CertificateThumbprint = $Layer7Results.CertificateThumbprint
                    $urlObj.IntermediateCertificateIssuer = $Layer7Results.CertificateIntermediateIssuer
                    $urlObj.IntermediateCertificateSubject = $Layer7Results.CertificateIntermediateSubject
                    $urlObj.IntermediateCertificateThumbprint = $Layer7Results.CertificateIntermediateThumbprint
                    $urlObj.RootCertificateIssuer = $Layer7Results.CertificateRootIssuer
                    $urlObj.RootCertificateSubject = $Layer7Results.CertificateRootSubject
                    $urlObj.RootCertificateThumbprint = $Layer7Results.CertificateRootThumbprint    
                    
                } else {
                    if($IncludeTCPConnectivityTests.IsPresent){
                        $urlObj.TCPStatus = "Failed"
                    } else {
                        $urlObj.TCPStatus = "N/A"
                    }
                    $urlObj.IPAddress = $DNSCheck.IpAddress
                    $urlObj.Layer7Status = "Failed" # DNS entry does not exist, return failed
                    $urlObj.Layer7Response = "N/A"
                    $urlObj.Layer7ResponseTime = "N/A"
                    $urlObj.CertificateIssuer = "N/A"
                    $urlObj.CertificateSubject = "N/A"
                    $urlObj.CertificateThumbprint = "N/A"
                    $urlObj.IntermediateCertificateIssuer = "N/A"
                    $urlObj.IntermediateCertificateSubject = "N/A"
                    $urlObj.IntermediateCertificateThumbprint = "N/A"
                    $urlObj.RootCertificateIssuer = "N/A"
                    $urlObj.RootCertificateSubject = "N/A"
                    $urlObj.RootCertificateThumbprint = "N/A"

                }
            } else {
                # Not using Arc Gateway, skip the URL
                Write-Host "Skipping URL, (not using Arc Gateway): $($urlObj.URL)" -ForegroundColor Green
                $urlObj.URL = (Get-DomainFromURL -url $script:UpdatedArcGatewayURL).Domain
                $urlObj.TCPStatus = "Skipped"
                $urlObj.Note = "Skipped, not using Arc Gateway, as '-ArcGatewayDeployment' switch not present - $($urlObj.Note)"
                $urlObj.IPAddress = "N/A"
                $urlObj.Layer7Status = "Skipped" # Not failed, as not using Arc Gateway
                $urlObj.Layer7Response = "N/A"
                $urlObj.Layer7ResponseTime = "N/A"
                $urlObj.CertificateIssuer = "N/A"
                $urlObj.CertificateSubject = "N/A"
                $urlObj.CertificateThumbprint = "N/A"
                $urlObj.IntermediateCertificateIssuer = "N/A"
                $urlObj.IntermediateCertificateSubject = "N/A"
                $urlObj.IntermediateCertificateThumbprint = "N/A"
                $urlObj.RootCertificateIssuer = "N/A"
                $urlObj.RootCertificateSubject = "N/A"
                $urlObj.RootCertificateThumbprint = "N/A"
            }

        } elseif($urlObj.URL -eq "time.windows.com") {
            # Replace the URL with the user-defined NTP Time Server, if specified
            if([string]::IsNullOrWhiteSpace($NTPTimeServer)) {
                # Use default NTP Time Server
                # Do nothing, as the URL is already set to 'time.windows.com'
                Write-Host "Using default NTP Time Server: 'time.windows.com' for NTP connectivity test on UDP port 123, as the '`$NTPTimeServer' parameter was not specified" -ForegroundColor Yellow
            } else {
                Write-Host "Replacing default NTP Time Server 'time.windows.com' with user-defined NTP server: '$NTPTimeServer'" -ForegroundColor Green
                # Use user-defined NTP Time Server
                [string]$urlObj.URL = (Get-DomainFromURL -url $NTPTimeServer).Domain
                $urlObj.Source = "User-defined NTP server"
            }
            # Call function to check DNS entry exists for the URL
            $DNSCheck = Get-DnsRecord -url (Get-DomainFromURL -url $urlObj.URL).Domain
            if($DNSCheck.DNSExists){
                # Stopwatch to measure the time taken for the connectivity test
                $StopWatch = [Diagnostics.Stopwatch]::StartNew()
                # Test TCP connectivity
                $urlObj.TCPStatus, $urlObj.IPAddress = Test-NTPConnectivity -ntpServer $urlObj.URL
                if($urlObj.TCPStatus -eq "Success"){
                    # NTP does not have Layer 7 connectivity, however, we will set the Layer 7 status based on the TCP status
                    $urlObj.Layer7Status = "Success"
                } else {
                    # NTP does not have Layer 7 connectivity, however, we will set the Layer 7 status based on the TCP status
                    $urlObj.Layer7Status = "Failed"
                }
                if([string]::IsNullOrWhiteSpace($urlObj.IPAddress)){
                    $urlObj.IPAddress = $DNSCheck.IpAddress
                }
                $urlObj.Layer7Response = "UDP port 123"
                # Set the Layer 7 response time to the elapsed time of the stopwatch
                $urlObj.Layer7ResponseTime = [math]::round($StopWatch.Elapsed.TotalMilliseconds/1000, 3)
                # Stop the stopwatch
                $StopWatch.Stop()
                $urlObj.CertificateIssuer = "N/A"
                $urlObj.CertificateSubject = "N/A"
                $urlObj.CertificateThumbprint = "N/A"
                $urlObj.IntermediateCertificateIssuer = "N/A"
                $urlObj.IntermediateCertificateSubject = "N/A"
                $urlObj.IntermediateCertificateThumbprint = "N/A"
                $urlObj.RootCertificateIssuer = "N/A"
                $urlObj.RootCertificateSubject = "N/A"
                $urlObj.RootCertificateThumbprint = "N/A"

            } else {
                $urlObj.TCPStatus = "Failed"
                $urlObj.IPAddress = $DNSCheck.IpAddress
                $urlObj.Layer7Status = "Failed" # DNS entry does not exist, return failed
                $urlObj.Layer7Response = "N/A"
                $urlObj.Layer7ResponseTime = "N/A"
                $urlObj.CertificateIssuer = "N/A"
                $urlObj.CertificateSubject = "N/A"
                $urlObj.CertificateThumbprint = "N/A"
                $urlObj.IntermediateCertificateIssuer = "N/A"
                $urlObj.IntermediateCertificateSubject = "N/A"
                $urlObj.IntermediateCertificateThumbprint = "N/A"
                $urlObj.RootCertificateIssuer = "N/A"
                $urlObj.RootCertificateSubject = "N/A"
                $urlObj.RootCertificateThumbprint = "N/A"
            }
        } elseif($urlObj.URL -eq "yourhcikeyvaultname.vault.azure.net") {
            # Replace the URL with the user-defined KeyVault URL, if specified
            $urlObj.URL = (Get-DomainFromURL -url $script:UpdatedKeyVaultURL).Domain
            # Call function to check DNS entry exists for the URL
            $DNSCheck = Get-DnsRecord -url (Get-DomainFromURL -url $urlObj.URL).Domain
            if($DNSCheck.DNSExists){
                if($IncludeTCPConnectivityTests.IsPresent){
                    # Test TCP and Layer 7 connectivity
                    $urlObj.TCPStatus, $urlObj.IPAddress = Test-TCPConnectivity -url $urlObj.URL -port $urlObj.Port
                } else {
                    $urlObj.TCPStatus = "N/A"
                    $urlObj.IPAddress = $DNSCheck.IpAddress
                }
                # Test Layer 7 connectivity
                $Layer7Results = Test-Layer7Connectivity -url $urlObj.URL -port $urlObj.Port
                $urlObj.Layer7Status = $Layer7Results.Layer7Status
                $urlObj.Layer7Response = $Layer7Results.Layer7Response
                $urlObj.Layer7ResponseTime = $Layer7Results.Layer7ResponseTime
                $urlObj.CertificateIssuer = $Layer7Results.CertificateIssuer
                $urlObj.CertificateSubject = $Layer7Results.CertificateSubject
                $urlObj.CertificateThumbprint = $Layer7Results.CertificateThumbprint
                $urlObj.IntermediateCertificateIssuer = $Layer7Results.CertificateIntermediateIssuer
                $urlObj.IntermediateCertificateSubject = $Layer7Results.CertificateIntermediateSubject
                $urlObj.IntermediateCertificateThumbprint = $Layer7Results.CertificateIntermediateThumbprint
                $urlObj.RootCertificateIssuer = $Layer7Results.CertificateRootIssuer
                $urlObj.RootCertificateSubject = $Layer7Results.CertificateRootSubject
                $urlObj.RootCertificateThumbprint = $Layer7Results.CertificateRootThumbprint
            } else {
                if($IncludeTCPConnectivityTests.IsPresent){
                    $urlObj.TCPStatus = "Failed"
                } else {
                    $urlObj.TCPStatus = "N/A"
                }
                $urlObj.IPAddress = $DNSCheck.IpAddress
                $urlObj.Layer7Status = "Failed" # DNS entry does not exist, return failed
                $urlObj.Layer7Response = "N/A"
                $urlObj.Layer7ResponseTime = "N/A"
                $urlObj.CertificateIssuer = "N/A"
                $urlObj.CertificateSubject = "N/A"
                $urlObj.CertificateThumbprint = "N/A"
                $urlObj.IntermediateCertificateIssuer = "N/A"
                $urlObj.IntermediateCertificateSubject = "N/A"
                $urlObj.IntermediateCertificateThumbprint = "N/A"
                $urlObj.RootCertificateIssuer = "N/A"
                $urlObj.RootCertificateSubject = "N/A"
                $urlObj.RootCertificateThumbprint = "N/A"   

            }
            # Add note to indicate if the URL is user-defined or default for KeyVault URL
            if($KeyVaultURL.IsPresent){
                $urlObj.Note = "User-defined KeyVault URL '$($urlObj.URL)' - $($urlObj.Note)"
            } else {
                $urlObj.Note = "Default / example KeyVault URL '$($urlObj.URL)' - $($urlObj.Note)"
            }

        # //// Skipped URLs ////
        } elseif ($skipUrls.URL.Contains($urlObj.URL)) {
            Write-Host "Skipped URL: $($urlObj.URL)" -ForegroundColor Green
            $urlObj.TCPStatus = "Skipped"
            $urlObj.IPAddress = "N/A"
            $urlObj.Layer7Status = "Skipped"
            $urlObj.Layer7Response = "N/A"
            $urlObj.Layer7ResponseTime = "N/A"
            $urlObj.CertificateIssuer = "N/A"
            $urlObj.CertificateSubject = "N/A"
            $urlObj.CertificateThumbprint = "N/A"
            $urlObj.IntermediateCertificateIssuer = "N/A"
            $urlObj.IntermediateCertificateSubject = "N/A"
            $urlObj.IntermediateCertificateThumbprint = "N/A"
            $urlObj.RootCertificateIssuer = "N/A"
            $urlObj.RootCertificateSubject = "N/A"
            $urlObj.RootCertificateThumbprint = "N/A"   
            $urlObj.Note = "Skipped URL - $($urlObj.Note)"
        
        # //// Wildcard URLs ////
        } elseif ($urlObj.IsWildcard) {
            Write-Host "Wildcard URL: '$($urlObj.URL)' will be processed later" -ForegroundColor Yellow
            $urlObj.TCPStatus = "Skipped"
            $urlObj.IPAddress = "N/A"
            $urlObj.Layer7Status = "Skipped"
            $urlObj.Layer7Response = "N/A"
            $urlObj.Layer7ResponseTime = "N/A"
            $urlObj.CertificateIssuer = "N/A"
            $urlObj.CertificateSubject = "N/A"
            $urlObj.CertificateThumbprint = "N/A"
            $urlObj.IntermediateCertificateIssuer = "N/A"
            $urlObj.IntermediateCertificateSubject = "N/A"
            $urlObj.IntermediateCertificateThumbprint = "N/A"
            $urlObj.RootCertificateIssuer = "N/A"
            $urlObj.RootCertificateSubject = "N/A"
            $urlObj.RootCertificateThumbprint = "N/A"   
            $urlObj.Note = "$($urlObj.Note)"

        # //// Non-wildcard URLs ////
        } else {
            # All other URLs: Test DNS, TCP, and Layer 7 connectivity
            # Call function to check DNS entry exists for the URL
            $DNSCheck = Get-DnsRecord -url (Get-DomainFromURL -url $urlObj.URL).Domain
            if($DNSCheck.DNSExists){
                if($IncludeTCPConnectivityTests.IsPresent){
                    # Test TCP and Layer 7 connectivity
                    $urlObj.TCPStatus, $urlObj.IPAddress = Test-TCPConnectivity -url $urlObj.URL -port $urlObj.Port
                } else {
                    $urlObj.TCPStatus = "N/A"
                    $urlObj.IPAddress = $DNSCheck.IpAddress
                }
                $Layer7Results = Test-Layer7Connectivity -url $urlObj.URL -port $urlObj.Port
                $urlObj.Layer7Status = $Layer7Results.Layer7Status
                $urlObj.Layer7Response = $Layer7Results.Layer7Response
                $urlObj.Layer7ResponseTime = $Layer7Results.Layer7ResponseTime
                $urlObj.CertificateIssuer = $Layer7Results.CertificateIssuer
                $urlObj.CertificateSubject = $Layer7Results.CertificateSubject
                $urlObj.CertificateThumbprint = $Layer7Results.CertificateThumbprint
                $urlObj.IntermediateCertificateIssuer = $Layer7Results.CertificateIntermediateIssuer
                $urlObj.IntermediateCertificateSubject = $Layer7Results.CertificateIntermediateSubject
                $urlObj.IntermediateCertificateThumbprint = $Layer7Results.CertificateIntermediateThumbprint
                $urlObj.RootCertificateIssuer = $Layer7Results.CertificateRootIssuer
                $urlObj.RootCertificateSubject = $Layer7Results.CertificateRootSubject
                $urlObj.RootCertificateThumbprint = $Layer7Results.CertificateRootThumbprint

            } else {
                if($IncludeTCPConnectivityTests.IsPresent){
                    $urlObj.TCPStatus = "Failed"
                } else {
                    $urlObj.TCPStatus = "N/A"
                }
                $urlObj.IPAddress = $DNSCheck.IpAddress
                $urlObj.Layer7Status = "Failed" # DNS entry does not exist, return failed
                $urlObj.Layer7Response = "N/A"
                $urlObj.Layer7ResponseTime = "N/A"
                $urlObj.CertificateIssuer = "N/A"
                $urlObj.CertificateSubject = "N/A"
                $urlObj.CertificateThumbprint = "N/A"
                $urlObj.IntermediateCertificateIssuer = "N/A"
                $urlObj.IntermediateCertificateSubject = "N/A"
                $urlObj.IntermediateCertificateThumbprint = "N/A"
                $urlObj.RootCertificateIssuer = "N/A"
                $urlObj.RootCertificateSubject = "N/A"
                $urlObj.RootCertificateThumbprint = "N/A"   
            }
        }
        # Add line return after each URL
        Write-Host ""
    }

    # ////////////////////////////////////////////////////////////////////////////
    # Dynamically expand wildcard URLs
    Write-Host "Expanding Wildcard URLs Dynamically..." -ForegroundColor Green
    # Call Function to expand wildcard URLs, passing the results array
    Expand-WildcardUrlsDynamically

    # ////////////////////////////////////////////////////////////////////////////
    # Process Redirected URLs, unless the ExcludeRedirectedUrls switch is used
    if(-not($ExcludeRedirectedUrls.IsPresent)) {
        
        # Added Layer 7 test for redirected URLs, done afterwards to prevent infinite loops
        Write-Host "`nProcessing Redirected URLs...`n" -ForegroundColor Green
        [int]$redirectedCount = 0
        ForEach($row in $script:RedirectedResults){
            $redirectedCount++
            Write-Host "Processing Redirected URL $redirectedCount of $($script:RedirectedResults.Count): $($row.URL)"

            # If using Arc Gateway, skip URLs that support Arc Gateway, if already tested
            if($ArcGatewayDeployment -and ($PreArcGatewayRemoval.URL -contains $row.URL)){
                # Remove the URL from the list, as it was already tested
                Write-Verbose "Info: Url $($row.URL) supports Arc Gateway and already exists in the results, skipping`n"
                Continue
            }

            # Check if the URL already exists in the results
            if($script:Results.URL -contains $row.URL){
                Write-Verbose "Info: Url $($row.URL) already exists in the results, skipping`n"
                Continue
            }

            # Remove DNSCheck variable if it exists, on each loop
            Remove-Variable DNSCheck -ErrorAction SilentlyContinue
            # Call function to check DNS entry exists for the URL
            $DNSCheck = Get-DnsRecord -url (Get-DomainFromURL -url $row.URL).Domain
            if($DNSCheck.DNSExists) {
                if($IncludeTCPConnectivityTests.IsPresent){
                    # Test TCP and Layer 7 connectivity
                    $row.TCPStatus, $row.IPAddress = Test-TCPConnectivity -url $row.URL -port $row.Port
                } else {
                    $row.TCPStatus = "N/A"
                    $row.IPAddress = $DNSCheck.IpAddress
                }
                # Test Layer 7 connectivity
                # Call function to test Layer 7 connectivity
                $Layer7Results = Test-Layer7Connectivity -url $row.redirect -port $row.Port
                $row.Layer7Status = $Layer7Results.Layer7Status
                $row.Layer7Response = $Layer7Results.Layer7Response
                $row.Layer7ResponseTime = $Layer7Results.Layer7ResponseTime
                $row.CertificateIssuer = $Layer7Results.CertificateIssuer
                $row.CertificateSubject = $Layer7Results.CertificateSubject
                $row.CertificateThumbprint = $Layer7Results.CertificateThumbprint
                $row.IntermediateCertificateIssuer = $Layer7Results.CertificateIntermediateIssuer
                $row.IntermediateCertificateSubject = $Layer7Results.CertificateIntermediateSubject
                $row.IntermediateCertificateThumbprint = $Layer7Results.CertificateIntermediateThumbprint
                $row.RootCertificateIssuer = $Layer7Results.CertificateRootIssuer
                $row.RootCertificateSubject = $Layer7Results.CertificateRootSubject
                $row.RootCertificateThumbprint = $Layer7Results.CertificateRootThumbprint

                # Add the row to the results array
                $script:Results += $row

            } else {
                if($IncludeTCPConnectivityTests.IsPresent){
                    $row.TCPStatus = "Failed"
                } else {
                    $row.TCPStatus = "N/A"
                }
                $row.IPAddress = $DNSCheck.IpAddress
                $row.Layer7Status = "N/A"
                $row.Layer7Response = "N/A"
                $row.Layer7ResponseTime = "N/A"
                $row.CertificateIssuer = "N/A"
                $row.CertificateSubject = "N/A"
                $row.CertificateThumbprint = "N/A"
                $row.IntermediateCertificateIssuer = "N/A"
                $row.IntermediateCertificateSubject = "N/A"
                $row.IntermediateCertificateThumbprint = "N/A"
                $row.RootCertificateIssuer = "N/A"
                $row.RootCertificateSubject = "N/A"
                $row.RootCertificateThumbprint = "N/A"

                # Add the row to the results array, even if the DNS entry does not exist
                $script:Results += $row
            }
            Write-Host ""
        }
        # Add redirected URLs to the results
        # $script:Results += $script:RedirectedResults
    }

    # ////////////////////////////////////////////////////////////////////////////
    # Check for any URLs that were not tested, in case RedirectedURLs were added, when Processing Redirected URLs above
    ForEach($Result in ($script:Results | Where-Object { $_.Layer7Status -eq "" })) {
      Write-Host "Processing remaining URL $($Result.URL)"
      # Remove DNSCheck variable if it exists, on each loop
      Remove-Variable DNSCheck -ErrorAction SilentlyContinue
      # Call function to check DNS entry exists for the URL
      $DNSCheck = Get-DnsRecord -url (Get-DomainFromURL -url $Result.URL).Domain
      if($DNSCheck.DNSExists) {
          if($IncludeTCPConnectivityTests.IsPresent){
              # Test TCP and Layer 7 connectivity
              $Result.TCPStatus, $Result.IPAddress = Test-TCPConnectivity -url $Result.URL -port $Result.Port
          } else {
              $Result.TCPStatus = "N/A"
              $Result.IPAddress = $DNSCheck.IpAddress
          }
          # Test Layer 7 connectivity
          # Call function to test Layer 7 connectivity
          $Layer7Results = Test-Layer7Connectivity -url $Result.URL -port $Result.Port
          $Result.Layer7Status = $Layer7Results.Layer7Status
          $Result.Layer7Response = $Layer7Results.Layer7Response
          $Result.Layer7ResponseTime = $Layer7Results.Layer7ResponseTime
          $Result.CertificateIssuer = $Layer7Results.CertificateIssuer
          $Result.CertificateSubject = $Layer7Results.CertificateSubject
          $Result.CertificateThumbprint = $Layer7Results.CertificateThumbprint
          $Result.IntermediateCertificateIssuer = $Layer7Results.CertificateIntermediateIssuer
          $Result.IntermediateCertificateSubject = $Layer7Results.CertificateIntermediateSubject
          $Result.IntermediateCertificateThumbprint = $Layer7Results.CertificateIntermediateThumbprint
          $Result.RootCertificateIssuer = $Layer7Results.CertificateRootIssuer
          $Result.RootCertificateSubject = $Layer7Results.CertificateRootSubject
          $Result.RootCertificateThumbprint = $Layer7Results.CertificateRootThumbprint

      } else {
          if($IncludeTCPConnectivityTests.IsPresent){
              $Result.TCPStatus = "Failed"
          } else {
              $Result.TCPStatus = "N/A"
          }
          $Result.IPAddress = $DNSCheck.IpAddress
          $Result.Layer7Status = "N/A"
          $Result.Layer7Response = "N/A"
          $Result.Layer7ResponseTime = "N/A"
          $Result.CertificateIssuer = "N/A"
          $Result.CertificateSubject = "N/A"
          $Result.CertificateThumbprint = "N/A"
          $Result.IntermediateCertificateIssuer = "N/A"
          $Result.IntermediateCertificateSubject = "N/A"
          $Result.IntermediateCertificateThumbprint = "N/A"
          $Result.RootCertificateIssuer = "N/A"
          $Result.RootCertificateSubject = "N/A"
          $Result.RootCertificateThumbprint = "N/A"
      }
      Write-Host ""
    }

    # ////////////////////////////////////////////////////////////////////////////
    # Do not perform extended tests for Wildcard URLs if switch is NOT present
    if(-not($ExcludeWildcardTests.IsPresent)){
        # Test manually defined subdomains
        Write-Host "Testing manually defined subdomains for Wildcard endpoints..." -ForegroundColor Green
        # Call function to test manually defined subdomains
        Test-ManuallyDefinedSubdomains
    } else {
        Write-Host "Skipping manually defined subdomains for Wildcard endpoints, as the '-ExcludeWildcardTests' switch was used" -ForegroundColor Yellow
    }

    # ////////////////////////////////////////////////////////////////////////////
    # Process and publish the final results
    # Publish-Results -results $allTestedUrls
    Publish-Results -results $script:Results

    # ////////////////////////////////////////////////////////////////////////////
    # If SSL Inspection is detected, display a warning message
    if($script:SSLInspectionDetected){
        Write-Host "`t`nWARNING: $($script:SSLInspectedURLs.Count) x endpoint(s) have possible SSL Inspection detected. Please check the CSV file for detailed information." -ForegroundColor Red
        Write-Host "`n`t`tEndpoint(s) with untrusted certificates:`n"
        ForEach($SSLEndpoint in $script:SSLInspectedURLs) { Write-Host "`t`t'$SSLEndpoint'`n" }
        Write-Host "`n`tPlease check with your network administrator, to verify if your firewall and/or proxy is performing SSL Inspection.`n" -ForegroundColor Yellow

    } else {
        Write-Host "`t`nNo SSL Inspection detected from the (limited) testing performed.`n" -ForegroundColor Green
    }

    # ////////////////////////////////////////////////////////////////////////////
    # Test download speed, check for proxy settings
    if($Proxy.Enabled){
        # Check if the proxy server string has a semi-colon which can be used for Arc Gateway
        if($Proxy.Server -match ";"){
            # In case the proxy server is set from netsh winhttp show advproxy output
            [array]$ProxyStrings = $Proxy.Server.ToString().Split(";")
        }
        if($ProxyStrings){
            # Check if the proxy server string has a semi-colon which can be used for Arc Gateway
            # Multiple proxy servers detected
            ForEach($ProxyServer in $ProxyStrings){
                # Trim the proxy server string
                $ProxyServer = $ProxyServer.Trim()
                if($ProxyServer -like "http=*"){
                    # HTTP proxy server (should be customer proxy server)
                    # Do nothing has download test is https://download.microsoft.com
                } elseif($ProxyServer -like "https=*"){
                    # HTTPS proxy server (might be Arc Gateway Agent, "localhost")
                    # Check if the proxy server is set from netsh winhttp show advproxy output
                    Write-Host "Testing download speed using web proxy: $($ProxyServer)" -ForegroundColor Green
                } else {
                    # Other proxy server detected
                    # Check if the proxy server is set from netsh winhttp show advproxy output
                    Write-Host "Testing download speed using web proxy: $($ProxyServer)" -ForegroundColor Green
                }
            } # End ForEach
        } else {
            Write-Host "///// Testing download speed using web proxy: $($Proxy.Server) /////" -ForegroundColor Green
        }
    } else {
        Write-Host "///// Testing direct download speed /////" -ForegroundColor Green
    }
    Write-Host "Download file endpoint location: 'https://download.microsoft.com'" -ForegroundColor Green
    Write-Host "Download timeout configured to maximum of 10 minutes, please wait..." -ForegroundColor Yellow

    # ////////////////////////////////////////////////////////////////////////////
    # Call function to test download speed
    $DownloadSpeedInMegaBitsPerSecond = Test-DownloadSpeed

    if($DownloadSpeedInMegaBitsPerSecond) {
        if($DownloadSpeedInMegaBitsPerSecond -eq 0){
            $DownloadSpeedInMegaBitsPerSecond = "Failed"
            Write-Host "`nDownload speed test failed!" -ForegroundColor Red
            Write-Host "`nVerify connectivity to endpoint 'https://download.microsoft.com' is allowed from this device." -ForegroundColor Yellow

        } elseif($DownloadSpeedInMegaBitsPerSecond -lt 25){
            Write-Host "`nDownload speed is $($DownloadSpeedInMegaBitsPerSecond) Megabits/second" -ForegroundColor Yellow
            Write-Host "`n`tWarning: Limited ingress bandwidth (less than 25 MBits/sec) could impact servicing operations, such as increasing the duration of solution updates." -ForegroundColor Yellow
        
        } else {
            Write-Host "`nDownload speed is $($DownloadSpeedInMegaBitsPerSecond) Megabits/second" -ForegroundColor Green
            
        }
    } else {
        $DownloadSpeedInMegaBitsPerSecond = "Failed"
        Write-Host "`nDownload speed test failed!" -ForegroundColor Red
        Write-Host "`nVerify connectivity to endpoint 'https://download.microsoft.com' is allowed from this device." -ForegroundColor Yellow
    }
    
    # ////////////////////////////////////////////////////////////////////////////
    # Get the Azure region from the environment
    # Export the results to a CSV file
    [PsCustomObject]@{
        RowID = "N/A";
        URL = "Download speed: $($DownloadSpeedInMegaBitsPerSecond.ToString()) Megabits/second";
    } | Export-Csv -Append -NoTypeInformation -Path $script:csvFile -Force

    # ////////////////////////////////////////////////////////////////////////////
    # Export the Azure region to the CSV file
    [PsCustomObject]@{
        RowID = "N/A";
        URL = "Azure Region = $AzureRegion";
    } | Export-Csv -Append -NoTypeInformation -Path $script:csvFile -Force

    # ////////////////////////////////////////////////////////////////////////////
    # Display the path to the CSV file
    Write-Host "`nDetailed test results have been saved to file: '$script:csvFile'" -ForegroundColor Green
    
    # Display the disclaimer
    Write-Host "`n`tDisclaimer:" -ForegroundColor Yellow
    Write-Host "`tThis function is not supported under any Microsoft standard support program or service."
    Write-Host "`tThe function is provided AS IS without warranty of any kind. Microsoft further disclaims"
    Write-Host "`tall implied warranties including, without limitation, any implied warranties of"
    Write-Host "`tmerchantability or of fitness for a particular purpose."

    Write-Host "`nThis module is intended for testing, support, and/or troubleshooting purposes only."

    # Function complete
    Write-Host "`nAzure Local Connectivity validation script complete`n" -ForegroundColor Green

    # Stop the Transcript block
    Stop-Transcript -ErrorAction SilentlyContinue

    # Call function to redact "user name" and "Run As" username from the transcript file, in case uploaded
    Remove-PIIFromTranscriptFile -TranscriptFilePath $script:TranscriptFile

    # Check if the Send-DiagnosticData function is available, Send-DiagnosticData is part of the DiagnosticsInitializer module
    # Call function to check if the Send-DiagnosticData function exists
    if(Test-CommandExists Send-DiagnosticData) {
        # Call Function to ask if the user wants to upload the results to using Send-DiagnosticData
        Invoke-UploadDiagnosticResults
    } else {
        # Function not found, skip the upload prompt
        Write-Host "`nInfo: Send-DiagnosticData function not found, skipping prompt to upload test results to Microsoft.`n" -ForegroundColor Yellow
    }

    # End of the script

} # End Function Test-AzStackHciLocalConnectivity

# ////////////////////////////////////////////////////////////////////////////
# Function to check if a command exists
Function Test-CommandExists
{
    Param ($command)

    $oldPreference = $ErrorActionPreference
    $ErrorActionPreference = "Stop"
    [bool]$CommandExists = $false
    try {
        if(Get-Command $command){
            $CommandExists = $true
        }
    } Catch {
        $CommandExists = $false
    }Finally {
        $ErrorActionPreference=$oldPreference
    }
    return $CommandExists

} # End function Test-CommandExists

# ////////////////////////////////////////////////////////////////////////////
Function Invoke-UploadDiagnosticResults {
    # Ask the user if they want to upload the results using Send-DiagnosticData
    Write-Host ""
    $UploadResults = Read-Host "`tOptional: Do you want to securely upload the Connectivity Test Results to Microsoft?`n`n`tNote: This should take approx. 1 minute to complete.`n`tThe data includes the PowerShell Transcript and CSV output file.`n`nUpload test results? (Y/N)"
    if($UploadResults -eq "Y"){
        # Call the Send-DiagnosticData function to upload the results
        Write-Host "`n`tUploading test results to Microsoft..." -ForegroundColor Green
        # Upload the results using Send-DiagnosticData
        # Send the diagnostic information to Microsoft, using: " -BypassObsAgent -NoLogCollection -SupplementaryLogs <PathToFolder>" parameters
        # https://learn.microsoft.com/en-us/azure/azure-local/manage/collect-logs?view=azloc-24113&tabs=powershell#send-diagnosticdata-command-reference
        try {
            Write-Host "Sending the Connectivity Test results information to Microsoft...`n"

            # Script block to execute the Send-DiagnosticData function, to find unwanted output
            [scriptblock]$scriptblock = { Send-DiagnosticData -BypassObsAgent -NoLogCollection -SupplementaryLogs "C:\ProgramData\AzStackHci.DiagnosticSettings\" -ErrorAction Continue }
            # Execute the script block in a new PowerShell process, to avoid unwanted output
            $UploadResults = Invoke-Command -ComputerName localhost -ScriptBlock $scriptblock -ErrorAction Continue -ErrorVariable UploadError
            if($UploadError){
                Write-Host "`nUpload error: $($UploadError.Exception.Message)`n`n" -ForegroundColor Red
            } else {
                Write-Host "`nUpload complete.`n`n" -ForegroundColor Green
            }

        } catch {
            Write-Error "Failed to send results to Microsoft using Send-DiagnosticData $($_.Exception.Message)"
        }
    } else {
        Write-Host "`nUser requested to skip uploading test results to Microsoft.`n" -ForegroundColor Green
    }
} # End Function Invoke-UploadDiagnosticResults

# ////////////////////////////////////////////////////////////////////////////
Function Get-AzStackHciEnvironmentCheckerModule {

    # Check if the AzStackHci.EnvironmentChecker module is installed and running the latest version
    $Module = "AzStackHci.EnvironmentChecker"

    # Check if the module is installed
    [Array]$InstalledVersions = @()
    $InstalledVersions = (Get-Module -Name $Module -ListAvailable -ErrorAction SilentlyContinue) | Sort-Object -Property Version
    
    # If only one version is installed, set the installed version
    if($InstalledVersions){
        if($InstalledVersions.Count -eq 1) {
            [version]$InstalledVersion = $InstalledVersions.Version
        } elseif($InstalledVersions.Count -gt 1) {
            # Multiple versions installed, use the latest version
            [version]$InstalledVersion = $InstalledVersions[0].Version
            Write-Warning "There are $($InstalledVersions.Count) versions of 'AzStackHci.EnvironmentChecker' module installed"
        }
        Write-Host "'AzStackHci.EnvironmentChecker' module version $($InstalledVersion.ToString()) is installed" -ForegroundColor "Green"
        # Do nothing, continue with the script

    } elseif(-not($InstalledVersions)){
        # Module not found, ask user to install the module
        Write-Host "Azure Stack HCI Environment Checker module is not installed" -ForegroundColor "Red"
        $InstallModulePrompt = Read-Host "Would you like to install the dependant module '$Module' now? (Y/N)"
        if(([string]::IsNullOrWhiteSpace($InstallModulePrompt)) -or ($InstallModulePrompt -ne "Y")){
            Throw "Error: Null or not 'Y' response. Exiting script, please install the '$Module' module and re-run the function"

        } elseif($InstallModulePrompt -eq "Y"){
            Write-Host "Installing module '$Module'...." -ForegroundColor "Green"
            try {
                # Requires -AllowClobber as some functions are included in Az.StackHCI
                Install-Module -Name $Module -Repository PSGallery -Force -ErrorVariable ModuleInstallError -AllowClobber -Scope AllUsers -Confirm:$false
            } catch {
                Throw "Error installing module '$Module' - $($_.Exception.Message)"
            }
            if(-not($ModuleInstallError)){
                Write-Host "Module '$Module' installed successfully" -ForegroundColor "Green"
            } else {
                Throw "Error installing module '$Module' - $ModuleInstallError"
            }
        } else {
            # All other responses
            Write-Host "Invalid response, please enter 'Y' to install the module..." -ForegroundColor "Red"
            Throw "Exiting script, please install the '$Module' module and re-run the function"
        }
    }

    Import-Module -Name $Module -Force
    # Load URLs from environment checker (Targets.json), check if multiple module version are installed, if so, use the latest version
    $Location = ((Get-Module -Name $Module -ListAvailable) | Sort-Object -Property Version -Descending | Select-Object -First 1).ModuleBase
    if(-not($Location)){
        Write-Host "Azure Stack HCI Environment Checker module not found, please install the module" -ForegroundColor Red
        Write-Host "Run: 'Install-Module -Name AzStackHci.EnvironmentChecker' and troubleshoot any installation issues" -ForegroundColor Green
        Exit
    }

    # Return the module location
    Return $Location
} # End Function Get-AzStackHciEnvironmentCheckerModule

# ////////////////////////////////////////////////////////////////////////////
# Function to get the latest version of a module from PSGallery
# This function will check for the latest version of a module in the PowerShell Gallery
Function Get-LatestModuleVersion {

    [CmdletBinding()]
    param (
        [Parameter(Mandatory=$true, Position=0)]
        [ValidateNotNullOrEmpty()]
        [string]$ModuleName
    )
    
    # ////////////////////////////////////////////
    # ///// Check for latest module version //////
    # ////////////////////////////////////////////

    # Get Existing Most Recent Module Version:
    [version]$ExistingModuleVersion = (Get-InstalledModule -Name $ModuleName -ErrorAction SilentlyContinue).Version

    # Force remove of old module versions:
    $OldModuleVersions = (Get-Module -Name $ModuleName -ListAvailable | Where-Object { $PSItem.Version -ne $ExistingModuleVersion }).ModuleBase
    if($OldModuleVersions) {
        ForEach($Path in $OldModuleVersions){
            # Remove files within folder(s)
            Remove-Item -Path "$Path\*" -Force -Recurse -ErrorAction SilentlyContinue
            # Remove folder(s)
            Remove-Item -Path $Path -Force -ErrorAction SilentlyContinue
        }
    }

    # Double check older versions clean-up, from previous Auto Updates
    Get-Module -Name $ModuleName -ListAvailable | Where-Object { $PSItem.Version -ne $ExistingModuleVersion } | Uninstall-Module -Force -ErrorAction SilentlyContinue

    # Boolean for Stopping the Do While Loop
    [bool]$StopModuleVersion = $false
    # Create variable that will be updated on each iteration of the Loop
    [version]$ModuleVersion = $ExistingModuleVersion

    # Loop and check if a new version exists:
    Do {
        [int]$Major = $ModuleVersion.Major
        [int]$Minor = $ModuleVersion.Minor
        [int]$Build = $ModuleVersion.Build

        # Add 0.0.1 to the existing build and check PSGallery
        if($Build -lt 9){
            $Build += 1
        } else {
            $Build = 0
            if($Minor -lt 9) {
                $Minor += 1
            } else {
                $Minor = 0
                $Major += 1
            }
        }

        [version]$NewModuleVersion = "$Major.$Minor.$Build"

        # Check PSGallery for Incremented Module Version
        Clear-Variable ModuleVersion -ErrorAction SilentlyContinue
        Write-Debug "Checking if module v$($NewModuleVersion) exists in PowerShell Gallery..."
        [version]$ModuleVersion = (Find-Module -Name "$ModuleName" -RequiredVersion $NewModuleVersion -ErrorAction SilentlyContinue).Version

        if($ModuleVersion) {
            Write-Debug "Higher module version found v$($ModuleVersion.ToString())"
            [version]$LatestModuleVersion = $ModuleVersion
        } else {
            # Stop loop when no match found in PSGallery
            Write-Debug "No match for module v$($NewModuleVersion.ToString())"
            $StopModuleVersion = $true
        }

    } While (-not($StopModuleVersion)) # Do While Loop $StopModuleVersion not equal to $true

    # Return Function output
    if($LatestModuleVersion) {
        Return [version]$LatestModuleVersion
    } else {
        Return [version]$ExistingModuleVersion
    }
} # End Function Get-LatestModuleVersion

# ////////////////////////////////////////////////////////////////////////////
# Auto Update Module Function
Function Update-ModuleVersion {

    [CmdletBinding()]
    param (
        [Parameter(Mandatory=$true, Position=0)]
        [ValidateNotNullOrEmpty()]
        [string]$ModuleName

    )

    # Check for Updates to the Module using PSGallery
    Write-Host -ForegroundColor Green "`n`tChecking for updates..."
    [version]$InstalledModuleVersion = (Get-Module -Name "$ModuleName" -ListAvailable | Sort-Object -Property Version | Select-Object -First 1).Version
    
    # Call function to determine the latest version of module in PSGallery
    [version]$LatestModule = Get-LatestModuleVersion -ModuleName $ModuleName
    
    [bool]$ModuleUpdated = $false

    # Check if the module is installed
    if($InstalledModuleVersion) {
        # Check if the installed version is less than the latest version
        if($InstalledModuleVersion -lt $LatestModule) {

            # Update required
            Write-Host -ForegroundColor Yellow "`n`tINFO: " -NoNewLine
            if($InstalledModuleVersion){
                Write-Host "`t$ModuleName module needs updating from v$($InstalledModuleVersion.ToString()) to v$($LatestModule.ToString())`n"
                Write-Host -ForegroundColor Green "`n`t`tAuto-updating....`n"
            } else {
                Write-Host "$ModuleName module is not installed`n"
            }
        
            # Install latest module version
            try {
                Install-Module -Name $ModuleName -RequiredVersion $LatestModule -ErrorAction Continue -Force -ErrorVariable ModuleInstallError
            } catch {
                Write-Warning "Error installing latest version $($LatestModule.ToString()) of module $ModuleName. $($_.Exception.Message)"
            }
            # Attempt to Uninstall the previous version
            try {
                Uninstall-Module -Name $ModuleName -RequiredVersion $InstalledModuleVersion -Force -ErrorAction SilentlyContinue -ErrorVariable ModuleUninstallError
            } catch {
                Write-Warning "Error uninstalling previous version $($InstalledModuleVersion.ToString()) of module $ModuleName. $($_.Exception.Message)"
            }
            # Import the new module version
            try {
                Import-Module -Name $ModuleName -RequiredVersion $LatestModule -Force -ErrorAction Continue -ErrorVariable ModuleImportError
            } catch {
                Write-Warning "Error importing latest version $($LatestModule.ToString()) of module $ModuleName. $($_.Exception.Message)"
            }

            if(-not($ModuleInstallError)) {
                [bool]$ModuleUpdated = $true
                Write-Host -ForegroundColor Green "`t`tModule updated successfully!`n"
                Write-Host -ForegroundColor Green "`tPlease re-run the function to continue.`n`n"
            } elseif($ModuleUninstallError) {
                Write-Warning "Error uninstalling previous version $($InstalledModuleVersion.ToString()) of module $ModuleName. $ModuleUninstallError"
                [bool]$ModuleUpdated = $false
            } else {
                Write-Warning "Error updating module $ModuleName. $ModuleInstallError"
                [bool]$ModuleUpdated = $false
            }

            # /// Work-around for Updated Module NOT importing automatically
            # Unload existing module from the active Pwsh session
            Remove-Module -Name $ModuleName -ErrorAction SilentlyContinue
            # Force Import of the Updated Module version by file path
            (Get-Module -Name $ModuleName -ListAvailable -ErrorAction SilentlyContinue | Sort-Object Version -Descending | Select-Object -First 1).Path | Import-Module -Force

        } else {
            # No Update Required
            Write-Host -ForegroundColor Green "`n`tINFO: " -NoNewLine
            Write-Host "$ModuleName module is up-to-date at v$($InstalledModuleVersion.ToString())`n"
        }
    # Module not installed
    } else {
        Write-Host -ForegroundColor Red "`n`tError: " -NoNewLine
        Write-Host "Module '$ModuleName' is not installed.`n" -ForegroundColor Yellow
        [bool]$ModuleUpdated = $false
    }

    # Return Function with boolean for Module Updated or not
    Return $ModuleUpdated

} # End Function Update-ModuleVersion

# ////////////////////////////////////////////////////////////////////////////
# Test if proxy is enabled and return boolean
function Get-Proxy
{
    <#
    .SYNOPSIS
        Check if proxy is enabled and return boolean and proxy server uri
    #>


    $line1, $line2, $line3, $JsonLines = netsh winhttp show advproxy
    $ProxyInformation = $JsonLines | ConvertFrom-Json -ErrorAction SilentlyContinue
    # Return True/False and the Proxy server uri as a PSObject
    $ProxyReturnVariable = New-Object PsObject -Property @{
        # True/False
        Enabled = [bool]$ProxyInformation.Proxy
        # Proxy server URI
        Server = $ProxyInformation.Proxy
        # Proxy bypass list
        # ProxyBypass = $proxy.Bypass
    }
    # Check if the Proxy is enabled and if the proxy server is set
    if($ProxyReturnVariable.Enabled) {
        if(-not($ProxyReturnVariable.Server)){
            # In case the proxy server is not set from netsh winhttp show advproxy output
            $proxyUri = [Uri]$null
            $ProxyTest = [System.Net.WebRequest]::GetSystemWebProxy()
            $ProxyTest.Credentials = [System.Net.CredentialCache]::DefaultCredentials
            # use a known URL to test the proxy server, but http, in case of Arc Gateway, to prevent "localhost" from being returned
            # This is a known URL that should be accessible from any network
            $TestUrl = [System.Uri]::new("http://oneocsp.microsoft.com")
            # Test the proxy server using a known URL
            $proxyUri = $ProxyTest.GetProxy($TestUrl)
            if($proxyUri -eq $TestUrl) {
                # Proxy is not required, so do not use it
                $ProxyReturnVariable.Enabled = $false
                Write-Host "`t`nNo proxy server detected, using direct connection...`n" -ForegroundColor Green
            } else {
                # Proxy is required, so use it
                $ProxyReturnVariable.Server = $proxyUri
            }
        }
        # Check if the proxy server string has a semi-colon which can be used for Arc Gateway
        if($ProxyReturnVariable.Server -match ";"){
            # In case the proxy server is set from netsh winhttp show advproxy output
            [array]$ProxyStrings = $ProxyReturnVariable.Server.ToString().Split(";")
        }
        if($ProxyStrings){
            # Check if the proxy server string has a semi-colon which can be used for Arc Gateway
            # Multiple proxy servers detected
            Write-Host "`n`tArc Gateway scenario detected (possible), as multiple proxy servers detected ('netsh winhttp show advproxy')..."
            ForEach($ProxyServer in $ProxyStrings){
                # Trim the proxy server string
                $ProxyServer = $ProxyServer.Trim()
                if($ProxyServer -like "http=*"){
                    # HTTP proxy server (should be customer proxy server)
                    # Check if the proxy server is set from netsh winhttp show advproxy output
                    Write-Host "`tHTTP Proxy server detected, using proxy: $($ProxyServer)" -ForegroundColor Green    
                } elseif($ProxyServer -like "https=*"){
                    # HTTPS proxy server (might be Arc Gateway Agent, "localhost")
                    # Check if the proxy server is set from netsh winhttp show advproxy output
                    Write-Host "`tHTTPS Proxy server detected, using proxy: $($ProxyServer)" -ForegroundColor Green    
                } else {
                    # Other proxy server detected
                    # Check if the proxy server is set from netsh winhttp show advproxy output
                    Write-Host "`tProxy server detected, using proxy: $($ProxyServer)" -ForegroundColor Green    
                }
            } # End ForEach
            Write-Host ""
        } else {
            # Single proxy detected
            Write-Host "`t`nProxy server detected, using proxy: $($ProxyReturnVariable.Server)`n" -ForegroundColor Green
        }

    } else {
        # Proxy is NOT enabled
        # No proxy server detected, so use direct connection
        Write-Host "`t`nNo proxy server detected, using direct connection...`n" -ForegroundColor Green
    }
    Return $ProxyReturnVariable
} # End Function Get-Proxy

# ////////////////////////////////////////////////////////////////////////////
# This function retrieves the SSL certificate chain from a remote HTTPS endpoint
# It uses the System.Net.Http.HttpClient class to make the request and capture the certificate chain
function Get-SslCertificateChain
{
    <#
    .SYNOPSIS
        Retrieve remote ssl certificate & chain from https endpoint for Desktop and Core
    .NOTES
        Credit: https://github.com/markekraus
    #>

    [CmdletBinding()]
    param (
        [system.uri]
        $url,

        [Parameter()]
        [bool]
        $AllowAutoRedirect,

        [Parameter()]
        [string]
        $Proxy,

        [Parameter()]
        [pscredential]
        $ProxyCredential
    )
    try
    {
        $cs = @'
    using System;
    using System.Collections.Generic;
    using System.Net.Http;
    using System.Net.Security;
    using System.Security.Cryptography.X509Certificates;
 
    namespace CertificateCapture
    {
        public class Utility
        {
            public static Func<HttpRequestMessage,X509Certificate2,X509Chain,SslPolicyErrors,Boolean> ValidationCallback =
                (message, cert, chain, errors) => {
                    CapturedCertificates.Clear();
                    var newCert = new X509Certificate2(cert);
                    var newChain = new X509Chain();
                    newChain.Build(newCert);
                    CapturedCertificates.Add(new CapturedCertificate(){
                        Certificate = newCert,
                        CertificateChain = newChain,
                        PolicyErrors = errors,
                        URI = message.RequestUri
                    });
                    return true;
                };
            public static List<CapturedCertificate> CapturedCertificates = new List<CapturedCertificate>();
        }
 
        public class CapturedCertificate
        {
            public X509Certificate2 Certificate { get; set; }
            public X509Chain CertificateChain { get; set; }
            public SslPolicyErrors PolicyErrors { get; set; }
            public Uri URI { get; set; }
        }
    }
'@


        try
        {
            if (-not ('CertificateCapture.Utility' -as [type]))
            {
                if ($PSEdition -ne 'Core')
                {
                    Add-Type -AssemblyName System.Net.Http
                    Add-Type $cs -ReferencedAssemblies System.Net.Http
                }
                else
                {
                    Add-Type $cs
                }
            }
        }
        catch
        {
            if ($_.Exception.Message -notmatch 'Definition of new types is not supported in this language mode')
            {
                throw "Language mode does not allow this test Error: $_"
            }
        }

        $Certs = [CertificateCapture.Utility]::CapturedCertificates
        $Handler = [System.Net.Http.HttpClientHandler]::new()
        # This is important to capture the certificate chain
        if($AllowAutoRedirect -eq $false)
        {
            # Set the handler to not allow auto redirects
            # https://learn.microsoft.com/en-us/dotnet/api/system.net.http.httpclienthandler.allowautoredirect
            $Handler.AllowAutoRedirect = $false
        }
        if ($Proxy)
        {
            $Handler.Proxy = New-Object System.Net.WebProxy($proxy)
            if ($proxyCredential)
            {
                $Handler.DefaultProxyCredentials = $ProxyCredential
            }
        }
        $Handler.ServerCertificateCustomValidationCallback = [CertificateCapture.Utility]::ValidationCallback
        $Client = [System.Net.Http.HttpClient]::new($Handler)
        $null = $Client.GetAsync($url).Result
        return $Certs.CertificateChain
    }
    catch
    {
        throw $_
    }
} # End Function Get-SslCertificateChain

# ////////////////////////////////////////////////////////////////////////////
# This function checks if the script is running with elevated privileges
function Test-Elevation
{
    # Check if the script is running with elevated privileges
    # Return true if running as administrator, false otherwise
    Return ([Security.Principal.WindowsPrincipal][Security.Principal.WindowsIdentity]::GetCurrent()).IsInRole([Security.Principal.WindowsBuiltInRole] 'Administrator')
}

# This function invokes a job with an animation in the console
# It takes a job name and a script block as parameters
function Invoke-JobWithAnimation {
    
    Param (
        [Parameter(Mandatory=$true)]
        [string]$JobName,

        [Parameter(Mandatory=$true)]
        [scriptblock]$JobScriptBlock
    )

    $cursorTop = [Console]::CursorTop
    
    try {
        [Console]::CursorVisible = $false
        
        $counter = 0
        $frames = '|', '/', '-', '\' 
        $StartTime = (Get-Date)
        $script:job = Start-Job -Name $JobName -ScriptBlock $JobScriptBlock
    
        while($job.JobStateInfo.State -eq "Running") {
            $frame = $frames[$counter % $frames.Length]
            $RunningDuration = ((Get-Date) - $StartTime).ToString("hh\:mm\:ss")
            Write-Host -ForegroundColor Green "Download in progress: $frame Time elapsed: $RunningDuration" -NoNewLine
            [Console]::SetCursorPosition(0, $cursorTop)
            
            $counter += 1
            Start-Sleep -Milliseconds 125
        }
   
    } finally {
        [Console]::SetCursorPosition(0, $cursorTop)
        [Console]::CursorVisible = $true
    }
    # Wait for the job to complete and capture the output from the child jobs
    $JobOutput = $job | Receive-Job -Wait -WriteJobInResults
    if(($JobOutput).Output){
        # Do nothing
    } else {
        # If the job output is empty, check the child jobs
        # and receive their output
        $JobOutput = $job.ChildJobs | Receive-Job -Wait -WriteJobInResults
    }
    
    Write-Host ""
    Return $JobOutput
} # End Function Invoke-JobWithAnimation


# ////////////////////////////////////////////////////////////////////////////
Function Test-DownloadSpeed {
    <#
    .SYNOPSIS
        Test download speed from a specified URL and measure the time taken to download a file.
    .DESCRIPTION
        This function downloads a file from a specified URL and measures the download speed in Mbits/sec.
        It uses the Invoke-WebRequest cmdlet to download the file and a stopwatch to measure the elapsed time.
        The script also includes a function to display a processing animation while the download is in progress.
    #>

    [CmdletBinding()]
    param (
        [Parameter(Mandatory=$false)]
        [string]$DownloadFileUrl = 'https://aka.ms/WACDownload',

        [Parameter(Mandatory=$false)]
        [string]$DestinationFolder = 'C:\ProgramData\AzStackHci.DiagnosticSettings',

        [Parameter(Mandatory=$false)]
        [string]$DestinationPath = "$DestinationFolder\WindowsAdminCenter_DownloadSpeedTest.exe"
    )
    
    # Linux:
    # t=$(date +"%s"); wget https://aka.ms/WACDownload -O ->/dev/null ; echo -n "MBit/s: "; expr 8 \* 100 / $(($(date +"%s")-$t))

    # Windows, download the file using Invoke-WebRequest
    # Windows Admin Center download speed test
    # smaller file
    # https://aka.ms/WACDownload

    # Direct file link
    # https://download.microsoft.com/download/1/0/5/1059800B-F375-451C-B37E-758FFC7C8C8B/WindowsAdminCenter2410.exe


    # This downloads a file from a specified URL and measures the download speed in Mbits/sec.
    # It uses the Invoke-WebRequest cmdlet to download the file and a stopwatch to measure the elapsed time.
    # The script also includes a function to display a processing animation while the download is in progress.

    # Create the destination folder if it doesn't exist
    if (-not (Test-Path -Path $destinationFolder)) {
        New-Item -ItemType Directory -Path $destinationFolder | Out-Null
    }

    # Script block to download the file
    # This script block is executed in a separate job to allow for progress animation
    [scriptblock]$DownloadFileSpeedTest = {

        # Hashtable of parameters for the Invoke-WebRequest cmdlet
        $startWebRequestSplat = @{
            Uri = $using:DownloadFileUrl
            OutFile = $using:destinationPath
            UseBasicParsing = $true
            ErrorAction = 'SilentlyContinue'
            ErrorVariable = 'webRequestError'
            TimeoutSec = 600
        }
        # Set the progress preference to 'SilentlyContinue' to suppress progress output
        # This prevents the progress bar from impacting download speed
        $ProgressPreference = 'SilentlyContinue'

        try {
            # Invoke-WebRequest to download the file, use -PassThru to get the result
            $iwrResult = Invoke-WebRequest @startWebRequestSplat -PassThru
        } catch {
            # Handle any exceptions that occur during the web request
            Write-Output "Error: $($_.Exception.Message.ToString())"
        }

        # Check if the download was successful
        if($iwrResult){
            if ($iwrResult.StatusCode -eq 200) {
                Write-Output "Download completed with Status Code: $($iwrResult.StatusCode)."
            } else {
                Write-Output "Download failed status code $($iwrResult.StatusCode)."
                Write-Output "Error: Status Description: $($iwrResult.StatusDescription)"
            }
        } elseif($webRequestError) {
            Write-Output "Error: $($webRequestError.Exception.Message.ToString())"
        } else {
            Write-Output "Error: Unknown error occurred during download."
        }

    }
    
    # Stopwatch object to measure download time
    $StopWatch = [System.Diagnostics.Stopwatch]::new()
    # Start the stopwatch
    $StopWatch.Start()
    # Start the download with animation
    Write-Host "Starting download of 126 MB test file..."
    $DownloadResult = Invoke-JobWithAnimation -JobName 'DownloadFileSpeedTest' -JobScriptBlock $DownloadFileSpeedTest
    # Check if the download was successful
    if($DownloadResult.State -eq 'Completed') {
        # Write-Host "Result: $($DownloadResult.Output)"
        # Check if the download was successful
        if($DownloadResult.Output) {
            if($DownloadResult.Output -like "*Status Code: 200*"){
                Write-Host "Download completed successfully." -ForegroundColor Green
            } else {
                Write-Host "Download error: $($DownloadResult.Output)"
            }
        }
    # Job not completed
    } elseif ($DownloadResult.State -eq 'Failed') {
        Write-Host "Download test failed to complete." -ForegroundColor Red
        Write-Host "Download error! Information: $($DownloadResult.Output) Error: $($DownloadResult.Error)"
    } else {
        Write-Host "Download test failed to complete." -ForegroundColor Red
        Write-Host "Download error! Information: $($DownloadResult.Output) Error: $($DownloadResult.Error)"
    }
    # clean up the job
    $job | Remove-Job -Force

    # Calculate the elapsed time
    [double]$downloadTime = $StopWatch.ElapsedMilliseconds/1000
    # Stop the stopwatch
    $StopWatch.Stop()
    # Output the elapsed time
    Write-Host "Download duration time: $($downloadTime) seconds"

    # Get the file size in bytes
    try {
        $fileSizeInBytes = (Get-Item $destinationPath).Length
    } catch {
        Write-Host "Error: Failed to get the file size. File may not exist." -ForegroundColor Red
        Write-Host "Error: $($_.Exception.Message)" -ForegroundColor
        $fileSizeInBytes = 0
    }

    # Output the file size
    Write-Host "Download test file size: $([math]::Round($fileSizeInBytes / 1Mb, 2)) MB"
    Write-Host "Calculating download speed..."
    # Calculate the download speed in Mbps
    # The formula for Mbits/sec is (file size in bytes * 8) / download time in seconds / 1,000,000
    # Breakdown of the formula:
    # File Size (bytes): The size of the file you are downloading.
    # Multiplying by 8: This is the conversion factor to convert file size from bytes to bits (1 byte = 8 bits).
    # Dividing by 1,000,000: This is the conversion factor to convert bits to megabits (1 megabit = 1,000,000 bits).
    # The formula calculates the download speed in megabits per second (Mbps).
    # The file size is multiplied by 8 to convert it from bytes to bits.
    # The download time is used to calculate the speed in seconds.
    # The result is divided by 1,000,000 to convert bits to megabits.
    # The result is rounded to 2 decimal places for better readability.
    # The final result is the download speed in megabits per second (Mbps).
    # The formula for Mbits/sec is (file size in bytes * 8) / download time in seconds / 1,000,000
    [double]$DownloadSpeed = $([math]::Round($fileSizeInBytes * 8 / $downloadTime / 1000000, 2))
    
    # Clean up the downloaded file
    if (Test-Path -Path $destinationPath) {
        try { 
            # Remove the downloaded file
            Remove-Item -Path $destinationPath -Force
        } catch {
            Write-Host "Error: Failed to delete the file: $($destinationPath)."
            Write-Host "Error: $($_.Exception.Message)"
        }
        # Check if the file was deleted successfully
        if (-not (Test-Path -Path $destinationPath)) {
            # Do nothing, successfully deleted
        } else {
            Write-Host "Error: File cleanup failed for file: $($destinationPath)."
        }
    } else {
        # Do nothing, no file to clean up
    }

    # Return the download speed
    # The download speed is returned as a double value
    Return $DownloadSpeed
}
# ////////////////////////////////////////////////////////////////////////////

# Function to remove PII from the transcript file
# ////////////////////////////////////////////////////////////////////////////
Function Remove-PIIFromTranscriptFile {
    <#
    .SYNOPSIS
        Redact the transcript file by removing sensitive information.
    .DESCRIPTION
        This function reads a transcript file, removes sensitive information, and writes the redacted content to a new file.
        The redacted content is saved in the same directory as the original file with "_redacted" appended to the filename.
    #>

    [CmdletBinding()]
    param (
        [Parameter(Mandatory=$true)]
        [string]$TranscriptFilePath
    )

    # Read the transcript file content
    try {
        $transcriptContent = Get-Content -Path $TranscriptFilePath -ErrorAction SilentlyContinue
    } catch {
        Write-Host "Error: Failed to read the transcript file. $($_.Exception.Message)" -ForegroundColor Red
        Return
    }

    # Check if the transcript file was read successfully
    if($transcriptContent) {
        
        # Set the variable to the original transcript file contents
        $redactedContent = $transcriptContent 
        
        # Redact sensitive information:
        # Update content to replace "Username: <domain>\<username>" with "<REDACTED>"
        $redactedContent = $redactedContent -replace '(?i)(Username: )([a-zA-Z0-9]+\\[a-zA-Z0-9]+)', '$1<REDACTED>'
        # Update content to replace "RunAs User: <domain>\<username>" with "<REDACTED>"
        $redactedContent = $redactedContent -replace '(?i)(RunAs User: )([a-zA-Z0-9]+\\[a-zA-Z0-9]+)', '$1<REDACTED>'

        # Write the redacted content to a new file
        try {
            Set-Content -Path $TranscriptFilePath -Value $redactedContent -ErrorAction SilentlyContinue -Force
            # Check if the file was updated successfully
        } catch {
            Write-Host "Error: Failed to update transcript file." -ForegroundColor Red
        }
    } else {
        Write-Host "Error: Failed to read the transcript file." -ForegroundColor Red
    }
} # End Function Remove-PIIFromTranscriptFile

# ////////////////////////////////////////////////////////////////////////////
# Function to display an ASCII art banner
# This function displays an ASCII art banner in the console
# Font = Slant, credit web-site: https://patorjk.com/software/taag/
Function Show-ASCIIArtBanner {
# ASCI Art Banner variable
[string]$banner=@'
////////////////////////////////////////////////////////////////////////////////////////
                 ___ __ __
                / |____ __ __________ / / ____ _________ _/ /
               / /| /_ / / / / / ___/ _ \ / / / __ \/ ___/ __ `/ /
              / ___ |/ /_/ /_/ / / / __/ / /___/ /_/ / /__/ /_/ / /
             /_/ |_/___/\__,_/_/ \___/ /_____/\____/\___/\__,_/_/
   ______ __ _ _ __ ______ __
  / ____/___ ____ ____ ___ _____/ /_(_) __(_) /___ __ /_ __/__ _____/ /______
 / / / __ \/ __ \/ __ \/ _ \/ ___/ __/ / | / / / __/ / / / / / / _ \/ ___/ __/ ___/
/ /___/ /_/ / / / / / / / __/ /__/ /_/ /| |/ / / /_/ /_/ / / / / __(__ ) /_(__ )
\____/\____/_/ /_/_/ /_/\___/\___/\__/_/ |___/_/\__/\__, / /_/ \___/____/\__/____/
                                                   /____/
////////////////////////////////////////////////////////////////////////////////////////
'@


Write-Host -ForegroundColor Green `n$banner`n
}