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]$Mandatory # short cut property to mandatory from tags
    [bool]$System # targets for system checks such as proxy traversal
}

#Create additional classes to help with writing/report results
class Diagnostics : AzStackHciConnectivityTarget {}
class DnsResult : AzStackHciConnectivityTarget {}
class ProxyDiagnostics : AzStackHciConnectivityTarget {}

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

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

    # scriptblock to test dns resolution for each dns server
    $testDnsSb = {
        $AdditionalData = @()

        # Get local DNS servers
        $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

        if (-not $dnsServers)
        {
            $AdditionalData += New-Object PsObject -Property @{
                Resource  = 'Missing DNS Server'
                Status    = 'Failed'
                TimeStamp = [datetime]::UtcNow
                Source    = $ENV:COMPUTERNAME
                Detail = 'DNS not configured on this node.'
            }
        }
        else
        {
            foreach ($dnsServer in $dnsServers)
            {
                $dnsResult = $false
                $dnsResult = Resolve-DnsName -Name microsoft.com -Server $dnsServer -DnsOnly -ErrorAction SilentlyContinue -QuickTimeout -Type A
                $detail = "Queried dns server {0} for {1} on {2}. Result returned {3} A records: {4}, expected at least 1." -f $dnsServer, 'microsoft.com', $ENV:COMPUTERNAME, [int]($dnsResult.count), ($dnsResult.IpAddress -join ',')

                if ($dnsResult)
                {
                    if ($dnsResult[0] -is [Microsoft.DnsClient.Commands.DnsRecord])
                    {
                        $status = 'Succeeded'
                    }
                    else
                    {
                        $status = 'Failed'
                    }
                }
                else
                {
                    $status = 'Failed'
                }
                $AdditionalData += New-Object PsObject -Property @{
                    Resource  = $dnsServer
                    Status    = $status
                    TimeStamp = [datetime]::UtcNow
                    Source    = $ENV:COMPUTERNAME
                    Detail    = $detail
                }
            }
        }
        $AdditionalData
    }

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

    # build result
    $now = [datetime]::UtcNow

    # Write result to verbose log
    $testDnsServer | Foreach-Object {
        Log-Info $_.Detail -Type $(if ( $_.Status -eq 'Failed' ){ "Warning" } else { "Info" } )
    }

    $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
        EndPoint           = @("microsoft.com")
        Service            = 'System'
        Remediation        = 'https://learn.microsoft.com/en-us/azure-stack/hci/deploy/deployment-tool-checklist'
        TargetResourceID   = 'c644bad4-044d-4066-861d-ceb93b64f046'
        TargetResourceName = "Test_DNS_$TargetComputerName"
        TargetResourceType = 'DNS'
        Timestamp          = $now
        Status             = $aggregateStatus
        AdditionalData     = $testDnsServer
        HealthCheckSource  = $ENV:EnvChkrId
    }
    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 }
                }
            }
        }

        # Always add Mandatory targets
        $executionTargets += $Script:AzStackHciConnectivityTargets | Where-Object Mandatory | ForEach-Object {
            if ($PSITEM -notin $executionTargets)
            {
                $PSITEM
            }
        }

        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
        Write-Verbose ("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 HealthCheckSource -Value $ENV:EnvChkrId
                    $target.TargetResourceID = $target.EndPoint -join '_'
                    $target.TargetResourceName = $target.EndPoint -join '_'
                    $target.TargetResourceType = 'External Endpoint'
                    $Script:AzStackHciConnectivityTargets += [AzStackHciConnectivityTarget]$target
                }
            }
            catch
            {
                Log-Info -Message ("Unable to read {0}. Error: {1}" -f (Split-Path -Path $targetFile -Leaf), $_.Exception.Message) -Type Warning
            }
        }
    }
    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-SigningRootChain
{
    <#
    .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]
            $GetSslCertChainFunction = $args[3]

            # Check if helper function is locally available
            Import-Module -Name AzStackHci.EnvironmentChecker -Force -ErrorAction SilentlyContinue -Scope Local
            if (-not (Get-Command -Name Get-SslCertificateChain -ErrorAction SilentlyContinue))
            {
                throw "Cannot find Get-SslCertificateChain in AzStackHci.EnvironmentChecker.Utilities module"
            }
            else
            {
                Write-Verbose "Found Get-SslCertificateChain in AzStackHci.EnvironmentChecker.Utilities module"
                $chain = Get-SslCertificateChain -Url $Uri -Proxy $Proxy -ProxyCredential $ProxyCredential
            }
            return $chain.ChainElements.Certificate
        }
        $ChainElements = if ($PsSession)
        {
            Invoke-Command -Session $PsSession -ScriptBlock $sb -ArgumentList $Uri, $Proxy, $ProxyCredential,${function:Get-SslCertificateChain}
        }
        else
        {
            Invoke-Command -ScriptBlock $sb -ArgumentList $Uri, $Proxy, $ProxyCredential,${function:Get-SslCertificateChain}
        }
        return $ChainElements
    }
    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
    #>

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

        [Parameter()]
        [string]
        $Proxy,

        [Parameter()]
        [pscredential]
        $ProxyCredential
    )
    try
    {
        if ($Script:AzStackHciConnectivityTargets)
        {
            $rootCATarget = $Script:AzStackHciConnectivityTargets | Where-Object Name -EQ System_Check_SSL_Inspection_Detection
            if ($rootCATarget.count -ne 1)
            {
                throw "Expected 1 System_RootCA, found $($rootCATarget.count)"
            }
            Install-UtilityModule -PsSession $PsSession -CmdletName Get-SslCertificateChain
            # We have two endpoints to check, they expire 6 months apart
            # meaning we should get a warning if criteria needs to change
            # 1 only require 1 endpoint to not be re-encrypted to succeed.
            $rootCATargetUrls = @()
            $rootCATarget.EndPoint | Foreach-Object {
                foreach ($p in $rootCATarget.Protocol) {
                    $rootCATargetUrls += "{0}://{1}" -f $p,$PSITEM
                }
            }

            $AdditionalData = @()

            foreach ($rootCATargetUrl in $rootCATargetUrls) {
                Log-Info "Testing SSL chain for $rootCATargetUrl"
                [array]$ChainElements = Get-SigningRootChain -Uri $rootCATargetUrl -PsSession $PsSession -Proxy $Proxy -ProxyCredential $ProxyCredential
                # This is our canary internet endpoint, if we can't get the chain we probably don't have internet access.
                if ($null -eq $ChainElements)
                {
                    $Status = 'Failed'
                    $detail = "Failed to get certificate chain for $rootCATargetUrl. Ensure the endpoint is accessible and proxy configuration is correct."
                    Log-Info $detail -Type Warning
                }
                else
                {
                    # Remove the leaf as this will always contain O=Microsoft in its subject
                    $ChainElements = $ChainElements[1..($ChainElements.Length-1)]
                    $subjectMatchCount = 0
                    # We check for 2 expected subjects and only require 1 to succeed
                    $rootCATarget.Tags.ExpectedSubject | Foreach-Object {
                        if ($ChainElements.Subject -match $PSITEM)
                        {
                            $subjectMatchCount++
                        }
                    }
                    if ($subjectMatchCount -ge 1)
                    {
                        $Status = 'Succeeded'
                        $detail = "Expected at least 1 chain certificate subject to match $($rootCATarget.Tags.ExpectedSubject -join ' or '). $subjectMatchCount matched."
                        Log-Info $detail
                    }
                    else
                    {
                        $Status = 'Failed'
                        $detail = "Expected at least 1 chain certificate subjects to match $($rootCATarget.Tags.ExpectedSubject -join ' or '). $subjectMatchCount matched. Actual subjects $($ChainElements.Subject -join ','). SSL decryption and re-encryption detected."
                        Log-Info $detail -Type Error
                    }
                }
                $AdditionalData += New-Object -TypeName PSObject -Property @{
                    Source    = if ([string]::IsNullOrEmpty($PsSession.ComputerName)) { $ENV:COMPUTERNAME } else { $PsSession.ComputerName }
                    Resource  = $rootCATargetUrl
                    Status    = $Status
                    Detail    = $detail
                    TimeStamp = [datetime]::UtcNow
                }
            }
            $rootCATarget.AdditionalData = $AdditionalData
            $rootCATarget.TimeStamp = [datetime]::UtcNow
            $rootCATarget.Status = if ('Succeeded' -in $rootCATarget.AdditionalData.Status) { 'Succeeded' } else { 'Failed'}
            Remove-UtilityModule -PsSession $PsSession
            return $rootCATarget
        }
        else
        {
            throw "No AzStackHciConnectivityTargets"
        }
    }
    catch
    {
        Log-Info "Test-RootCA failed with error: $($_.exception.message)" -Type Warning
    }
}

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 = {
        $EndPoints = $args[0]
        $Protocol = $args[1]
        $TimeoutSecs = $args[2]
        $Proxy = $args[3]
        $ProxyCredential = $args[4]

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

        foreach ($uri in $EndPoints)
        {
            foreach ($p in $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'
                    $measure = Measure-Command -Expression {
                        $result = Invoke-WebRequest @invokeParams
                    }
                    $StatusCode = $result.StatusCode
                }
                catch
                {
                    $webResponse = $_.Exception.Response
                    if ($webResponse)
                    {
                        $StatusCode = $webResponse.StatusCode.value__
                    }
                    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'
                }

                $source = if ([string]::IsNullOrEmpty($PsSession.ComputerName))
                {
                    $ENV:COMPUTERNAME
                }
                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
                    Detail       = $StatusCode
                    MilliSeconds = $measure.TotalMilliseconds
                }
            }
        }
        return $AdditionalData
    }
    # Create a copy of the Target object
    $result = $Target | Select-Object -Property *
    $sessionArgs = @()
    if ($result)
    {
        $sessionArgs += @($result.EndPoint, $result.Protocol,$result.Tags.TimeoutSecs)
    }
    if ($Proxy)
    {
        $sessionArgs += $Proxy
    }
    if ($ProxyCredential)
    {
        $sessionArgs += $ProxyCredential
    }
    $result.AdditionalData = if ($PsSession)
    {
        Invoke-Command -Session $PsSession -ScriptBlock $ScriptBlock -ArgumentList $sessionArgs
    }
    else
    {
        Invoke-Command -ScriptBlock $ScriptBlock -ArgumentList $sessionArgs
    }
    if ($result.AdditionalData.Status -contains 'Failed')
    {
        $result.Status = 'Failed'
    }
    else
    {
        $result.Status = 'Succeeded'
    }
    $result.AdditionalData | ForEach-Object {
        Log-Info ("{0}: {1}" -f $_.Status, $_.Resource) -Type $(if ( $_.Status -eq 'Failed' ){ "Warning" } else { "Info" } )
    }
    $result.HealthCheckSource = $ENV:EnvChkrId
    return $result
}

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'
        # Create a copy of the Target object
        $result = $Target | Select-Object -Property *
        $result.TimeStamp = [datetime]::UtcNow
        $result.HealthCheckSource = $ENV:EnvChkrId

        # Create ScriptBlock
        $scriptBlock = {
            $EndPoints = $args[0]
            $Protocols = $args[1]
            $AdditionalData = @()
            foreach ($endPoint in $EndPoints)
            {
                foreach ($p in $Protocols)
                {
                    # 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'
                    }
                    $measure = Measure-Command -Expression {
                        $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    = $ENV:COMPUTERNAME
                        Resource  = $uri.OriginalString
                        Protocol  = $p
                        Status    = if ($tncResult.TcpTestSucceeded) { "Succeeded" } else { "Failed" }
                        TimeStamp = [datetime]::UtcNow
                        MilliSeconds = $measure.TotalMilliseconds
                    }
                }
            }
            return $AdditionalData
        }

        # Run Invoke-Command
        $icmParam = @{
            ScriptBlock  = $scriptBlock
            ArgumentList = @($result.EndPoint, $result.Protocol)
        }
        if ($PsSession)
        {
            $icmParam += @{
                Session = $PsSession
            }
        }
        $result.AdditionalData = Invoke-Command @icmParam
        if ($result.AdditionalData.Status -contains 'Failed')
        {
            $result.Status = 'Failed'
        }
        else
        {
            $result.Status = 'Succeeded'
        }
        $result.AdditionalData | ForEach-Object {
            Log-Info ("{0}: {1}" -f $_.Status, $_.Resource) -Type $(if ( $_.Status -eq 'Failed' ){ "Warning" } else { "Info" } )
        }
        return $result
    }
    catch
    {
        throw $_
    }
    finally
    {
        $ProgressPreference = 'Continue'
    }
}

