NestedModules/HttpConnectivityTester/HttpConnectivityTester.psm1

Set-StrictMode -Version 4

$rateLimitCount = 0
$sleepSeconds = 5 * 60

Function Get-ErrorMessage() {
<#
    .SYNOPSIS
    Gets a formatted error message from an error record.
 
    .DESCRIPTION
    Gets a formatted error message from an error record.
 
    .EXAMPLE
    Get-ErrorMessage -ErrorRecords $_
    #>

    [CmdletBinding()]
    [OutputType([string])]
    Param(
        [Parameter(Mandatory=$true, HelpMessage='The PowerShell error record object to get information from')]
        [ValidateNotNullOrEmpty()]
        [System.Management.Automation.ErrorRecord]$ErrorRecord
    )
    Process {
        $msg = [System.Environment]::NewLine,'Exception Message: ',$ErrorRecord.Exception.Message -join ''

        if($null -ne $ErrorRecord.Exception.HResult) {
            $msg = $msg,[System.Environment]::NewLine,'Exception HRESULT: ',('{0:X}' -f $ErrorRecord.Exception.HResult),$ErrorRecord.Exception.HResult -join ''
        }

        if($null -ne $ErrorRecord.Exception.StackTrace) {
            $msg = $msg,[System.Environment]::NewLine,'Exception Stacktrace: ',$ErrorRecord.Exception.StackTrace -join ''
        }

        if ($null -ne ($ErrorRecord.Exception | Get-Member | Where-Object { $_.Name -eq 'WasThrownFromThrowStatement'})) {
            $msg = $msg,[System.Environment]::NewLine,'Explicitly Thrown: ',$ErrorRecord.Exception.WasThrownFromThrowStatement -join ''
        }

        if ($null -ne $ErrorRecord.Exception.InnerException) {
            if ($ErrorRecord.Exception.InnerException.Message -ne $ErrorRecord.Exception.Message) {
                $msg = $msg,[System.Environment]::NewLine,'Inner Exception: ',$ErrorRecord.Exception.InnerException.Message -join ''
            }

            if($null -ne $ErrorRecord.Exception.InnerException.HResult) {
                $msg = $msg,[System.Environment]::NewLine,'Inner Exception HRESULT: ',('{0:X}' -f $ErrorRecord.Exception.InnerException.HResult),$ErrorRecord.Exception.InnerException.HResult -join ''
            }
        }

        $msg = $msg,[System.Environment]::NewLine,'Call Site: ',$ErrorRecord.InvocationInfo.PositionMessage -join ''

        if ($null -ne ($ErrorRecord | Get-Member | Where-Object { $_.Name -eq 'ScriptStackTrace'})) {
            $msg = $msg,[System.Environment]::NewLine,"Script Stacktrace: ",$ErrorRecord.ScriptStackTrace -join ''
        }

        return $msg
    }
}

Function Get-BlueCoatSiteReview() {
    [CmdletBinding()]
    [OutputType([psobject])]
    Param (
        [Parameter(Mandatory=$true, HelpMessage='The URL to get BlueCoat Site Review information for.')]
        [ValidateNotNullOrEmpty()]
        [Uri]$Url,

        [Parameter(Mandatory=$false, HelpMessage='The user agent.')]
        [ValidateNotNullOrEmpty()]
        [string]$UserAgent = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/67.0.3396.62 Safari/537.36',

        [Parameter(Mandatory=$false, HelpMessage='Disable throttling.')]
        [switch]$NoThrottle
    )

    if ($Url.OriginalString.ToLower().StartsWith('http://') -or $Url.OriginalString.ToLower().StartsWith('https://')) {
        $testUri = $Url
    } else {
        $testUri = [Uri]('http://{0}' -f $Url.OriginalString)
    }

    $newLine = [System.Environment]::NewLine

    $throttle = !$NoThrottle

    if ($throttle) {
        $rateLimitCount++

        if($rateLimitCount -gt 10) {
            $nowTime = [DateTime]::Now
            $resumeTime = $nowTime.AddSeconds($sleepSeconds)

            Write-Verbose -Message ('Paused for {0} seconds. Current time: {1} Resume time: {2}' -f $sleepSeconds,$nowTime,$resumeTime)

            Start-Sleep -Seconds $sleepSeconds

            $nowTime = [DateTime]::Now

            Write-Verbose -Message ('Resumed at {0}' -f $nowTime)

            $rateLimitCount = 1 # needs to be 1 since BlueCoat Site Review API is called when exiting this if statement. If left at 0, then will hit the rate limit on successive calls to this cmdlet
        }
    }


    $uri = $testUri

    $proxyUri = [System.Net.WebRequest]::GetSystemWebProxy().GetProxy($uri)

    $params = @{
        Uri = 'https://sitereview.bluecoat.com/resource/lookup';
        Method = 'POST';
        ProxyUseDefaultCredentials = (([string]$proxyUri) -ne $uri);
        UseBasicParsing = $true;
        UserAgent = $UserAgent
        ContentType = 'application/json';
        Body = (@{url = $uri; captcha = ''} | ConvertTo-Json);
        Headers = @{Referer = 'https://sitereview.bluecoat.com'} ;
        Verbose = $false
    }

    if (([string]$proxyUri) -ne $uri) {
       $params.Add('Proxy',$proxyUri)
    }

    $ProgressPreference = [System.Management.Automation.ActionPreference]::SilentlyContinue

    $statusCode = 0
    $statusDescription = ''

    try {
        $response = Invoke-WebRequest @params

        $statusCode = $response.StatusCode
    } catch [System.Net.WebException] {
        $statusCode = [int]$_.Exception.Response.StatusCode
        $statusDescription = $_.Exception.Response.StatusDescription
    }

    if ($statusCode -ne 200) {
        throw "BlueCoat Site Review REST API request failed. Status code: $statusCode Status description: $statusDescription"
    }

    $returnedJson = $response.Content

    #Write-Verbose -Message ('JSON: {0}' -f $returnedJson)

    $siteReview = $returnedJson | ConvertFrom-Json

    if ($siteReview.PSObject.Properties.Name -contains 'errorType') {
        throw ('Error retrieving Blue Coat data. Error Type: {0} Error Message: {1}' -f $siteReview.errorType, $siteReview.error)
    }

    $cats = @{}

    $siteReview.categorization | ForEach-Object {
        $link = ('https://sitereview.bluecoat.com/catdesc.jsp?catnum={0}' -f $_.num)
        $cats.Add($_.name,$link)
    }

    $dateMatched = $siteReview.rateDate -match 'Last Time Rated/Reviewed:\s*(.+)\s*{{.*'

    $lastRated = ''

    if($dateMatched -and $matches.Count -ge 2) {
        $lastRated = $matches[1].Trim()
    }

    $siteReviewObject = [pscustomobject]@{
        SubmittedUri = $Uri;
        ReturnedUri = [System.Uri]$siteReview.url;
        Rated = $siteReview.unrated -eq 'false'
        LastedRated = $lastRated;
        Locked = $siteReview.locked -eq 'true';
        LockMessage = if ($siteReview.locked -eq 'true') {[string]$siteReview.lockedMessage} else {''};
        Pending = $siteReview.multiple -eq 'true';
        PendingMessage = if ($siteReview.multiple -eq 'true') {[string]$siteReview.multipleMessage} else {''};
        Categories = $cats;
    }

    Write-Verbose -Message ('{0}Rated: {1}{2}Last Rated: {3}{4}Locked: {5}{6}Lock Message: {7}{8}Pending: {9}{10}Pending Message: {11}{12}Categories: {13}{14}{15}' -f $newLine,$siteReviewObject.Rated,$newLine,$siteReviewObject.LastedRated,$newLine,$siteReviewObject.Locked,$newLine,$siteReviewObject.LockMessage,$newLine,$siteReviewObject.Pending,$newLine,$siteReviewObject.PendingMessage,$newLine,($siteReviewObject.Categories.Keys -join ','),$newLine,$newLine)

    return $siteReviewObject
}

