AzureLocalNetworkInfraConnection/AzureLocal.NetworkInfraConnection.TlsInspection.Helpers.psm1

<#############################################################
 # #
 # Copyright (C) Microsoft Corporation. All rights reserved. #
 # #
 #############################################################>


function InfraIpTlsInspectionCheck {
    <#
    .SYNOPSIS
        Checks endpoint certificate chains from an infrastructure IP.
    .DESCRIPTION
        Connects to each configured TLS inspection target from the specified infrastructure
        IP and physical adapter. It compares the served signing chain with the target's
        Tags.ExpectedSubject values. HTTPS endpoints use their configured port, including
        non-standard ports. HTTP endpoints are logged and skipped.
 
        Returns lightweight endpoint results for New-AggregatedTestResult. Connection
        failures are logged and skipped because endpoint reachability is validated separately.
    .PARAMETER SourceIp
        Infrastructure IP address used as the local source address for each TLS connection.
    .PARAMETER PhysicalAdapterUsed
        Physical adapter carrying traffic from the temporary infrastructure-IP vNIC.
    .PARAMETER SslInspectionTargets
        Service definitions containing EndPoint, Severity, and Tags.ExpectedSubject values.
    .PARAMETER LanguageText
        Localized strings used to build endpoint result details.
    .PARAMETER ProxyHost
        Optional HTTP proxy host. When specified, the probe establishes an HTTP CONNECT tunnel.
    .PARAMETER ProxyPort
        HTTP proxy port used with ProxyHost.
    .PARAMETER TimeoutInSeconds
        Connection and socket timeout for each endpoint probe. The default is 15 seconds.
    .PARAMETER ProbeAttemptCount
        Maximum number of attempts for each endpoint probe. The default is three attempts.
    .PARAMETER ProbeRetryDelaySeconds
        Delay between failed endpoint probe attempts. The default is three seconds.
    .PARAMETER UseConfiguredSeverity
        Uses each service definition's Severity when true. Uses INFORMATIONAL during SDP
        phase one when false.
    .PARAMETER ProbeSkipReason
        Optional reason that prevents probing every target. When supplied, the function emits
        successful informational skip observations without opening network connections.
    .OUTPUTS
        Hashtable objects created by New-LightweightResult. One object is returned for each
        configured endpoint.
    .EXAMPLE
        InfraIpTlsInspectionCheck -SourceIp '192.168.1.220' `
            -PhysicalAdapterUsed 'Ethernet' `
            -SslInspectionTargets $targets `
            -LanguageText $lnTxt
 
        Checks the configured endpoints directly and returns informational lightweight results.
    .EXAMPLE
        InfraIpTlsInspectionCheck -SourceIp '192.168.1.220' `
            -PhysicalAdapterUsed 'Ethernet' `
            -SslInspectionTargets $targets `
            -LanguageText $lnTxt `
            -ProxyHost 'proxy.contoso.com' `
            -ProxyPort 8080 `
            -UseConfiguredSeverity $true
 
        Checks the endpoints through an HTTP proxy and applies their configured severities.
    #>

    param (
        [Parameter(Mandatory = $true)]
        [System.String] $SourceIp,

        [Parameter(Mandatory = $true)]
        [System.String] $PhysicalAdapterUsed,

        [Parameter(Mandatory = $true)]
        [PSObject[]] $SslInspectionTargets,

        [Parameter(Mandatory = $true)]
        $LanguageText,

        [Parameter(Mandatory = $false)]
        [System.String] $ProxyHost,

        [Parameter(Mandatory = $false)]
        [System.Int32] $ProxyPort = 0,

        [Parameter(Mandatory = $false)]
        [System.UInt16] $TimeoutInSeconds = 15,

        [Parameter(Mandatory = $false)]
        [ValidateRange(1, 10)]
        [System.UInt16] $ProbeAttemptCount = 3,

        [Parameter(Mandatory = $false)]
        [ValidateRange(0, 300)]
        [System.UInt16] $ProbeRetryDelaySeconds = 3,

        [Parameter(Mandatory = $false)]
        [System.Boolean] $UseConfiguredSeverity = $false,

        [Parameter(Mandatory = $false)]
        [System.String] $ProbeSkipReason
    )

    [System.Collections.Hashtable[]] $detailResults = @()

    foreach ($service in $SslInspectionTargets) {
        $expectedSubject = @(
            $service.Tags.ExpectedSubject |
                Where-Object { -not [System.String]::IsNullOrWhiteSpace([System.String]$_) } |
                ForEach-Object { ([System.String]$_).Trim() }
        )
        $configuredSeverity = if ([System.String]::IsNullOrWhiteSpace($service.Severity)) { 'CRITICAL' } else { $service.Severity.ToUpperInvariant() }
        foreach ($endpointInService in $service.EndPoint) {
            if (-not [System.String]::IsNullOrWhiteSpace($ProbeSkipReason)) {
                $detailResults += New-InfraIpTlsInspectionSkippedResult -SourceIp $SourceIp `
                                                                            -PhysicalAdapterUsed $PhysicalAdapterUsed `
                                                                            -Endpoint ([System.String]$endpointInService) `
                                                                            -Reason $ProbeSkipReason `
                                                                            -LanguageText $LanguageText
                continue
            }

            $endpointUriText = [System.String]$endpointInService
            if ($endpointUriText -notmatch '^[a-z][a-z0-9+.-]*://') {
                $httpsProtocol = @($service.Protocol | Where-Object { $_ -ieq 'https' } | Select-Object -First 1)
                if ($httpsProtocol.Count -eq 0) {
                    Log-Info " >> Skipping TLS inspection check for $($endpointInService): the service does not define HTTPS." -Type Warning
                    $detailResults += New-InfraIpTlsInspectionSkippedResult -SourceIp $SourceIp -PhysicalAdapterUsed $PhysicalAdapterUsed -Endpoint $endpointInService -Reason 'The service does not define HTTPS.' -LanguageText $LanguageText
                    continue
                }
                $endpointUriText = "https://$($endpointUriText)"
            }

            [System.Uri] $endpointUri = $null
            if (-not [System.Uri]::TryCreate($endpointUriText, [System.UriKind]::Absolute, [ref]$endpointUri)) {
                Log-Info " >> Skipping TLS inspection check for $($endpointInService): the endpoint is not a valid absolute URI." -Type Warning
                $detailResults += New-InfraIpTlsInspectionSkippedResult -SourceIp $SourceIp -PhysicalAdapterUsed $PhysicalAdapterUsed -Endpoint $endpointInService -Reason 'The endpoint is not a valid absolute URI.' -LanguageText $LanguageText
                continue
            }
            if ($endpointUri.Scheme -ine 'https') {
                Log-Info " >> Skipping TLS inspection check for $($endpointInService): TLS certificate inspection requires HTTPS." -Type Warning
                $detailResults += New-InfraIpTlsInspectionSkippedResult -SourceIp $SourceIp -PhysicalAdapterUsed $PhysicalAdapterUsed -Endpoint $endpointUri.Authority -Reason 'TLS certificate inspection requires HTTPS.' -LanguageText $LanguageText
                continue
            }

            $targetHost = $endpointUri.DnsSafeHost
            $targetPort = $endpointUri.Port
            $targetResource = $endpointUri.Authority

            if ($expectedSubject.Count -eq 0) {
                Log-Info " >> Skipping TLS inspection check for $($service.Name): Tags.ExpectedSubject is not configured." -Type Warning
                $detailResults += New-InfraIpTlsInspectionSkippedResult -SourceIp $SourceIp -PhysicalAdapterUsed $PhysicalAdapterUsed -Endpoint $targetResource -Reason 'Tags.ExpectedSubject is not configured.' -LanguageText $LanguageText
                continue
            }

            $probeParams = @{
                SourceIp   = $SourceIp
                TargetHost = $targetHost
                TargetPort = $targetPort
                TimeoutMs  = [System.Int32]($TimeoutInSeconds * 1000)
            }
            if (-not [System.String]::IsNullOrWhiteSpace($ProxyHost)) {
                $probeParams.ProxyHost = $ProxyHost
                $probeParams.ProxyPort = $ProxyPort
            }

            $chain = $null
            $lastProbeFailure = $null
            for ($probeAttempt = 1; $probeAttempt -le $ProbeAttemptCount; $probeAttempt++) {
                try {
                    $chain = Get-SslCertificateChainFromSource @probeParams
                    if ($chain -and $chain.Count -gt 0) {
                        break
                    }
                    $lastProbeFailure = 'No certificate chain was returned.'
                } catch {
                    $lastProbeFailure = $_.Exception.Message
                }

                if ($probeAttempt -lt $ProbeAttemptCount) {
                    Log-Info " >> TLS inspection probe attempt $probeAttempt of $ProbeAttemptCount failed for $($targetHost) from infra IP $($SourceIp): $lastProbeFailure. Retrying in $ProbeRetryDelaySeconds seconds." -Type Warning
                    Start-Sleep -Seconds $ProbeRetryDelaySeconds
                }
            }

            if (-not $chain -or $chain.Count -eq 0) {
                Log-Info " >> Skipping TLS inspection check for $($targetHost) from infra IP $($SourceIp) after $ProbeAttemptCount attempts: $lastProbeFailure" -Type Warning
                $detailResults += New-InfraIpTlsInspectionSkippedResult -SourceIp $SourceIp -PhysicalAdapterUsed $PhysicalAdapterUsed -Endpoint $targetResource -Reason $lastProbeFailure -LanguageText $LanguageText
                continue
            }

            try {
                # Exclude the leaf because an inspection proxy can preserve its expected organization.
                $signingSubjects = @($chain[0].Issuer)
                if ($chain.Count -ge 2) {
                    $signingSubjects += $chain[1..($chain.Count - 1)].Subject
                }
                $matchCount = Test-SigningSubjectMatchesExpected -SigningSubject $signingSubjects -ExpectedSubject $expectedSubject
                $status = if ($matchCount -ge 1) { 'SUCCESS' } else { 'FAILURE' }
                $severity = if ($UseConfiguredSeverity) { $configuredSeverity } else { 'INFORMATIONAL' }
                $detail = if ($status -eq 'SUCCESS') {
                    $LanguageText.InfraConnectionInfraPoolIPTlsInspectionEndpointPASSED -f $SourceIp, $PhysicalAdapterUsed, $targetResource, ($expectedSubject -join '; ')
                } else {
                    $LanguageText.InfraConnectionInfraPoolIPTlsInspectionEndpointFAILED -f $SourceIp, $PhysicalAdapterUsed, $targetResource, ($signingSubjects -join '; '), ($expectedSubject -join '; ')
                }

                if ($status -eq 'SUCCESS') {
                    Log-Info " >> [PASS] No TLS inspection from infra IP $($SourceIp) to $($targetResource)."
                } else {
                    Log-Info " >> [FAIL] TLS inspection detected from infra IP $($SourceIp) to $($targetResource). Signed by: $($signingSubjects -join '; ')" -Type Error
                }

                $detailResults += New-LightweightResult `
                    -Name 'AzureLocal_NetworkInfraConnection_Test_Infra_IP_TLS_Inspection' `
                    -Status $status `
                    -Severity $severity `
                    -TargetResourceName $SourceIp `
                    -Source "$SourceIp ($PhysicalAdapterUsed)" `
                    -Resource $targetResource `
                    -Detail $detail
            } finally {
                foreach ($certificate in @($chain)) {
                    if ($certificate -is [System.IDisposable]) {
                        $certificate.Dispose()
                    }
                }
            }
        }
    }

    return $detailResults
}

