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 } |