Function Get-IPAddress() {
    <#
    .SYNOPSIS
    Gets the IP address(es) for a URL.
 
    .DESCRIPTION
    Gets the IP address(es) for a URL.
 
    .EXAMPLE
    Get-IPAddress -Url http://www.site.com
    #>

    [CmdletBinding()]
    [OutputType([string[]])]
    Param (
        [Parameter(Mandatory=$true, HelpMessage='The URL to get the IP address for.')]
        [ValidateNotNullOrEmpty()]
        [System.Uri]$Url
    )

    $addresses = [string[]]@()

    $dnsResults = $null

    $dnsResults = @(Resolve-DnsName -Name $Url.Host -NoHostsFile -Type A_AAAA -QuickTimeout -ErrorAction SilentlyContinue | Where-Object {$_.Type -eq 'A'})

    $addresses = [string[]]@($dnsResults | ForEach-Object { try { $_.IpAddress } catch [System.Management.Automation.PropertyNotFoundException] {Write-Verbose "No IP in Object."} }) # IpAddress results in a PropertyNotFoundException when a URL is blocked upstream

    return [string[]](,$addresses)
}

Function Get-IPAlias() {
    <#
    .SYNOPSIS
    Gets DNS alias for a URL.
 
    .DESCRIPTION
    Gets DNS alias for a URL.
 
    .EXAMPLE
    Get-IPAlias -Url http://www.site.com
    #>

    [CmdletBinding()]
    [OutputType([string[]])]
    Param (
        [Parameter(Mandatory=$true, HelpMessage='The URL to get the alias address for.')]
        [ValidateNotNullOrEmpty()]
        [System.Uri]$Url
    )

    $aliases = [string[]]@()

    $dnsResults = $null

    $dnsResults = @(Resolve-DnsName -Name $Url.Host -NoHostsFile -QuickTimeout -ErrorAction SilentlyContinue | Where-Object { $_.Type -eq 'CNAME' })

    #$aliases = [string[]]@($dnsResults | ForEach-Object { try { $_.NameHost } catch [System.Management.Automation.PropertyNotFoundException] {} }) # NameHost results in a PropertyNotFoundException when a URL is blocked upstream
    $aliases = [string[]]@($dnsResults | ForEach-Object { $_.NameHost })

    return [string[]](,$aliases)
}