function New-InfraIpTlsInspectionSkippedResult {
    <#
    .SYNOPSIS
        Creates a lightweight result for a skipped TLS inspection probe.
    .DESCRIPTION
        Records a successful informational endpoint observation when a TLS inspection probe
        cannot run. The skip reason remains visible in the per-IP aggregate result and telemetry.
    .PARAMETER SourceIp
        Infrastructure IP that would have sourced the TLS probe.
    .PARAMETER PhysicalAdapterUsed
        Physical adapter that would have carried the TLS probe.
    .PARAMETER Endpoint
        Endpoint definition or authority that was not probed.
    .PARAMETER Reason
        Diagnostic reason the TLS probe was skipped.
    .PARAMETER LanguageText
        Localized strings used to build the result detail.
    .OUTPUTS
        Hashtable created by New-LightweightResult with SUCCESS status and INFORMATIONAL severity.
    .EXAMPLE
        New-InfraIpTlsInspectionSkippedResult `
            -SourceIp '192.168.1.220' `
            -PhysicalAdapterUsed 'Ethernet' `
            -Endpoint 'mcr.microsoft.com' `
            -Reason 'The enabled proxy configuration could not be resolved.' `
            -LanguageText $lnTxt
    #>

    param (
        [Parameter(Mandatory = $true)]
        [System.String] $SourceIp,

        [Parameter(Mandatory = $true)]
        [System.String] $PhysicalAdapterUsed,

        [Parameter(Mandatory = $true)]
        [System.String] $Endpoint,

        [Parameter(Mandatory = $true)]
        [System.String] $Reason,

        [Parameter(Mandatory = $true)]
        $LanguageText
    )

    Log-Info " >> TLS inspection check skipped for $Endpoint from infra IP $SourceIp via $PhysicalAdapterUsed. Reason: $Reason" -Type Warning
    $result = New-LightweightResult `
        -Name 'AzureLocal_NetworkInfraConnection_Test_Infra_IP_TLS_Inspection' `
        -Status 'SUCCESS' `
        -Severity 'INFORMATIONAL' `
        -TargetResourceName $SourceIp `
        -Source "$SourceIp ($PhysicalAdapterUsed)" `
        -Resource $Endpoint `
        -Detail ($LanguageText.InfraConnectionInfraPoolIPTlsInspectionEndpointSKIPPED -f $SourceIp, $PhysicalAdapterUsed, $Endpoint, $Reason)
    $result.AdditionalData.Outcome = 'SKIPPED'
    return $result
}