function Get-ProxyDiagnostics
{
    [CmdletBinding()]
    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
{
    [CmdletBinding()]
    param(
        [Parameter()]
        [System.Management.Automation.Runspaces.PSSession]
        $PsSession,

        [Parameter()]
        [string]
        $Proxy
    )

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

function Get-WinHttp
{
    [CmdletBinding()]
    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
        Remediation        = "https://docs.microsoft.com/en-us/azure-stack/hci/concepts/firewall-requirements?tabs=allow-table#set-up-a-proxy-server"
        TargetResourceID   = '767c0b95-a3c9-43dd-b112-76dff50f2c75'
        TargetResourceName = $TargetResourceName
        TargetResourceType = 'Proxy_Setting'
        Timestamp          = [datetime]::UtcNow
        Status             = 'Succeeded'
        Service            = 'System'
        AdditionalData     = New-object PsObject -Property @{
            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 = $ENV:EnvChkrId
    }
    return $winHttpProxy
}

function Get-ProxyEnvironmentVariable
{
    <#
    .SYNOPSIS
        Get Proxy configuration from environment variables
    .DESCRIPTION
        Get Proxy configuration from environment variables
    .EXAMPLE
        PS C:\> Get-ProxyEnvironmentVariable
        Explanation of what the example does
    .INPUTS
        URI
    .OUTPUTS
        Output (if any)
    .NOTES
    #>

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

    $envProxySb = {
        $AdditionalData = @()

        $AdditionalData += New-Object PsObject -Property @{
            Source   = "{0}_{1}" -f $ENV:COMPUTERNAME, "HTTPS_PROXY"
            Resource = if ($env:HTTPS_PROXY) { $env:HTTPS_PROXY } else { '<Not configured>' }
            Status   = 'Succeeded'
        }

        $AdditionalData += New-Object PsObject -Property @{
            Source   = "{0}_{1}" -f $ENV:COMPUTERNAME, "HTTP_PROXY"
            Resource = if ($env:HTTP_PROXY) { $env:HTTP_PROXY } else { '<Not configured>' }
            Status   = 'Succeeded'
        }
        return $AdditionalData
    }
    [array]$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
        Remediation        = "https://docs.microsoft.com/en-us/azure-stack/aks-hci/set-proxy-settings"
        TargetResourceID   = 'cb019485-676c-4c7d-98a8-fde6e5f35dfb'
        TargetResourceName = $TargetResourceName
        TargetResourceType = 'Proxy_Setting'
        Timestamp          = [datetime]::UtcNow
        Status             = 'Succeeded'
        Service            = 'System'
        AdditionalData     = $EnvironmentProxyOutput
        HealthCheckSource  = $ENV:EnvChkrId
    }
    return $EnvProxy
}

function Get-IEProxy
{
    <#
    .SYNOPSIS
        Get Proxy configuration from IE
    .DESCRIPTION
        Get Proxy configuration from IE
    .EXAMPLE
        PS C:\> Get-IEProxy
        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
            New-Object PsObject -Property @{
                Source   = "$($ENV:COMPUTERNAME)"
                Resource = if ([string]::IsNullOrEmpty($IeProxySettings.ProxyServer) -and [string]::IsNullOrEmpty($IeProxySettings.ProxyEnable)) {
                    '<Not configured>'
                } else {
                    "{0} (Enabled:{1})" -f $IeProxySettings.ProxyServer, $IeProxySettings.ProxyEnable
                }
                Detail   = $IeProxySettings
                Status   = 'Succeeded'
            }
        }
    }
    [array]$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
            Remediation        = "https://docs.microsoft.com/en-us/azure-stack/hci/concepts/firewall-requirements?tabs=allow-table#set-up-a-proxy-server"
            TargetResourceID   = 'fe961ba6-295d-4880-82aa-2dd7322658d5'
            TargetResourceName = $TargetResourceName
            TargetResourceType = 'Proxy_Setting'
            Timestamp          = [datetime]::UtcNow
            Status             = 'Succeeded'
            Service            = 'System'
            AdditionalData     = $AdditionalData
            HealthCheckSource  = $ENV:EnvChkrId
        }
        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
    }
}

