param( [Parameter(Mandatory=$true)] [string]$XmlPath, [Parameter(Mandatory=$false)] [int]$MaxHosts = 0, # Will be set to total hosts if 0 [Parameter(Mandatory=$false)] [int]$RequestTimeout = 10, [Parameter(Mandatory=$false)] [int]$MaxResponseSize = 20MB ) # Function to count total hosts in the XML file function Get-TotalHosts { param( [string]$XmlPath ) try { $settings = New-Object System.Xml.XmlReaderSettings $settings.DtdProcessing = [System.Xml.DtdProcessing]::Parse $reader = [System.Xml.XmlReader]::Create($XmlPath, $settings) $count = 0 while ($reader.Read()) { if ($reader.NodeType -eq [System.Xml.XmlNodeType]::Element -and $reader.Name -eq "host") { $count++ } } return $count } finally { if ($reader) { $reader.Close() } } } # Function to format time span function Format-TimeSpan { param ( [TimeSpan]$TimeSpan ) if ($TimeSpan.TotalHours -ge 1) { return "{0:0}h {1:0}m {2:0}s" -f $TimeSpan.Hours, $TimeSpan.Minutes, $TimeSpan.Seconds } elseif ($TimeSpan.TotalMinutes -ge 1) { return "{0:0}m {1:0}s" -f $TimeSpan.Minutes, $TimeSpan.Seconds } else { return "{0:0}s" -f $TimeSpan.TotalSeconds } } function Get-AvailableTlsProtocols { # Define possible protocols in order from newest to oldest $possibleProtocols = @( @{ Name = "Tls13"; Enum = "Tls13" }, @{ Name = "Tls12"; Enum = "Tls12" }, @{ Name = "Tls11"; Enum = "Tls11" }, @{ Name = "Tls"; Enum = "Tls" }, @{ Name = "Ssl3"; Enum = "Ssl3" }, @{ Name = "Ssl2"; Enum = "Ssl2" } ) $availableProtocols = @() # Dynamically check which protocols are defined in the current .NET version foreach ($protocol in $possibleProtocols) { try { # Try to access the enum value $enumValue = [System.Security.Authentication.SslProtocols]::($protocol.Enum) $availableProtocols += @{ Name = $protocol.Name; Value = $enumValue } Write-Host " → Protocol $($protocol.Name) is available" } catch { Write-Host " → Protocol $($protocol.Name) is not supported on this system" -ForegroundColor Yellow } } if ($availableProtocols.Count -eq 0) { Write-Host " → Warning: No SSL/TLS protocols are available on this system" -ForegroundColor Red throw "No SSL/TLS protocols available" } Write-Host " → Available protocols (from newest to oldest): $($availableProtocols.Name -join ', ')" return $availableProtocols } # Detect available TLS/SSL protocols once at script startup Write-Host "Detecting available TLS/SSL protocols..." $GlobalAvailableTlsProtocols = Get-AvailableTlsProtocols Write-Host "TLS/SSL protocol detection complete. Ready to scan hosts." # Function to enable all locally supported TLS/SSL versions function Set-MaximumTlsSupport { $protocols = [enum]::GetValues([System.Net.SecurityProtocolType]) | Where-Object { $_ -ne 'SystemDefault' } $supportedProtocols = $protocols | ForEach-Object { try { [System.Net.ServicePointManager]::SecurityProtocol = $_ $_ } catch { Write-Host "Protocol $_ not supported" $null } } | Where-Object { $_ -ne $null } $finalProtocol = [System.Net.SecurityProtocolType]($supportedProtocols -join ',') [System.Net.ServicePointManager]::SecurityProtocol = $finalProtocol Write-Host "Enabled protocols: $finalProtocol" } function Get-CertificateDetails { param( [string]$Hostname, [int]$Port, [int]$Timeout = 10, [array]$AvailableProtocols = $GlobalAvailableTlsProtocols # Use the global variable by default ) Write-Host (" → Retrieving SSL certificate from ${Hostname}:${Port}") # Helper function to display inner exceptions function Get-FullExceptionDetails { param([Exception]$Exception) $details = $Exception.Message $currentEx = $Exception # Traverse inner exceptions while ($currentEx.InnerException) { $currentEx = $currentEx.InnerException $details += "`n → Inner Exception: $($currentEx.Message)" # Check for specific exception types if ($currentEx -is [System.Security.Authentication.AuthenticationException]) { $details += " (Authentication error - likely certificate or protocol mismatch)" } elseif ($currentEx -is [System.IO.IOException]) { $details += " (IO error - likely connection reset or timeout)" } } return $details } # Helper function to check if an exception is timeout-related function Test-IsTimeoutException { param([string]$ExceptionMessage) return ($ExceptionMessage -match "timed? out|timeout|aborted|could not establish trust|operation has timed out|connection failure|connection attempt failed") } # Method 1: Try all SSL/TLS protocols from newest to oldest try { Write-Host " → Method 1: Using TLS/SSL protocols from newest to oldest" # Skip protocol detection - use the provided list if ($AvailableProtocols.Count -eq 0) { Write-Host " → Warning: No SSL/TLS protocols are available" -ForegroundColor Red throw "No SSL/TLS protocols available" } # Try each protocol with different SNI options (empty or hostname) $sniOptions = @("", $Hostname) $timeoutEncountered = $false foreach ($protocol in $AvailableProtocols) { # If we already encountered a timeout, skip remaining protocols if ($timeoutEncountered) { Write-Host " → Skipping protocol $($protocol.Name) due to previous timeout" -ForegroundColor Yellow continue } foreach ($sniValue in $sniOptions) { $tcpClient = $null $sslStream = $null try { Write-Host " → Trying handshake with $($protocol.Name), SNI='$sniValue'" # Create a completely new TCP client for each attempt $tcpClient = New-Object System.Net.Sockets.TcpClient $tcpClient.ReceiveTimeout = $Timeout * 1000 $tcpClient.SendTimeout = $Timeout * 1000 # Set a connection timeout $connectResult = $tcpClient.BeginConnect($Hostname, $Port, $null, $null) $connectSuccess = $connectResult.AsyncWaitHandle.WaitOne($Timeout * 1000) if (-not $connectSuccess) { $timeoutEncountered = $true throw "TCP connection timed out after $Timeout seconds" } # Complete the connection $tcpClient.EndConnect($connectResult) # Create SSL stream with callback that ignores ALL certificate errors $sslStream = New-Object System.Net.Security.SslStream( $tcpClient.GetStream(), $false, # don't leave inner stream open { param($sender, $cert, $chain, $errors) # Log the specific errors for troubleshooting if ($errors -ne [System.Net.Security.SslPolicyErrors]::None) { # This is just for debugging, we'll still return true Write-Host " → Certificate errors: $errors" -ForegroundColor Yellow } return $true # Always accept the certificate regardless of errors } ) # Try SSL/TLS handshake try { # Use a reasonable timeout for the handshake (2x the regular timeout) # We'll set read/write timeouts first $sslStream.ReadTimeout = $Timeout * 2000 $sslStream.WriteTimeout = $Timeout * 2000 # Then try the handshake $sslStream.AuthenticateAsClient($sniValue, $null, $protocol.Value, $false) } catch [Exception] { # Get detailed exception info $exDetails = Get-FullExceptionDetails -Exception $_.Exception # Check if this is a timeout-related exception if (Test-IsTimeoutException -ExceptionMessage $exDetails) { $timeoutEncountered = $true Write-Host " → Timeout encountered during handshake - will skip remaining protocol attempts" -ForegroundColor Yellow } throw $exDetails } Write-Host " → SSL/TLS handshake successful with $($protocol.Name)!" -ForegroundColor Green # Get the certificate $remoteCertificate = $sslStream.RemoteCertificate if ($remoteCertificate -eq $null) { Write-Host " → No certificate found despite successful handshake" -ForegroundColor Yellow continue } # Convert to X509Certificate2 for more details $x509cert = New-Object System.Security.Cryptography.X509Certificates.X509Certificate2($remoteCertificate) # Display basic certificate info Write-Host " → Certificate details retrieved:" -ForegroundColor Green Write-Host " Subject: $($x509cert.Subject)" Write-Host " Issuer: $($x509cert.Issuer)" Write-Host " Valid from: $($x509cert.NotBefore) to $($x509cert.NotAfter)" # Extract Subject and Issuer components $subjectParts = $x509cert.Subject -split ', ' $issuerParts = $x509cert.Issuer -split ', ' $subjectCN = ($subjectParts | Where-Object { $_ -match '^CN=' } | ForEach-Object { $_ -replace '^CN=' })[0] $issuerCN = ($issuerParts | Where-Object { $_ -match '^CN=' } | ForEach-Object { $_ -replace '^CN=' })[0] # Check if self-signed $isSelfSigned = ($x509cert.Subject -eq $x509cert.Issuer) if ($isSelfSigned) { Write-Host " Self-signed: Yes" -ForegroundColor Yellow } else { Write-Host " Self-signed: No" } # Calculate days until expiration $daysUntilExpiration = [math]::Round(($x509cert.NotAfter - (Get-Date)).TotalDays, 1) # Return certificate details return @{ Status = "Success" Method = "DirectTLS" Protocol = $protocol.Name SNI = $sniValue Subject = @{ CN = $subjectCN O = ($subjectParts | Where-Object { $_ -match '^O=' } | ForEach-Object { $_ -replace '^O=' }) OU = ($subjectParts | Where-Object { $_ -match '^OU=' } | ForEach-Object { $_ -replace '^OU=' }) C = ($subjectParts | Where-Object { $_ -match '^C=' } | ForEach-Object { $_ -replace '^C=' }) S = ($subjectParts | Where-Object { $_ -match '^S=' -or $_ -match '^ST=' } | ForEach-Object { $_ -replace '^S=|^ST=' }) } Issuer = @{ CN = $issuerCN O = ($issuerParts | Where-Object { $_ -match '^O=' } | ForEach-Object { $_ -replace '^O=' }) OU = ($issuerParts | Where-Object { $_ -match '^OU=' } | ForEach-Object { $_ -replace '^OU=' }) C = ($issuerParts | Where-Object { $_ -match '^C=' } | ForEach-Object { $_ -replace '^C=' }) S = ($issuerParts | Where-Object { $_ -match '^S=' -or $_ -match '^ST=' } | ForEach-Object { $_ -replace '^S=|^ST=' }) } Validity = @{ NotBefore = $x509cert.NotBefore.ToString('yyyy-MM-dd HH:mm:ss') NotAfter = $x509cert.NotAfter.ToString('yyyy-MM-dd HH:mm:ss') DaysUntilExpiration = $daysUntilExpiration } SerialNumber = $x509cert.SerialNumber Thumbprint = @{ SHA256 = $x509cert.GetCertHashString('SHA256') SHA1 = $x509cert.GetCertHashString('SHA1') } SignatureAlgorithm = $x509cert.SignatureAlgorithm.FriendlyName Version = $x509cert.Version SelfSigned = $isSelfSigned TLSSupport = @{ SupportedProtocols = @($protocol.Name) } } } catch { $exMessage = $_ Write-Host " → Attempt failed: $exMessage" -ForegroundColor Yellow # Check if this is a timeout-related exception if (Test-IsTimeoutException -ExceptionMessage $exMessage) { $timeoutEncountered = $true Write-Host " → Timeout detected - will skip remaining protocol attempts" -ForegroundColor Yellow break # Exit this SNI loop } # Clean up resources properly if ($sslStream) { try { $sslStream.Dispose() } catch {} } if ($tcpClient) { try { $tcpClient.Dispose() } catch {} } } } # If we encountered a timeout in the inner loop, break out of the outer loop too if ($timeoutEncountered) { break } } if ($timeoutEncountered) { Write-Host " → Skipping remaining protocol attempts due to timeout" -ForegroundColor Yellow } else { Write-Host " → All SSL/TLS protocol and SNI combinations failed" -ForegroundColor Yellow } } catch { $exDetails = Get-FullExceptionDetails -Exception $_.Exception Write-Host " → Method 1 failed: $exDetails" -ForegroundColor Yellow } # Method 2: WebRequest with custom TLS setting # Only try Method 2 if we didn't encounter a timeout in Method 1 $skipMethod2 = $false if (Test-Path variable:timeoutEncountered) { $skipMethod2 = $timeoutEncountered } if ($skipMethod2) { Write-Host " → Skipping Method 2 due to timeout in Method 1" -ForegroundColor Yellow } else { try { Write-Host " → Method 2: Using WebRequest with custom TLS settings" # Save original settings $originalProtocol = [System.Net.ServicePointManager]::SecurityProtocol $originalCallback = [System.Net.ServicePointManager]::ServerCertificateValidationCallback try { # Try to use all possible TLS/SSL protocols # We'll use reflection to get all possible values for SecurityProtocolType $allValues = 0 # Get all protocols via reflection from SecurityProtocolType enum $protocolType = [System.Net.SecurityProtocolType] $protocolFields = $protocolType.GetFields() | Where-Object { $_.IsStatic -and $_.IsLiteral } foreach ($field in $protocolFields) { try { $value = $field.GetValue($null) Write-Host " → Adding protocol $($field.Name) ($value)" $allValues = $allValues -bor $value } catch { Write-Host " → Couldn't add protocol $($field.Name): $($_.Exception.Message)" -ForegroundColor Yellow } } # If we couldn't get any protocols, fall back to hardcoded combination if ($allValues -eq 0) { Write-Host " → Using fallback protocol combination" -ForegroundColor Yellow try { $allValues = [System.Net.SecurityProtocolType]::Tls12 -bor [System.Net.SecurityProtocolType]::Tls11 -bor [System.Net.SecurityProtocolType]::Tls -bor [System.Net.SecurityProtocolType]::Ssl3 } catch { # If even that fails, try just TLS 1.2 $allValues = [System.Net.SecurityProtocolType]::Tls12 } } # Apply our protocol selection [System.Net.ServicePointManager]::SecurityProtocol = $allValues # Accept all certificates regardless of errors [System.Net.ServicePointManager]::ServerCertificateValidationCallback = { param($sender, $cert, $chain, $errors) # Log the specific certificate errors for diagnostics if ($errors -ne [System.Net.Security.SslPolicyErrors]::None) { Write-Host " → Certificate validation errors: $errors" -ForegroundColor Yellow } return $true } # Create HTTP request $url = "https://${Hostname}:${Port}" $webRequest = [System.Net.WebRequest]::Create($url) $webRequest.Method = "HEAD" $webRequest.Timeout = $Timeout * 1000 $webRequest.AllowAutoRedirect = $false # Attempt to get response (may fail but we might still get the certificate) try { $response = $webRequest.GetResponse() $response.Close() } catch [System.Net.WebException] { # Extract detailed information from the WebException $ex = $_.Exception $statusCode = -1 $statusDesc = "Unknown" if ($ex.Response -ne $null) { $statusCode = [int]$ex.Response.StatusCode $statusDesc = $ex.Response.StatusDescription } Write-Host " → WebException: Status Code=$statusCode, Description=$statusDesc" -ForegroundColor Yellow # Check if we have inner exception details if ($ex.InnerException) { $innerExMsg = Get-FullExceptionDetails -Exception $ex.InnerException Write-Host " → Inner exception details: $innerExMsg" -ForegroundColor Yellow # Check if this is a timeout related exception if (Test-IsTimeoutException -ExceptionMessage $innerExMsg) { throw "Timeout encountered in WebRequest method" } } # Check if this is a timeout-related exception directly if (Test-IsTimeoutException -ExceptionMessage $ex.Message) { throw "Timeout encountered in WebRequest method" } Write-Host " → Still checking for certificate..." -ForegroundColor Yellow } # Try to get certificate from the ServicePoint $cert = $webRequest.ServicePoint.Certificate if ($cert -eq $null) { throw "No certificate found in ServicePoint" } # Convert to X509Certificate2 $x509cert = New-Object System.Security.Cryptography.X509Certificates.X509Certificate2($cert) # Display basic certificate info Write-Host " → Certificate details retrieved via WebRequest:" -ForegroundColor Green Write-Host " Subject: $($x509cert.Subject)" Write-Host " Issuer: $($x509cert.Issuer)" Write-Host " Valid from: $($x509cert.NotBefore) to $($x509cert.NotAfter)" # Extract Subject and Issuer components $subjectParts = $x509cert.Subject -split ', ' $issuerParts = $x509cert.Issuer -split ', ' $subjectCN = ($subjectParts | Where-Object { $_ -match '^CN=' } | ForEach-Object { $_ -replace '^CN=' })[0] $issuerCN = ($issuerParts | Where-Object { $_ -match '^CN=' } | ForEach-Object { $_ -replace '^CN=' })[0] # Calculate days until expiration $daysUntilExpiration = [math]::Round(($x509cert.NotAfter - (Get-Date)).TotalDays, 1) # Return certificate details return @{ Status = "Success" Method = "WebRequest" Protocol = "Unknown (WebRequest)" Subject = @{ CN = $subjectCN O = ($subjectParts | Where-Object { $_ -match '^O=' } | ForEach-Object { $_ -replace '^O=' }) OU = ($subjectParts | Where-Object { $_ -match '^OU=' } | ForEach-Object { $_ -replace '^OU=' }) C = ($subjectParts | Where-Object { $_ -match '^C=' } | ForEach-Object { $_ -replace '^C=' }) S = ($subjectParts | Where-Object { $_ -match '^S=' -or $_ -match '^ST=' } | ForEach-Object { $_ -replace '^S=|^ST=' }) } Issuer = @{ CN = $issuerCN O = ($issuerParts | Where-Object { $_ -match '^O=' } | ForEach-Object { $_ -replace '^O=' }) OU = ($issuerParts | Where-Object { $_ -match '^OU=' } | ForEach-Object { $_ -replace '^OU=' }) C = ($issuerParts | Where-Object { $_ -match '^C=' } | ForEach-Object { $_ -replace '^C=' }) S = ($issuerParts | Where-Object { $_ -match '^S=' -or $_ -match '^ST=' } | ForEach-Object { $_ -replace '^S=|^ST=' }) } Validity = @{ NotBefore = $x509cert.NotBefore.ToString('yyyy-MM-dd HH:mm:ss') NotAfter = $x509cert.NotAfter.ToString('yyyy-MM-dd HH:mm:ss') DaysUntilExpiration = $daysUntilExpiration } SerialNumber = $x509cert.SerialNumber Thumbprint = @{ SHA256 = $x509cert.GetCertHashString('SHA256') SHA1 = $x509cert.GetCertHashString('SHA1') } SignatureAlgorithm = $x509cert.SignatureAlgorithm.FriendlyName Version = $x509cert.Version SelfSigned = ($x509cert.Subject -eq $x509cert.Issuer) TLSSupport = @{ SupportedProtocols = @("Unknown - WebRequest method") } } } finally { # Restore original settings [System.Net.ServicePointManager]::SecurityProtocol = $originalProtocol [System.Net.ServicePointManager]::ServerCertificateValidationCallback = $originalCallback } } catch { $exDetails = Get-FullExceptionDetails -Exception $_.Exception # Check if this was a timeout if (Test-IsTimeoutException -ExceptionMessage $exDetails) { Write-Host " → Method 2 timed out: $exDetails" -ForegroundColor Yellow } else { Write-Host " → Method 2 failed: $exDetails" -ForegroundColor Yellow } } } # If all methods fail, return a consistent failure object Write-Host " → Certificate retrieval failed - all methods exhausted" -ForegroundColor Yellow # Return a failure object return @{ Status = "Failed" Method = "AllMethodsFailed" Error = "Could not retrieve certificate from ${Hostname}:${Port} after trying all methods" Subject = @{ CN = "Unknown"; O = @(); OU = @() } Issuer = @{ CN = "Unknown"; O = @(); OU = @() } Validity = @{ NotBefore = "Unknown" NotAfter = "Unknown" DaysUntilExpiration = $null } SerialNumber = "Unknown" SelfSigned = $null TLSSupport = @{ SupportedProtocols = @() } } } function Test-DNSResolution { param( [string]$Hostname, [string]$IP ) try { $dnsResults = [System.Net.Dns]::GetHostEntry($Hostname) $ipAddresses = $dnsResults.AddressList | ForEach-Object { $_.IPAddressToString } # Check if the IP we have matches any of the DNS results $ipMatch = $ipAddresses -contains $IP return @{ Resolves = $true IPAddresses = $ipAddresses IPMatch = $ipMatch Error = $null } } catch { return @{ Resolves = $false IPAddresses = @() IPMatch = $false Error = $_.Exception.Message } } } # Enhanced URL sanitization function function Sanitize-Url { param( [string]$Url ) if ([string]::IsNullOrWhiteSpace($Url)) { return $Url } # Remove any backslashes from the URL, but preserve double forward slashes $Url = $Url.Replace('\', '/') # Fix any double forward slashes that aren't part of the protocol if ($Url -match '^(?:https?:\/\/)') { $protocol = $matches[0] $rest = $Url.Substring($protocol.Length) $rest = $rest -replace '//+', '/' $Url = $protocol + $rest } # Ensure no trailing slashes in hostname portion if ($Url -match '^(https?:\/\/[^\/]+)(.*)$') { $hostname = $matches[1].TrimEnd('/') $path = $matches[2] $Url = $hostname + $path } return $Url.Trim() } function Get-TargetHostname { param( [string]$Hostname, [string]$IP ) if (-not $Hostname) { Write-Host " No hostname provided, using IP: $IP" return $IP } $dnsCheck = Test-DNSResolution -Hostname $Hostname -IP $IP if (-not $dnsCheck.Resolves) { Write-Host " Hostname '$Hostname' does not resolve in DNS, using IP: $IP" return $IP } if (-not $dnsCheck.IPMatch) { Write-Host " Warning: Hostname '$Hostname' resolves to different IPs: $($dnsCheck.IPAddresses -join ', ')" Write-Host " Target IP '$IP' not in DNS results, using IP instead of hostname" return $IP } Write-Host " Hostname '$Hostname' resolves correctly to IP: $IP" return $Hostname } function Get-Favicon { param( [string]$BaseUrl, [bool]$IsSSL, [int]$Timeout = 10 ) try { # First try standard /favicon.ico $faviconUrl = "{0}/favicon.ico" -f $BaseUrl Write-Host " → Attempting: $faviconUrl" if ($IsSSL) { $response = Invoke-WebRequest -Uri $faviconUrl -MaximumRedirection 0 -SkipCertificateCheck -TimeoutSec $Timeout -ErrorAction SilentlyContinue } else { $response = Invoke-WebRequest -Uri $faviconUrl -MaximumRedirection 0 -TimeoutSec $Timeout -ErrorAction SilentlyContinue } if ($response.StatusCode -eq 200 -and $response.RawContentLength -gt 0) { return [Convert]::ToBase64String($response.Content) } } catch { # Don't output anything - the error is expected for sites without favicon.ico } return $null } # Enable all supported TLS/SSL versions Set-MaximumTlsSupport function Sanitize-Hostname { param( [string]$Hostname ) return $Hostname.Replace('\', '').Trim() } function Fetch-Url { param( [string]$TargetHost, [int]$Port, [bool]$IsSSL, [int]$Timeout = 10 ) $MaxRedirects = 10 $RedirectCount = 0 $results = @() $protocol = if ($IsSSL) { "https" } else { "http" } $currentUrl = "${protocol}://${TargetHost}:${Port}" $originalHost = $TargetHost $originalPort = $Port # Helper function to check if an exception is timeout-related function Test-IsTimeoutException { param([string]$ExceptionMessage) return ($ExceptionMessage -match "timed? out|timeout|aborted|could not establish trust|operation has timed out|connection failure|connection attempt failed") } # Get certificate details early for SSL connections $certDetails = $null if ($IsSSL) { Write-Host " → Getting certificate details for ${TargetHost}:${Port}" $certDetails = Get-CertificateDetails -Hostname $TargetHost -Port $Port -Timeout $Timeout -AvailableProtocols $GlobalAvailableTlsProtocols } # Create the initial result object with certificate info $requestResult = [ordered]@{ Url = $currentUrl RedirectDepth = $RedirectCount StatusCode = $null Message = $null # Changed from "Error" to "Message" Headers = @{} Content = $null Favicon = $null Title = $null RedirectedRequest = $null Certificate = $certDetails } # Check if we should skip HTTPS connection due to certificate failure or timeout $skipHttpsRequest = $false if ($IsSSL) { # Check if certificate retrieval failed or timed out if (-not $certDetails -or $certDetails.Status -eq "Failed") { # Check if the error indicates a timeout $timeoutError = $false if ($certDetails -and $certDetails.Error -and (Test-IsTimeoutException -ExceptionMessage $certDetails.Error)) { $timeoutError = $true Write-Host " → Certificate retrieval timed out - skipping HTTPS connection attempt" -ForegroundColor Yellow $requestResult.Message = "HTTPS connection skipped due to certificate retrieval timeout" } else { Write-Host " → Certificate retrieval failed - skipping HTTPS connection attempt" -ForegroundColor Yellow $requestResult.Message = "HTTPS connection skipped due to certificate retrieval failure" } $results += $requestResult $skipHttpsRequest = $true } } # Only attempt HTTP/HTTPS connections if not skipped if (-not $skipHttpsRequest) { while ($true) { try { # Sanitize the current URL before making the request $currentUrl = Sanitize-Url -Url $currentUrl # Update URL in the result object $requestResult.Url = $currentUrl Write-Host " → Attempting: $currentUrl" # Create WebRequest to check content length first $webRequest = [System.Net.WebRequest]::Create($currentUrl) $webRequest.Method = "HEAD" $webRequest.Timeout = $Timeout * 1000 # Only set certificate validation callback for HTTPS requests if ($IsSSL) { [System.Net.ServicePointManager]::ServerCertificateValidationCallback = { $true } } try { $headResponse = $webRequest.GetResponse() $contentLength = $headResponse.ContentLength if ($contentLength -gt $MaxResponseSize) { Write-Host " → Warning: Response too large ($([math]::Round($contentLength/1MB, 2)) MB). Skipping." -ForegroundColor Yellow $requestResult.Message = "Response size ($([math]::Round($contentLength/1MB, 2)) MB) exceeds maximum allowed size ($([math]::Round($MaxResponseSize/1MB)) MB)" $results += $requestResult break } } catch { # Check if this is a timeout exception if (Test-IsTimeoutException -ExceptionMessage $_.Exception.Message) { Write-Host " → HEAD request timed out" -ForegroundColor Yellow $requestResult.Message = "HEAD request timed out: $($_.Exception.Message)" $results += $requestResult break } # If HEAD request fails with non-timeout error, proceed with normal GET but limit the read size Write-Host " → HEAD request failed: $($_.Exception.Message). Trying GET instead." -ForegroundColor Yellow } # Use different Invoke-WebRequest parameters based on PowerShell version $psVersion = $PSVersionTable.PSVersion.Major $webRequestParams = @{ Uri = $currentUrl MaximumRedirection = 0 TimeoutSec = $Timeout MaximumRetryCount = 0 ErrorAction = 'SilentlyContinue' } # Different handling for HTTP and HTTPS if ($IsSSL) { # HTTPS handling if ($psVersion -ge 6) { # For PowerShell 6+ we can use SkipCertificateCheck $webRequestParams.Add('SkipCertificateCheck', $true) $response = Invoke-WebRequest @webRequestParams } else { # For older PowerShell versions try { # Temporarily set certificate callback $originalCallback = [System.Net.ServicePointManager]::ServerCertificateValidationCallback [System.Net.ServicePointManager]::ServerCertificateValidationCallback = { $true } $response = Invoke-WebRequest @webRequestParams } finally { # Restore original callback [System.Net.ServicePointManager]::ServerCertificateValidationCallback = $originalCallback } } } else { # HTTP handling - no SSL/TLS specific code $response = Invoke-WebRequest @webRequestParams } # Check actual content length after download $actualLength = if ($response.RawContentLength -gt 0) { $response.RawContentLength } else { [System.Text.Encoding]::UTF8.GetByteCount($response.Content) } if ($actualLength -gt $MaxResponseSize) { Write-Host " → Warning: Response too large ($([math]::Round($actualLength/1MB, 2)) MB). Truncating." -ForegroundColor Yellow $requestResult.Content = $response.Content.Substring(0, $MaxResponseSize) $requestResult.Message = "Response truncated: exceeded maximum size of $([math]::Round($MaxResponseSize/1MB)) MB" } else { $requestResult.Content = $response.Content Write-Host " → Received $([math]::Round($actualLength/1KB, 2)) KB" } # Extract title from HTML content if available if ($requestResult.Content) { if ($requestResult.Content -match '