function New-InfraIpTlsInspectionResult {
    <#
    .SYNOPSIS
        Aggregates TLS endpoint checks for an infrastructure IP.
    .DESCRIPTION
        Uses New-AggregatedTestResult to combine lightweight endpoint results, then retargets
        each aggregate from AllNodes to the infrastructure IP.
 
        During SDP phase one, endpoint details use INFORMATIONAL severity and produce one result
        per IP. After promotion, configured endpoint severities produce one result per IP and severity.
    .PARAMETER SourceIp
        Infrastructure IP represented by the aggregate result.
    .PARAMETER DetailResults
        Lightweight endpoint results returned by InfraIpTlsInspectionCheck.
    .PARAMETER LanguageText
        Localized strings used for the aggregate description and remediation.
    .PARAMETER UseConfiguredSeverity
        Marks the aggregate as promoted when true and adds its severity to the target identity.
    .OUTPUTS
        PSCustomObject instances returned by New-AggregatedTestResult and retargeted to SourceIp.
        Returns no objects when DetailResults is empty.
    .EXAMPLE
        New-InfraIpTlsInspectionResult `
            -SourceIp '192.168.1.220' `
            -DetailResults $endpointResults `
            -LanguageText $lnTxt
 
        Returns one informational SDP-phase result for the infrastructure IP.
    .EXAMPLE
        New-InfraIpTlsInspectionResult `
            -SourceIp '192.168.1.220' `
            -DetailResults $endpointResults `
            -LanguageText $lnTxt `
            -UseConfiguredSeverity $true
 
        Returns one aggregate per configured endpoint severity for the infrastructure IP.
    #>

    param (
        [Parameter(Mandatory = $true)]
        [System.String] $SourceIp,

        [Parameter(Mandatory = $true)]
        [AllowEmptyCollection()]
        [PSObject[]] $DetailResults,

        [Parameter(Mandatory = $true)]
        $LanguageText,

        [Parameter(Mandatory = $false)]
        [System.Boolean] $UseConfiguredSeverity = $false
    )

    if (@($DetailResults).Count -eq 0) {
        return @()
    }

    $results = @(New-AggregatedTestResult `
        -TestName 'Test-InfraIpTlsInspection' `
        -DisplayName ($LanguageText.InfraConnectionInfraPoolIPTlsInspectionAggregate -f $SourceIp) `
        -Description ($LanguageText.InfraConnectionInfraPoolIPTlsInspectionAggregate -f $SourceIp) `
        -DetailResults $DetailResults `
        -ValidatorName 'NetworkInfraConnection' `
        -ResourceType 'Infrastructure IP' `
        -Remediation ($LanguageText.InfraConnectionInfraPoolIPTlsInspectionAggregateRemediation -f $SourceIp))

    foreach ($result in $results) {
        $targetSuffix = if ($UseConfiguredSeverity) { "_$($result.Severity)" } else { '' }
        $targetName = "Infra_IP_TLS_Inspection_$($SourceIp)$($targetSuffix)"
        $result.TargetResourceID = $targetName
        $result.TargetResourceName = $targetName
        $result.TargetResourceType = 'Infrastructure IP'
        $result.Description = $LanguageText.InfraConnectionInfraPoolIPTlsInspectionAggregate -f $SourceIp
        $result.AdditionalData.Source = $env:COMPUTERNAME
        $result.AdditionalData.Resource = $SourceIp
        $result.AdditionalData.Remove('NodeCount')
        $result.AdditionalData.Remove('TotalNodes')
        $result.AdditionalData.InfrastructureIpCount = 1
        $result.Tags.Remove('NodeCount')
        $result.Tags.InfrastructureIpCount = 1
        $result.Tags.SdpPhaseOne = -not $UseConfiguredSeverity

        if ($UseConfiguredSeverity) {
            $result.Tags.ConfiguredSeverity = [string]$result.Severity
        }

        $severityResults = @($DetailResults | Where-Object { [string]$_.Severity -eq [string]$result.Severity })
        $skippedResults = @($severityResults | Where-Object { $_.AdditionalData.Outcome -eq 'SKIPPED' })
        if ($skippedResults.Count -gt 0) {
            $successfulChecks = @($severityResults | Where-Object {
                $_.Status -eq 'SUCCESS' -and $_.AdditionalData.Outcome -ne 'SKIPPED'
            }).Count
            $skipSummary = $LanguageText.InfraConnectionInfraPoolIPTlsInspectionAggregateWithSkipped -f `
                $SourceIp, $successfulChecks, $severityResults.Count, $skippedResults.Count
            $skipDetails = @($skippedResults | ForEach-Object { $_.AdditionalData.Detail })

            if ($result.Status -eq 'SUCCESS') {
                $result.AdditionalData.Detail = (@($skipSummary) + $skipDetails) -join "`n"
            } else {
                $result.AdditionalData.Detail = (@($result.AdditionalData.Detail, $skipSummary) + $skipDetails) -join "`n"
            }

            $result.AdditionalData.TotalChecks = $severityResults.Count
            $result.AdditionalData.SuccessfulChecks = $successfulChecks
            $result.AdditionalData.SkippedChecks = $skippedResults.Count
            $result.AdditionalData.HasSkippedChecks = $true
            $result.Tags.SkippedChecks = $skippedResults.Count
            $result.Tags.HasSkippedChecks = $true
        }

        Log-Info " >> TLS inspection summary for infra IP $($SourceIp), severity $($result.Severity): $($result.Status), $($result.AdditionalData.TotalChecks) checks."
    }

    return $results
}

function Get-InfraIpTlsInspectionProxyConfiguration {
    <#
    .SYNOPSIS
        Resolves proxy settings for infrastructure IP TLS inspection.
    .DESCRIPTION
        Returns the proxy host and port required by the source-bound TLS probe. When proxy use
        is disabled, the returned configuration is ready for direct probing. An enabled proxy
        that cannot be resolved returns a skip reason so the probe cannot bypass the proxy.
    .PARAMETER ProxyEnabled
        Indicates whether the host is configured to use a proxy.
    .OUTPUTS
        PSCustomObject containing Host, Port, Ready, and SkipReason properties.
    .EXAMPLE
        Get-InfraIpTlsInspectionProxyConfiguration -ProxyEnabled $true
 
        Resolves the WinHTTP proxy used by infrastructure IP TLS inspection.
    #>

    param (
        [Parameter(Mandatory = $true)]
        [System.Boolean] $ProxyEnabled
    )

    $configuration = [PSCustomObject]@{
        Host       = $null
        Port       = 0
        Ready      = -not $ProxyEnabled
        SkipReason = $null
    }

    if (-not $ProxyEnabled) {
        return $configuration
    }

    try {
        $winProxy = EnvValidatorNwkLibGetWinProxyConfiguration -ProxyType WinHttp
        $proxyEndpoint = if (-not [System.String]::IsNullOrWhiteSpace($winProxy.HttpsProxy)) { $winProxy.HttpsProxy } else { $winProxy.HttpProxy }
        if ([System.String]::IsNullOrWhiteSpace($proxyEndpoint)) {
            $configuration.SkipReason = 'The enabled proxy configuration could not be resolved.'
            Log-Info 'Proxy is enabled, but no HTTP or HTTPS proxy endpoint was found. Infra IP TLS inspection checks will be skipped.' -Type Warning
            return $configuration
        }

        $proxyEndpoint = $proxyEndpoint -replace '^https?://', '' -replace '/.*$', ''
        $proxyParts = $proxyEndpoint -split ':'
        $configuration.Host = $proxyParts[0]
        $configuration.Port = if ($proxyParts.Count -ge 2) { [System.Int32] $proxyParts[1] } else { 8080 }
        $configuration.Ready = $true
        Log-Info "Resolved proxy $($configuration.Host):$($configuration.Port) for infra IP TLS inspection probe."
    } catch {
        $configuration.SkipReason = 'The enabled proxy configuration could not be resolved.'
        Log-Info "Could not resolve proxy for infra IP TLS inspection probe: $($_.Exception.Message)" -Type Warning
    }

    return $configuration
}

