AzStackHciConnectivity/AzStackHci.Connectivity.Helpers.psm1

class HealthModel
{
    # Attributes for Azure Monitor schema
    [string]$Name #Name of the individual test/rule/alert that was executed. Unique, not exposed to the customer.
    [string]$Title #User-facing name; one or more sentences indicating the direct issue.
    [string]$Severity #Severity of the result (Critical, Warning, Informational, Hidden) – this answers how important the result is. Critical is the only update-blocking severity.
    [string]$Description #Detailed overview of the issue and what impact the issue has on the stamp.
    [psobject]$Tags #Key-value pairs that allow grouping/filtering individual tests. For example, "Group": "ReadinessChecks", "UpdateType": "ClusterAware"
    [string]$Status #The status of the check running (i.e. Failed, Succeeded, In Progress) – this answers whether the check ran, and passed or failed.
    [string]$Remediation #Set of steps that can be taken to resolve the issue found.
    [string]$TargetResourceID #The unique identifier for the affected resource (such as a node or drive).
    [string]$TargetResourceName #The name of the affected resource.
    [string]$TargetResourceType #The type of resource being referred to (well-known set of nouns in infrastructure, aligning with Monitoring).
    [datetime]$Timestamp #The Time in which the HealthCheck was called.
    [psobject]$AdditionalData #Property bag of key value pairs for additional information.
    [string]$HealthCheckSource #The name of the services called for the HealthCheck (I.E. Test-AzureStack, Test-Cluster).
}

class AzStackHciConnectivityTarget : HealthModel
{
    # Attribute for performing check
    [string[]]$EndPoint
    [string[]]$Protocol

    # Additional Attributes for end user interaction
    [string[]]$Service # short cut property to Service from tags
    [string[]]$OperationType # short cut property to Operation Type from tags
    [string[]]$Group # short cut property to group from tags
    [bool]$System # targets for system checks such as proxy traversal
}

#Create additional classes to help with writing/report results
class Diagnostics : HealthModel {}
class DnsResult : HealthModel {}
class ProxyDiagnostics : HealthModel {}
function Get-DnsServer
{
    <#
    .SYNOPSIS
        Get DNS Servers
    .DESCRIPTION
        Get DNS Servers of network adapter that are up
    #>

    param (
        [System.Management.Automation.Runspaces.PSSession]
        $PsSession
    )

    $getDnsServersb = {
        $dnsServers = @()
        $netAdapter = Get-NetAdapter | Where-Object Status -EQ Up
        $dnsServer = Get-DnsClientServerAddress -InterfaceIndex $netAdapter.ifIndex -AddressFamily IPv4
        $dnsServers += $dnsServer | ForEach-Object { $PSITEM.Address } | Sort-Object | Get-Unique
        $dnsServers
    }
    $dnsServer = if ($PsSession)
    {
        Invoke-Command -Session $PsSession -ScriptBlock $getDnsServersb
    }
    else
    {
        Invoke-Command -ScriptBlock $getDnsServersb
    }
    return $dnsServer
}

function Test-Dns
{
    <#
    .SYNOPSIS
        Test DNS Resolution
    #>

    param (
        [System.Management.Automation.Runspaces.PSSession]
        $PsSession
    )
    $dnsServers = Get-DnsServer -PsSession $PsSession
    Log-Info ("DNS Servers discovered: {0}" -f ($dnsServers -join ','))

    # scriptblock to test dns resolution for each dns server
    $testDnsSb = {
        $AdditionalData = @()
        if (-not $dnsServers)
        {
            $AdditionalData += @{
                Resource  = 'Missing DNS Server'
                Status    = 'Failed'
                TimeStamp = Get-Date
                Source    = $ENV:COMPUTERNAME
            }
        }
        else
        {
            foreach ($dnsServer in $dnsServers)
            {
                $dnsResult = $false
                try
                {
                    $dnsResult = Resolve-DnsName -Name microsoft.com -Server $dnsServer -DnsOnly -ErrorAction SilentlyContinue -QuickTimeout -Type A
                }
                catch
                {
                    $dnsResult = $_.Exception.Message
                }
                if ($dnsResult)
                {
                    if ($dnsResult[0] -is [Microsoft.DnsClient.Commands.DnsRecord])
                    {
                        $status = 'Succeeded'
                    }
                    else
                    {
                        $status = 'Failed'
                    }
                }
                else
                {
                    $status = 'Failed'
                }
                $AdditionalData += @{
                    Resource  = $dnsServer
                    Status    = $status
                    TimeStamp = Get-Date
                    Source    = $ENV:COMPUTERNAME
                    Detail    = $dnsResult
                }
            }
        }
        $AdditionalData
    }

    # run scriptblock
    $testDnsServer = if ($PsSession)
    {
        Invoke-Command -Session $PsSession -ScriptBlock $testDnsSb
    }
    else
    {
        Invoke-Command -ScriptBlock $testDnsSb
    }

    # build result
    $now = Get-Date
    $TargetComputerName = if ($PsSession.PSComputerName) { $PsSession.PSComputerName } else { $ENV:COMPUTERNAME }
    $aggregateStatus = if ($testDnsServer.Status -contains 'Succeeded') { 'Succeeded' } else { 'Failed' }
    $testDnsResult = New-Object -Type DnsResult -Property @{
        Name               = 'AzStackHci_Connectivity_Test_Dns'
        Title              = 'Test DNS'
        Severity           = 'Critical'
        Description        = 'Test DNS Resolution'
        Tags               = $null
        Remediation        = 'https://aka.ms/fixthis'
        TargetResourceID   = 'c644bad4-044d-4066-861d-ceb93b64f046'
        TargetResourceName = "Test_DNS_$TargetComputerName"
        TargetResourceType = 'DNS'
        Timestamp          = $now
        Status             = $aggregateStatus
        AdditionalData     = $testDnsServer
        HealthCheckSource  = ((Get-PSCallStack)[-1].Command)
    }
    return $testDnsResult
}

function Get-AzStackHciConnectivityServiceName
{
    <#
    .SYNOPSIS
        Retrieve Services from built target packs
    .DESCRIPTION
        Retrieve Services from built target packs
    .EXAMPLE
        PS C:\> Get-AzStackHciServices
        Explanation of what the example does
    .INPUTS
        Service
    .OUTPUTS
        PSObject
    .NOTES
    #>

    [CmdletBinding()]
    param (
        [Parameter(Mandatory = $false)]
        [string[]]
        $Service,

        [Parameter(Mandatory = $false)]
        [switch]
        $IncludeSystem
    )
    try
    {
        Get-AzStackHciConnectivityTarget -IncludeSystem:$IncludeSystem | Select-Object -ExpandProperty Service | Sort-Object | Get-Unique
    }
    catch
    {
        throw "Failed to get services names. Error: $($_.Exception.Message)"
    }
}

function Get-AzStackHciConnectivityOperationName
{
    <#
    .SYNOPSIS
        Retrieve Operation Types from built target packs
    .DESCRIPTION
        Retrieve Operation Types from built target packs e.g. Deployment, Update, Secret Rotation.
    .EXAMPLE
        PS C:\> Get-AzStackHciConnectivityOperationName
        Explanation of what the example does
    .INPUTS
        Service
    .OUTPUTS
        PSObject
    .NOTES
    #>

    [CmdletBinding()]
    param (
        [Parameter(Mandatory = $false)]
        [string]
        $OperationType
    )
    try
    {
        Get-AzStackHciConnectivityTarget | Select-Object -ExpandProperty OperationType | Sort-Object | Get-Unique
    }
    catch
    {
        throw "Failed to get services names. Error: $($_.Exception.Message)"
    }
}