function Select-AzStackHciConnectivityTarget
{
    <#
    .SYNOPSIS
        Apply user exclusions to Connectivity Targets
    #>


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

        [Parameter()]
        [string[]]
        $Exclude,

        [Parameter()]
        [string]
        $FilePath = "$PSScriptRoot\..\ExcludeTests.txt"
    )

    try
    {
        $returnList = @($Targets)
        if ($exclude)
        {
            Log-Info "Removing tests $($exclude -join ',')"
            $returnList = $returnList | Where-Object { $_.Service | Select-String -Pattern $exclude -NotMatch }
        }
        if ($returnList.count -eq 0)
        {
            throw "No tests to perform after filtering"
        }
        if (Test-Path -Path $FilePath)
        {
            $fileExclusion = Get-Content -Path $FilePath
            Log-Info "Reading exclusion file $FilePath" -ConsoleOut
            Log-Info "Applying file exclusions: $($fileExclusion -join ',')" -ConsoleOut
            $returnList = $returnList | Where-Object {( $_.Service | Select-String -Pattern $fileExclusion -NotMatch ) -and ( $_.endpoint | Select-String -Pattern $fileExclusion -NotMatch )}
        }

        Log-Info "Test list: $($returnList -join ',')"
        if ($returnList.Count -eq 0)
        {
            Log-Info -Message "No tests to run." -ConsoleOut -Type Warning
            break noTestsBreak
        }
        return $returnList
    }
    catch
    {
        Log-Info "Failed to filter test list. Error: $($_.exception)" -Type Warning
    }
}
# SIG # Begin signature block
# MIInkwYJKoZIhvcNAQcCoIInhDCCJ4ACAQExDzANBglghkgBZQMEAgEFADB5Bgor
# BgEEAYI3AgEEoGswaTA0BgorBgEEAYI3AgEeMCYCAwEAAAQQH8w7YFlLCE63JNLG
# KX7zUQIBAAIBAAIBAAIBAAIBADAxMA0GCWCGSAFlAwQCAQUABCCPmZpWO7AzYNve
# HckJnF6YrSYuv4mnyH6GZ7FzyIqCbKCCDXYwggX0MIID3KADAgECAhMzAAADTrU8
# esGEb+srAAAAAANOMA0GCSqGSIb3DQEBCwUAMH4xCzAJBgNVBAYTAlVTMRMwEQYD
# VQQIEwpXYXNoaW5ndG9uMRAwDgYDVQQHEwdSZWRtb25kMR4wHAYDVQQKExVNaWNy
# b3NvZnQgQ29ycG9yYXRpb24xKDAmBgNVBAMTH01pY3Jvc29mdCBDb2RlIFNpZ25p
# bmcgUENBIDIwMTEwHhcNMjMwMzE2MTg0MzI5WhcNMjQwMzE0MTg0MzI5WjB0MQsw
# CQYDVQQGEwJVUzETMBEGA1UECBMKV2FzaGluZ3RvbjEQMA4GA1UEBxMHUmVkbW9u
# ZDEeMBwGA1UEChMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMR4wHAYDVQQDExVNaWNy
# b3NvZnQgQ29ycG9yYXRpb24wggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIB
# AQDdCKiNI6IBFWuvJUmf6WdOJqZmIwYs5G7AJD5UbcL6tsC+EBPDbr36pFGo1bsU
# p53nRyFYnncoMg8FK0d8jLlw0lgexDDr7gicf2zOBFWqfv/nSLwzJFNP5W03DF/1
# 1oZ12rSFqGlm+O46cRjTDFBpMRCZZGddZlRBjivby0eI1VgTD1TvAdfBYQe82fhm
# WQkYR/lWmAK+vW/1+bO7jHaxXTNCxLIBW07F8PBjUcwFxxyfbe2mHB4h1L4U0Ofa
# +HX/aREQ7SqYZz59sXM2ySOfvYyIjnqSO80NGBaz5DvzIG88J0+BNhOu2jl6Dfcq
# jYQs1H/PMSQIK6E7lXDXSpXzAgMBAAGjggFzMIIBbzAfBgNVHSUEGDAWBgorBgEE
# AYI3TAgBBggrBgEFBQcDAzAdBgNVHQ4EFgQUnMc7Zn/ukKBsBiWkwdNfsN5pdwAw
# RQYDVR0RBD4wPKQ6MDgxHjAcBgNVBAsTFU1pY3Jvc29mdCBDb3Jwb3JhdGlvbjEW
# MBQGA1UEBRMNMjMwMDEyKzUwMDUxNjAfBgNVHSMEGDAWgBRIbmTlUAXTgqoXNzci
# tW2oynUClTBUBgNVHR8ETTBLMEmgR6BFhkNodHRwOi8vd3d3Lm1pY3Jvc29mdC5j
# b20vcGtpb3BzL2NybC9NaWNDb2RTaWdQQ0EyMDExXzIwMTEtMDctMDguY3JsMGEG
# CCsGAQUFBwEBBFUwUzBRBggrBgEFBQcwAoZFaHR0cDovL3d3dy5taWNyb3NvZnQu
# Y29tL3BraW9wcy9jZXJ0cy9NaWNDb2RTaWdQQ0EyMDExXzIwMTEtMDctMDguY3J0
# MAwGA1UdEwEB/wQCMAAwDQYJKoZIhvcNAQELBQADggIBAD21v9pHoLdBSNlFAjmk
# mx4XxOZAPsVxxXbDyQv1+kGDe9XpgBnT1lXnx7JDpFMKBwAyIwdInmvhK9pGBa31
# TyeL3p7R2s0L8SABPPRJHAEk4NHpBXxHjm4TKjezAbSqqbgsy10Y7KApy+9UrKa2
# kGmsuASsk95PVm5vem7OmTs42vm0BJUU+JPQLg8Y/sdj3TtSfLYYZAaJwTAIgi7d
# hzn5hatLo7Dhz+4T+MrFd+6LUa2U3zr97QwzDthx+RP9/RZnur4inzSQsG5DCVIM
# pA1l2NWEA3KAca0tI2l6hQNYsaKL1kefdfHCrPxEry8onJjyGGv9YKoLv6AOO7Oh
# JEmbQlz/xksYG2N/JSOJ+QqYpGTEuYFYVWain7He6jgb41JbpOGKDdE/b+V2q/gX
# UgFe2gdwTpCDsvh8SMRoq1/BNXcr7iTAU38Vgr83iVtPYmFhZOVM0ULp/kKTVoir
# IpP2KCxT4OekOctt8grYnhJ16QMjmMv5o53hjNFXOxigkQWYzUO+6w50g0FAeFa8
# 5ugCCB6lXEk21FFB1FdIHpjSQf+LP/W2OV/HfhC3uTPgKbRtXo83TZYEudooyZ/A
# Vu08sibZ3MkGOJORLERNwKm2G7oqdOv4Qj8Z0JrGgMzj46NFKAxkLSpE5oHQYP1H
# tPx1lPfD7iNSbJsP6LiUHXH1MIIHejCCBWKgAwIBAgIKYQ6Q0gAAAAAAAzANBgkq
# hkiG9w0BAQsFADCBiDELMAkGA1UEBhMCVVMxEzARBgNVBAgTCldhc2hpbmd0b24x
# EDAOBgNVBAcTB1JlZG1vbmQxHjAcBgNVBAoTFU1pY3Jvc29mdCBDb3Jwb3JhdGlv
# bjEyMDAGA1UEAxMpTWljcm9zb2Z0IFJvb3QgQ2VydGlmaWNhdGUgQXV0aG9yaXR5
# IDIwMTEwHhcNMTEwNzA4MjA1OTA5WhcNMjYwNzA4MjEwOTA5WjB+MQswCQYDVQQG
# EwJVUzETMBEGA1UECBMKV2FzaGluZ3RvbjEQMA4GA1UEBxMHUmVkbW9uZDEeMBwG
# A1UEChMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMSgwJgYDVQQDEx9NaWNyb3NvZnQg
# Q29kZSBTaWduaW5nIFBDQSAyMDExMIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIIC
# CgKCAgEAq/D6chAcLq3YbqqCEE00uvK2WCGfQhsqa+laUKq4BjgaBEm6f8MMHt03
# a8YS2AvwOMKZBrDIOdUBFDFC04kNeWSHfpRgJGyvnkmc6Whe0t+bU7IKLMOv2akr
# rnoJr9eWWcpgGgXpZnboMlImEi/nqwhQz7NEt13YxC4Ddato88tt8zpcoRb0Rrrg
# OGSsbmQ1eKagYw8t00CT+OPeBw3VXHmlSSnnDb6gE3e+lD3v++MrWhAfTVYoonpy
# 4BI6t0le2O3tQ5GD2Xuye4Yb2T6xjF3oiU+EGvKhL1nkkDstrjNYxbc+/jLTswM9
# sbKvkjh+0p2ALPVOVpEhNSXDOW5kf1O6nA+tGSOEy/S6A4aN91/w0FK/jJSHvMAh
# dCVfGCi2zCcoOCWYOUo2z3yxkq4cI6epZuxhH2rhKEmdX4jiJV3TIUs+UsS1Vz8k
# A/DRelsv1SPjcF0PUUZ3s/gA4bysAoJf28AVs70b1FVL5zmhD+kjSbwYuER8ReTB
# w3J64HLnJN+/RpnF78IcV9uDjexNSTCnq47f7Fufr/zdsGbiwZeBe+3W7UvnSSmn
# Eyimp31ngOaKYnhfsi+E11ecXL93KCjx7W3DKI8sj0A3T8HhhUSJxAlMxdSlQy90
# lfdu+HggWCwTXWCVmj5PM4TasIgX3p5O9JawvEagbJjS4NaIjAsCAwEAAaOCAe0w
# ggHpMBAGCSsGAQQBgjcVAQQDAgEAMB0GA1UdDgQWBBRIbmTlUAXTgqoXNzcitW2o
# ynUClTAZBgkrBgEEAYI3FAIEDB4KAFMAdQBiAEMAQTALBgNVHQ8EBAMCAYYwDwYD
# VR0TAQH/BAUwAwEB/zAfBgNVHSMEGDAWgBRyLToCMZBDuRQFTuHqp8cx0SOJNDBa
# BgNVHR8EUzBRME+gTaBLhklodHRwOi8vY3JsLm1pY3Jvc29mdC5jb20vcGtpL2Ny
# bC9wcm9kdWN0cy9NaWNSb29DZXJBdXQyMDExXzIwMTFfMDNfMjIuY3JsMF4GCCsG
# AQUFBwEBBFIwUDBOBggrBgEFBQcwAoZCaHR0cDovL3d3dy5taWNyb3NvZnQuY29t
# L3BraS9jZXJ0cy9NaWNSb29DZXJBdXQyMDExXzIwMTFfMDNfMjIuY3J0MIGfBgNV
# HSAEgZcwgZQwgZEGCSsGAQQBgjcuAzCBgzA/BggrBgEFBQcCARYzaHR0cDovL3d3
# dy5taWNyb3NvZnQuY29tL3BraW9wcy9kb2NzL3ByaW1hcnljcHMuaHRtMEAGCCsG
# AQUFBwICMDQeMiAdAEwAZQBnAGEAbABfAHAAbwBsAGkAYwB5AF8AcwB0AGEAdABl
# AG0AZQBuAHQALiAdMA0GCSqGSIb3DQEBCwUAA4ICAQBn8oalmOBUeRou09h0ZyKb
# C5YR4WOSmUKWfdJ5DJDBZV8uLD74w3LRbYP+vj/oCso7v0epo/Np22O/IjWll11l
# hJB9i0ZQVdgMknzSGksc8zxCi1LQsP1r4z4HLimb5j0bpdS1HXeUOeLpZMlEPXh6
# I/MTfaaQdION9MsmAkYqwooQu6SpBQyb7Wj6aC6VoCo/KmtYSWMfCWluWpiW5IP0
# wI/zRive/DvQvTXvbiWu5a8n7dDd8w6vmSiXmE0OPQvyCInWH8MyGOLwxS3OW560
# STkKxgrCxq2u5bLZ2xWIUUVYODJxJxp/sfQn+N4sOiBpmLJZiWhub6e3dMNABQam
# ASooPoI/E01mC8CzTfXhj38cbxV9Rad25UAqZaPDXVJihsMdYzaXht/a8/jyFqGa
# J+HNpZfQ7l1jQeNbB5yHPgZ3BtEGsXUfFL5hYbXw3MYbBL7fQccOKO7eZS/sl/ah
# XJbYANahRr1Z85elCUtIEJmAH9AAKcWxm6U/RXceNcbSoqKfenoi+kiVH6v7RyOA
# 9Z74v2u3S5fi63V4GuzqN5l5GEv/1rMjaHXmr/r8i+sLgOppO6/8MO0ETI7f33Vt
# Y5E90Z1WTk+/gFcioXgRMiF670EKsT/7qMykXcGhiJtXcVZOSEXAQsmbdlsKgEhr
# /Xmfwb1tbWrJUnMTDXpQzTGCGXMwghlvAgEBMIGVMH4xCzAJBgNVBAYTAlVTMRMw
# EQYDVQQIEwpXYXNoaW5ndG9uMRAwDgYDVQQHEwdSZWRtb25kMR4wHAYDVQQKExVN
# aWNyb3NvZnQgQ29ycG9yYXRpb24xKDAmBgNVBAMTH01pY3Jvc29mdCBDb2RlIFNp
# Z25pbmcgUENBIDIwMTECEzMAAANOtTx6wYRv6ysAAAAAA04wDQYJYIZIAWUDBAIB
# BQCgga4wGQYJKoZIhvcNAQkDMQwGCisGAQQBgjcCAQQwHAYKKwYBBAGCNwIBCzEO
# MAwGCisGAQQBgjcCARUwLwYJKoZIhvcNAQkEMSIEIBgd8PsX+cSDQg3joCjYqCPN
# 7fJVtd9QR/YUqFTOt7OAMEIGCisGAQQBgjcCAQwxNDAyoBSAEgBNAGkAYwByAG8A
# cwBvAGYAdKEagBhodHRwOi8vd3d3Lm1pY3Jvc29mdC5jb20wDQYJKoZIhvcNAQEB
# BQAEggEAcd+nAi/HhIeBkKfSDAIhscs0raVTPVXAJ1W2yrxzjMp4tWOPBTGCr9Kp
# gsLrg++k4/PHUZks3DrA61jChEMdQnJNvDYMSvFAMMN/tPrQw3dg0Kcr/QKwSMg0
# KY/F1SFufzuFVdNQkkdENBzo2m2hH9/xV7ibW8F/48TDJmTkIZFlgC8THWJWlNT+
# eoX0OY4J3y5oj3AX50CmOe2FQQtzRVlCbCRmdWDgNRoCyxAOvuPcyt1hYP8mjSoL
# g9AuKQdcyDxEe1PWxjJgIB7tDb/OSSpxcqWjyl/5gXFJ5BISeev4BZkR8xhmvFuE
# 7CD59uCVE1AIx2idCbZIhApmu/JSiaGCFv0wghb5BgorBgEEAYI3AwMBMYIW6TCC
# FuUGCSqGSIb3DQEHAqCCFtYwghbSAgEDMQ8wDQYJYIZIAWUDBAIBBQAwggFRBgsq
# hkiG9w0BCRABBKCCAUAEggE8MIIBOAIBAQYKKwYBBAGEWQoDATAxMA0GCWCGSAFl
# AwQCAQUABCDIrnHwhM03HqBB1qUKI8T1xZsW8yqGY6n7OaUWX3C89AIGZDfp1lxE
# GBMyMDIzMDUwNDE1NTkzOS4yMzJaMASAAgH0oIHQpIHNMIHKMQswCQYDVQQGEwJV
# UzETMBEGA1UECBMKV2FzaGluZ3RvbjEQMA4GA1UEBxMHUmVkbW9uZDEeMBwGA1UE
# ChMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMSUwIwYDVQQLExxNaWNyb3NvZnQgQW1l
# cmljYSBPcGVyYXRpb25zMSYwJAYDVQQLEx1UaGFsZXMgVFNTIEVTTjo0OUJDLUUz
# N0EtMjMzQzElMCMGA1UEAxMcTWljcm9zb2Z0IFRpbWUtU3RhbXAgU2VydmljZaCC
# EVQwggcMMIIE9KADAgECAhMzAAABwFWkjcNkFcVLAAEAAAHAMA0GCSqGSIb3DQEB
# CwUAMHwxCzAJBgNVBAYTAlVTMRMwEQYDVQQIEwpXYXNoaW5ndG9uMRAwDgYDVQQH
# EwdSZWRtb25kMR4wHAYDVQQKExVNaWNyb3NvZnQgQ29ycG9yYXRpb24xJjAkBgNV
# BAMTHU1pY3Jvc29mdCBUaW1lLVN0YW1wIFBDQSAyMDEwMB4XDTIyMTEwNDE5MDEy
# NVoXDTI0MDIwMjE5MDEyNVowgcoxCzAJBgNVBAYTAlVTMRMwEQYDVQQIEwpXYXNo
# aW5ndG9uMRAwDgYDVQQHEwdSZWRtb25kMR4wHAYDVQQKExVNaWNyb3NvZnQgQ29y
# cG9yYXRpb24xJTAjBgNVBAsTHE1pY3Jvc29mdCBBbWVyaWNhIE9wZXJhdGlvbnMx
# JjAkBgNVBAsTHVRoYWxlcyBUU1MgRVNOOjQ5QkMtRTM3QS0yMzNDMSUwIwYDVQQD
# ExxNaWNyb3NvZnQgVGltZS1TdGFtcCBTZXJ2aWNlMIICIjANBgkqhkiG9w0BAQEF
# AAOCAg8AMIICCgKCAgEAvO1g+2NhhmBQvlGlCTOMaFw3jbIhUdDTqkaQhRpdHVb+
# huU/0HNhLmoRYvrp7z5vIoL1MPAkVBFWJIkrcG7sSrednyZwreY207C9n8XivL9Z
# BOQeiUeL/TMlJ6VinrcafbhdnkNO5JDlPozC9dGySiubryds5GKtu69D1wNat9DI
# Ql6alFO6pncZK4RIzfv+KzkM7RkY3vHphV0C8EFUpF+lysaGJXFf9QsUUHwj9XKW
# Hfc9BfhLoCReXUzvgrspdFmVnA9ATYXmidSjrshf8A+E0/FpTdhXPI9XXqsZDHBq
# r7DlYoSCU3lvrVDRu1p5pHHf7s3kM16HpK6arDtY3ai1soASmEpv3C2N/y5MDBAp
# Dd4SpSkLMa7+6es/daeS7zdH1qdCa2RoJPM6Eh/6YmBfofhfLQofKPJl34ALlZWK
# 5AzVtFRNOXacoj6MAG2dT8Rc5fpKCH1E3n7Zje0dK24QVfSv/YOxw52ECaMLlW5P
# hHT3ZINNaCmRgcHCTClOKzC2FOr03YBc2zPOW6bIVdXloPmBMVaE+thXqPmANBw0
# YsncaOkVggjDb5O5VqOp98MklHpJoJI6pk5zAlx8/OtC7FutrdtYNUC6ykXzMAPF
# uYkWGgx/W7A0itKW8WzYzwO3bAhprwznouGZmRiw2k8pen80BzqzdyPvbzTxQsMC
# AwEAAaOCATYwggEyMB0GA1UdDgQWBBQARMZ480jwpK3P6quVWUEJ0c30hTAfBgNV
# HSMEGDAWgBSfpxVdAF5iXYP05dJlpxtTNRnpcjBfBgNVHR8EWDBWMFSgUqBQhk5o
# dHRwOi8vd3d3Lm1pY3Jvc29mdC5jb20vcGtpb3BzL2NybC9NaWNyb3NvZnQlMjBU
# aW1lLVN0YW1wJTIwUENBJTIwMjAxMCgxKS5jcmwwbAYIKwYBBQUHAQEEYDBeMFwG
# CCsGAQUFBzAChlBodHRwOi8vd3d3Lm1pY3Jvc29mdC5jb20vcGtpb3BzL2NlcnRz
# L01pY3Jvc29mdCUyMFRpbWUtU3RhbXAlMjBQQ0ElMjAyMDEwKDEpLmNydDAMBgNV
# HRMBAf8EAjAAMBMGA1UdJQQMMAoGCCsGAQUFBwMIMA0GCSqGSIb3DQEBCwUAA4IC
# AQCtTh0EQn16kKQyCeVk9Vc10m6L0EwLRo3ATRouP7Yd2hWeEB2Y4ZF4CJKe9qfX
# WGJKzV7tMUm6DAsBKYH/nT+8ybI8uJiHGnfnVi6Sh7gFjnTpfh1j1T90H/uLeoFj
# pOn/+eoCoJmorW5Gb2ezlTlo5I0kNAubxtCxqbLizuPNPob8kRAKQgv+4/CC1Jmi
# UFG0uKINlKj9SsHcrWeBBQHX62nNgziIwT44JqHrA02I6cmQAi9BZcsf57OOLpRY
# lzoPH3x/+ldSySXAmyLq2uSbWtQuD84I/0ZgS/B5L3ewqTdiE1KbKX89MW5JqCK/
# yI/mAIQammAlHPqU9eZZTMPOHQs0XrpCijlk+qyo2JaHiySww6nuPqXzU3sEj3VW
# 00YiVSayKEu1IrRzzX3La8qe6OqLTvK/6gu5XdKq7TT852nB6IP0QM+Budtr4Fbx
# 4/svpKHGpK9/zBuaHHDXX5AoSksh/kSDYKfefQIhIfQJJzoE3X+MimMJrgrwZXlt
# b6j1IL0HY3qCpa03Ghgi0ITzqfkw3Man3G8kB1Ql+SeNciPUj73Kn2veJenGLtT8
# JkUM9RUi0woO0iuY4tJnYuS+SeqavXUOWqUYVY19FIr1PLqpmWkbrO5xKjkyOHoA
# mLxjNbKjOnkAwft+1G00kulKqzqPbm+Sn+47JsGQFhNGbTCCB3EwggVZoAMCAQIC
# EzMAAAAVxedrngKbSZkAAAAAABUwDQYJKoZIhvcNAQELBQAwgYgxCzAJBgNVBAYT
# AlVTMRMwEQYDVQQIEwpXYXNoaW5ndG9uMRAwDgYDVQQHEwdSZWRtb25kMR4wHAYD
# VQQKExVNaWNyb3NvZnQgQ29ycG9yYXRpb24xMjAwBgNVBAMTKU1pY3Jvc29mdCBS
# b290IENlcnRpZmljYXRlIEF1dGhvcml0eSAyMDEwMB4XDTIxMDkzMDE4MjIyNVoX
# DTMwMDkzMDE4MzIyNVowfDELMAkGA1UEBhMCVVMxEzARBgNVBAgTCldhc2hpbmd0
# b24xEDAOBgNVBAcTB1JlZG1vbmQxHjAcBgNVBAoTFU1pY3Jvc29mdCBDb3Jwb3Jh
# dGlvbjEmMCQGA1UEAxMdTWljcm9zb2Z0IFRpbWUtU3RhbXAgUENBIDIwMTAwggIi
# MA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQDk4aZM57RyIQt5osvXJHm9DtWC
# 0/3unAcH0qlsTnXIyjVX9gF/bErg4r25PhdgM/9cT8dm95VTcVrifkpa/rg2Z4VG
# Iwy1jRPPdzLAEBjoYH1qUoNEt6aORmsHFPPFdvWGUNzBRMhxXFExN6AKOG6N7dcP
# 2CZTfDlhAnrEqv1yaa8dq6z2Nr41JmTamDu6GnszrYBbfowQHJ1S/rboYiXcag/P
# XfT+jlPP1uyFVk3v3byNpOORj7I5LFGc6XBpDco2LXCOMcg1KL3jtIckw+DJj361
# VI/c+gVVmG1oO5pGve2krnopN6zL64NF50ZuyjLVwIYwXE8s4mKyzbnijYjklqwB
# Sru+cakXW2dg3viSkR4dPf0gz3N9QZpGdc3EXzTdEonW/aUgfX782Z5F37ZyL9t9
# X4C626p+Nuw2TPYrbqgSUei/BQOj0XOmTTd0lBw0gg/wEPK3Rxjtp+iZfD9M269e
# wvPV2HM9Q07BMzlMjgK8QmguEOqEUUbi0b1qGFphAXPKZ6Je1yh2AuIzGHLXpyDw
# wvoSCtdjbwzJNmSLW6CmgyFdXzB0kZSU2LlQ+QuJYfM2BjUYhEfb3BvR/bLUHMVr
# 9lxSUV0S2yW6r1AFemzFER1y7435UsSFF5PAPBXbGjfHCBUYP3irRbb1Hode2o+e
# FnJpxq57t7c+auIurQIDAQABo4IB3TCCAdkwEgYJKwYBBAGCNxUBBAUCAwEAATAj
# BgkrBgEEAYI3FQIEFgQUKqdS/mTEmr6CkTxGNSnPEP8vBO4wHQYDVR0OBBYEFJ+n
# FV0AXmJdg/Tl0mWnG1M1GelyMFwGA1UdIARVMFMwUQYMKwYBBAGCN0yDfQEBMEEw
# PwYIKwYBBQUHAgEWM2h0dHA6Ly93d3cubWljcm9zb2Z0LmNvbS9wa2lvcHMvRG9j
# cy9SZXBvc2l0b3J5Lmh0bTATBgNVHSUEDDAKBggrBgEFBQcDCDAZBgkrBgEEAYI3
# FAIEDB4KAFMAdQBiAEMAQTALBgNVHQ8EBAMCAYYwDwYDVR0TAQH/BAUwAwEB/zAf
# BgNVHSMEGDAWgBTV9lbLj+iiXGJo0T2UkFvXzpoYxDBWBgNVHR8ETzBNMEugSaBH
# hkVodHRwOi8vY3JsLm1pY3Jvc29mdC5jb20vcGtpL2NybC9wcm9kdWN0cy9NaWNS
# b29DZXJBdXRfMjAxMC0wNi0yMy5jcmwwWgYIKwYBBQUHAQEETjBMMEoGCCsGAQUF
# BzAChj5odHRwOi8vd3d3Lm1pY3Jvc29mdC5jb20vcGtpL2NlcnRzL01pY1Jvb0Nl
# ckF1dF8yMDEwLTA2LTIzLmNydDANBgkqhkiG9w0BAQsFAAOCAgEAnVV9/Cqt4Swf
# ZwExJFvhnnJL/Klv6lwUtj5OR2R4sQaTlz0xM7U518JxNj/aZGx80HU5bbsPMeTC
# j/ts0aGUGCLu6WZnOlNN3Zi6th542DYunKmCVgADsAW+iehp4LoJ7nvfam++Kctu
# 2D9IdQHZGN5tggz1bSNU5HhTdSRXud2f8449xvNo32X2pFaq95W2KFUn0CS9QKC/
# GbYSEhFdPSfgQJY4rPf5KYnDvBewVIVCs/wMnosZiefwC2qBwoEZQhlSdYo2wh3D
# YXMuLGt7bj8sCXgU6ZGyqVvfSaN0DLzskYDSPeZKPmY7T7uG+jIa2Zb0j/aRAfbO
# xnT99kxybxCrdTDFNLB62FD+CljdQDzHVG2dY3RILLFORy3BFARxv2T5JL5zbcqO
# Cb2zAVdJVGTZc9d/HltEAY5aGZFrDZ+kKNxnGSgkujhLmm77IVRrakURR6nxt67I
# 6IleT53S0Ex2tVdUCbFpAUR+fKFhbHP+CrvsQWY9af3LwUFJfn6Tvsv4O+S3Fb+0
# zj6lMVGEvL8CwYKiexcdFYmNcP7ntdAoGokLjzbaukz5m/8K6TT4JDVnK+ANuOaM
# mdbhIurwJ0I9JZTmdHRbatGePu1+oDEzfbzL6Xu/OHBE0ZDxyKs6ijoIYn/ZcGNT
# TY3ugm2lBRDBcQZqELQdVTNYs6FwZvKhggLLMIICNAIBATCB+KGB0KSBzTCByjEL
# MAkGA1UEBhMCVVMxEzARBgNVBAgTCldhc2hpbmd0b24xEDAOBgNVBAcTB1JlZG1v
# bmQxHjAcBgNVBAoTFU1pY3Jvc29mdCBDb3Jwb3JhdGlvbjElMCMGA1UECxMcTWlj
# cm9zb2Z0IEFtZXJpY2EgT3BlcmF0aW9uczEmMCQGA1UECxMdVGhhbGVzIFRTUyBF
# U046NDlCQy1FMzdBLTIzM0MxJTAjBgNVBAMTHE1pY3Jvc29mdCBUaW1lLVN0YW1w
# IFNlcnZpY2WiIwoBATAHBgUrDgMCGgMVABAQ7ExF19KkwVL1E3Ad8k0Peb6doIGD
# MIGApH4wfDELMAkGA1UEBhMCVVMxEzARBgNVBAgTCldhc2hpbmd0b24xEDAOBgNV
# BAcTB1JlZG1vbmQxHjAcBgNVBAoTFU1pY3Jvc29mdCBDb3Jwb3JhdGlvbjEmMCQG
# A1UEAxMdTWljcm9zb2Z0IFRpbWUtU3RhbXAgUENBIDIwMTAwDQYJKoZIhvcNAQEF
# BQACBQDn/ha9MCIYDzIwMjMwNTA0MTkzNDIxWhgPMjAyMzA1MDUxOTM0MjFaMHQw
# OgYKKwYBBAGEWQoEATEsMCowCgIFAOf+Fr0CAQAwBwIBAAICBhYwBwIBAAICEZ8w
# CgIFAOf/aD0CAQAwNgYKKwYBBAGEWQoEAjEoMCYwDAYKKwYBBAGEWQoDAqAKMAgC
# AQACAwehIKEKMAgCAQACAwGGoDANBgkqhkiG9w0BAQUFAAOBgQBvmVUu6a4jUdEW
# 1a4rYkxEpTh9MRpdXvKJGy2coHpKI2knh61llfNUruZxcN4aXg0TfnZNknuLo7tU
# Lqfmc1iVaMfP2RQHI0t9VEkdmnYWxvc2t+6wkK9QHRVsHYMtURoj0wH7lD+h31+j
# aXoVmKppPZl5CnjDkycP1+cpbGV2pjGCBA0wggQJAgEBMIGTMHwxCzAJBgNVBAYT
# AlVTMRMwEQYDVQQIEwpXYXNoaW5ndG9uMRAwDgYDVQQHEwdSZWRtb25kMR4wHAYD
# VQQKExVNaWNyb3NvZnQgQ29ycG9yYXRpb24xJjAkBgNVBAMTHU1pY3Jvc29mdCBU
# aW1lLVN0YW1wIFBDQSAyMDEwAhMzAAABwFWkjcNkFcVLAAEAAAHAMA0GCWCGSAFl
# AwQCAQUAoIIBSjAaBgkqhkiG9w0BCQMxDQYLKoZIhvcNAQkQAQQwLwYJKoZIhvcN
# AQkEMSIEIJ6x8zMyXCWrfkncZ0sin/P4EqwGn1AejHaElimHw7BIMIH6BgsqhkiG
# 9w0BCRACLzGB6jCB5zCB5DCBvQQgWvFYolIIXME0zK/W6XsCkkYX7lYNb9yA8Jxw
# Y04Pk08wgZgwgYCkfjB8MQswCQYDVQQGEwJVUzETMBEGA1UECBMKV2FzaGluZ3Rv
# bjEQMA4GA1UEBxMHUmVkbW9uZDEeMBwGA1UEChMVTWljcm9zb2Z0IENvcnBvcmF0
# aW9uMSYwJAYDVQQDEx1NaWNyb3NvZnQgVGltZS1TdGFtcCBQQ0EgMjAxMAITMwAA
# AcBVpI3DZBXFSwABAAABwDAiBCB3PBPjxvXc25OxNAovtAfWu2gJpsY254VhG/rP
# ukRY9jANBgkqhkiG9w0BAQsFAASCAgAkvWkgcnX5WOS6OchqI20gR5a0bwD/VZFr
# qmbwzmTgoFL3cqI4jQSAnXOb13CPyLX/8wVrlThIoO4zjmxBmZfS+KzZIhf14KFX
# z75NY7FQOYdGiYP67AjIf6ptlQJ8BxNHXImFONQgm2uxtV9j/UnWtusGNmRnMYHI
# xVdsfmFfYwWypY2p9708NVUQluXmth00jdTMzrI1nCT6gyZtFSCCdHU4r4hVNE/w
# oWFgqEDn1caeInzcz1EJiQrBeisODg93wOfct/7sIJ7KDxaRRAv/T8HNDibJB07g
# 7ethL9Y/ih1czY8SIuJBZJr+0KN3RIYki+I7aG61EPeaUmfin8nQXNwfHFTiq/4z
# Ll9BPcVYg+bJ9kbgtIyS5g6zUvmMmTHCYvBsTOk6uqQTrPDMauLRDpUyMNABkQDN
# d5uK9ejg8q4eW1zolxLt65GcMiEpbK/s0aM7HQgzt41c1tOmNmxw6B1oyBoR2m2d
# xdd2xorY32Y0GDCxuQyHmlB0W5eBEgmlWyrVN+cZrrz1h7io84GRIKVwEjsKOqsU
# pvdHiSKWjL2/ZX23fKaZOnZU/1G2L/SdtX8UI58RLLFxtQtWTxm0VsXC38+T1sNQ
# y0g86DPAf3xusF9hboIdi+4MAJzyuuafIr8MGjTsXH+kgW3L9pcD+1+iHHBP8obi
# nrIX4c0ZeA==
# SIG # End signature block