function Invoke-InfraIpTlsInspectionCheckForAdapter {
    <#
    .SYNOPSIS
        Runs infrastructure IP TLS inspection checks through one physical adapter.
    .DESCRIPTION
        Runs the configured endpoint checks from an infrastructure IP and adapter. The function
        applies resolved proxy settings or emits successful skip observations when an enabled
        proxy cannot be resolved.
    .PARAMETER SourceIp
        Infrastructure IP used as the local source address.
    .PARAMETER PhysicalAdapterUsed
        Physical adapter carrying traffic from the temporary infrastructure-IP vNIC.
    .PARAMETER SslInspectionTargets
        Service definitions selected for TLS inspection.
    .PARAMETER LanguageText
        Localized strings used to build result details.
    .PARAMETER ProxyConfiguration
        Configuration returned by Get-InfraIpTlsInspectionProxyConfiguration.
    .PARAMETER UseConfiguredSeverity
        Uses endpoint severities after SDP promotion when true.
    .PARAMETER TimeoutInSeconds
        Connection and socket timeout for each endpoint probe. The default is 15 seconds.
    .PARAMETER ProbeAttemptCount
        Maximum number of attempts for each endpoint probe. The default is three attempts.
    .PARAMETER ProbeRetryDelaySeconds
        Delay between failed endpoint probe attempts. The default is three seconds.
    .OUTPUTS
        Hashtable objects returned by InfraIpTlsInspectionCheck.
    .EXAMPLE
        Invoke-InfraIpTlsInspectionCheckForAdapter `
            -SourceIp '192.168.1.220' `
            -PhysicalAdapterUsed 'Ethernet' `
            -SslInspectionTargets $targets `
            -LanguageText $lnTxt `
            -ProxyConfiguration $proxyConfiguration
    #>

    param (
        [Parameter(Mandatory = $true)]
        [System.String] $SourceIp,

        [Parameter(Mandatory = $true)]
        [System.String] $PhysicalAdapterUsed,

        [Parameter(Mandatory = $true)]
        [AllowNull()]
        [AllowEmptyCollection()]
        [PSObject[]] $SslInspectionTargets,

        [Parameter(Mandatory = $true)]
        $LanguageText,

        [Parameter(Mandatory = $true)]
        [PSObject] $ProxyConfiguration,

        [Parameter(Mandatory = $false)]
        [System.Boolean] $UseConfiguredSeverity = $false,

        [Parameter(Mandatory = $false)]
        [System.UInt16] $TimeoutInSeconds = 15,

        [Parameter(Mandatory = $false)]
        [ValidateRange(1, 10)]
        [System.UInt16] $ProbeAttemptCount = 3,

        [Parameter(Mandatory = $false)]
        [ValidateRange(0, 300)]
        [System.UInt16] $ProbeRetryDelaySeconds = 3
    )

    if (-not $SslInspectionTargets -or $SslInspectionTargets.Count -eq 0) {
        Log-Info "TLS inspection targets are null or empty. No TLS inspection checks will run for infra IP $SourceIp via physical adapter $PhysicalAdapterUsed."
        return @()
    }

    if (-not $ProxyConfiguration.Ready) {
        Log-Info "TLS inspection checks are skipped because the enabled proxy configuration could not be resolved."
        return InfraIpTlsInspectionCheck -SourceIp $SourceIp `
                                         -PhysicalAdapterUsed $PhysicalAdapterUsed `
                                         -SslInspectionTargets $SslInspectionTargets `
                                         -LanguageText $LanguageText `
                                         -UseConfiguredSeverity:$UseConfiguredSeverity `
                                         -ProbeSkipReason $ProxyConfiguration.SkipReason
    }

    Log-Info "TLS inspection targets found ($($SslInspectionTargets.Count)). Starting checks for infra IP $SourceIp via physical adapter $PhysicalAdapterUsed."
    return InfraIpTlsInspectionCheck -SourceIp $SourceIp `
                                     -PhysicalAdapterUsed $PhysicalAdapterUsed `
                                     -SslInspectionTargets $SslInspectionTargets `
                                     -LanguageText $LanguageText `
                                     -ProxyHost $ProxyConfiguration.Host `
                                     -ProxyPort $ProxyConfiguration.Port `
                                     -TimeoutInSeconds $TimeoutInSeconds `
                                     -ProbeAttemptCount $ProbeAttemptCount `
                                     -ProbeRetryDelaySeconds $ProbeRetryDelaySeconds `
                                     -UseConfiguredSeverity:$UseConfiguredSeverity
}

function New-InfraIpTlsInspectionAggregateResults {
    <#
    .SYNOPSIS
        Creates TLS inspection aggregate results for all tested infrastructure IPs.
    .DESCRIPTION
        Converts the endpoint observations grouped by infrastructure IP into validator results.
        SDP phase one produces one informational result per IP; promoted checks may produce one
        result per IP and configured severity.
    .PARAMETER DetailResultsByIp
        Hashtable whose keys are infrastructure IPs and values are lightweight endpoint results.
    .PARAMETER LanguageText
        Localized strings used to build aggregate results.
    .PARAMETER UseConfiguredSeverity
        Uses endpoint severities after SDP promotion when true.
    .OUTPUTS
        PSCustomObject instances returned by New-InfraIpTlsInspectionResult.
    .EXAMPLE
        New-InfraIpTlsInspectionAggregateResults `
            -DetailResultsByIp $tlsDetailsByIp `
            -LanguageText $lnTxt
    #>

    param (
        [Parameter(Mandatory = $true)]
        [System.Collections.Hashtable] $DetailResultsByIp,

        [Parameter(Mandatory = $true)]
        $LanguageText,

        [Parameter(Mandatory = $false)]
        [System.Boolean] $UseConfiguredSeverity = $false
    )

    $results = @()
    foreach ($sourceIp in @($DetailResultsByIp.Keys | Sort-Object)) {
        $results += @(New-InfraIpTlsInspectionResult -SourceIp $sourceIp `
                                                     -DetailResults $DetailResultsByIp[$sourceIp] `
                                                     -LanguageText $LanguageText `
                                                     -UseConfiguredSeverity:$UseConfiguredSeverity)
    }

    return $results
}

Export-ModuleMember -Function InfraIpTlsInspectionCheck,
                                    New-InfraIpTlsInspectionSkippedResult,
                                    New-InfraIpTlsInspectionResult,
                                    Get-InfraIpTlsInspectionProxyConfiguration,
                                    Invoke-InfraIpTlsInspectionCheckForAdapter,
                                    New-InfraIpTlsInspectionAggregateResults