function Get-AzStackHciConnectivityTarget
{
    <#
        .SYNOPSIS
            Retrieve Endpoints from built target packs
        .DESCRIPTION
            Retrieve Endpoints from built target packs
        .EXAMPLE
            PS> Get-AzStackHciConnectivityTarget
            Get all connectivity targets
        .EXAMPLE
            Get-AzStackHciConnectivityTarget -Service ARC | ft Name, Title, Service, OperationType -AutoSize
            Get all ARC connectivity targets
        .EXAMPLE
            PS> Get-AzStackHciConnectivityTarget -Service ARC -OperationType Workload | ft Name, Title, Service, OperationType -AutoSize
            Get all ARC targets for workloads
        .EXAMPLE
            PS> Get-AzStackHciConnectivityTarget -OperationType Workload | ft Name, Title, Service, OperationType -AutoSize
            Get all targets for workloads
        .EXAMPLE
            PS> Get-AzStackHciConnectivityTarget -OperationType ARC -OperationType Update -Additive | ft Name, Title, Service, OperationType -AutoSize
            Get all ARC targets and all targets for Update
        .INPUTS
            Service - String array
            OperationType - String array
            Additive - Switch
        .OUTPUTS
            PSObject
        .NOTES
    #>

    [CmdletBinding()]
    param (

        [Parameter(Mandatory = $false)]
        [string[]]
        $Service,

        [Parameter(Mandatory = $false)]
        [string[]]
        $OperationType,

        [Parameter(Mandatory = $false)]
        [switch]
        $Additive,

        [Parameter(Mandatory = $false)]
        [switch]
        $IncludeSystem

    )
    try
    {
        Import-AzStackHciConnectivityTarget
        $executionTargets = @()
        # Additive allows the user to "-OR" their parameter values
        if ($Additive)
        {
            Log-Info -Message "Getting targets additively"
            if (-not [string]::IsNullOrEmpty($Service))
            {
                Log-Info -Message ("Getting targets by Service: {0}" -f ($Service -join ','))
                foreach ($svc in $Service)
                {
                    $executionTargets += $Script:AzStackHciConnectivityTargets | Where-Object { $svc -in $_.Service }
                }
            }
            if (-not [string]::IsNullOrEmpty($OperationType))
            {
                Log-Info -Message ("Getting targets by Operation Type: {0}" -f ($OperationType -join ','))
                foreach ($Op in $OperationType)
                {
                    $executionTargets += $Script:AzStackHciConnectivityTargets | Where-Object { $Op -in $_.OperationType }
                }
            }
            if ([string]::IsNullOrEmpty($OperationType) -and [string]::IsNullOrEmpty($Service))
            {
                $executionTargets += $Script:AzStackHciConnectivityTargets
            }
        }
        else
        {
            if ([string]::IsNullOrEmpty($OperationType) -and [string]::IsNullOrEmpty($Service))
            {
                $executionTargets += $Script:AzStackHciConnectivityTargets
            }
            elseif (-not [string]::IsNullOrEmpty($Service) -and [string]::IsNullOrEmpty($OperationType))
            {
                Log-Info -Message ("Getting targets by Service: {0}" -f ($Service -join ','))
                foreach ($svc in $Service)
                {
                    $executionTargets += $Script:AzStackHciConnectivityTargets | Where-Object { $svc -in $_.Service }
                }
            }
            elseif (-not [string]::IsNullOrEmpty($OperationType) -and [string]::IsNullOrEmpty($Service))
            {
                Log-Info -Message ("Getting targets by Operation Type: {0}" -f ($OperationType -join ','))
                foreach ($Op in $OperationType)
                {
                    $executionTargets += $Script:AzStackHciConnectivityTargets | Where-Object { $Op -in $_.OperationType }
                }
            }
            else
            {
                Log-Info -Message ("Getting targets by Operation Type: {0} and Service: {1}" -f ($OperationType -join ','), ($Service -join ','))
                $executionTargetsByOp = @()
                foreach ($Op in $OperationType)
                {
                    $executionTargetsByOp += $Script:AzStackHciConnectivityTargets | Where-Object { $Op -in $_.OperationType }
                }
                foreach ($svc in $Service)
                {
                    $executionTargets += $executionTargetsByOp | Where-Object { $svc -in $_.Service }
                }
            }
        }
        if ($IncludeSystem)
        {
            return $executionTargets
        }
        else
        {
            return ($executionTargets | Where-Object Service -NotContains 'System')
        }
    }
    catch
    {
        throw "Get failed: $($_.exception)"
    }
}