Function Get-CertificateErrorMessage() {
    <#
    .SYNOPSIS
    Gets certificate error messages for an HTTPS URL.
 
    .DESCRIPTION
    Gets certificate error messages for an HTTPS URL.
 
    .EXAMPLE
    Get-CertificateErrorMessage -Url http://www.site.com -Certificate $certificate -Chain $chain -PolicyError $policyError
    #>

    [CmdletBinding()]
    [OutputType([string])]
    Param(
        [Parameter(Mandatory=$true, HelpMessage='The URL to test')]
        [ValidateNotNullOrEmpty()]
        [Uri]$Url,

        [Parameter(Mandatory=$true, HelpMessage='The certificate')]
        [ValidateNotNull()]
        [Security.Cryptography.X509Certificates.X509Certificate]$Certificate,

        [Parameter(Mandatory=$true, HelpMessage='The certificate chain')]
        [ValidateNotNull()]
        $Chain, # had to drop [Security.Cryptography.X509Certificates.X509Chain] otherwise call to Get-CertificateErrorMessage fails with "Cannot process argument transformation on parameter 'Chain'. Cannot create object of type "System.Security.Cryptography.X509Certificates.X509Chain". "ChainContext" is a ReadOnly property."

        [Parameter(Mandatory=$true, HelpMessage='The SSL error')]
        [ValidateNotNull()]
        [Net.Security.SslPolicyErrors]$PolicyError
    )

    $details = ''

    if($PolicyError -ne [Net.Security.SslPolicyErrors]::None) {
        switch ($PolicyError) {
            'RemoteCertificateChainErrors' {

                if ($Chain.ChainElements.Count -gt 0 -and $Chain.ChainStatus.Count -gt 0) {
                    if ($Chain.ChainElements.Count -gt 0 -or $Chain.ChainStatus.Count -gt 0) {
                        Write-Verbose -Message ('Multiple remote certificate chain elements exist. ChainElement Count: {0} ChainStatus Count: {1}' -f $Chain.ChainElements.Count,$Chain.ChainStatus.Count)
                    }

                    #todo support more than one chain
                    $element = $Chain.ChainElements[0]
                    $status = $Chain.ChainStatus[0]
                    $details = ('Certificate chain error. Error: {0} Reason: {1} Certificate: {2}' -f $status.Status, $status.StatusInformation,$element.Certificate.ToString($false))
                } else {
                    $details = ('Certificate chain error. Certificate: {0}' -f $Certificate.ToString($false))
                }
                break
            }
            'RemoteCertificateNameMismatch' {
                $cert = New-Object Security.Cryptography.X509Certificates.X509Certificate2 -ArgumentList $Certificate

                $sanExtension = $cert.Extensions | Where-Object { $_.Oid.FriendlyName -eq 'Subject Alternative Name' }

                if ($null -eq $sanExtension) {
                    $subject = $cert.Subject.Split(',')[0].Replace('CN=', '')
                    $details = ('Remote certificate name mismatch. Host: {0} Subject: {1}' -f $Url.Host,$subject)
                } else {
                    $subject = $certificate.Subject.Split(',')[0].Replace('CN=', '')
                    $asnData = New-Object Security.Cryptography.AsnEncodedData -ArgumentList $sanExtension.Oid,$sanExtension.RawData
                    $sans = $asnData.Format($false).Replace('DNS Name=', '').Replace(',', '').Split(@(' '), [StringSplitOptions]::RemoveEmptyEntries)
                    $details = ('Remote certificate name mismatch. Host: {0} Subject: {1} SANs: {2}' -f $Url.Host,$subject,($sans -join ', '))
                }
                break
            }
            'RemoteCertificateNotAvailable' {
                $details = 'Remote certificate not available.'
            }
            'None' {
                break
            }
            default {
                $details = ('Unrecognized remote certificate error. {0}' -f $PolicyError)
                break
            }
        }
    }

    return $details
}