# SIG # Begin signature block
# MIInUAYJKoZIhvcNAQcCoIInQTCCJz0CAQExDzANBglghkgBZQMEAgEFADB5Bgor
# BgEEAYI3AgEEoGswaTA0BgorBgEEAYI3AgEeMCYCAwEAAAQQH8w7YFlLCE63JNLG
# KX7zUQIBAAIBAAIBAAIBAAIBADAxMA0GCWCGSAFlAwQCAQUABCBZeNm0okWpHip4
# e3yhlFFuzBFASyY628le5V9iKquXvaCCDMkwggYEMIID7KADAgECAhMzAAACHPrN
# xZvoL37EAAAAAAIcMA0GCSqGSIb3DQEBCwUAMFcxCzAJBgNVBAYTAlVTMR4wHAYD
# VQQKExVNaWNyb3NvZnQgQ29ycG9yYXRpb24xKDAmBgNVBAMTH01pY3Jvc29mdCBD
# b2RlIFNpZ25pbmcgUENBIDIwMjQwHhcNMjYwNDE2MTg1OTQxWhcNMjcwNDE1MTg1
# OTQxWjB0MQswCQYDVQQGEwJVUzETMBEGA1UECBMKV2FzaGluZ3RvbjEQMA4GA1UE
# BxMHUmVkbW9uZDEeMBwGA1UEChMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMR4wHAYD
# VQQDExVNaWNyb3NvZnQgQ29ycG9yYXRpb24wggEiMA0GCSqGSIb3DQEBAQUAA4IB
# DwAwggEKAoIBAQDVsZfgOKmM31HPfoWOoNEiw0SlCiIxUMC0I9NMWbucKOw/e9lP
# oAoehQVu6SG65V4EPzrYsnBnFPNoi4/HoOdjhz1qkrEt4I6tEcxXU6oOeY9zGveC
# /3iBeuhLYxM3M/PkcUoebF+Nednm8OkdSPoDu8imViHPQq/8CQUu0WRR4rE+dMRf
# rpVqfmNi2qWCX94T4MsepijGVkwE//tJg0ryAiYdHT34LSnlG/RSBZmQRGWZ5g8j
# qnKjRParSqMft1gvjuUTVgtWNZfgcLFSK5Wa0myrq8OPcgTGGsRgun+tnSS+IxDT
# xVsAPH1OzvPjwomguByhUe/OcvUN0D5Wmp7xAgMBAAGjggGqMIIBpjAOBgNVHQ8B
# Af8EBAMCB4AwHwYDVR0lBBgwFgYKKwYBBAGCN0wIAQYIKwYBBQUHAwMwHQYDVR0O
# BBYEFNoH7a2YDjOSwpkp6DHcmUS7J+0yMFQGA1UdEQRNMEukSTBHMS0wKwYDVQQL
# EyRNaWNyb3NvZnQgSXJlbGFuZCBPcGVyYXRpb25zIExpbWl0ZWQxFjAUBgNVBAUT
# DTIzMDAxMis1MDc1NjkwHwYDVR0jBBgwFoAUf1k/VCHarU/vBeXmo9ctBpQSCDEw
# YAYDVR0fBFkwVzBVoFOgUYZPaHR0cDovL3d3dy5taWNyb3NvZnQuY29tL3BraW9w
# cy9jcmwvTWljcm9zb2Z0JTIwQ29kZSUyMFNpZ25pbmclMjBQQ0ElMjAyMDI0LmNy
# bDBtBggrBgEFBQcBAQRhMF8wXQYIKwYBBQUHMAKGUWh0dHA6Ly93d3cubWljcm9z
# b2Z0LmNvbS9wa2lvcHMvY2VydHMvTWljcm9zb2Z0JTIwQ29kZSUyMFNpZ25pbmcl
# MjBQQ0ElMjAyMDI0LmNydDAMBgNVHRMBAf8EAjAAMA0GCSqGSIb3DQEBCwUAA4IC
# AQAUnEqhaRXe0T3hIJjvdQErEkrA/7bByjn6t5IArODkkRjzkYwtKMc2yYj2quaN
# rLutWw2YZcngKPy1b71YyDJQTy4NDRwaSh9Tw5thrk3NmcPrAHia5vtcBJ1CgtKK
# 7mQbIcQ22d/N3813ayCDDFewu1+jsZmX+r/aTEqaOM4TVxVtRSkuCy8nAXKuChOK
# Li/zA4XuH8iEYqIsj2YoNaeSxVmeGiERXpKdo3dDmYi0kO5w2D8VS4c3+9h6gElY
# BaAAg/dYErBg27qT3vv0zRDJhJufvCNylA8S7/+8H5E/PV5cng6na9VV/w9OV3qu
# uND6zdGa2EX38Glp50F9AIQk3p2xXmcvorDeM4XJ7UlWYBi6g80J1SSOQnInCYFE
# msfUNn3+1AaTJKSJL83quKArTac2pKhu0Yzzzrzo6HrsRiQKzpnRBb1/dMa6P3hz
# 75XbMRBctNsFhZC07WCmjExdLg2eHW5uV0TY8D5+6wozJf7vF3+WHkYPO85Z+BC6
# U4FkNbYNycZ9cE4j1tXRdyDCfml6c0HWPHjNVDObrv9lKt3qUqFpX38VCqVCyNOO
# 1UcXfQiVjJw32U2WUKZjt/neJKHEBsm9kFsLuWzkQ53+qcaSaytmsCnk2gOglrlD
# 5d3kKyvvAw+rzm0lT8K38P6PLxfZQHhu4W8dV7Av8N2ZmDCCBr0wggSloAMCAQIC
# EzMAAAA5O7Y3Gb8GHWcAAAAAADkwDQYJKoZIhvcNAQEMBQAwgYgxCzAJBgNVBAYT
# AlVTMRMwEQYDVQQIEwpXYXNoaW5ndG9uMRAwDgYDVQQHEwdSZWRtb25kMR4wHAYD
# VQQKExVNaWNyb3NvZnQgQ29ycG9yYXRpb24xMjAwBgNVBAMTKU1pY3Jvc29mdCBS
# b290IENlcnRpZmljYXRlIEF1dGhvcml0eSAyMDExMB4XDTI0MDgwODIwNTQxOFoX
# DTM2MDMyMjIyMTMwNFowVzELMAkGA1UEBhMCVVMxHjAcBgNVBAoTFU1pY3Jvc29m
# dCBDb3Jwb3JhdGlvbjEoMCYGA1UEAxMfTWljcm9zb2Z0IENvZGUgU2lnbmluZyBQ
# Q0EgMjAyNDCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBANgBnB7jOMeq
# lRYHNa265v4IY9fH8TKhemHfPINe1gpLaV3dhg324WwH06LcHbpnsBukCDNitryo
# 0dtS/EW6I/yEL/bLSY8hKpbfQuWusBPr9qazYcDxCW/qnjb5JsI1s8bNOg3bVATv
# QVL4tcf03aTycsz8QeCdM0l/yHRObJ9QqazM1r6VPEOJ7LL+uEEb73w6QCuhs89a
# 1uv1zerOYMnsneRRwCbpyW11IcggU0cRKDDq1pjVJzIbIF6+oiXXbReOsgeI8zu1
# FyQfK0fVkaya8SmVHQ/tOf23mZ4W9k0Ri22QW9p3UgSC5OUDktKxxcCmGL6tXLfO
# GSWHIIV4YrTJTT6PNty5REojHJuZHArkF9VnHTERWoTjAzfI3kP+5b4alUdhgAZ7
# ttOu1bVnXfHaqPYl2rPs20ji03LOVWsh/radgE17es5hL+t6lV0eVHrVhsssROWJ
# uz2MXMCt7iw7lFPG9LXKGjsmonn2gotGdHIuEg5JnJMJVmixd5LRlkmgYRZKzhxS
# CwyoGIq0PhaA7Y+VPct5pCHkijcIIDm0nlkK+0KyepolcqGm0T/GYQRMhHJlGOOm
# VQop36wUVUYklUy++vDWeEgEo4s7hxN6mIbf2MSIQ/iIfMZgJxC69oukMUXCrOC3
# SkE/xIkgpfl22MM1itkZ35nNXkMolU1lAgMBAAGjggFOMIIBSjAOBgNVHQ8BAf8E
# BAMCAYYwEAYJKwYBBAGCNxUBBAMCAQAwHQYDVR0OBBYEFH9ZP1Qh2q1P7wXl5qPX
# LQaUEggxMBkGCSsGAQQBgjcUAgQMHgoAUwB1AGIAQwBBMA8GA1UdEwEB/wQFMAMB
# Af8wHwYDVR0jBBgwFoAUci06AjGQQ7kUBU7h6qfHMdEjiTQwWgYDVR0fBFMwUTBP
# oE2gS4ZJaHR0cDovL2NybC5taWNyb3NvZnQuY29tL3BraS9jcmwvcHJvZHVjdHMv
# TWljUm9vQ2VyQXV0MjAxMV8yMDExXzAzXzIyLmNybDBeBggrBgEFBQcBAQRSMFAw
# TgYIKwYBBQUHMAKGQmh0dHA6Ly93d3cubWljcm9zb2Z0LmNvbS9wa2kvY2VydHMv
# TWljUm9vQ2VyQXV0MjAxMV8yMDExXzAzXzIyLmNydDANBgkqhkiG9w0BAQwFAAOC
# AgEAFJQfOChP7onn6fLIMKrSlN1WYKwDFgAddymOUO3FrM8d7B/W/iQ6DxXsDn7D
# 5W4wMwYeLystcEqfkjz4NURRgazyMu5yRzQh4LqjA4tStTcJh1opExo7nn5PuPBY
# nbu0+THSuVHTe0VTTPVhily/piFrDo3axQ9P4C+Ol5yet+2gTfekICS5xS+cYfSI
# vgn0JksVBVMYVI5QFu/qhnLhsEFEUzG8fvv0hjgkO+lkpV9ty6GkN4vdnd7ya6Q6
# aR9y34aiM1qmxaxBi6OUnyNl6fkuun/diTFnYDLTppOkr/mg5WSfCiDVMNCxtj4w
# PKC5OmHm1DQIt/MNokbbH3UGsFP1QbzsLocuSqLCvH09Io3fDPTmscR9Y75G4qX7
# RTX8AdBPo0I6OEojf39zuFZt0qOHm65YWQE69cZM2ueE1MB05dNNgHK9gTE7zKvK
# /fg8B2qjW88MT/WF5V5uvZGtqa9FSL2RazArA+rDPuf6JGYz4HpgMZHB4S6szWSK
# YBv0VisCzfxgeU+dquXW9bd0auYlOB58DPcOYKdc3Se94g+xL4pcEhbB54JOgAkw
# YTu/9dLeH2pDqeJZAABVDWRQCaXfO5LgyKwKCLYXpigrZYCjUSBcr+Ve8PFWMhVT
# Ql0v4q8J/AUmQN5W4n101cY2L4A7GTQG1h32HHAvfQESWP0xghndMIIZ2QIBATBu
# MFcxCzAJBgNVBAYTAlVTMR4wHAYDVQQKExVNaWNyb3NvZnQgQ29ycG9yYXRpb24x
# KDAmBgNVBAMTH01pY3Jvc29mdCBDb2RlIFNpZ25pbmcgUENBIDIwMjQCEzMAAAIc
# +s3Fm+gvfsQAAAAAAhwwDQYJYIZIAWUDBAIBBQCggZAwGQYJKoZIhvcNAQkDMQwG
# CisGAQQBgjcCAQQwLwYJKoZIhvcNAQkEMSIEIHmgCP9xwSes2QTYft6oiohL4AFK
# 5Id+2PYY/FqXjbzxMEIGCisGAQQBgjcCAQwxNDAyoBSAEgBNAGkAYwByAG8AcwBv
# AGYAdKEagBhodHRwOi8vd3d3Lm1pY3Jvc29mdC5jb20wDQYJKoZIhvcNAQEBBQAE
# ggEAOiL9ia4XN/gDWEi5ousfOOINdEVi8zj8d/7h6ipzTMMLLvSx6h6/Xyh5X6F5
# PVPlhTfMzigBKD7+Y4CNwFqDfRdaKEmKsvwbqGqZwdf1s46WK+Ubk3Gx4ERkKa/e
# xbb58io4SdAlyKbGFqeCTXIhaLOk20qPBACCKz9JQXaiIJCeJUaQyWM5gvRobDlb
# rIMR5xuhU/XuIR46b26733k9OTdKyx5ctgk+pZAuPzxmpiC6PoVqriyKIX5rV7/Y
# GqvMVKKl4T0M7FJBe4qz6eGWZu8OKDlD+/+UPKAKnkJ0e+KPyCtlUM4A6L/X54bw
# pQXavcn0ySQP4QK6L8r7/JsUGaGCF60wghepBgorBgEEAYI3AwMBMYIXmTCCF5UG
# CSqGSIb3DQEHAqCCF4YwgheCAgEDMQ8wDQYJYIZIAWUDBAIBBQAwggFaBgsqhkiG
# 9w0BCRABBKCCAUkEggFFMIIBQQIBAQYKKwYBBAGEWQoDATAxMA0GCWCGSAFlAwQC
# AQUABCAKBF8ITgiYa5CiYoB8SpFWI6VfWRC7CeZXJrjpF8u7ggIGaoigikNaGBMy
# MDI2MDgyNzE2MzgzOC45MThaMASAAgH0oIHZpIHWMIHTMQswCQYDVQQGEwJVUzET
# MBEGA1UECBMKV2FzaGluZ3RvbjEQMA4GA1UEBxMHUmVkbW9uZDEeMBwGA1UEChMV
# TWljcm9zb2Z0IENvcnBvcmF0aW9uMS0wKwYDVQQLEyRNaWNyb3NvZnQgSXJlbGFu
# ZCBPcGVyYXRpb25zIExpbWl0ZWQxJzAlBgNVBAsTHm5TaGllbGQgVFNTIEVTTjoy
# RDFBLTA1RTAtRDk0NzElMCMGA1UEAxMcTWljcm9zb2Z0IFRpbWUtU3RhbXAgU2Vy
# dmljZaCCEfswggcoMIIFEKADAgECAhMzAAACEtEIBjzKGE+qAAEAAAISMA0GCSqG
# SIb3DQEBCwUAMHwxCzAJBgNVBAYTAlVTMRMwEQYDVQQIEwpXYXNoaW5ndG9uMRAw
# DgYDVQQHEwdSZWRtb25kMR4wHAYDVQQKExVNaWNyb3NvZnQgQ29ycG9yYXRpb24x
# JjAkBgNVBAMTHU1pY3Jvc29mdCBUaW1lLVN0YW1wIFBDQSAyMDEwMB4XDTI1MDgx
# NDE4NDgxNVoXDTI2MTExMzE4NDgxNVowgdMxCzAJBgNVBAYTAlVTMRMwEQYDVQQI
# EwpXYXNoaW5ndG9uMRAwDgYDVQQHEwdSZWRtb25kMR4wHAYDVQQKExVNaWNyb3Nv
# ZnQgQ29ycG9yYXRpb24xLTArBgNVBAsTJE1pY3Jvc29mdCBJcmVsYW5kIE9wZXJh
# dGlvbnMgTGltaXRlZDEnMCUGA1UECxMeblNoaWVsZCBUU1MgRVNOOjJEMUEtMDVF
# MC1EOTQ3MSUwIwYDVQQDExxNaWNyb3NvZnQgVGltZS1TdGFtcCBTZXJ2aWNlMIIC
# IjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAr0zToDkpWQtsZekS0cV0quDd
# KSTGkovvBaZH0OAIEi0O3CcO77JiX8c4Epq9uibHVZZ1W/LoufE172vkRXO+QYNt
# WWorECJ2AcZQ10bpAltkhZNiXlVJ8L3QzhKgrXrmMkm2J+/g81U23JPcO4wXHEft
# onT3wpd//936rjmwxMm7NkbsygbJf+4AVBMNr4aMPQhBd76od0KMB6WrvyEGOOU0
# 893OFufS5EDey4n44WgaxJE0Vnv3/OOvuOw5Kp1KPqjjYJ+L9ywLuBMtcDfLpNQO
# /h1eFEoMrbiEM67TOfNlXfxbDz4MlsYvLioxgd2Xzey1QxrV1+i+JyVDJMiSe9gK
# OuzpiQQFE19DUPgsidyjLTzXEhSVLBlRor0eCVf7gC6Rfk8NY3rO2sggOL79vU5F
# uDKTh/sIOtcUHeHC42jBGB+tfdKC1KOBR+UlN9aOzg8mpUNI2FgqQvirVP9ppbeM
# Ufvp2wA9voyTiRWvDgzCxo8xlJ1nscYTHIQrmkF9j/Ca0IDmt8fvOn64nnlJOGUY
# ZYHMC1l0xtgkYTE1ESUqqkawKk7iqbxdnLyycS+dR+zaxPudMDLrQFz8lgfy9obk
# 0D8HC2dzhWpYNn5hdkoPEzgCqQUOp8v3Qj/sd4anyupe5KoCkjABOP3yhSQ4W9Z+
# DrJnhM/rbsXC7oTv26cCAwEAAaOCAUkwggFFMB0GA1UdDgQWBBRSBblSxb5cYKYO
# wvd/VfoXOfu33jAfBgNVHSMEGDAWgBSfpxVdAF5iXYP05dJlpxtTNRnpcjBfBgNV
# HR8EWDBWMFSgUqBQhk5odHRwOi8vd3d3Lm1pY3Jvc29mdC5jb20vcGtpb3BzL2Ny
# bC9NaWNyb3NvZnQlMjBUaW1lLVN0YW1wJTIwUENBJTIwMjAxMCgxKS5jcmwwbAYI
# KwYBBQUHAQEEYDBeMFwGCCsGAQUFBzAChlBodHRwOi8vd3d3Lm1pY3Jvc29mdC5j
# b20vcGtpb3BzL2NlcnRzL01pY3Jvc29mdCUyMFRpbWUtU3RhbXAlMjBQQ0ElMjAy
# MDEwKDEpLmNydDAMBgNVHRMBAf8EAjAAMBYGA1UdJQEB/wQMMAoGCCsGAQUFBwMI
# MA4GA1UdDwEB/wQEAwIHgDANBgkqhkiG9w0BAQsFAAOCAgEAXnSAkmX79Rc7lxS1
# wOozXJ7V0ou5DntVcOJplIkDjvEN8BIQph4U+gSOLZuVReP/z9YdUiUkcPwL1PM2
# 45/kEX1EegpxNc8HDA6hKCHg0ALNEcuxnGOlgKLokXfUer1D5hiW8PABM9R+neit
# eTgPaaRlJFvGTYvotc0uqGiES5hMQhL8RNFhpS9RcIWHtnQGEnrdOUvCAhs4FeVi
# awcmLTKv+1870c/MeTHi0QDdeR+7/Wg4qhkJ2k1iEHJdmYf8rIV0NRBZcdRTTdHe
# e35SXP5neNCfAkjDIuZycRud6jzPLCNLiNYzGXBswzJygj4EeSORT7wMvaFuKeRA
# XoXC3wwYvgIsI1zn3DGY625Y+yZSi8UNSNHuri36Zv9a+Q4vJwDpYK36S0TB2pf7
# xLiiH32nk7YK73Rg98W6fZ2INuzYzZ7Ghgvfffkj4EUXg1E0EffY1pEqkbpDTP7h
# /DBqtzoPXsyw2MUh+7yvWcq2BGZSuca6CY6X4ioMuc5PWpsmvOOli7ARNA7Ab8kK
# dCc2gNDLacglsweZEc9/VQB6hls/b6Kk32nkwuHExKlaeoSVrKB5U9xlp1+c8J/7
# GJj4Rw7AiQ8tcp+WmfyD8KxX2QlKbDi4SUjnglv4617R8+a/cDWJyaMt8279Wn7f
# 2yMedN7kfGIQ5SZj66RdhdlZOq8wggdxMIIFWaADAgECAhMzAAAAFcXna54Cm0mZ
# AAAAAAAVMA0GCSqGSIb3DQEBCwUAMIGIMQswCQYDVQQGEwJVUzETMBEGA1UECBMK
# V2FzaGluZ3RvbjEQMA4GA1UEBxMHUmVkbW9uZDEeMBwGA1UEChMVTWljcm9zb2Z0
# IENvcnBvcmF0aW9uMTIwMAYDVQQDEylNaWNyb3NvZnQgUm9vdCBDZXJ0aWZpY2F0
# ZSBBdXRob3JpdHkgMjAxMDAeFw0yMTA5MzAxODIyMjVaFw0zMDA5MzAxODMyMjVa
# MHwxCzAJBgNVBAYTAlVTMRMwEQYDVQQIEwpXYXNoaW5ndG9uMRAwDgYDVQQHEwdS
# ZWRtb25kMR4wHAYDVQQKExVNaWNyb3NvZnQgQ29ycG9yYXRpb24xJjAkBgNVBAMT
# HU1pY3Jvc29mdCBUaW1lLVN0YW1wIFBDQSAyMDEwMIICIjANBgkqhkiG9w0BAQEF
# AAOCAg8AMIICCgKCAgEA5OGmTOe0ciELeaLL1yR5vQ7VgtP97pwHB9KpbE51yMo1
# V/YBf2xK4OK9uT4XYDP/XE/HZveVU3Fa4n5KWv64NmeFRiMMtY0Tz3cywBAY6GB9
# alKDRLemjkZrBxTzxXb1hlDcwUTIcVxRMTegCjhuje3XD9gmU3w5YQJ6xKr9cmmv
# Haus9ja+NSZk2pg7uhp7M62AW36MEBydUv626GIl3GoPz130/o5Tz9bshVZN7928
# jaTjkY+yOSxRnOlwaQ3KNi1wjjHINSi947SHJMPgyY9+tVSP3PoFVZhtaDuaRr3t
# pK56KTesy+uDRedGbsoy1cCGMFxPLOJiss254o2I5JasAUq7vnGpF1tnYN74kpEe
# HT39IM9zfUGaRnXNxF803RKJ1v2lIH1+/NmeRd+2ci/bfV+AutuqfjbsNkz2K26o
# ElHovwUDo9Fzpk03dJQcNIIP8BDyt0cY7afomXw/TNuvXsLz1dhzPUNOwTM5TI4C
# vEJoLhDqhFFG4tG9ahhaYQFzymeiXtcodgLiMxhy16cg8ML6EgrXY28MyTZki1ug
# poMhXV8wdJGUlNi5UPkLiWHzNgY1GIRH29wb0f2y1BzFa/ZcUlFdEtsluq9QBXps
# xREdcu+N+VLEhReTwDwV2xo3xwgVGD94q0W29R6HXtqPnhZyacaue7e3PmriLq0C
# AwEAAaOCAd0wggHZMBIGCSsGAQQBgjcVAQQFAgMBAAEwIwYJKwYBBAGCNxUCBBYE
# FCqnUv5kxJq+gpE8RjUpzxD/LwTuMB0GA1UdDgQWBBSfpxVdAF5iXYP05dJlpxtT
# NRnpcjBcBgNVHSAEVTBTMFEGDCsGAQQBgjdMg30BATBBMD8GCCsGAQUFBwIBFjNo
# dHRwOi8vd3d3Lm1pY3Jvc29mdC5jb20vcGtpb3BzL0RvY3MvUmVwb3NpdG9yeS5o
# dG0wEwYDVR0lBAwwCgYIKwYBBQUHAwgwGQYJKwYBBAGCNxQCBAweCgBTAHUAYgBD
# AEEwCwYDVR0PBAQDAgGGMA8GA1UdEwEB/wQFMAMBAf8wHwYDVR0jBBgwFoAU1fZW
# y4/oolxiaNE9lJBb186aGMQwVgYDVR0fBE8wTTBLoEmgR4ZFaHR0cDovL2NybC5t
# aWNyb3NvZnQuY29tL3BraS9jcmwvcHJvZHVjdHMvTWljUm9vQ2VyQXV0XzIwMTAt
# MDYtMjMuY3JsMFoGCCsGAQUFBwEBBE4wTDBKBggrBgEFBQcwAoY+aHR0cDovL3d3
# dy5taWNyb3NvZnQuY29tL3BraS9jZXJ0cy9NaWNSb29DZXJBdXRfMjAxMC0wNi0y
# My5jcnQwDQYJKoZIhvcNAQELBQADggIBAJ1VffwqreEsH2cBMSRb4Z5yS/ypb+pc
# FLY+TkdkeLEGk5c9MTO1OdfCcTY/2mRsfNB1OW27DzHkwo/7bNGhlBgi7ulmZzpT
# Td2YurYeeNg2LpypglYAA7AFvonoaeC6Ce5732pvvinLbtg/SHUB2RjebYIM9W0j
# VOR4U3UkV7ndn/OOPcbzaN9l9qRWqveVtihVJ9AkvUCgvxm2EhIRXT0n4ECWOKz3
# +SmJw7wXsFSFQrP8DJ6LGYnn8AtqgcKBGUIZUnWKNsIdw2FzLixre24/LAl4FOmR
# sqlb30mjdAy87JGA0j3mSj5mO0+7hvoyGtmW9I/2kQH2zsZ0/fZMcm8Qq3UwxTSw
# ethQ/gpY3UA8x1RtnWN0SCyxTkctwRQEcb9k+SS+c23Kjgm9swFXSVRk2XPXfx5b
# RAGOWhmRaw2fpCjcZxkoJLo4S5pu+yFUa2pFEUep8beuyOiJXk+d0tBMdrVXVAmx
# aQFEfnyhYWxz/gq77EFmPWn9y8FBSX5+k77L+DvktxW/tM4+pTFRhLy/AsGConsX
# HRWJjXD+57XQKBqJC4822rpM+Zv/Cuk0+CQ1ZyvgDbjmjJnW4SLq8CdCPSWU5nR0
# W2rRnj7tfqAxM328y+l7vzhwRNGQ8cirOoo6CGJ/2XBjU02N7oJtpQUQwXEGahC0
# HVUzWLOhcGbyoYIDVjCCAj4CAQEwggEBoYHZpIHWMIHTMQswCQYDVQQGEwJVUzET
# MBEGA1UECBMKV2FzaGluZ3RvbjEQMA4GA1UEBxMHUmVkbW9uZDEeMBwGA1UEChMV
# TWljcm9zb2Z0IENvcnBvcmF0aW9uMS0wKwYDVQQLEyRNaWNyb3NvZnQgSXJlbGFu
# ZCBPcGVyYXRpb25zIExpbWl0ZWQxJzAlBgNVBAsTHm5TaGllbGQgVFNTIEVTTjoy
# RDFBLTA1RTAtRDk0NzElMCMGA1UEAxMcTWljcm9zb2Z0IFRpbWUtU3RhbXAgU2Vy
# dmljZaIjCgEBMAcGBSsOAwIaAxUA5VHBr4h00EN7jUdQ33SE+qbk/8CggYMwgYCk
# fjB8MQswCQYDVQQGEwJVUzETMBEGA1UECBMKV2FzaGluZ3RvbjEQMA4GA1UEBxMH
# UmVkbW9uZDEeMBwGA1UEChMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMSYwJAYDVQQD
# Ex1NaWNyb3NvZnQgVGltZS1TdGFtcCBQQ0EgMjAxMDANBgkqhkiG9w0BAQsFAAIF
# AO46XqswIhgPMjAyNjA4MjcwNjU4NTFaGA8yMDI2MDgyODA2NTg1MVowdDA6Bgor
# BgEEAYRZCgQBMSwwKjAKAgUA7jpeqwIBADAHAgEAAgIXqDAHAgEAAgISEDAKAgUA
# 7juwKwIBADA2BgorBgEEAYRZCgQCMSgwJjAMBgorBgEEAYRZCgMCoAowCAIBAAID
# B6EgoQowCAIBAAIDAYagMA0GCSqGSIb3DQEBCwUAA4IBAQBynmH3cLGKEeWqFf7N
# wlkJYXm6QsKw2V1AtD1LKn1NR3vEcsQbWL3z8or9rzZj8cQL31BfqSe6BWZqRbcJ
# jbMcAknNS84usfl8AqJ/uROVVYBrUtkHHZeDZm4C6doP0+SlaXUCL8Hgz2MCdbTc
# dJL+O3511vZP0OrmCDDjfVYRDJFFnHYM4OMgSVCJ9+ZPEGLIaU0p6KJrmJqxaq7d
# H0+VltNC5BnmZO7sWgCiZtbptUDreL5YIMZnkE/bnFPRPlmwCJPC5oRVx0kOs68K
# jrw/fCaHKuOjCr1B0Px7bjpmiQZwytJztS82y9vW4osReTDWylvj6pCpv2pVFUKW
# cf0AMYIEDTCCBAkCAQEwgZMwfDELMAkGA1UEBhMCVVMxEzARBgNVBAgTCldhc2hp
# bmd0b24xEDAOBgNVBAcTB1JlZG1vbmQxHjAcBgNVBAoTFU1pY3Jvc29mdCBDb3Jw
# b3JhdGlvbjEmMCQGA1UEAxMdTWljcm9zb2Z0IFRpbWUtU3RhbXAgUENBIDIwMTAC
# EzMAAAIS0QgGPMoYT6oAAQAAAhIwDQYJYIZIAWUDBAIBBQCgggFKMBoGCSqGSIb3
# DQEJAzENBgsqhkiG9w0BCRABBDAvBgkqhkiG9w0BCQQxIgQgMWWr0gVX/WenEL0F
# LQYNviheCris5m5DjafF1EBDvmswgfoGCyqGSIb3DQEJEAIvMYHqMIHnMIHkMIG9
# BCBz+X5GvO7WngknH4BZeYU+BzBL1Jy5oJ8wVlTNIxfYgzCBmDCBgKR+MHwxCzAJ
# BgNVBAYTAlVTMRMwEQYDVQQIEwpXYXNoaW5ndG9uMRAwDgYDVQQHEwdSZWRtb25k
# MR4wHAYDVQQKExVNaWNyb3NvZnQgQ29ycG9yYXRpb24xJjAkBgNVBAMTHU1pY3Jv
# c29mdCBUaW1lLVN0YW1wIFBDQSAyMDEwAhMzAAACEtEIBjzKGE+qAAEAAAISMCIE
# IHGRAiSPTSkiYEvB0iYMI6k6CML5AKkdzI2PhaGR645QMA0GCSqGSIb3DQEBCwUA
# BIICACI3ziSG9NSHtfBJCzBJ+hVzvrioxp0m5uvpAyDAGvgs93w5BsDqCVHxLIZx
# OFlFgGHpDv1w46ez2H7gxn0Qxq2h0ylOBluTU2JnfB6TqG9vib7+9u+dEkefhTb/
# pn+vc94m28826RAEIXMTC/JdSM4gjpbiYGxl58llsfvWO9kTcPWLuDcEnc+oGz3F
# cntepRFXs+KrXbtLJ6ahhN4dwW3qC+lbuStF4JVQRQ/QpTkqsf5wxIPyn8MHjlAi
# SetE6BQbujNfPu5keUbShskPwWqwdGuXrX6Hup1AH48yYy/X/bwdfFxoJhRFyqjA
# c7m5new/N+TMNEdC1tezkYen9aE5hbM6K1HXQHUV0gJwJ1yMCx9gKJbLtSzUiVTA
# dSCRyHQW+Qx6dEFMz+FvA+ynNBDtQveOLfZ9oH3GsH7bHpQqlcY8wsx30pj5cYxH
# MuctDD7DeMVVYC+awD0HykMz+djKxmRESE5X24BXWF0ADNW4E6JSxAJqJwP4ZFF1
# jueHb+nuNElq+GVt80zkbEeU4cBYg1PSqJMNA4l+IJFdEIVIVvZiZmrrDfxlC1GS
# 2PYvu4CvUHwqnuy4+wa7u1RZ9esUSzaYumUv6nBGX5uzSogOlhMCFsBQIUzDFrJk
# I7+iVFhx4/DF2hIm074SD+qIeLBDtxbolckbm9C190bd09cp
# SIG # End signature block