function Import-AzStackHciConnectivityTarget
{
    <#
    .SYNOPSIS
        Retrieve Endpoints from built target packs
    .DESCRIPTION
        Retrieve Endpoints from built target packs
    .EXAMPLE
        PS C:\> Import-AzStackHciConnectivityTarget
        Explanation of what the example does
    .INPUTS
        URI
    .OUTPUTS
        PSObject
    .NOTES
    #>

    [CmdletBinding()]
    param ()
    try
    {
        $Script:AzStackHciConnectivityTargets = @()
        $targetFiles = Get-ChildItem -Path "$PSScriptRoot\Targets\*.json" | Select-Object -ExpandProperty FullName
        Log-Info -Message ("Importing {0}" -f ($targetFiles -join ','))
        ForEach ($targetFile in $targetFiles)
        {
            try
            {
                # TO DO - Add validations:
                # - protocol should not contain ://
                $targetPackContent = Get-Content -Path $targetFile | ConvertFrom-Json -WarningAction SilentlyContinue
                foreach ($target in $targetPackContent)
                {
                    #Set Name of the individual test/rule/alert that was executed. Unique, not exposed to the customer.
                    $target | Add-Member -MemberType NoteProperty -Name Name -Value ("AzStackHci_Connectivity_{0}" -f $Target.TargetResourceName)
                    $target | Add-Member -MemberType NoteProperty -Name HealthCheckSource -Value ((Get-PSCallStack).Command -join '\')
                    $target | Add-Member -MemberType NoteProperty -Name Service -Value $Target.Tags.Service
                    $target | Add-Member -MemberType NoteProperty -Name OperationType -Value $Target.Tags.OperationType
                    $target | Add-Member -MemberType NoteProperty -Name Group -Value $Target.Tags.Group

                    # TO DO: Determine the proper use of TargetResourceID
                    $target.TargetResourceID = (New-Guid).Guid.ToString()
                    $Script:AzStackHciConnectivityTargets += [AzStackHciConnectivityTarget]$target
                }
            }
            catch
            {
                Log-Info -Message -Type Warning -Message ("Unable to read {0}. Error: {1}" -f (Split-Path -Path $targetFile -Leaf), $_.Exception.Message)
            }
        }
    }
    catch
    {
        throw "Import failed: $($_.exception)"
    }
}

function Get-CloudEndpointFromManifest
{
    <#
    .SYNOPSIS
        Retrieve Endpoints to test from Cloud Manifest
    .DESCRIPTION
        Retrieve Endpoints to test from Cloud Manifest
    .EXAMPLE
        PS C:\> Get-CloudEndpointFromManifest -Uri
        Explanation of what the example does
    .INPUTS
        URI
    .OUTPUTS
        Output (if any)
    .NOTES
        URL: https://docs.microsoft.com/en-us/javascript/api/@azure/arm-azurestack/cloudmanifestfile?view=azure-node-preview
    #>

    [CmdletBinding()]
    param (
        [Parameter()]
        [System.Uri]
        $Uri
    )
    throw "Not implemented"
}

function Get-SystemProxy
{
    <#
    .SYNOPSIS
        Get Proxy set on system
    .DESCRIPTION
        Get Proxy set on system
    .EXAMPLE
        PS C:\> Get-SystemProxy
        Explanation of what the example does
    .OUTPUTS
        Output (if any)
    .NOTES
    #>

    [CmdletBinding()]
    param ()
    throw "Not implemented"
}

function Get-SigningRootThumbprint
{
    <#
    .SYNOPSIS
        Get signing root for https endpoint
    .DESCRIPTION
        Get signing root for https endpoint
    .EXAMPLE
        PS C:\> Get-SigningRoot -uri MicrosoftOnline.com
        Explanation of what the example does
    .INPUTS
        URI
    .OUTPUTS
        Output (if any)
    .NOTES
    #>

    [CmdletBinding()]
    param (
        [Parameter()]
        [System.Uri]
        $Uri,

        [Parameter()]
        [System.Management.Automation.Runspaces.PSSession]
        $PsSession,

        [Parameter()]
        [string]
        $Proxy,

        [Parameter()]
        [pscredential]
        $proxyCredential
    )
    try
    {
        $sb = {
            $uri = $args[0]
            $proxy = $args[1]
            $proxyCredential = $args[2]
            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()]
                    [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; }
                    }
                }
'@


                    if ($PSEdition -ne 'Core')
                    {
                        Add-Type -AssemblyName System.Net.Http
                        Add-Type $cs -ReferencedAssemblies System.Net.Http
                    }
                    else
                    {
                        Add-Type $cs
                    }

                    $Certs = [CertificateCapture.Utility]::CapturedCertificates
                    $Handler = [System.Net.Http.HttpClientHandler]::new()
                    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 $_
                }
            }

            $chain = Get-SslCertificateChain -Url $Uri -Proxy $Proxy -ProxyCredential $ProxyCredential
            if ($chain.ChainElements.Certificate.Count -le 1)
            {
                throw ("Unexpected certificate chain in response. Expected 2 or more certificates in chain, found {0}. {1}" -f $chain.ChainElements.Certificate.Count, `
                    ($chain.ChainElements.Certificate | ForEach-Object { "Thumbprint: {0}, Subject: {1}, Issuer: {2}" -f $_.Thumbprint, $_.Subject, $_.Issuer }))
            }
            $Thumbprint = $chain.ChainElements.Certificate | Where-Object { $_.Subject -eq $_.Issuer } | Select-Object -ExpandProperty Thumbprint
            if ($Thumbprint.Count -ne 1)
            {
                throw "Expected 1 thumbprint from Certificate Chain Root"
            }
            return $Thumbprint
        }
        $thumbprint = if ($PsSession)
        {
            Invoke-Command -Session $PsSession -ScriptBlock $sb -ArgumentList $Uri, $Proxy, $ProxyCredential
        }
        else
        {
            Invoke-Command -ScriptBlock $sb -ArgumentList $Uri, $Proxy, $ProxyCredential
        }
        return $thumbprint
    }
    catch
    {
        throw $_
    }
}

function Test-RootCA
{
    <#
    .SYNOPSIS
        Short description
    .DESCRIPTION
        Long description
    .EXAMPLE
        PS C:\> <example usage>
        Explanation of what the example does
    .INPUTS
        Inputs (if any)
    .OUTPUTS
        Output (if any)
    .NOTES
        General notes
    #>

    param(
        [Parameter()]
        [System.Management.Automation.Runspaces.PSSession]
        $PsSession,

        [Parameter()]
        [string]
        $Proxy,

        [Parameter()]
        [pscredential]
        $ProxyCredential
    )
    try
    {
        if ($Script:AzStackHciConnectivityTargets)
        {
            $rootCATarget = $Script:AzStackHciConnectivityTargets | Where-Object TargetResourceName -EQ System_RootCA
            if ($rootCATarget.count -ne 1)
            {
                throw "Expected 1 System_RootCA, found $($rootCATarget.count)"
            }

            $rootCATargetUrl = ("{0}://{1}" -f ($rootCATarget.Protocol | Select-Object -First 1), ($rootCATarget.EndPoint | Select-Object -First 1))
            Log-Info "Testing root CA for $rootCATargetUrl"
            $thumbprint = Get-SigningRootThumbprint -Uri $rootCATargetUrl -PsSession $PsSession -Proxy $Proxy -ProxyCredential $ProxyCredential
            if ($thumbprint -eq $rootCATarget.Tags.RootCAThumbprint)
            {
                $rootCATarget.Status = 'Succeeded'
            }
            else
            {
                $rootCATarget.Status = 'Failed'
                Log-Info "Expected RootCA thumbprint $($rootCATarget.Tags.RootCAThumbprint). Actual $thumbprint. SSL decryption and re-encryption detected." -Type Error
            }
            $rootCATarget.AdditionalData = New-Object -TypeName PSObject -Property @{
                Source    = if ([string]::IsNullOrEmpty($PsSession.ComputerName)) { [System.Net.Dns]::GetHostName() } else { $PsSession.ComputerName }
                Resource  = $rootCATargetUrl
                Protocol  = $rootCATarget.Protocol
                Status    = $rootCATarget.Status
                Detail    = "Expected RootCA thumbprint $($rootCATarget.Tags.RootCAThumbprint). Actual $thumbprint. SSL decryption and re-encryption detected."
                TimeStamp = [datetime]::UtcNow
            }
            return $rootCATarget
        }
        else
        {
            throw "No AzStackHciConnectivityTargets"
        }
    }
    catch
    {
        Log-Info "Test-RootCA failed with error: $($_.exception.message)"
    }
}

function Invoke-WebRequestEx
{
    <#
    .SYNOPSIS
        Get Connectivity via Invoke-WebRequest
    .DESCRIPTION
        Get Connectivity via Invoke-WebRequest, supporting proxy
    .EXAMPLE
        PS C:\> Invoke-WebRequestEx -Target $Target
        Explanation of what the example does
    .INPUTS
        URI
    .OUTPUTS
        Output (if any)
    .NOTES
    #>

    [CmdletBinding()]
    param (
        [Parameter()]
        [psobject]
        $Target,

        [Parameter()]
        [System.Management.Automation.Runspaces.PSSession[]]
        $PsSession,

        [Parameter()]
        [string]
        $Proxy,

        [Parameter()]
        [pscredential]
        $ProxyCredential

    )
    $ScriptBlock = {
        $target = $args[0]
        $Proxy = $args[1]
        $ProxyCredential = $args[2]

        $failedTarget = $false
        $target.TimeStamp = [datetime]::UtcNow
        $AdditionalData = @()
        $timeoutSecondsDefault = 10
        if ([string]::IsNullOrEmpty($Target.Tags.TimeoutSecs))
        {
            $timeout = $timeoutSecondsDefault
        }
        else
        {
            $timeout = $Target.Tags.TimeoutSecs
        }

        foreach ($uri in $Target.EndPoint)
        {
            foreach ($p in $Target.Protocol)
            {
                # TO DO handle wildcards
                $invokeParams = @{
                    Uri             = "{0}://{1}" -f $p, $Uri.Replace('*', 'www')
                    UseBasicParsing = $true
                    Timeout         = $timeout
                    ErrorAction     = 'SilentlyContinue'
                }

                if (-not [string]::IsNullOrEmpty($Proxy))
                {
                    $invokeParams += @{
                        Proxy = $Proxy
                    }
                }

                if (-not [string]::IsNullOrEmpty($ProxyCredential))
                {
                    $invokeParams += @{
                        ProxyCredential = $ProxyCredential
                    }
                }

                try
                {
                    $ProgressPreference = 'SilentlyContinue'
                    $stopwatch = [System.Diagnostics.Stopwatch]::new()
                    $Stopwatch.Start()
                    $result = Invoke-WebRequest @invokeParams
                    $Stopwatch.Stop()
                    $StatusCode = $result.StatusCode
                }
                catch
                {
                    $webResponse = $_.Exception.Response
                    if ($webResponse)
                    {
                        try
                        {
                            $StatusCode = $webResponse.StatusCode.value__
                            $headers = @{}
                            $content = [System.Text.Encoding]::UTF8.GetString($webResponse.GetResponseStream().ToArray())

                            foreach ($header in $webResponse.Headers)
                            {
                                $headers.$header = $webResponse.GetResponseHeader($header)
                            }

                            if ($webResponse.ContentType -eq 'application/json')
                            {
                                $content = ConvertFrom-Json -InputObject $content -WarningAction SilentlyContinue
                            }
                        }
                        catch {}
                    }
                    else
                    {
                        $statusCode = $_.Exception.Message
                    }

                    # if proxy is not null
                    # check the responseuri matches a proxy set the status code to the exception
                    # so ps5 behaves similar to ps7
                    $ProxyLookup = [Regex]::Escape($Proxy)
                    if (-not [string]::IsNullOrEmpty($Proxy) -and $webResponse.ResponseUri.OriginalString -match $ProxyLookup)
                    {
                        $statusCode = $_.Exception.Message
                    }
                }
                finally
                {
                    $ProgressPreference = 'Continue'
                }

                if ($StatusCode -isnot [int])
                {
                    $failedTarget = $true
                }


                $source = if ([string]::IsNullOrEmpty($PsSession.ComputerName))
                {
                    [System.Net.Dns]::GetHostName()
                }
                else
                {
                    $PsSession.ComputerName
                }
                if (-not [string]::IsNullOrEmpty($Proxy))
                {
                    $source = $source + "($Proxy)"
                }

                $AdditionalData += New-Object -TypeName PSObject -Property @{
                    Source       = $source
                    Resource     = $invokeParams.uri
                    Protocol     = $p
                    Status       = if ($StatusCode -is [int]) { "Succeeded" } else { "Failed" }
                    TimeStamp    = [datetime]::UtcNow
                    StatusCode   = $StatusCode
                    MilliSeconds = $Stopwatch.Elapsed.Milliseconds
                }
            }
        }
        if ($failedTarget)
        {
            $target.Status = 'Failed'
            Log-Info "$($target.Title) failed:"
        }
        else
        {
            $target.Status = 'Succeeded'
            Log-Info "$($target.Title) succeeded:"
        }
        $AdditionalData | ForEach-Object { Log-Info ("Endpoint detail {0}: {1}, {2}" -f $_.Status, $_.Resource, $_.StatusCode) }
        $Target.AdditionalData = $AdditionalData
        $Target.HealthCheckSource = ((Get-PSCallStack)[-1].Command)
        return $Target
    }

    $sessionArgs = @()
    if ($Target)
    {
        $sessionArgs += $Target
    }
    if ($Proxy)
    {
        $sessionArgs += $Proxy
    }
    if ($ProxyCredential)
    {
        $sessionArgs += $ProxyCredential
    }
    if ($PsSession)
    {
        Invoke-Command -Session $PsSession -ScriptBlock $ScriptBlock -ArgumentList $sessionArgs
    }
    else
    {
        Invoke-Command -ScriptBlock $ScriptBlock -ArgumentList $sessionArgs
    }
}

function Invoke-TestNetConnection
{
    <#
    .SYNOPSIS
        Get endpoint via Test-NetConnection
    .DESCRIPTION
        Get endpoint via Test-NetConnection, quicker simplier proxy-less check.
    .EXAMPLE
        PS C:\> Invoke-TestNetConnection -Target $Target
        Explanation of what the example does
    .INPUTS
        URI
    .OUTPUTS
        Output (if any)
    .NOTES
    #>

    [CmdletBinding()]
    param (
        [Parameter()]
        [psobject]
        $Target,

        [Parameter()]
        [System.Management.Automation.Runspaces.PSSession[]]
        $PsSession
    )
    try
    {
        $ProgressPreference = 'SilentlyContinue'
        $target.TimeStamp = [datetime]::UtcNow
        $Target.HealthCheckSource = ((Get-PSCallStack)[-1].Command)

        # Create ScriptBlock
        $scriptBlock = {
            $Target = $args[0]
            $AdditionalData = @()
            $failedTarget = $false
            foreach ($endPoint in $Target.EndPoint)
            {
                foreach ($p in $Target.Protocol)
                {
                    # Run check
                    # TO DO remove wildcard
                    $uri = [system.uri]("{0}://{1}" -f $p, $endPoint.Replace('*', 'wildcardsdontwork'))
                    $tncParams = @{
                        ComputerName    = $uri.Host
                        Port            = $Uri.Port
                        WarningAction   = 'SilentlyContinue'
                        WarningVariable = 'warnVar'
                        ErrorAction     = 'SilentlyContinue'
                        ErrorVariable   = 'ErrorVar'
                    }
                    $tncResult = Test-NetConnection @tncParams

                    # Write/Clean errors
                    $tncResult | Add-Member -NotePropertyName Warnings -NotePropertyValue $warnVar -Force -ErrorAction SilentlyContinue
                    $tncResult | Add-Member -NotePropertyName Errors -NotePropertyValue $errorVar -Force -ErrorAction SilentlyContinue
                    Clear-Variable warnVar, errorVar -Force -ErrorAction SilentlyContinue

                    # Write result
                    $AdditionalData += New-Object -TypeName PSObject -Property @{
                        Source    = [System.Net.Dns]::GetHostName()
                        Resource  = $uri.OriginalString
                        Protocol  = $p
                        Status    = if ($tncResult.TcpTestSucceeded) { "Succeeded" } else { "Failed" }
                        TimeStamp = [datetime]::UtcNow
                    }
                    if ($tncResult.TcpTestSucceeded -eq $false)
                    {
                        $target.Status = 'Failed'
                        $failedTarget = $true
                    }
                }
            }
            $AdditionalData | ForEach-Object { Log-Info ("{0}: {1}" -f $_.Status, $_.Resource) }
            $target.AdditionalData = $AdditionalData
            if ($failedTarget)
            {
                $target.Status = 'Failed'
            }
            else
            {
                $target.Status = 'Succeeded'
            }
            return $target
        }

        # Run Invoke-Command
        $icmParam = @{
            ScriptBlock  = $scriptBlock
            ArgumentList = $Target
        }
        if ($PsSession)
        {
            $icmParam += @{
                Session = $PsSession
            }
        }
        Invoke-Command @icmParam
    }
    catch
    {
        throw $_
    }
    finally
    {
        $ProgressPreference = 'Continue'
    }
}

function Get-ProxyDiagnostics
{
    param(
        [Parameter()]
        [System.Management.Automation.Runspaces.PSSession]
        $PsSession,

        [Parameter()]
        [string]
        $Proxy
    )
    Log-Info "Gathering proxy diagnostics"
    $proxyConfigs = @()
    if (-not [string]::IsNullOrEmpty($Proxy))
    {
        $proxyConfigs += Test-ProxyServer -PsSession $PsSession -Proxy $Proxy
    }
    $proxyConfigs += Get-WinHttp -PsSession $PsSession
    $proxyConfigs += Get-ProxyEnvironmentVariable -PsSession $PsSession
    $proxyConfigs += Get-IEProxy -PsSession $PsSession
    Log-Info ("Proxy details: {0}" -f $(($proxyConfigs | ConvertTo-Json -Depth 20) -replace "`r`n", ''))
    return $proxyConfigs
}

function Test-ProxyServer
{
    param(
        [Parameter()]
        [System.Management.Automation.Runspaces.PSSession]
        $PsSession,

        [Parameter()]
        [string]
        $Proxy
    )

    Log-Info "Testing User specified Proxy"
    $userProxy = $Script:AzStackHciConnectivityTargets | Where-Object TargetResourceName -EQ System_User_Proxy
    $UserProxyUri = [system.uri]$Proxy
    $userProxy.EndPoint = "{0}:{1}" -f $UserProxyUri.Host, $UserProxyUri.Port
    $userProxy.Protocol = $UserProxyUri.Scheme
    $UserProxyResult = Invoke-WebRequestEx -Target $userProxy -PsSession $PsSession
    return $UserProxyResult
}

function Get-WinHttp
{
    param(
        [Parameter()]
        [System.Management.Automation.Runspaces.PSSession]
        $PsSession
    )
    Log-Info "Gathering WinHttp Proxy settings"
    $netshSb = {
        #$netsh = netsh winhttp show proxy
        @{
            Source   = $ENV:COMPUTERNAME
            Resource = netsh winhttp show proxy
            Status   = 'Succeeded'
        }
    }

    $netsh = if ($PsSession)
    {
        Invoke-Command -Session $PsSession -ScriptBlock $netshSb
        $TargetResourceName = "WinHttp_Proxy_$($PsSession.ComputerName)"
    }
    else
    {
        Invoke-Command -ScriptBlock $netshSb
        $TargetResourceName = "WinHttp_Proxy_$($ENV:COMPUTERNAME)"
    }

    $winHttpProxy = New-Object -Type ProxyDiagnostics -Property @{
        Name               = 'AzStackHci_Connectivity_Collect_Proxy_Diagnostics_winHttp'
        Title              = 'WinHttp Proxy Settings'
        Severity           = 'Informational'
        Description        = 'Collects proxy configuration for WinHttp'
        Tags               = $null
        TargetResourceID   = '767c0b95-a3c9-43dd-b112-76dff50f2c75'
        TargetResourceName = $TargetResourceName
        TargetResourceType = 'Proxy_Setting'
        Timestamp          = Get-Date
        Status             = 'Succeeded'
        AdditionalData     = @{
            source   = $netsh.Source
            resource = if ($netsh.resource -like '*Direct access (no proxy server)*') { '<Not configured>' } else { [string]$netsh.resource -replace "`r`n", "" -replace 'Current WinHTTP proxy settings:', '' -replace ' ', '' }
            status   = if ([string]::IsNullOrEmpty($netsh.status)) { 'Failed' } else { 'Succeeded' }
            detail   = $netsh.resource
        }
        HealthCheckSource  = ((Get-PSCallStack)[-1].Command)
    }
    return $winHttpProxy
}

function Get-ProxyEnvironmentVariable
{
    <#
    .SYNOPSIS
        Get signing root for https endpoint
    .DESCRIPTION
        Get signing root for https endpoint
    .EXAMPLE
        PS C:\> Get-SigningRoot -uri MicrosoftOnline.com
        Explanation of what the example does
    .INPUTS
        URI
    .OUTPUTS
        Output (if any)
    .NOTES
    #>

    param (
        [Parameter()]
        [System.Management.Automation.Runspaces.PSSession]
        $PsSession
    )
    Log-Info "Gathering Proxy settings from environment variables"

    $envProxySb = {
        Foreach ($num in 0..2)
        {
            Foreach ($varName in 'https_proxy', 'http_proxy')
            {
                $environmentValue = [System.Environment]::GetEnvironmentVariable("$varName", $num)
                $scope = switch ($num)
                {
                    2 { 'machine' }
                    1 { 'user' }
                    0 { 'process' }
                }
                @{
                    Source   = "{0}_{1}_{2}" -f $ENV:COMPUTERNAME, $varName, $scope
                    Resource = if ($environmentValue) { $environmentValue } else { '<Not configured>' }
                    Status   = 'Succeeded'
                }
            }
        }
    }
    $EnvironmentProxyOutput = if ($PsSession)
    {
        Invoke-Command -Session $PsSession -ScriptBlock $envProxySb
        $TargetResourceName = "Environment_Proxy_$($PsSession.ComputerName)"
        $Source = $PsSession.ComputerName
    }
    else
    {
        Invoke-Command -ScriptBlock $envProxySb
        $TargetResourceName = "Environment_Proxy_$($ENV:COMPUTERNAME)"
        $Source = $ENV:COMPUTERNAME
    }

    $EnvProxy = New-Object -Type ProxyDiagnostics -Property @{
        Name               = 'AzStackHci_Connectivity_Collect_Proxy_Diagnostics_Environment'
        Title              = 'Environment Proxy Settings'
        Severity           = 'Informational'
        Description        = 'Collects proxy configuration from environment variables'
        Tags               = $null
        TargetResourceID   = 'cb019485-676c-4c7d-98a8-fde6e5f35dfb'
        TargetResourceName = $TargetResourceName
        TargetResourceType = 'Proxy_Setting'
        Timestamp          = Get-Date
        Status             = 'Succeeded'
        AdditionalData     = $EnvironmentProxyOutput
        HealthCheckSource  = ((Get-PSCallStack)[-1].Command)
    }
    return $EnvProxy
}

function Get-IEProxy
{
    <#
    .SYNOPSIS
        Get signing root for https endpoint
    .DESCRIPTION
        Get signing root for https endpoint
    .EXAMPLE
        PS C:\> Get-SigningRoot -uri MicrosoftOnline.com
        Explanation of what the example does
    .INPUTS
        URI
    .OUTPUTS
        Output (if any)
    .NOTES
    [System.Net.WebProxy]::GetDefaultProxy()
        Address :
        BypassProxyOnLocal : False
        BypassList : {}
        Credentials :
        UseDefaultCredentials : False
        BypassArrayList : {}
    #>

    [CmdletBinding()]
    param (
        [Parameter()]
        [System.Management.Automation.Runspaces.PSSession]
        $PsSession
    )
    Log-Info "Gathering IE Proxy settings"
    $ieProxySb = {
        $ErrorActionPreference = 'SilentlyContinue'
        if ($PSVersionTable['Platform'] -eq 'Win32NT' -or $PSVersionTable['PSEdition'] -eq 'Desktop' )
        {
            $IeProxySettings = Get-ItemProperty -Path 'HKCU:\Software\Microsoft\Windows\CurrentVersion\Internet Settings' | Select-Object ProxyServer, ProxyEnable
            @{
                Source   = "$($ENV:COMPUTERNAME)"
                Resource = if ([string]::IsNullOrEmpty($IeProxySettings.ProxyServer) -and $IeProxySettings.ProxyEnable -eq 0) { '<Not configured>' } else { "{0} (Enabled:{1})" -f $IeProxySettings.ProxyServer, $IeProxySettings.ProxyEnable }
                Detail   = $IeProxySettings
                Status   = 'Succeeded'
            }
        }
    }
    $AdditionalData = if ($PsSession)
    {
        Invoke-Command -Session $PsSession -ScriptBlock $ieProxySb
        $TargetResourceName = "IE_Proxy_$($PsSession.ComputerName)"
    }
    else
    {
        Invoke-Command -ScriptBlock $ieProxySb
        $TargetResourceName = "IE_Proxy_$($ENV:COMPUTERNAME)"
    }

    if (-not $AdditionalData)
    {
        Log-Info "No IE Proxy settings available"
        return $null
    }
    else
    {
        $ieProxy = New-Object -Type ProxyDiagnostics -Property @{
            Name               = 'AzStackHci_Connectivity_Collect_Proxy_Diagnostics_IEProxy'
            Title              = 'IE Proxy Settings'
            Severity           = 'Informational'
            Description        = 'Collects Proxy configuration from IE'
            Tags               = $null
            TargetResourceID   = 'fe961ba6-295d-4880-82aa-2dd7322658d5'
            TargetResourceName = $TargetResourceName
            TargetResourceType = 'Proxy_Setting'
            Timestamp          = Get-Date
            Status             = 'Succeeded'
            AdditionalData     = $AdditionalData
            HealthCheckSource  = ((Get-PSCallStack)[-1].Command)
        }
        return $ieProxy
    }

}

function Write-FailedUrls
{
    [CmdletBinding()]
    param (
        $result
    )
    if (-not [string]::IsNullOrEmpty($Global:AzStackHciEnvironmentLogFile))
    {
        $file = Join-Path -Path (Split-Path $Global:AzStackHciEnvironmentLogFile -Parent) -ChildPath FailedUrls.txt
    }
    $failedUrls = $result.AdditionalData | Where-Object Status -NE Succeeded | Select-Object -ExpandProperty Resource
    if ($failedUrls.count -gt 0)
    {
        Log-Info ("[Over]Writing {0} to {1}" -f ($failedUrls -split ','), $file)
        $failedUrls | Out-File $file -Force
        Log-Info "`nFailed Urls log: $file" -ConsoleOut
    }
}
# SIG # Begin signature block
# MIInuwYJKoZIhvcNAQcCoIInrDCCJ6gCAQExDzANBglghkgBZQMEAgEFADB5Bgor
# BgEEAYI3AgEEoGswaTA0BgorBgEEAYI3AgEeMCYCAwEAAAQQH8w7YFlLCE63JNLG
# KX7zUQIBAAIBAAIBAAIBAAIBADAxMA0GCWCGSAFlAwQCAQUABCAc7ajVPWFmOOMD
# iBIeGe79vCn3xu0SRw0d8T2azYhtsqCCDYUwggYDMIID66ADAgECAhMzAAACU+OD
# 3pbexW7MAAAAAAJTMA0GCSqGSIb3DQEBCwUAMH4xCzAJBgNVBAYTAlVTMRMwEQYD
# VQQIEwpXYXNoaW5ndG9uMRAwDgYDVQQHEwdSZWRtb25kMR4wHAYDVQQKExVNaWNy
# b3NvZnQgQ29ycG9yYXRpb24xKDAmBgNVBAMTH01pY3Jvc29mdCBDb2RlIFNpZ25p
# bmcgUENBIDIwMTEwHhcNMjEwOTAyMTgzMzAwWhcNMjIwOTAxMTgzMzAwWjB0MQsw
# CQYDVQQGEwJVUzETMBEGA1UECBMKV2FzaGluZ3RvbjEQMA4GA1UEBxMHUmVkbW9u
# ZDEeMBwGA1UEChMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMR4wHAYDVQQDExVNaWNy
# b3NvZnQgQ29ycG9yYXRpb24wggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIB
# AQDLhxHwq3OhH+4J+SX4qS/VQG8HybccH7tnG+BUqrXubfGuDFYPZ29uCuHfQlO1
# lygLgMpJ4Geh6/6poQ5VkDKfVssn6aA1PCzIh8iOPMQ9Mju3sLF9Sn+Pzuaie4BN
# rp0MuZLDEXgVYx2WNjmzqcxC7dY9SC3znOh5qUy2vnmWygC7b9kj0d3JrGtjc5q5
# 0WfV3WLXAQHkeRROsJFBZfXFGoSvRljFFUAjU/zdhP92P+1JiRRRikVy/sqIhMDY
# +7tVdzlE2fwnKOv9LShgKeyEevgMl0B1Fq7E2YeBZKF6KlhmYi9CE1350cnTUoU4
# YpQSnZo0YAnaenREDLfFGKTdAgMBAAGjggGCMIIBfjAfBgNVHSUEGDAWBgorBgEE
# AYI3TAgBBggrBgEFBQcDAzAdBgNVHQ4EFgQUlZpLWIccXoxessA/DRbe26glhEMw
# VAYDVR0RBE0wS6RJMEcxLTArBgNVBAsTJE1pY3Jvc29mdCBJcmVsYW5kIE9wZXJh
# dGlvbnMgTGltaXRlZDEWMBQGA1UEBRMNMjMwMDEyKzQ2NzU5ODAfBgNVHSMEGDAW
# gBRIbmTlUAXTgqoXNzcitW2oynUClTBUBgNVHR8ETTBLMEmgR6BFhkNodHRwOi8v
# d3d3Lm1pY3Jvc29mdC5jb20vcGtpb3BzL2NybC9NaWNDb2RTaWdQQ0EyMDExXzIw
# MTEtMDctMDguY3JsMGEGCCsGAQUFBwEBBFUwUzBRBggrBgEFBQcwAoZFaHR0cDov
# L3d3dy5taWNyb3NvZnQuY29tL3BraW9wcy9jZXJ0cy9NaWNDb2RTaWdQQ0EyMDEx
# XzIwMTEtMDctMDguY3J0MAwGA1UdEwEB/wQCMAAwDQYJKoZIhvcNAQELBQADggIB
# AKVY+yKcJVVxf9W2vNkL5ufjOpqcvVOOOdVyjy1dmsO4O8khWhqrecdVZp09adOZ
# 8kcMtQ0U+oKx484Jg11cc4Ck0FyOBnp+YIFbOxYCqzaqMcaRAgy48n1tbz/EFYiF
# zJmMiGnlgWFCStONPvQOBD2y/Ej3qBRnGy9EZS1EDlRN/8l5Rs3HX2lZhd9WuukR
# bUk83U99TPJyo12cU0Mb3n1HJv/JZpwSyqb3O0o4HExVJSkwN1m42fSVIVtXVVSa
# YZiVpv32GoD/dyAS/gyplfR6FI3RnCOomzlycSqoz0zBCPFiCMhVhQ6qn+J0GhgR
# BJvGKizw+5lTfnBFoqKZJDROz+uGDl9tw6JvnVqAZKGrWv/CsYaegaPePFrAVSxA
# yUwOFTkAqtNC8uAee+rv2V5xLw8FfpKJ5yKiMKnCKrIaFQDr5AZ7f2ejGGDf+8Tz
# OiK1AgBvOW3iTEEa/at8Z4+s1CmnEAkAi0cLjB72CJedU1LAswdOCWM2MDIZVo9j
# 0T74OkJLTjPd3WNEyw0rBXTyhlbYQsYt7ElT2l2TTlF5EmpVixGtj4ChNjWoKr9y
# TAqtadd2Ym5FNB792GzwNwa631BPCgBJmcRpFKXt0VEQq7UXVNYBiBRd+x4yvjqq
# 5aF7XC5nXCgjbCk7IXwmOphNuNDNiRq83Ejjnc7mxrJGMIIHejCCBWKgAwIBAgIK
# YQ6Q0gAAAAAAAzANBgkqhkiG9w0BAQsFADCBiDELMAkGA1UEBhMCVVMxEzARBgNV
# BAgTCldhc2hpbmd0b24xEDAOBgNVBAcTB1JlZG1vbmQxHjAcBgNVBAoTFU1pY3Jv
# c29mdCBDb3Jwb3JhdGlvbjEyMDAGA1UEAxMpTWljcm9zb2Z0IFJvb3QgQ2VydGlm
# aWNhdGUgQXV0aG9yaXR5IDIwMTEwHhcNMTEwNzA4MjA1OTA5WhcNMjYwNzA4MjEw
# OTA5WjB+MQswCQYDVQQGEwJVUzETMBEGA1UECBMKV2FzaGluZ3RvbjEQMA4GA1UE
# BxMHUmVkbW9uZDEeMBwGA1UEChMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMSgwJgYD
# VQQDEx9NaWNyb3NvZnQgQ29kZSBTaWduaW5nIFBDQSAyMDExMIICIjANBgkqhkiG
# 9w0BAQEFAAOCAg8AMIICCgKCAgEAq/D6chAcLq3YbqqCEE00uvK2WCGfQhsqa+la
# UKq4BjgaBEm6f8MMHt03a8YS2AvwOMKZBrDIOdUBFDFC04kNeWSHfpRgJGyvnkmc
# 6Whe0t+bU7IKLMOv2akrrnoJr9eWWcpgGgXpZnboMlImEi/nqwhQz7NEt13YxC4D
# dato88tt8zpcoRb0RrrgOGSsbmQ1eKagYw8t00CT+OPeBw3VXHmlSSnnDb6gE3e+
# lD3v++MrWhAfTVYoonpy4BI6t0le2O3tQ5GD2Xuye4Yb2T6xjF3oiU+EGvKhL1nk
# kDstrjNYxbc+/jLTswM9sbKvkjh+0p2ALPVOVpEhNSXDOW5kf1O6nA+tGSOEy/S6
# A4aN91/w0FK/jJSHvMAhdCVfGCi2zCcoOCWYOUo2z3yxkq4cI6epZuxhH2rhKEmd
# X4jiJV3TIUs+UsS1Vz8kA/DRelsv1SPjcF0PUUZ3s/gA4bysAoJf28AVs70b1FVL
# 5zmhD+kjSbwYuER8ReTBw3J64HLnJN+/RpnF78IcV9uDjexNSTCnq47f7Fufr/zd
# sGbiwZeBe+3W7UvnSSmnEyimp31ngOaKYnhfsi+E11ecXL93KCjx7W3DKI8sj0A3
# T8HhhUSJxAlMxdSlQy90lfdu+HggWCwTXWCVmj5PM4TasIgX3p5O9JawvEagbJjS
# 4NaIjAsCAwEAAaOCAe0wggHpMBAGCSsGAQQBgjcVAQQDAgEAMB0GA1UdDgQWBBRI
# bmTlUAXTgqoXNzcitW2oynUClTAZBgkrBgEEAYI3FAIEDB4KAFMAdQBiAEMAQTAL
# BgNVHQ8EBAMCAYYwDwYDVR0TAQH/BAUwAwEB/zAfBgNVHSMEGDAWgBRyLToCMZBD
# uRQFTuHqp8cx0SOJNDBaBgNVHR8EUzBRME+gTaBLhklodHRwOi8vY3JsLm1pY3Jv
# c29mdC5jb20vcGtpL2NybC9wcm9kdWN0cy9NaWNSb29DZXJBdXQyMDExXzIwMTFf
# MDNfMjIuY3JsMF4GCCsGAQUFBwEBBFIwUDBOBggrBgEFBQcwAoZCaHR0cDovL3d3
# dy5taWNyb3NvZnQuY29tL3BraS9jZXJ0cy9NaWNSb29DZXJBdXQyMDExXzIwMTFf
# MDNfMjIuY3J0MIGfBgNVHSAEgZcwgZQwgZEGCSsGAQQBgjcuAzCBgzA/BggrBgEF
# BQcCARYzaHR0cDovL3d3dy5taWNyb3NvZnQuY29tL3BraW9wcy9kb2NzL3ByaW1h
# cnljcHMuaHRtMEAGCCsGAQUFBwICMDQeMiAdAEwAZQBnAGEAbABfAHAAbwBsAGkA
# YwB5AF8AcwB0AGEAdABlAG0AZQBuAHQALiAdMA0GCSqGSIb3DQEBCwUAA4ICAQBn
# 8oalmOBUeRou09h0ZyKbC5YR4WOSmUKWfdJ5DJDBZV8uLD74w3LRbYP+vj/oCso7
# v0epo/Np22O/IjWll11lhJB9i0ZQVdgMknzSGksc8zxCi1LQsP1r4z4HLimb5j0b
# pdS1HXeUOeLpZMlEPXh6I/MTfaaQdION9MsmAkYqwooQu6SpBQyb7Wj6aC6VoCo/
# KmtYSWMfCWluWpiW5IP0wI/zRive/DvQvTXvbiWu5a8n7dDd8w6vmSiXmE0OPQvy
# CInWH8MyGOLwxS3OW560STkKxgrCxq2u5bLZ2xWIUUVYODJxJxp/sfQn+N4sOiBp
# mLJZiWhub6e3dMNABQamASooPoI/E01mC8CzTfXhj38cbxV9Rad25UAqZaPDXVJi
# hsMdYzaXht/a8/jyFqGaJ+HNpZfQ7l1jQeNbB5yHPgZ3BtEGsXUfFL5hYbXw3MYb
# BL7fQccOKO7eZS/sl/ahXJbYANahRr1Z85elCUtIEJmAH9AAKcWxm6U/RXceNcbS
# oqKfenoi+kiVH6v7RyOA9Z74v2u3S5fi63V4GuzqN5l5GEv/1rMjaHXmr/r8i+sL
# gOppO6/8MO0ETI7f33VtY5E90Z1WTk+/gFcioXgRMiF670EKsT/7qMykXcGhiJtX
# cVZOSEXAQsmbdlsKgEhr/Xmfwb1tbWrJUnMTDXpQzTGCGYwwghmIAgEBMIGVMH4x
# CzAJBgNVBAYTAlVTMRMwEQYDVQQIEwpXYXNoaW5ndG9uMRAwDgYDVQQHEwdSZWRt
# b25kMR4wHAYDVQQKExVNaWNyb3NvZnQgQ29ycG9yYXRpb24xKDAmBgNVBAMTH01p
# Y3Jvc29mdCBDb2RlIFNpZ25pbmcgUENBIDIwMTECEzMAAAJT44Pelt7FbswAAAAA
# AlMwDQYJYIZIAWUDBAIBBQCgga4wGQYJKoZIhvcNAQkDMQwGCisGAQQBgjcCAQQw
# HAYKKwYBBAGCNwIBCzEOMAwGCisGAQQBgjcCARUwLwYJKoZIhvcNAQkEMSIEIMwr
# +P1Fdx9KDyFLZR9h/xt5CNMqlJHEdwTBpN+Kpo3IMEIGCisGAQQBgjcCAQwxNDAy
# oBSAEgBNAGkAYwByAG8AcwBvAGYAdKEagBhodHRwOi8vd3d3Lm1pY3Jvc29mdC5j
# b20wDQYJKoZIhvcNAQEBBQAEggEAQjvxzLnMnSZlpyDC7MD3Pk5/0PepHp4ntZGj
# 2+tgFYmPKzszr3EB3H1qANy49FcvpFlq1kbcsJXvLfxbXITOEoTsBHNR5JcDfvq8
# U4TB7UDIvAk1bapmHRbSGEUYEpyuvruO+YvutO4Vcrnp/IbPdO3F5Qr7fqgOompp
# eeqkIC6K94Vt+Nzz3+DKWKOsoyD4eV9Tx97StVK1a/tcVOMXCkSgBYsN1bMaIYXD
# FcbO9h/2Mibwhw72xtAA3nHzMHSXGM7Fcy3DvKchBUGXPqT9Xmzly7AcVgZsKvvd
# M8uGI57NkoYvv6cbHj7hFxO9jsyy+9yk5FDRis+5iAEKnz8aZqGCFxYwghcSBgor
# BgEEAYI3AwMBMYIXAjCCFv4GCSqGSIb3DQEHAqCCFu8wghbrAgEDMQ8wDQYJYIZI
# AWUDBAIBBQAwggFZBgsqhkiG9w0BCRABBKCCAUgEggFEMIIBQAIBAQYKKwYBBAGE
# WQoDATAxMA0GCWCGSAFlAwQCAQUABCA1L/2AN01txliB+JErWriq3nQfQ2TGMBj6
# bTN9ZgLwIAIGYrtRCf+5GBMyMDIyMDcwNDEyMzA1My43MjdaMASAAgH0oIHYpIHV
# MIHSMQswCQYDVQQGEwJVUzETMBEGA1UECBMKV2FzaGluZ3RvbjEQMA4GA1UEBxMH
# UmVkbW9uZDEeMBwGA1UEChMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMS0wKwYDVQQL
# EyRNaWNyb3NvZnQgSXJlbGFuZCBPcGVyYXRpb25zIExpbWl0ZWQxJjAkBgNVBAsT
# HVRoYWxlcyBUU1MgRVNOOkZDNDEtNEJENC1EMjIwMSUwIwYDVQQDExxNaWNyb3Nv
# ZnQgVGltZS1TdGFtcCBTZXJ2aWNloIIRZTCCBxQwggT8oAMCAQICEzMAAAGOWdtG
# AKgQlMwAAQAAAY4wDQYJKoZIhvcNAQELBQAwfDELMAkGA1UEBhMCVVMxEzARBgNV
# BAgTCldhc2hpbmd0b24xEDAOBgNVBAcTB1JlZG1vbmQxHjAcBgNVBAoTFU1pY3Jv
# c29mdCBDb3Jwb3JhdGlvbjEmMCQGA1UEAxMdTWljcm9zb2Z0IFRpbWUtU3RhbXAg
# UENBIDIwMTAwHhcNMjExMDI4MTkyNzQ1WhcNMjMwMTI2MTkyNzQ1WjCB0jELMAkG
# A1UEBhMCVVMxEzARBgNVBAgTCldhc2hpbmd0b24xEDAOBgNVBAcTB1JlZG1vbmQx
# HjAcBgNVBAoTFU1pY3Jvc29mdCBDb3Jwb3JhdGlvbjEtMCsGA1UECxMkTWljcm9z
# b2Z0IElyZWxhbmQgT3BlcmF0aW9ucyBMaW1pdGVkMSYwJAYDVQQLEx1UaGFsZXMg
# VFNTIEVTTjpGQzQxLTRCRDQtRDIyMDElMCMGA1UEAxMcTWljcm9zb2Z0IFRpbWUt
# U3RhbXAgU2VydmljZTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAKoj
# AqujjMy2ucK7XH+wX/X9Vl1vZKamzgc4Dyb2hi62Ru7cIMKk0Vn9RZI6SSgThuUD
# yEcu2uiBVQMtFvrQWhV+CJ+A2wX9rRrm8mPfoUVPoUXsDyR+QmDr6T4e+xXxjOt/
# jpcEV6eWBEerQtFkSp95q8lqbeAsAA7hr9Cw9kI54YYLUVYnbIg55/fmi4zLjWqV
# IbLRqgq+yXEGbdGaz1B1v06kycpnlNXqoDaKxG03nelEMi2k1QJoVzUFwwoX2udu
# p1u0UOy+LV1/S3NKILogkpD5buXazQOjTPM/lF0DgB8VXyEF5ovmN0ldoa9nXMW8
# vZ5U82L3+GQ6+VqXMLe7U3USCYm1x7F1jCq5js4pYhg06C8d+Gv3LWRODTi55ayk
# FjfWRvjsec0WqytRIUoWoTNLkDYW+gSY6d/nNHjczBSdqi2ag6dv92JeUPuJPjAx
# y04qT+lQXcXHVX3eJoK1U8d2nzuSjX4DJ4Bhn4UmsBq2kVtvBIayzrKZiMYovdhO
# 7453CdrXI4SwowQK1aT4d3GRuYN2VcuYogGqA2rMKTYJzBQCuVJ9a3ivjBYT4vYj
# J71D8LUwwybeWBA+QwE95gVMaeUB97e0YWcACTS1i7aU3hhe7m/NbEimL9mq3Wsw
# HvVy0tdLVdqDj63J4hic5V1u1T78akDcXvJQgwNtAgMBAAGjggE2MIIBMjAdBgNV
# HQ4EFgQU7EH5M/YE+ODf+RvLzR2snqfmleQwHwYDVR0jBBgwFoAUn6cVXQBeYl2D
# 9OXSZacbUzUZ6XIwXwYDVR0fBFgwVjBUoFKgUIZOaHR0cDovL3d3dy5taWNyb3Nv
# ZnQuY29tL3BraW9wcy9jcmwvTWljcm9zb2Z0JTIwVGltZS1TdGFtcCUyMFBDQSUy
# MDIwMTAoMSkuY3JsMGwGCCsGAQUFBwEBBGAwXjBcBggrBgEFBQcwAoZQaHR0cDov
# L3d3dy5taWNyb3NvZnQuY29tL3BraW9wcy9jZXJ0cy9NaWNyb3NvZnQlMjBUaW1l
# LVN0YW1wJTIwUENBJTIwMjAxMCgxKS5jcnQwDAYDVR0TAQH/BAIwADATBgNVHSUE
# DDAKBggrBgEFBQcDCDANBgkqhkiG9w0BAQsFAAOCAgEANVCvccyHk5SoUmy59G3p
# EeYGIemwdV0KZbgqggNebJGd+1IpWhScPPhpJQy85TYUj9pjojs1cgqvJJKap31H
# NNWWgXs0MYO+6nr49ojMoN/WCX3ogiIcWDhboMHqWKzzvDJQf6Lnv1YSIg29XjWE
# 5T0pr96WpbILZK29KKNBdLlpl+BEFRikaNFBDbWXrVSMWtCfQ6VHY0Fj3hIfXBDP
# kYBNuucOVgFW/ljcdIloheIk2wpq1mlRDl/dnTagZvW09VO5xsDeQsoKTQIBGmJ6
# 0zMdTeAI8TmwAgzeQ3bxpbvztA3zFlXOqpOoigxQulqV0EpDJa5VyCPzYaftPp6F
# OrXxKRyi7e32JvaH+Yv0KJnAsKP3pIjgo2JLad/d6L6AtTtri7Wy5zFZROa2gSwT
# UmyDWekC8YgONZV51VSyMw4oVC/DFPQjLxuLHW4ZNhV/M767D+T3gSMNX2npzGbs
# 9Fd1FwrVOTpMeX5oqFooi2UgotZY2sV/gRMEIopwovrxOfW02CORW7kfLQ7hi4lb
# vyUqVRV681jD9ip9dbAiwBhI6iWFJjtbUWNvSnex3CI9p4kgdD0Dgo2JZwp8sJw4
# p6ktQl70bIrI1ZUtUaeE5rpLPqRsYjBsxefM3G/oaBSsjjbi92/rYMUwM97BdwVV
# /bpPTORfjhKHsi8hny3pDQIwggdxMIIFWaADAgECAhMzAAAAFcXna54Cm0mZAAAA
# AAAVMA0GCSqGSIb3DQEBCwUAMIGIMQswCQYDVQQGEwJVUzETMBEGA1UECBMKV2Fz
# aGluZ3RvbjEQMA4GA1UEBxMHUmVkbW9uZDEeMBwGA1UEChMVTWljcm9zb2Z0IENv
# cnBvcmF0aW9uMTIwMAYDVQQDEylNaWNyb3NvZnQgUm9vdCBDZXJ0aWZpY2F0ZSBB
# dXRob3JpdHkgMjAxMDAeFw0yMTA5MzAxODIyMjVaFw0zMDA5MzAxODMyMjVaMHwx
# CzAJBgNVBAYTAlVTMRMwEQYDVQQIEwpXYXNoaW5ndG9uMRAwDgYDVQQHEwdSZWRt
# b25kMR4wHAYDVQQKExVNaWNyb3NvZnQgQ29ycG9yYXRpb24xJjAkBgNVBAMTHU1p
# Y3Jvc29mdCBUaW1lLVN0YW1wIFBDQSAyMDEwMIICIjANBgkqhkiG9w0BAQEFAAOC
# Ag8AMIICCgKCAgEA5OGmTOe0ciELeaLL1yR5vQ7VgtP97pwHB9KpbE51yMo1V/YB
# f2xK4OK9uT4XYDP/XE/HZveVU3Fa4n5KWv64NmeFRiMMtY0Tz3cywBAY6GB9alKD
# RLemjkZrBxTzxXb1hlDcwUTIcVxRMTegCjhuje3XD9gmU3w5YQJ6xKr9cmmvHaus
# 9ja+NSZk2pg7uhp7M62AW36MEBydUv626GIl3GoPz130/o5Tz9bshVZN7928jaTj
# kY+yOSxRnOlwaQ3KNi1wjjHINSi947SHJMPgyY9+tVSP3PoFVZhtaDuaRr3tpK56
# KTesy+uDRedGbsoy1cCGMFxPLOJiss254o2I5JasAUq7vnGpF1tnYN74kpEeHT39
# IM9zfUGaRnXNxF803RKJ1v2lIH1+/NmeRd+2ci/bfV+AutuqfjbsNkz2K26oElHo
# vwUDo9Fzpk03dJQcNIIP8BDyt0cY7afomXw/TNuvXsLz1dhzPUNOwTM5TI4CvEJo
# LhDqhFFG4tG9ahhaYQFzymeiXtcodgLiMxhy16cg8ML6EgrXY28MyTZki1ugpoMh
# XV8wdJGUlNi5UPkLiWHzNgY1GIRH29wb0f2y1BzFa/ZcUlFdEtsluq9QBXpsxREd
# cu+N+VLEhReTwDwV2xo3xwgVGD94q0W29R6HXtqPnhZyacaue7e3PmriLq0CAwEA
# AaOCAd0wggHZMBIGCSsGAQQBgjcVAQQFAgMBAAEwIwYJKwYBBAGCNxUCBBYEFCqn
# Uv5kxJq+gpE8RjUpzxD/LwTuMB0GA1UdDgQWBBSfpxVdAF5iXYP05dJlpxtTNRnp
# cjBcBgNVHSAEVTBTMFEGDCsGAQQBgjdMg30BATBBMD8GCCsGAQUFBwIBFjNodHRw
# Oi8vd3d3Lm1pY3Jvc29mdC5jb20vcGtpb3BzL0RvY3MvUmVwb3NpdG9yeS5odG0w
# EwYDVR0lBAwwCgYIKwYBBQUHAwgwGQYJKwYBBAGCNxQCBAweCgBTAHUAYgBDAEEw
# CwYDVR0PBAQDAgGGMA8GA1UdEwEB/wQFMAMBAf8wHwYDVR0jBBgwFoAU1fZWy4/o
# olxiaNE9lJBb186aGMQwVgYDVR0fBE8wTTBLoEmgR4ZFaHR0cDovL2NybC5taWNy
# b3NvZnQuY29tL3BraS9jcmwvcHJvZHVjdHMvTWljUm9vQ2VyQXV0XzIwMTAtMDYt
# MjMuY3JsMFoGCCsGAQUFBwEBBE4wTDBKBggrBgEFBQcwAoY+aHR0cDovL3d3dy5t
# aWNyb3NvZnQuY29tL3BraS9jZXJ0cy9NaWNSb29DZXJBdXRfMjAxMC0wNi0yMy5j
# cnQwDQYJKoZIhvcNAQELBQADggIBAJ1VffwqreEsH2cBMSRb4Z5yS/ypb+pcFLY+
# TkdkeLEGk5c9MTO1OdfCcTY/2mRsfNB1OW27DzHkwo/7bNGhlBgi7ulmZzpTTd2Y
# urYeeNg2LpypglYAA7AFvonoaeC6Ce5732pvvinLbtg/SHUB2RjebYIM9W0jVOR4
# U3UkV7ndn/OOPcbzaN9l9qRWqveVtihVJ9AkvUCgvxm2EhIRXT0n4ECWOKz3+SmJ
# w7wXsFSFQrP8DJ6LGYnn8AtqgcKBGUIZUnWKNsIdw2FzLixre24/LAl4FOmRsqlb
# 30mjdAy87JGA0j3mSj5mO0+7hvoyGtmW9I/2kQH2zsZ0/fZMcm8Qq3UwxTSwethQ
# /gpY3UA8x1RtnWN0SCyxTkctwRQEcb9k+SS+c23Kjgm9swFXSVRk2XPXfx5bRAGO
# WhmRaw2fpCjcZxkoJLo4S5pu+yFUa2pFEUep8beuyOiJXk+d0tBMdrVXVAmxaQFE
# fnyhYWxz/gq77EFmPWn9y8FBSX5+k77L+DvktxW/tM4+pTFRhLy/AsGConsXHRWJ
# jXD+57XQKBqJC4822rpM+Zv/Cuk0+CQ1ZyvgDbjmjJnW4SLq8CdCPSWU5nR0W2rR
# nj7tfqAxM328y+l7vzhwRNGQ8cirOoo6CGJ/2XBjU02N7oJtpQUQwXEGahC0HVUz
# WLOhcGbyoYIC1DCCAj0CAQEwggEAoYHYpIHVMIHSMQswCQYDVQQGEwJVUzETMBEG
# A1UECBMKV2FzaGluZ3RvbjEQMA4GA1UEBxMHUmVkbW9uZDEeMBwGA1UEChMVTWlj
# cm9zb2Z0IENvcnBvcmF0aW9uMS0wKwYDVQQLEyRNaWNyb3NvZnQgSXJlbGFuZCBP
# cGVyYXRpb25zIExpbWl0ZWQxJjAkBgNVBAsTHVRoYWxlcyBUU1MgRVNOOkZDNDEt
# NEJENC1EMjIwMSUwIwYDVQQDExxNaWNyb3NvZnQgVGltZS1TdGFtcCBTZXJ2aWNl
# oiMKAQEwBwYFKw4DAhoDFQA9YivqT04R6oKWucbD5omK7llbjKCBgzCBgKR+MHwx
# CzAJBgNVBAYTAlVTMRMwEQYDVQQIEwpXYXNoaW5ndG9uMRAwDgYDVQQHEwdSZWRt
# b25kMR4wHAYDVQQKExVNaWNyb3NvZnQgQ29ycG9yYXRpb24xJjAkBgNVBAMTHU1p
# Y3Jvc29mdCBUaW1lLVN0YW1wIFBDQSAyMDEwMA0GCSqGSIb3DQEBBQUAAgUA5m0P
# nzAiGA8yMDIyMDcwNDE1MDUwM1oYDzIwMjIwNzA1MTUwNTAzWjB0MDoGCisGAQQB
# hFkKBAExLDAqMAoCBQDmbQ+fAgEAMAcCAQACAiSPMAcCAQACAhILMAoCBQDmbmEf
# AgEAMDYGCisGAQQBhFkKBAIxKDAmMAwGCisGAQQBhFkKAwKgCjAIAgEAAgMHoSCh
# CjAIAgEAAgMBhqAwDQYJKoZIhvcNAQEFBQADgYEAZ/y2+njM+4uqvjL5q9srUfGz
# jVofL9yaCTHeysuexdqTMPxv3WgOBkJ9Hgrc2MPbmbuy/ImzQ8T4O/nm5EJgZWm/
# IU8w5NhprcaDeYdrq4h5/MTP0O5d9gZbjNLR1XSLIHLuJBHKDMXMszxv5Zq7LnAO
# UGSb7ebSh0YZIEQeAhsxggQNMIIECQIBATCBkzB8MQswCQYDVQQGEwJVUzETMBEG
# A1UECBMKV2FzaGluZ3RvbjEQMA4GA1UEBxMHUmVkbW9uZDEeMBwGA1UEChMVTWlj
# cm9zb2Z0IENvcnBvcmF0aW9uMSYwJAYDVQQDEx1NaWNyb3NvZnQgVGltZS1TdGFt
# cCBQQ0EgMjAxMAITMwAAAY5Z20YAqBCUzAABAAABjjANBglghkgBZQMEAgEFAKCC
# AUowGgYJKoZIhvcNAQkDMQ0GCyqGSIb3DQEJEAEEMC8GCSqGSIb3DQEJBDEiBCDL
# Ez/xgO/OqG8U2O3kZW1Ufm0IpPlwZRAuH4MZKyvb+DCB+gYLKoZIhvcNAQkQAi8x
# geowgecwgeQwgb0EIL0FjyE74oGlLlefn/5VrNwV2cCf5dZn/snpbuZ15sQlMIGY
# MIGApH4wfDELMAkGA1UEBhMCVVMxEzARBgNVBAgTCldhc2hpbmd0b24xEDAOBgNV
# BAcTB1JlZG1vbmQxHjAcBgNVBAoTFU1pY3Jvc29mdCBDb3Jwb3JhdGlvbjEmMCQG
# A1UEAxMdTWljcm9zb2Z0IFRpbWUtU3RhbXAgUENBIDIwMTACEzMAAAGOWdtGAKgQ
# lMwAAQAAAY4wIgQg7kNyeotUj+kf7/s6vFEsQ3eKPITr/uAdkmxls0s5eP8wDQYJ
# KoZIhvcNAQELBQAEggIADhiTxT21n9Jg/GR8JupXGxyeG5KQ4BqrtwmAis7J4JWS
# QjZEOBw1IICQIRDXDIv+Yt/g1GMNfKN9GSVWQgg0GRU6FwbRFKxBk231z/ivAbVw
# oPNrGfbzS7XdJJfy0iCgwqIOKo72ZBIxkcrQz25n+kmQjQ2dDnj3DgRaGpeIX0QG
# WcilBotDOFXrsTQch+o56J65r9GT4VicDgdoa3n/+RD77O+z1iiQLCkzIF4wPVcW
# flilkYAvdjXfYz6kR4pIJSY5FbdVc+5Tol8be9XSGGhYxI6khma+cBguJZizQszi
# A0JK3XxMR+Z5kThC1TP94u5zmtU1tyGh2hCgchioXqgy+DGfDwYwbe8YZOB0x4w6
# ues9rZKoM8RR4A5Fwft0O36ZmJy0SJESCn/tjxnOWlHkOOiWBcdSnDFEXwlX6MdG
# fb5oZbM3S946rSPq6WlYymU6/NgTy5RrRrZtnNFkGYKcY5Gp+lIdOUFBb3Zt8FzR
# AIO76bzakcjkXkszF5WBsl2Z+B6xxzNf9S4qE/X+29G3F681gNpv7UtJIvLX0QI8
# ZVAg1gfr8hQh9TYjmOZ3yJAFSmDkeOumQtCpGOO0d7CexCPzlUke5AQuTSzsUPk8
# zVQZ/xYiezMv66OTmCs7D/FW1slR7wkloU+4ZKPTeTbDRvNGcZKe2Ligyv2z3bY=
# SIG # End signature block