Function Get-HttpConnectivity() {
    <#
    .SYNOPSIS
    Gets HTTP connectivity information for a URL.
 
    .DESCRIPTION
    Gets HTTP connectivity information for a URL.
 
    .EXAMPLE
    Get-HttpConnectivity -TestUrl http://www.site.com
 
    .EXAMPLE
    Get-HttpConnectivity -TestUrl http://www.site.com -UrlPattern http://*.site.com
 
    .EXAMPLE
    Get-HttpConnectivity -TestUrl http://www.site.com -Method POST
 
    .EXAMPLE
    Get-HttpConnectivity -TestUrl http://www.site.com -ExpectedStatusCode 400
 
    .EXAMPLE
    Get-HttpConnectivity -TestUrl http://www.site.com -Description 'A site that does something'
 
    .EXAMPLE
    Get-HttpConnectivity -TestUrl http://www.site.com -UserAgent 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/68.0.3440.84 Safari/537.36''
 
    .EXAMPLE
    Get-HttpConnectivity -TestUrl http://www.site.com -IgnoreCertificateValidationErrors
 
    .EXAMPLE
    Get-HttpConnectivity -TestUrl http://www.site.com -PerformBluecoatLookup
    #>

    [CmdletBinding()]
    [OutputType([void])]
    Param(
        [Parameter(Mandatory=$true, HelpMessage='The URL to test.')]
        [ValidateNotNullOrEmpty()]
        [Uri]$TestUrl,

        [Parameter(Mandatory=$false, HelpMessage='The URL pattern to unblock when the URL to unblock is not a literal URL.')]
        [ValidateNotNullOrEmpty()]
        [string]$UrlPattern,

        [Parameter(Mandatory=$false, HelpMessage="The HTTP method used to test the URL. Defaults to 'GET'.")]
        [ValidateNotNullOrEmpty()]
        [ValidateSet('HEAD','GET', 'POST', IgnoreCase=$true)]
        [string]$Method = 'GET',

        [Parameter(Mandatory=$false, HelpMessage='The HTTP status code expected to be returned. Defaults to 200.')]
        [ValidateNotNullOrEmpty()]
        [Int32]$ExpectedStatusCode = 200,

        [Parameter(Mandatory=$false, HelpMessage='A description of the connectivity test or purpose of the URL.')]
        [ValidateNotNullOrEmpty()]
        [string]$Description,

        [Parameter(Mandatory=$false, HelpMessage='The HTTP user agent. Defaults to the Chrome browser user agent.')]
        [ValidateNotNullOrEmpty()]
        [string]$UserAgent = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/67.0.3396.87 Safari/537.36',

        [Parameter(Mandatory=$false, HelpMessage="Whether to ignore certificate validation errors so they don't affect the connectivity test. Some HTTPS endpoints are not meant to be accessed by a browser so the endpoint will not validate against browser security requirements.")]
        [switch]$IgnoreCertificateValidationErrors,
        [Parameter(Mandatory=$false, HelpMessage='Whether to perform a Symantec BlueCoat Site Review lookup on the URL. Warning: The BlueCoat Site Review REST API is rate limited. Automatic throttling is performed when this parameter is used.')]
        [switch]$PerformBluecoatLookup
    )

    $parameters = $PSBoundParameters

    $isVerbose = $verbosePreference -eq 'Continue'

    if ($TestUrl.OriginalString.ToLower().StartsWith('http://') -or $TestUrl.OriginalString.ToLower().StartsWith('https://')) {
        $testUri = $TestUrl
    } else {
        $testUri = [Uri]('http://{0}' -f $testUri.OriginalString)
    }

    if($parameters.ContainsKey('UrlPattern')) {
        $UnblockUrl = $UrlPattern
    } else {
        $UnblockUrl = $testUri.OriginalString # ('{0}//{1}' -f $testUri.Scheme,$testUri.Host)
    }

    $newLine = [System.Environment]::NewLine

    Write-Verbose -Message ('{0}*************************************************{1}Testing {2}{3}*************************************************{4}' -f $newLine,$newLine,$testUri,$newLine,$newLine)

    $script:ServerCertificate = $null
    $script:ServerCertificateChain = $null
    $script:ServerCertificateError = $null

    # can't use Invoke-WebRequest and override the callback due to PowerShell Runspace errors described in this post: http://huddledmasses.org/blog/validating-self-signed-certificates-properly-from-powershell/

    if($IgnoreCertificateValidationErrors) {
        $RemoteCertificateValidationCallback = {
            param([object]$sender, [Security.Cryptography.X509Certificates.X509Certificate]$certificate, [Security.Cryptography.X509Certificates.X509Chain]$chain, [Net.Security.SslPolicyErrors]$sslPolicyErrors)

            $script:ServerCertificate = New-Object System.Security.Cryptography.X509Certificates.X509Certificate2 $certificate
            $script:ServerCertificateChain = $chain | Select-Object * # clone chain object otherwise we lose ChainElements and ChainStatus property contents on variable assignment... weird
            $script:ServerCertificateError = $sslPolicyErrors
            return $true
        }
    } else {
        $RemoteCertificateValidationCallback = {
            param([object]$sender, [Security.Cryptography.X509Certificates.X509Certificate]$certificate, [Security.Cryptography.X509Certificates.X509Chain]$chain, [Net.Security.SslPolicyErrors]$sslPolicyErrors)

            $script:ServerCertificate = New-Object System.Security.Cryptography.X509Certificates.X509Certificate2 $certificate
            $script:ServerCertificateChain = $chain | Select-Object * # clone chain object otherwise we lose ChainElements and ChainStatus property contents on variable assignment... weird
            $script:ServerCertificateError = $sslPolicyErrors

            return [Net.Security.SslPolicyErrors]::None -eq $sslPolicyErrors
        }
    }

    [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 -bor [Net.SecurityProtocolType]::Tls11

    $proxyUri = [Net.WebRequest]::GetSystemWebProxy().GetProxy($testUri)

    $request = [Net.WebRequest]::CreateHttp($testUri)
    $request.Proxy = if ($testUri -ne $proxyUri) { [Net.WebRequest]::DefaultWebProxy } else { $null }
    $request.UseDefaultCredentials = ($testUri -ne $proxyUri)
    $request.UserAgent = $UserAgent;
    $request.Method = $Method
    $request.ServerCertificateValidationCallback = $RemoteCertificateValidationCallback

    $statusCode = 0
    $statusMessage = ''
    $response = $null

    try {
        $response = $request.GetResponse()
        $httpResponse = $response -as [Net.HttpWebResponse]

        $statusCode = $httpResponse.StatusCode
        $statusMessage = $httpResponse.StatusDescription
    } catch [System.Net.WebException] {
        # useful WINHTTP error message code values and descriptions. will be in the exception
        # https://msdn.microsoft.com/en-us/library/windows/desktop/aa383770(v=vs.85).aspx
        # https://msdn.microsoft.com/en-us/library/windows/desktop/aa384110(v=vs.85).aspx

        $statusMessage = Get-ErrorMessage -ErrorRecord $_

        try {
            $statusCode = [int]$_.Exception.Response.StatusCode # StatusCode property results in a PropertyNotFoundException exception when the URL is blocked upstream
        } catch [System.Management.Automation.PropertyNotFoundException] {
            Write-Verbose -Message ('Unable to access {0} due to {1}' -f $testUri,$statusMessage)
        }
    } finally {
        if ($null -ne $response) {
            $response.Close()
        }
    }

    $hasServerCertificateError = if ($null -eq $script:ServerCertificateError) { $false } else { $script:ServerCertificateError -ne [Net.Security.SslPolicyErrors]::None }

    $serverCertificateErrorMessage = ''

    if ($testUri.Scheme.ToLower() -eq 'https' -and $hasServerCertificateError) {
        $serverCertificateErrorMessage = Get-CertificateErrorMessage -Url $testUri -Certificate $script:ServerCertificate -Chain $script:ServerCertificateChain -PolicyError $script:ServerCertificateError
    }

    $serverCertificateObject = [pscustomobject]@{
        Certificate = $script:ServerCertificate | Select-Object -Property * -ExcludeProperty RawData; # RawData property makes JSON files to large when calling Save-HttpConnectivity
        Chain = $script:ServerCertificateChain;
        Error = $script:ServerCertificateError;
        ErrorMessage = $serverCertificateErrorMessage;
        HasError = $hasServerCertificateError;
        IgnoreError = $IgnoreCertificateValidationErrors;
    }

    $address = Get-IPAddress -Url $testUri -Verbose:$false
    $alias = Get-IPAlias -Url $testUri -Verbose:$false
    $resolved = (@($address)).Length -ge 1 -or (@($alias)).Length -ge 1
    $actualStatusCode = [int]$statusCode
    $isBlocked = $statusCode -eq 0 -and $resolved
    $urlType = if ($UnblockUrl.Contains('*')) { 'Pattern' } else { 'Literal' }

    $isUnexpectedStatus = !($statusCode -in @(200,400,403,404,500,501,503,504))
    $simpleStatusMessage = if ($isUnexpectedStatus) { $statusMessage } else { '' }

    $connectivitySummary = ('{0}Test Url: {1}{2}Url to Unblock: {3}{4}Url Type: {5}{6}Description: {7}{8}Resolved: {9}{10}IP Addresses: {11}{12}DNS Aliases: {13}{14}Actual Status Code: {15}{16}Expected Status Code: {17}{18}Is Unexpected Status Code: {19}{20}Status Message: {21}{22}Blocked: {23}{24}Certificate Error: {25}{26}Certificate Error Message: {27}{28}Ignore Certificate Validation Errors: {29}{30}{31}' -f $newLine,$testUri,$newLine,$UnblockUrl,$newLine,$urlType,$newLine,$Description,$newLine,$resolved,$newLine,($address -join ', '),$newLine,($alias -join ', '),$newLine,$actualStatusCode,$newLine,$ExpectedStatusCode,$newLine,$isUnexpectedStatus,$newLine,$simpleStatusMessage,$newLine,$isBlocked,$newLine,$serverCertificateObject.HasError,$newLine,$serverCertificateObject.ErrorMessage,$newLine,$serverCertificateObject.IgnoreError,$newLine,$newLine)
    Write-Verbose -Message $connectivitySummary

    $bluecoat = $null

    if ($PerformBluecoatLookup) {
        try {
            $bluecoat = Get-BlueCoatSiteReview -Url $testUri -Verbose:$isVerbose
        } catch {
            Write-Verbose -Message $_
        }
    }

    $connectivity = [pscustomobject]@{
        TestUrl = $testUri;
        UnblockUrl = $UnblockUrl;
        UrlType = $urlType;
        Resolved = $resolved;
        IpAddresses = [string[]]$address;
        DnsAliases = [string[]]$alias;
        Description = $Description;
        ActualStatusCode = [int]$actualStatusCode;
        ExpectedStatusCode = $ExpectedStatusCode;
        UnexpectedStatus = $isUnexpectedStatus;
        StatusMessage = $simpleStatusMessage;
        DetailedStatusMessage = $statusMessage;
        Blocked = $isBlocked;
        ServerCertificate = $serverCertificateObject;
        BlueCoat = $bluecoat;
    }

    return $connectivity
}

Function Save-HttpConnectivity() {
    <#
    .SYNOPSIS
    Saves HTTP connectivity objects to a JSON file.
 
    .DESCRIPTION
    Saves HTTP connectivity objects to a JSON file.
 
    .EXAMPLE
    Save-HttpConnectivity -FileName 'Connectivity' -Objects $connectivity
 
    .EXAMPLE
    Save-HttpConnectivity -FileName 'Connectivity' -Objects $connectivity -OutputPath "$env:userprofile\Documents\ConnectivityTestResults"
 
    .EXAMPLE
    Save-HttpConnectivity -FileName 'Connectivity' -Objects $connectivity -Compress
    #>

    [CmdletBinding()]
    [OutputType([void])]
    Param(
        [Parameter(Mandatory=$true, HelpMessage='The filename without the extension.')]
        [ValidateNotNullOrEmpty()]
        [string]$FileName,

        [Parameter(Mandatory=$true, HelpMessage='The connectivity object(s) to save.')]
        [System.Collections.Generic.List[pscustomobject]]$Objects,

        [Parameter(Mandatory=$false, HelpMessage="The path to save the file to. Defaults to the user's Desktop folder.")]
        [string]$OutputPath,

        [Parameter(Mandatory=$false, HelpMessage='Compress the JSON text output.')]
        [switch]$Compress
    )

    $parameters = $PSBoundParameters

    if (-not($parameters.ContainsKey('OutputPath'))) {
        $OutputPath = $env:USERPROFILE,'Desktop' -join [System.IO.Path]::DirectorySeparatorChar
    }

    $OutputPath = $ExecutionContext.SessionState.Path.GetUnresolvedProviderPathFromPSPath($OutputPath)

    if (-not(Test-Path -Path $OutputPath)) {
        New-Item -Path $OutputPath -ItemType Directory
    }

    #$fileName = ($targetUrl.OriginalString.Split([string[]][IO.Path]::GetInvalidFileNameChars(),[StringSplitOptions]::RemoveEmptyEntries)) -join '-'
    $json = $Objects | ConvertTo-Json -Depth 3 -Compress:$Compress
    $json | Out-File -FilePath "$OutputPath\$FileName.json" -NoNewline -Force
}

# SIG # Begin signature block
# MIIXxQYJKoZIhvcNAQcCoIIXtjCCF7ICAQExCzAJBgUrDgMCGgUAMGkGCisGAQQB
# gjcCAQSgWzBZMDQGCisGAQQBgjcCAR4wJgIDAQAABBAfzDtgWUsITrck0sYpfvNR
# AgEAAgEAAgEAAgEAAgEAMCEwCQYFKw4DAhoFAAQUu3KPWUJbOjEKOstaOLBVzsqu
# pKmgghL4MIID7jCCA1egAwIBAgIQfpPr+3zGTlnqS5p31Ab8OzANBgkqhkiG9w0B
# AQUFADCBizELMAkGA1UEBhMCWkExFTATBgNVBAgTDFdlc3Rlcm4gQ2FwZTEUMBIG
# A1UEBxMLRHVyYmFudmlsbGUxDzANBgNVBAoTBlRoYXd0ZTEdMBsGA1UECxMUVGhh
# d3RlIENlcnRpZmljYXRpb24xHzAdBgNVBAMTFlRoYXd0ZSBUaW1lc3RhbXBpbmcg
# Q0EwHhcNMTIxMjIxMDAwMDAwWhcNMjAxMjMwMjM1OTU5WjBeMQswCQYDVQQGEwJV
# UzEdMBsGA1UEChMUU3ltYW50ZWMgQ29ycG9yYXRpb24xMDAuBgNVBAMTJ1N5bWFu
# dGVjIFRpbWUgU3RhbXBpbmcgU2VydmljZXMgQ0EgLSBHMjCCASIwDQYJKoZIhvcN
# AQEBBQADggEPADCCAQoCggEBALGss0lUS5ccEgrYJXmRIlcqb9y4JsRDc2vCvy5Q
# WvsUwnaOQwElQ7Sh4kX06Ld7w3TMIte0lAAC903tv7S3RCRrzV9FO9FEzkMScxeC
# i2m0K8uZHqxyGyZNcR+xMd37UWECU6aq9UksBXhFpS+JzueZ5/6M4lc/PcaS3Er4
# ezPkeQr78HWIQZz/xQNRmarXbJ+TaYdlKYOFwmAUxMjJOxTawIHwHw103pIiq8r3
# +3R8J+b3Sht/p8OeLa6K6qbmqicWfWH3mHERvOJQoUvlXfrlDqcsn6plINPYlujI
# fKVOSET/GeJEB5IL12iEgF1qeGRFzWBGflTBE3zFefHJwXECAwEAAaOB+jCB9zAd
# BgNVHQ4EFgQUX5r1blzMzHSa1N197z/b7EyALt0wMgYIKwYBBQUHAQEEJjAkMCIG
# CCsGAQUFBzABhhZodHRwOi8vb2NzcC50aGF3dGUuY29tMBIGA1UdEwEB/wQIMAYB
# Af8CAQAwPwYDVR0fBDgwNjA0oDKgMIYuaHR0cDovL2NybC50aGF3dGUuY29tL1Ro
# YXd0ZVRpbWVzdGFtcGluZ0NBLmNybDATBgNVHSUEDDAKBggrBgEFBQcDCDAOBgNV
# HQ8BAf8EBAMCAQYwKAYDVR0RBCEwH6QdMBsxGTAXBgNVBAMTEFRpbWVTdGFtcC0y
# MDQ4LTEwDQYJKoZIhvcNAQEFBQADgYEAAwmbj3nvf1kwqu9otfrjCR27T4IGXTdf
# plKfFo3qHJIJRG71betYfDDo+WmNI3MLEm9Hqa45EfgqsZuwGsOO61mWAK3ODE2y
# 0DGmCFwqevzieh1XTKhlGOl5QGIllm7HxzdqgyEIjkHq3dlXPx13SYcqFgZepjhq
# IhKjURmDfrYwggSjMIIDi6ADAgECAhAOz/Q4yP6/NW4E2GqYGxpQMA0GCSqGSIb3
# DQEBBQUAMF4xCzAJBgNVBAYTAlVTMR0wGwYDVQQKExRTeW1hbnRlYyBDb3Jwb3Jh
# dGlvbjEwMC4GA1UEAxMnU3ltYW50ZWMgVGltZSBTdGFtcGluZyBTZXJ2aWNlcyBD
# QSAtIEcyMB4XDTEyMTAxODAwMDAwMFoXDTIwMTIyOTIzNTk1OVowYjELMAkGA1UE
# BhMCVVMxHTAbBgNVBAoTFFN5bWFudGVjIENvcnBvcmF0aW9uMTQwMgYDVQQDEytT
# eW1hbnRlYyBUaW1lIFN0YW1waW5nIFNlcnZpY2VzIFNpZ25lciAtIEc0MIIBIjAN
# BgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAomMLOUS4uyOnREm7Dv+h8GEKU5Ow
# mNutLA9KxW7/hjxTVQ8VzgQ/K/2plpbZvmF5C1vJTIZ25eBDSyKV7sIrQ8Gf2Gi0
# jkBP7oU4uRHFI/JkWPAVMm9OV6GuiKQC1yoezUvh3WPVF4kyW7BemVqonShQDhfu
# ltthO0VRHc8SVguSR/yrrvZmPUescHLnkudfzRC5xINklBm9JYDh6NIipdC6Anqh
# d5NbZcPuF3S8QYYq3AhMjJKMkS2ed0QfaNaodHfbDlsyi1aLM73ZY8hJnTrFxeoz
# C9Lxoxv0i77Zs1eLO94Ep3oisiSuLsdwxb5OgyYI+wu9qU+ZCOEQKHKqzQIDAQAB
# o4IBVzCCAVMwDAYDVR0TAQH/BAIwADAWBgNVHSUBAf8EDDAKBggrBgEFBQcDCDAO
# BgNVHQ8BAf8EBAMCB4AwcwYIKwYBBQUHAQEEZzBlMCoGCCsGAQUFBzABhh5odHRw
# Oi8vdHMtb2NzcC53cy5zeW1hbnRlYy5jb20wNwYIKwYBBQUHMAKGK2h0dHA6Ly90
# cy1haWEud3Muc3ltYW50ZWMuY29tL3Rzcy1jYS1nMi5jZXIwPAYDVR0fBDUwMzAx
# oC+gLYYraHR0cDovL3RzLWNybC53cy5zeW1hbnRlYy5jb20vdHNzLWNhLWcyLmNy
# bDAoBgNVHREEITAfpB0wGzEZMBcGA1UEAxMQVGltZVN0YW1wLTIwNDgtMjAdBgNV
# HQ4EFgQURsZpow5KFB7VTNpSYxc/Xja8DeYwHwYDVR0jBBgwFoAUX5r1blzMzHSa
# 1N197z/b7EyALt0wDQYJKoZIhvcNAQEFBQADggEBAHg7tJEqAEzwj2IwN3ijhCcH
# bxiy3iXcoNSUA6qGTiWfmkADHN3O43nLIWgG2rYytG2/9CwmYzPkSWRtDebDZw73
# BaQ1bHyJFsbpst+y6d0gxnEPzZV03LZc3r03H0N45ni1zSgEIKOq8UvEiCmRDoDR
# EfzdXHZuT14ORUZBbg2w6jiasTraCXEQ/Bx5tIB7rGn0/Zy2DBYr8X9bCT2bW+IW
# yhOBbQAuOA2oKY8s4bL0WqkBrxWcLC9JG9siu8P+eJRRw4axgohd8D20UaF5Mysu
# e7ncIAkTcetqGVvP6KUwVyyJST+5z3/Jvz4iaGNTmr1pdKzFHTx/kuDDvBzYBHUw
# ggUnMIIED6ADAgECAhAJT00SLqoJkIvAj67NF8OqMA0GCSqGSIb3DQEBCwUAMHIx
# CzAJBgNVBAYTAlVTMRUwEwYDVQQKEwxEaWdpQ2VydCBJbmMxGTAXBgNVBAsTEHd3
# dy5kaWdpY2VydC5jb20xMTAvBgNVBAMTKERpZ2lDZXJ0IFNIQTIgQXNzdXJlZCBJ
# RCBDb2RlIFNpZ25pbmcgQ0EwHhcNMTYwNjA2MDAwMDAwWhcNMTkwNjExMTIwMDAw
# WjBkMQswCQYDVQQGEwJDSDESMBAGA1UECBMJU29sb3RodXJuMREwDwYDVQQHDAhE
# w6RuaWtlbjEWMBQGA1UEChMNYmFzZVZJU0lPTiBBRzEWMBQGA1UEAxMNYmFzZVZJ
# U0lPTiBBRzCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAJ+YpjWmBGJ6
# 6p3mACb/iu1w1oUOFAPZVNSZ8nPOY2MNtzi8d2RRSf16+VVSBhy4wv5sg0QAu76I
# 1B5mwWA73gjDERH4LRvisNLrd5cR/CyS1DLZvHY01g7Ck7MtNSekjPEHIc6LFK/4
# 5gQ28nAPcanR2wo+RPGxu34QXKg3ceBH92POm1GDGGUMsTjP7ME7ZOeLKLScJD/V
# rmMH/B6K7ApfAF2O/szxFXrEo+5VcloWoCRHmbFe7nLnAC8k5I63ZBmiSi6EBc89
# ID+XaVWLYvVCNwI/PVEanmDxBG9SAxRnJtcUAYg62S84ClXNj2y53xPUbdZvz3mC
# RTivIlhjH9ECAwEAAaOCAcUwggHBMB8GA1UdIwQYMBaAFFrEuXsqCqOl6nEDwGD5
# LfZldQ5YMB0GA1UdDgQWBBR6hPT/LYCRb+slld/aUoR4eQYCQDAOBgNVHQ8BAf8E
# BAMCB4AwEwYDVR0lBAwwCgYIKwYBBQUHAwMwdwYDVR0fBHAwbjA1oDOgMYYvaHR0
# cDovL2NybDMuZGlnaWNlcnQuY29tL3NoYTItYXNzdXJlZC1jcy1nMS5jcmwwNaAz
# oDGGL2h0dHA6Ly9jcmw0LmRpZ2ljZXJ0LmNvbS9zaGEyLWFzc3VyZWQtY3MtZzEu
# Y3JsMEwGA1UdIARFMEMwNwYJYIZIAYb9bAMBMCowKAYIKwYBBQUHAgEWHGh0dHBz
# Oi8vd3d3LmRpZ2ljZXJ0LmNvbS9DUFMwCAYGZ4EMAQQBMIGEBggrBgEFBQcBAQR4
# MHYwJAYIKwYBBQUHMAGGGGh0dHA6Ly9vY3NwLmRpZ2ljZXJ0LmNvbTBOBggrBgEF
# BQcwAoZCaHR0cDovL2NhY2VydHMuZGlnaWNlcnQuY29tL0RpZ2lDZXJ0U0hBMkFz
# c3VyZWRJRENvZGVTaWduaW5nQ0EuY3J0MAwGA1UdEwEB/wQCMAAwDQYJKoZIhvcN
# AQELBQADggEBAI5wXkMjGctA2E/fchGVptw2Qzdp1a3C1ApX4STqxhkKaQMMJao7
# cHarrQctdjRo2YHEsEsPpOKpQcB2gEUnhWInaghmq618MC/UYZtL/hUcGraEhRO6
# PEDoM/2Xz1+EJJbgmS812YOih1xXrbzfgKE3Zl01VsoNjPvsD4XtEuD0Utjrwsh/
# Qy3gD9Wb925oYOuIz9hp1+jmnQu7hlRaVr7TtxR4aTtTqQdAv35FKPqJdXXUZ9Y9
# otWAWBgWb8YFqMTw6gig3EUORB+MyPXN/zCdwrbAcXlrMIPHhKsvJ6UkxfQkfb4Z
# oztVtMUBChHanEVcX4bVFQwNnDVcrlt8w6IwggUwMIIEGKADAgECAhAECRgbX9W7
# ZnVTQ7VvlVAIMA0GCSqGSIb3DQEBCwUAMGUxCzAJBgNVBAYTAlVTMRUwEwYDVQQK
# EwxEaWdpQ2VydCBJbmMxGTAXBgNVBAsTEHd3dy5kaWdpY2VydC5jb20xJDAiBgNV
# BAMTG0RpZ2lDZXJ0IEFzc3VyZWQgSUQgUm9vdCBDQTAeFw0xMzEwMjIxMjAwMDBa
# Fw0yODEwMjIxMjAwMDBaMHIxCzAJBgNVBAYTAlVTMRUwEwYDVQQKEwxEaWdpQ2Vy
# dCBJbmMxGTAXBgNVBAsTEHd3dy5kaWdpY2VydC5jb20xMTAvBgNVBAMTKERpZ2lD
# ZXJ0IFNIQTIgQXNzdXJlZCBJRCBDb2RlIFNpZ25pbmcgQ0EwggEiMA0GCSqGSIb3
# DQEBAQUAA4IBDwAwggEKAoIBAQD407Mcfw4Rr2d3B9MLMUkZz9D7RZmxOttE9X/l
# qJ3bMtdx6nadBS63j/qSQ8Cl+YnUNxnXtqrwnIal2CWsDnkoOn7p0WfTxvspJ8fT
# eyOU5JEjlpB3gvmhhCNmElQzUHSxKCa7JGnCwlLyFGeKiUXULaGj6YgsIJWuHEqH
# CN8M9eJNYBi+qsSyrnAxZjNxPqxwoqvOf+l8y5Kh5TsxHM/q8grkV7tKtel05iv+
# bMt+dDk2DZDv5LVOpKnqagqrhPOsZ061xPeM0SAlI+sIZD5SlsHyDxL0xY4PwaLo
# LFH3c7y9hbFig3NBggfkOItqcyDQD2RzPJ6fpjOp/RnfJZPRAgMBAAGjggHNMIIB
# yTASBgNVHRMBAf8ECDAGAQH/AgEAMA4GA1UdDwEB/wQEAwIBhjATBgNVHSUEDDAK
# BggrBgEFBQcDAzB5BggrBgEFBQcBAQRtMGswJAYIKwYBBQUHMAGGGGh0dHA6Ly9v
# Y3NwLmRpZ2ljZXJ0LmNvbTBDBggrBgEFBQcwAoY3aHR0cDovL2NhY2VydHMuZGln
# aWNlcnQuY29tL0RpZ2lDZXJ0QXNzdXJlZElEUm9vdENBLmNydDCBgQYDVR0fBHow
# eDA6oDigNoY0aHR0cDovL2NybDQuZGlnaWNlcnQuY29tL0RpZ2lDZXJ0QXNzdXJl
# ZElEUm9vdENBLmNybDA6oDigNoY0aHR0cDovL2NybDMuZGlnaWNlcnQuY29tL0Rp
# Z2lDZXJ0QXNzdXJlZElEUm9vdENBLmNybDBPBgNVHSAESDBGMDgGCmCGSAGG/WwA
# AgQwKjAoBggrBgEFBQcCARYcaHR0cHM6Ly93d3cuZGlnaWNlcnQuY29tL0NQUzAK
# BghghkgBhv1sAzAdBgNVHQ4EFgQUWsS5eyoKo6XqcQPAYPkt9mV1DlgwHwYDVR0j
# BBgwFoAUReuir/SSy4IxLVGLp6chnfNtyA8wDQYJKoZIhvcNAQELBQADggEBAD7s
# DVoks/Mi0RXILHwlKXaoHV0cLToaxO8wYdd+C2D9wz0PxK+L/e8q3yBVN7Dh9tGS
# dQ9RtG6ljlriXiSBThCk7j9xjmMOE0ut119EefM2FAaK95xGTlz/kLEbBw6RFfu6
# r7VRwo0kriTGxycqoSkoGjpxKAI8LpGjwCUR4pwUR6F6aGivm6dcIFzZcbEMj7uo
# +MUSaJ/PQMtARKUT8OZkDCUIQjKyNookAv4vcn4c10lFluhZHen6dGRrsutmQ9qz
# sIzV6Q3d9gEgzpkxYz0IGhizgZtPxpMQBvwHgfqL2vmCSfdibqFT+hKUGIUukpHq
# aGxEMrJmoecYpJpkUe8xggQ3MIIEMwIBATCBhjByMQswCQYDVQQGEwJVUzEVMBMG
# A1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3d3cuZGlnaWNlcnQuY29tMTEw
# LwYDVQQDEyhEaWdpQ2VydCBTSEEyIEFzc3VyZWQgSUQgQ29kZSBTaWduaW5nIENB
# AhAJT00SLqoJkIvAj67NF8OqMAkGBSsOAwIaBQCgeDAYBgorBgEEAYI3AgEMMQow
# CKACgAChAoAAMBkGCSqGSIb3DQEJAzEMBgorBgEEAYI3AgEEMBwGCisGAQQBgjcC
# AQsxDjAMBgorBgEEAYI3AgEVMCMGCSqGSIb3DQEJBDEWBBSGLBCCp+liyoCFj95O
# FeVIz4bIGDANBgkqhkiG9w0BAQEFAASCAQAsSb4l1/TgDhFs9SutE8BmDPQ+XXml
# z6EtE1MIPfpzeETmta4zwTJndTm+QyxEvkKHOPNdQOwiqjieX1nJoNBZiA2C7RX2
# Pub1eeh+O8gEb8ISl40usUe1EWzaX6bcMKSxbP3dtgLQaMeHXEkK0aRz8Su/b/CX
# 0OTopqQlDEeEUrSD4MMqXKtBVfk0mud0nVE8MgYSUT6k27Xxtr67UyilInIuTBWH
# 42PtSt5+aeL+xGNP6NchDmRg3ffh4s0sdquHsr53hNphEdtZ9F1//UU6zeK2AtxH
# V6z+Jw15hL2McfOqPQJhILsA1vec9mCebM1XYwECkX1Js8MiYHtPGtMYoYICCzCC
# AgcGCSqGSIb3DQEJBjGCAfgwggH0AgEBMHIwXjELMAkGA1UEBhMCVVMxHTAbBgNV
# BAoTFFN5bWFudGVjIENvcnBvcmF0aW9uMTAwLgYDVQQDEydTeW1hbnRlYyBUaW1l
# IFN0YW1waW5nIFNlcnZpY2VzIENBIC0gRzICEA7P9DjI/r81bgTYapgbGlAwCQYF
# Kw4DAhoFAKBdMBgGCSqGSIb3DQEJAzELBgkqhkiG9w0BBwEwHAYJKoZIhvcNAQkF
# MQ8XDTE5MDEwOTE1MjgxN1owIwYJKoZIhvcNAQkEMRYEFGsvyd6ZI34GAiySCm+n
# jy58SGh7MA0GCSqGSIb3DQEBAQUABIIBAICIiTfrhVUtNLm40vdtn1UhDBpHlhhw
# KDBcy/yw+nRwzGoQLW+04wjLABXwqTVhsA2AxSWnOZQcLaYXiU7kYwdocaYbPCwI
# kplcC4G+ZgBbL/VarH/e7qckL4qp9TGx6S3C6eCY9WqtsGDXerLnyzuYrGhwXLwA
# Z6n5DoeSqq5Lar6HtUY7izVnFa+W0PosrJHfmHN46VCFYombs5BrHsVR7y6u2ByA
# a7yyWKi2uuw2HoVYoVBr/CQztXeu6pAqjrGLohAo2g0pCjnYaD08pWe7Xsv6tTXs
# X6VUnRtvYrmOmGKRw+Ye2y0o3jv7ESLKdrB/mYKcs1LbHa54cUUHplE=
# SIG # End signature block