modules/AzStack.Insights/rules/AzStackHci.DiagnosticSettings.NetworkConnectivity.ps1

<#
.SYNOPSIS
    Rule to evaluate the results of network connectivity tests performed by the AzStackHci.DiagnosticSettings.NetworkConnectivity analyzer.
.DESCRIPTION
    This rule evaluates the results of network connectivity tests performed by the AzStackHci.DiagnosticSettings.NetworkConnectivity
    analyzer. It categorizes the results into success, failure, and skipped, and identifies any configuration gaps. The rule then
    sets its status based on the evaluation: any real failure results in a FAILURE status; configuration gaps (e.g. an endpoint
    skipped because an optional caller parameter such as -KeyVaultURL was not supplied) are treated as SUCCESS and surfaced via an
    informational note rather than a WARNING; and all other results result in a SUCCESS status.
 
    DESIGN NOTES (rationale behind the bucketing/heuristics in the rule body):
 
    1. Bucketing on 'ResultCategory'. AzStackHci.DiagnosticSettings stamps a machine-readable 'ResultCategory'
       column on every row (Success | ConnectivityFailure | ConfigGap | Skipped | SSLInspected | PrivateLink).
       We bucket on that field instead of the brittle free-text Note / raw Layer7Status, because the module's
       own classifier already accounts for precedence - a ConfigGap / SSLInspected / PrivateLink row usually
       ALSO carries Layer7Status='Failed', which naive logic would miscount as a hard connectivity failure.
       The categoriser is evaluated inline (no row mutation) so the shared result objects are never altered.
       SSLInspected / PrivateLink rows are deliberately NOT treated as connectivity failures here - dedicated
       rules (SslInspection / PrivateLinkConfiguration) own those signals.
 
    2. DNS-resolution sub-bucket ($dnsFailures). The module emits no dedicated 'DnsFailure' ResultCategory, so a
       name that fails to resolve lands in $failed alongside genuinely reachable-but-blocked endpoints. The DNS
       subset is an ADDITIVE triage projection only - the rows REMAIN in $failed and still drive the FAILURE
       status - it just lets a reviewer tell "name didn't resolve" apart from "resolved but unreachable". We
       detect a DNS failure with three complementary signals (all confirmed against real module output):
         a. Layer7Response carries the resolver error text - the AUTHORITATIVE signal, e.g.
            "The remote name could not be resolved: '<host>'". (The per-row Note field is just a STATIC endpoint
            description, NOT the error - so read Layer7Response, never Note, for the failure reason.)
         b. IPAddress carries the resolver error text instead of an IP (some module builds stamp e.g.
            "DNS name does not exist" into IPAddress with Layer7Status 'Failed').
         c. A null/blank IPAddress - on a resolution failure the module never obtains an IP, whereas a
            resolved-but-blocked endpoint carries a populated IPAddress.
       Matching on ANY of these makes the heuristic robust. We deliberately do NOT use TCPStatus - it defaults
       to 'N/A' on every row unless the run was invoked with -IncludeTCPConnectivityTests.
 
    3. CRL/OCSP revocation-offline sub-bucket ($revocationOfflineFailures). The module keeps a
       trusted-chain-but-revocation-offline parent row classified as ConnectivityFailure (it does NOT reclassify
       it like SSL inspection), so these rows legitimately remain in $failed and still drive FAILURE. We surface
       them as their own subset (purely triage, like the DNS subset) so a reviewer can separate "the CA's
       revocation responder / HTTP port 80 is unreachable" from a genuine endpoint outage. Detected three ways:
       the parent row carries the 'CRL/OCSP unreachable' marker the module appends to Layer7Response; the
       appended dependency rows carry Source='CRL/OCSP dependency for <parent>'; and the Note references
       certificate revocation. The Note pattern is anchored to '(check|list)' so it matches both the dependency
       rows ("...certificate revocation check of...") and the raw CRL/OCSP endpoint probes that are themselves
       down ("...certificate revocation list.") - the latter are the root-cause responder endpoints - while
       staying tight enough to avoid sweeping in unrelated revocation prose.
 
    4. ConfigGap rolls up to SUCCESS. Config gaps are NOT failures - they are missing optional caller-supplied
       parameters (for example -KeyVaultURL, only needed for the Cloud Deployment process), so they roll up to
       SUCCESS with an informational note rather than WARNING.
#>


[CmdletBinding()]
    param(
        [Parameter(Mandatory=$true)]
        [ValidateNotNull()]
        [PsCustomObject]$ConnectivityResults
    )

# Import localization strings from the locale directory
# this will natively import the psd1 file that has the same name as the script
# and the locale folder that matches the current culture of the user running the script
Import-LocalizedData -BindingVariable 'localizedData' -BaseDirectory "$PSScriptRoot\locale" -UICulture $PSUICulture
$insightRule = Initialize-InsightRule -Id $localizedData.Id -Properties $localizedData.Insight

<# START INSIGHT RULE LOGIC #>

try {
    # The 0.6.8 module returns a structured PSCustomObject: run-level telemetry as top-level properties and the
    # per-endpoint rows nested under .Results. Bucket over the rows collection.
    $rows = @($ConnectivityResults.Results)

    Write-InsightLog -Message "Evaluating $($rows.Count) network connectivity result(s)"

    # Bucket on the module's machine-readable 'ResultCategory' column (see DESIGN NOTE 1 in the header).
    $categorise = {
        param($row)
        if ($row.PSObject.Properties['ResultCategory'] -and $row.ResultCategory) {
            return [string]$row.ResultCategory
        }
        return 'ConnectivityFailure'
    }

    # Bucket by resolved category (no row mutation). SSLInspected / PrivateLink have their own dedicated rules.
    $passed       = @($rows | Where-Object { (& $categorise $_) -ieq 'Success' })
    $failed       = @($rows | Where-Object { (& $categorise $_) -ieq 'ConnectivityFailure' })
    $configGaps   = @($rows | Where-Object { (& $categorise $_) -ieq 'ConfigGap' })
    $skipped      = @($rows | Where-Object { (& $categorise $_) -ieq 'Skipped' })
    $sslInspected = @($rows | Where-Object { (& $categorise $_) -ieq 'SSLInspected' })
    $privateLink  = @($rows | Where-Object { (& $categorise $_) -ieq 'PrivateLink' })

    # DNS-resolution sub-bucket: an additive triage projection over $failed (rows stay in $failed and still drive
    # FAILURE). Detected via Layer7Response / IPAddress resolver-error text or a blank IPAddress - see DESIGN NOTE 2.
    $dnsErrorPattern = 'could not be resolved|No such host is known|name does not exist|temporary failure in name resolution|hostname resolution'
    $dnsFailures = @($failed | Where-Object {
        [string]::IsNullOrWhiteSpace([string]$_.IPAddress) -or
        ([string]$_.IPAddress -imatch $dnsErrorPattern) -or
        ([string]$_.Layer7Response -imatch $dnsErrorPattern)
    })

    # CRL/OCSP revocation-offline sub-bucket: an additive triage projection over $failed (rows stay in $failed and
    # still drive FAILURE). Detected via the Layer7Response marker, dependency-row Source, or Note - see DESIGN NOTE 3.
    $revocationOfflineFailures = @($failed | Where-Object {
        ([string]$_.Layer7Response -imatch 'CRL/OCSP unreachable') -or
        ([string]$_.Source -imatch '^(CRL|OCSP) dependency for ') -or
        ([string]$_.Note -imatch 'certificate revocation (check|list)')
    })

    Write-InsightLog -Message (
        "ResultCategory summary: Success={0}, ConnectivityFailure={1} (DnsResolutionFailures={2}), ConfigGap={3}, Skipped={4}, SSLInspected={5}, PrivateLink={6}" -f `
            $passed.Count, $failed.Count, $dnsFailures.Count, $configGaps.Count, $skipped.Count, $sslInspected.Count, $privateLink.Count
    )

    # Log details for failures to make triage easier, including the Note to provide context on the failure.
    # This will help identify if it's a connectivity issue or a config issue.
    foreach ($row in $failed) {
        Write-InsightLog -Level 'Error' -Message (
            "FAILED: '{0}:{1}' [{2}] Response='{3}' TCPStatus='{4}' IP='{5}' ResponseTime='{6}' Note='{7}'" -f `
                $row.URL, $row.Port, $row.Source, $row.Layer7Response,
                $row.TCPStatus, $row.IPAddress, $row.Layer7ResponseTime, $row.Note
        )
    }

    # Call out the DNS-resolution subset explicitly. We surface Layer7Response (the actual resolver error text,
    # e.g. "The remote name could not be resolved: '...'") rather than Note (a static endpoint description), so the
    # triage log shows the real reason "name didn't resolve" instead of forcing the reader to infer it.
    foreach ($row in $dnsFailures) {
        Write-InsightLog -Level 'Error' -Message (
            "DNS RESOLUTION FAILURE: '{0}' [{1}] could not be resolved. Layer7Response='{2}'" -f `
                $row.URL, $row.Source, $row.Layer7Response
        )
    }

    # Call out the revocation-offline subset explicitly. These are not name-resolution or raw-egress outages - the
    # certificate chain is trusted but the CA's CRL / OCSP responder (HTTP port 80) is unreachable. The dedicated
    # CrlRevocationOffline rule raises the run-level WARNING; here we just make the affected rows easy to spot.
    foreach ($row in $revocationOfflineFailures) {
        Write-InsightLog -Level 'Warning' -Message (
            "REVOCATION OFFLINE: '{0}' [{1}] certificate revocation check could not complete. Layer7Response='{2}'" -f `
                $row.URL, $row.Source, $row.Layer7Response
        )
    }

    # Log details for successes as Informational, including the Note to provide context on the test.
    # This can help identify if there are any unexpected successes
    # (e.g. if a test was expected to fail due to a config gap but passed anyway).
    foreach ($row in $passed) {
        Write-InsightLog -Level 'Informational' -Message (
            "SUCCESS: '{0}:{1}' [{2}] Response='{3}' TCPStatus='{4}' IP='{5}' ResponseTime='{6}' Note='{7}'" -f `
                $row.URL, $row.Port, $row.Source, $row.Layer7Response,
                $row.TCPStatus, $row.IPAddress, $row.Layer7ResponseTime, $row.Note
        )
    }

    # Log config gaps as informational to differentiate from real failures, and include the Note to clarify what's
    # missing. The KeyVault URL is the canonical example: the test was skipped because -KeyVaultURL wasn't provided
    # (a parameter only required for the Cloud Deployment process), so this is informational - not a connectivity failure.
    foreach ($row in $configGaps) {
        Write-InsightLog -Level 'Informational' -Message (
            "CONFIG GAP: '{0}' skipped - {1}" -f $row.URL, $row.Note
        )
    }

    # Decide status: any real failure -> FAILURE. Config gaps are NOT failures - they are missing optional
    # caller-supplied parameters (for example -KeyVaultURL, only needed for the Cloud Deployment process), so they
    # roll up to SUCCESS with an informational note rather than WARNING. SSLInspected / PrivateLink signals have
    # their own dedicated rules and are not considered here.
    $configGapNote = ''
    if ($failed.Count -gt 0) {
        $insightRule.Status       = [InsightStatus]::FAILURE
        $insightRule.ErrorMessage = ($localizedData.Insight.ErrorMessage -f $failed.Count, $rows.Count)
        $insightRule.ErrorCode    = $localizedData.Insight.ErrorCode
    }
    else {
        $insightRule.Status = [InsightStatus]::SUCCESS
        if ($configGaps.Count -gt 0) {
            $configGapNote = ($localizedData.ConfigGapMessage -f $configGaps.Count)
            Write-InsightLog -Level 'Informational' -Message $configGapNote
        }
    }

    # Keep Properties compact: summary + only the rows that need attention.
    # Include MachineName (v0.6.7, for multi-node attribution) and ResultCategory (the field we bucket on)
    # in the projection so a reviewer can see the classification the module assigned to each surfaced row.
    [string[]]$projection = 'RowID','MachineName','URL','Port','Source','IPAddress',
                'Layer7Status','Layer7Response','Layer7ResponseTime',
                'TCPStatus','ResultCategory','Note'

    $insightRule.Properties = [PSCustomObject]@{
        TotalEndpoints     = $rows.Count
        SuccessCount       = $passed.Count
        FailureCount       = $failed.Count
        SkippedCount       = $skipped.Count
        ConfigGapCount     = $configGaps.Count
        ConfigGapNote      = $configGapNote
        SSLInspectedCount  = $sslInspected.Count
        PrivateLinkCount   = $privateLink.Count
        DnsFailureCount    = $dnsFailures.Count
        RevocationOfflineCount = $revocationOfflineFailures.Count
        FailedEndpoints    = $failed       | Select-Object $projection
        DnsResolutionFailures = $dnsFailures | Select-Object $projection
        RevocationOfflineFailures = $revocationOfflineFailures | Select-Object $projection
        ConfigGapEndpoints = $configGaps   | Select-Object $projection
        SuccessfulEndpoints = $passed      | Select-Object $projection
    }

} catch {
    $_ | Write-InsightLog -Level 'Exception'
    $insightRule.ScriptStackTrace = Get-FormattedException -Exception $_.Exception
    $insightRule.Status = [InsightStatus]::UNKNOWN
}


<# END INSIGHT RULE LOGIC #>

# if the status is FAILURE or WARNING, set the failure code and remediation message
if ($insightRule.Status -eq [InsightStatus]::FAILURE -or $insightRule.Status -eq [InsightStatus]::WARNING) {
    $insightRule.ErrorCode = $localizedData.Insight.ErrorCode
    $insightRule.Remediation = $localizedData.Insight.Remediation
}

$insightRule.Duration = New-TimeSpan -Start $insightRule.OccurrenceTimeUTC -End $([System.DateTime]::UtcNow)
Write-InsightEvent -Insight $insightRule
return $insightRule

# SIG # Begin signature block
# MIInRgYJKoZIhvcNAQcCoIInNzCCJzMCAQExDzANBglghkgBZQMEAgEFADB5Bgor
# BgEEAYI3AgEEoGswaTA0BgorBgEEAYI3AgEeMCYCAwEAAAQQH8w7YFlLCE63JNLG
# KX7zUQIBAAIBAAIBAAIBAAIBADAxMA0GCWCGSAFlAwQCAQUABCD9JTityouzM68T
# R2sxBi4ZVru6jsszGbYC7UpONI/kFKCCDLowggX1MIID3aADAgECAhMzAAACHU0Z
# yE7XD1dIAAAAAAIdMA0GCSqGSIb3DQEBCwUAMFcxCzAJBgNVBAYTAlVTMR4wHAYD
# VQQKExVNaWNyb3NvZnQgQ29ycG9yYXRpb24xKDAmBgNVBAMTH01pY3Jvc29mdCBD
# b2RlIFNpZ25pbmcgUENBIDIwMjQwHhcNMjYwNDE2MTg1OTQzWhcNMjcwNDE1MTg1
# OTQzWjB0MQswCQYDVQQGEwJVUzETMBEGA1UECBMKV2FzaGluZ3RvbjEQMA4GA1UE
# BxMHUmVkbW9uZDEeMBwGA1UEChMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMR4wHAYD
# VQQDExVNaWNyb3NvZnQgQ29ycG9yYXRpb24wggEiMA0GCSqGSIb3DQEBAQUAA4IB
# DwAwggEKAoIBAQDQvewXxx9gZZFC6Ys1WBay8BJ8kGA4JQnH5CMafqOASlTpK9H8
# o5ZXTXt0caVQTNMUPt445wXYD+dFtaKWTwDn1I52oUSrC9vJin1Gsqt+zyKJL5Dg
# 3eQXbQNR61DmMy20GLTIO3SFed9Rfi/ophgCLGFLDR3r0KvHjwMb/jYWS0celV/4
# Lz27LfAekm8v9E5IXaeiXbAUYZKK090n4CVl3JBtbN+9DtI9SNu/yjvozW52/u7R
# X/Ttpa/KDlpuokZ+Zcbvmtd9ur9gFLvZzh41o9MsE/clQtdaFWGvuo6Jua/ntpgk
# ey3E5/vBFe+MJPG6phdnuo6r57ZudCudiI1bAgMBAAGjggGbMIIBlzAOBgNVHQ8B
# Af8EBAMCB4AwHwYDVR0lBBgwFgYKKwYBBAGCN0wIAQYIKwYBBQUHAwMwHQYDVR0O
# BBYEFH6QuMwqcPG0hQlQ6c5jCtTTLrVeMEUGA1UdEQQ+MDykOjA4MR4wHAYDVQQL
# ExVNaWNyb3NvZnQgQ29ycG9yYXRpb24xFjAUBgNVBAUTDTIzMDAxMis1MDc1NTkw
# HwYDVR0jBBgwFoAUf1k/VCHarU/vBeXmo9ctBpQSCDEwYAYDVR0fBFkwVzBVoFOg
# UYZPaHR0cDovL3d3dy5taWNyb3NvZnQuY29tL3BraW9wcy9jcmwvTWljcm9zb2Z0
# JTIwQ29kZSUyMFNpZ25pbmclMjBQQ0ElMjAyMDI0LmNybDBtBggrBgEFBQcBAQRh
# MF8wXQYIKwYBBQUHMAKGUWh0dHA6Ly93d3cubWljcm9zb2Z0LmNvbS9wa2lvcHMv
# Y2VydHMvTWljcm9zb2Z0JTIwQ29kZSUyMFNpZ25pbmclMjBQQ0ElMjAyMDI0LmNy
# dDAMBgNVHRMBAf8EAjAAMA0GCSqGSIb3DQEBCwUAA4ICAQBKTbYOjzwTG/DXGaz9
# s6+fQeaTtDcFmMY+5UyVFCyj7Pv+5i37qfX8lSL/tBIfYQfWsMuBQlfZurJD6r4H
# VJ2CeH+1fgiq8dcHdVKoZ3Sa2qXoX3cq9iS8cVb06B7+5/XJ7I0OxHH9fDsvJ3T3
# w5V/ZtAIFmLrl+P0CtG+92uzRsn0nTbdFjOkLMLWPLAU3THohKRlSEMgFJpPkm5n
# 5UAZ35xX6FWCrDLsSKb555bTifwa8mJBwdlof0bmfYidH+dxZ1FdDxvLnNl9zeKs
# A4kejaaIqqIPguhwAti5Ql7BlTNoJNwxCvBmqW2MQLnCkYN/VVUsR3V2x/rcTNzo
# Bf/Z/SpROvdaA2ZOOd1uioXJt3tdLQ7vHpqpib0KfWr/FWXW10q38VxfCnRQBqzb
# SuztR7nEMuzX7Ck+B/XaPDXd1qh72+QYyB0Z2VzWmO9zsnb9Uq/dwu8LGeQqnyu6
# 7SDGACvnXii2fb9+US492VTnXSnFKyqwgzUyFMtZK1/sHYTv6bG4TtQUygQxTN+Z
# V+aJIlKO2MqZ7bKrAnOzS9m6NgoTdWOq11bTOZwKlIEV/EhV9SWkDmdpR/hPPT2v
# 6TEj4F8PT/zHjRezIU5c/DGlt/VhY/pK0XkJtEyMmmS1BMtjU/rqBZVMIm3dnxQs
# /TBByr+Cf8Z1r7aifQVQ+WSqzjCCBr0wggSloAMCAQICEzMAAAA5O7Y3Gb8GHWcA
# AAAAADkwDQYJKoZIhvcNAQEMBQAwgYgxCzAJBgNVBAYTAlVTMRMwEQYDVQQIEwpX
# YXNoaW5ndG9uMRAwDgYDVQQHEwdSZWRtb25kMR4wHAYDVQQKExVNaWNyb3NvZnQg
# Q29ycG9yYXRpb24xMjAwBgNVBAMTKU1pY3Jvc29mdCBSb290IENlcnRpZmljYXRl
# IEF1dGhvcml0eSAyMDExMB4XDTI0MDgwODIwNTQxOFoXDTM2MDMyMjIyMTMwNFow
# VzELMAkGA1UEBhMCVVMxHjAcBgNVBAoTFU1pY3Jvc29mdCBDb3Jwb3JhdGlvbjEo
# MCYGA1UEAxMfTWljcm9zb2Z0IENvZGUgU2lnbmluZyBQQ0EgMjAyNDCCAiIwDQYJ
# KoZIhvcNAQEBBQADggIPADCCAgoCggIBANgBnB7jOMeqlRYHNa265v4IY9fH8TKh
# emHfPINe1gpLaV3dhg324WwH06LcHbpnsBukCDNitryo0dtS/EW6I/yEL/bLSY8h
# KpbfQuWusBPr9qazYcDxCW/qnjb5JsI1s8bNOg3bVATvQVL4tcf03aTycsz8QeCd
# M0l/yHRObJ9QqazM1r6VPEOJ7LL+uEEb73w6QCuhs89a1uv1zerOYMnsneRRwCbp
# yW11IcggU0cRKDDq1pjVJzIbIF6+oiXXbReOsgeI8zu1FyQfK0fVkaya8SmVHQ/t
# Of23mZ4W9k0Ri22QW9p3UgSC5OUDktKxxcCmGL6tXLfOGSWHIIV4YrTJTT6PNty5
# REojHJuZHArkF9VnHTERWoTjAzfI3kP+5b4alUdhgAZ7ttOu1bVnXfHaqPYl2rPs
# 20ji03LOVWsh/radgE17es5hL+t6lV0eVHrVhsssROWJuz2MXMCt7iw7lFPG9LXK
# Gjsmonn2gotGdHIuEg5JnJMJVmixd5LRlkmgYRZKzhxSCwyoGIq0PhaA7Y+VPct5
# pCHkijcIIDm0nlkK+0KyepolcqGm0T/GYQRMhHJlGOOmVQop36wUVUYklUy++vDW
# eEgEo4s7hxN6mIbf2MSIQ/iIfMZgJxC69oukMUXCrOC3SkE/xIkgpfl22MM1itkZ
# 35nNXkMolU1lAgMBAAGjggFOMIIBSjAOBgNVHQ8BAf8EBAMCAYYwEAYJKwYBBAGC
# NxUBBAMCAQAwHQYDVR0OBBYEFH9ZP1Qh2q1P7wXl5qPXLQaUEggxMBkGCSsGAQQB
# gjcUAgQMHgoAUwB1AGIAQwBBMA8GA1UdEwEB/wQFMAMBAf8wHwYDVR0jBBgwFoAU
# ci06AjGQQ7kUBU7h6qfHMdEjiTQwWgYDVR0fBFMwUTBPoE2gS4ZJaHR0cDovL2Ny
# bC5taWNyb3NvZnQuY29tL3BraS9jcmwvcHJvZHVjdHMvTWljUm9vQ2VyQXV0MjAx
# MV8yMDExXzAzXzIyLmNybDBeBggrBgEFBQcBAQRSMFAwTgYIKwYBBQUHMAKGQmh0
# dHA6Ly93d3cubWljcm9zb2Z0LmNvbS9wa2kvY2VydHMvTWljUm9vQ2VyQXV0MjAx
# MV8yMDExXzAzXzIyLmNydDANBgkqhkiG9w0BAQwFAAOCAgEAFJQfOChP7onn6fLI
# MKrSlN1WYKwDFgAddymOUO3FrM8d7B/W/iQ6DxXsDn7D5W4wMwYeLystcEqfkjz4
# NURRgazyMu5yRzQh4LqjA4tStTcJh1opExo7nn5PuPBYnbu0+THSuVHTe0VTTPVh
# ily/piFrDo3axQ9P4C+Ol5yet+2gTfekICS5xS+cYfSIvgn0JksVBVMYVI5QFu/q
# hnLhsEFEUzG8fvv0hjgkO+lkpV9ty6GkN4vdnd7ya6Q6aR9y34aiM1qmxaxBi6OU
# nyNl6fkuun/diTFnYDLTppOkr/mg5WSfCiDVMNCxtj4wPKC5OmHm1DQIt/MNokbb
# H3UGsFP1QbzsLocuSqLCvH09Io3fDPTmscR9Y75G4qX7RTX8AdBPo0I6OEojf39z
# uFZt0qOHm65YWQE69cZM2ueE1MB05dNNgHK9gTE7zKvK/fg8B2qjW88MT/WF5V5u
# vZGtqa9FSL2RazArA+rDPuf6JGYz4HpgMZHB4S6szWSKYBv0VisCzfxgeU+dquXW
# 9bd0auYlOB58DPcOYKdc3Se94g+xL4pcEhbB54JOgAkwYTu/9dLeH2pDqeJZAABV
# DWRQCaXfO5LgyKwKCLYXpigrZYCjUSBcr+Ve8PFWMhVTQl0v4q8J/AUmQN5W4n10
# 1cY2L4A7GTQG1h32HHAvfQESWP0xghniMIIZ3gIBATBuMFcxCzAJBgNVBAYTAlVT
# MR4wHAYDVQQKExVNaWNyb3NvZnQgQ29ycG9yYXRpb24xKDAmBgNVBAMTH01pY3Jv
# c29mdCBDb2RlIFNpZ25pbmcgUENBIDIwMjQCEzMAAAIdTRnITtcPV0gAAAAAAh0w
# DQYJYIZIAWUDBAIBBQCgga4wGQYJKoZIhvcNAQkDMQwGCisGAQQBgjcCAQQwHAYK
# KwYBBAGCNwIBCzEOMAwGCisGAQQBgjcCARUwLwYJKoZIhvcNAQkEMSIEICAPfjLP
# jrC18Fq8jfk1ijP/nFKvSaoBnTAAKN57ALuSMEIGCisGAQQBgjcCAQwxNDAyoBSA
# EgBNAGkAYwByAG8AcwBvAGYAdKEagBhodHRwOi8vd3d3Lm1pY3Jvc29mdC5jb20w
# DQYJKoZIhvcNAQEBBQAEggEApwiGmVr+ZRwUG7gsddIGfdFxPKaNr+OK9fv1Cv7M
# ZTVCN4xPAwKsDmpkeTXtXZc7rm4y8fHB+CvJ55Z9imNe9QOspMfGkjHSHCgTFyC1
# lWcGw4dxysaUVdKf7TeZEu4lHEdgkU7MA6Oqah0QDkdMO9TNjMklG5/12bO7mgmB
# i3+KGlUkh5/XD8BVlQz3KxXR974vJES5CfF5cqq1Mtm6eGrXttXxuCsO+4gTNmRG
# WW9UXSs/CmMFNC/1zQD+y2ADKi+JPA+1cwE/xzruMDeoI4fs+OifPPNJ/7SlZ0dK
# YYH9jV0pc1F6rjK0A+2JN6BTLzUbuyZyMOvNqnPoYuE55KGCF5QwgheQBgorBgEE
# AYI3AwMBMYIXgDCCF3wGCSqGSIb3DQEHAqCCF20wghdpAgEDMQ8wDQYJYIZIAWUD
# BAIBBQAwggFSBgsqhkiG9w0BCRABBKCCAUEEggE9MIIBOQIBAQYKKwYBBAGEWQoD
# ATAxMA0GCWCGSAFlAwQCAQUABCCc+/F8D2R8JxpU9L4yTKck4rR+G5hwFukalEZV
# 9otYXgIGajEzLxUGGBMyMDI2MDcxNjIyNTUwNC42MjJaMASAAgH0oIHRpIHOMIHL
# MQswCQYDVQQGEwJVUzETMBEGA1UECBMKV2FzaGluZ3RvbjEQMA4GA1UEBxMHUmVk
# bW9uZDEeMBwGA1UEChMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMSUwIwYDVQQLExxN
# aWNyb3NvZnQgQW1lcmljYSBPcGVyYXRpb25zMScwJQYDVQQLEx5uU2hpZWxkIFRT
# UyBFU046QTAwMC0wNUUwLUQ5NDcxJTAjBgNVBAMTHE1pY3Jvc29mdCBUaW1lLVN0
# YW1wIFNlcnZpY2WgghHqMIIHIDCCBQigAwIBAgITMwAAAiu7AFD/TTuaoQABAAAC
# KzANBgkqhkiG9w0BAQsFADB8MQswCQYDVQQGEwJVUzETMBEGA1UECBMKV2FzaGlu
# Z3RvbjEQMA4GA1UEBxMHUmVkbW9uZDEeMBwGA1UEChMVTWljcm9zb2Z0IENvcnBv
# cmF0aW9uMSYwJAYDVQQDEx1NaWNyb3NvZnQgVGltZS1TdGFtcCBQQ0EgMjAxMDAe
# Fw0yNjAyMTkxOTQwMTFaFw0yNzA1MTcxOTQwMTFaMIHLMQswCQYDVQQGEwJVUzET
# MBEGA1UECBMKV2FzaGluZ3RvbjEQMA4GA1UEBxMHUmVkbW9uZDEeMBwGA1UEChMV
# TWljcm9zb2Z0IENvcnBvcmF0aW9uMSUwIwYDVQQLExxNaWNyb3NvZnQgQW1lcmlj
# YSBPcGVyYXRpb25zMScwJQYDVQQLEx5uU2hpZWxkIFRTUyBFU046QTAwMC0wNUUw
# LUQ5NDcxJTAjBgNVBAMTHE1pY3Jvc29mdCBUaW1lLVN0YW1wIFNlcnZpY2UwggIi
# MA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQCX3mi6OD3syUqQm4QqgkrKPbcs
# K/Qx3fYctL8+VM1uOY3booi5GxwauTgQf6JFHITToxS7gjqKlK8OFLzL6UTl0jxE
# K5t6DuOcgJXdvutimoTlOS0C3kyITXBAXoj/gp6hRR9z6WRip1Ktkilb3dJXCjQq
# T9P2Cuujr+Vz8r+Z+jDl09ji/ic/4G34r3mVwjs//Gnx9Pu31V8rXFicNiAzxpub
# awpbd8pqfzlWT2vnG3kF9l6MiREbvJ3XHLUwHQsh0t/TrSFx/s/yCqpJWYJ6oClG
# 70tvsFH0aRP8wB4cP/CFa2ILvk26i3OcJBl+pqKjHTSBy9mvwTPEDlnzco0Nt8R6
# pSPTXZgBsscHhoKfC0WQmOzY2keXbAmRTcZMyXz5v/AJbmoI0y07Bazvt5NkXddG
# 9TErQWwtsFyIKrElDgWfHeCoTu1wu2ciD3dK72z3ca2gzoEDxT2j9BXIUKaiTzTd
# QPRsAMaO3dU0zaGwMMlwtSJyDh14YEgZoUu5vS8MugMqdrNjphyL65yKhjpAWbhY
# kIHO/0uZju95tP8zZNqXIRh4tdfWHJPATn9r+cxkyuh2x0VLdfx1lmK9X3NjH0Nt
# gAs5JB/wOlkyuudxmFTfWVyRrL37ispOZ8aPAFgvyR6cNTkGpkFo35JRjciNmZiU
# 4qT9Uty+V5gudFk1jwIDAQABo4IBSTCCAUUwHQYDVR0OBBYEFD4WjuQTUJbtbd3j
# mvZku0FZ2eU2MB8GA1UdIwQYMBaAFJ+nFV0AXmJdg/Tl0mWnG1M1GelyMF8GA1Ud
# HwRYMFYwVKBSoFCGTmh0dHA6Ly93d3cubWljcm9zb2Z0LmNvbS9wa2lvcHMvY3Js
# L01pY3Jvc29mdCUyMFRpbWUtU3RhbXAlMjBQQ0ElMjAyMDEwKDEpLmNybDBsBggr
# BgEFBQcBAQRgMF4wXAYIKwYBBQUHMAKGUGh0dHA6Ly93d3cubWljcm9zb2Z0LmNv
# bS9wa2lvcHMvY2VydHMvTWljcm9zb2Z0JTIwVGltZS1TdGFtcCUyMFBDQSUyMDIw
# MTAoMSkuY3J0MAwGA1UdEwEB/wQCMAAwFgYDVR0lAQH/BAwwCgYIKwYBBQUHAwgw
# DgYDVR0PAQH/BAQDAgeAMA0GCSqGSIb3DQEBCwUAA4ICAQDO/CKsciEM8kr1fqH4
# TlfT66ENoTjxXw810pyEq0PdrgLwfgT3x+1gz7CQHtUdevqMQ5qHyDLhm6pT911C
# YkGN+6g+MU7fMYTr6d3SxieJwBIoWkfR4g7SitGzMKU465KEYejfddoUgovC/xcR
# paALO5p3/A248ByhJiMttBQNDtsT/HaCFwRFCURby/f8c1kky8F8xkCXFz+/MtZ5
# d1lWFjwOI2geZHWq9XihDOgee5nS2koo5V6n8XG220UTevVf+pgmpIH71XKDVIYT
# GGZJs6yPlfJ2aXqw1ME4NR6okNsY3P1M31H6DMYRfJGNBNep595kXGh3YzA3cCiy
# g+jmJ58h/fTvjngIpuUFfODpDjFx0ic1YoLANxhCF3RhS9qYM7K40NEhKshYuaAk
# IG2XBKYig3r/0/b0sjvjBws55AYonMm3A8qcX/6k9Vfc0mv9dtonHuWGfA2b+qE2
# qpCnhzGbdDHq7iOSZEw01nNupAMf1c41k9IoTQ2z3iw6w4ZZoLOyg4TKMbp1krpT
# 4trip/y30Cv5khyqCDNqaXQpBkOYON8LgtoQ3amVOX7ix5jdrnx/vUxTUSigXvrW
# dL7Uk8kpmS0zto2Toy7aT5oBzCTvfj9iJ/BN/E1vhFBkhJCvZ7PVvsMSnTTmkx2F
# al2lVkztuAI44fD/uyLJdaMQSzCCB3EwggVZoAMCAQICEzMAAAAVxedrngKbSZkA
# AAAAABUwDQYJKoZIhvcNAQELBQAwgYgxCzAJBgNVBAYTAlVTMRMwEQYDVQQIEwpX
# YXNoaW5ndG9uMRAwDgYDVQQHEwdSZWRtb25kMR4wHAYDVQQKExVNaWNyb3NvZnQg
# Q29ycG9yYXRpb24xMjAwBgNVBAMTKU1pY3Jvc29mdCBSb290IENlcnRpZmljYXRl
# IEF1dGhvcml0eSAyMDEwMB4XDTIxMDkzMDE4MjIyNVoXDTMwMDkzMDE4MzIyNVow
# fDELMAkGA1UEBhMCVVMxEzARBgNVBAgTCldhc2hpbmd0b24xEDAOBgNVBAcTB1Jl
# ZG1vbmQxHjAcBgNVBAoTFU1pY3Jvc29mdCBDb3Jwb3JhdGlvbjEmMCQGA1UEAxMd
# TWljcm9zb2Z0IFRpbWUtU3RhbXAgUENBIDIwMTAwggIiMA0GCSqGSIb3DQEBAQUA
# A4ICDwAwggIKAoICAQDk4aZM57RyIQt5osvXJHm9DtWC0/3unAcH0qlsTnXIyjVX
# 9gF/bErg4r25PhdgM/9cT8dm95VTcVrifkpa/rg2Z4VGIwy1jRPPdzLAEBjoYH1q
# UoNEt6aORmsHFPPFdvWGUNzBRMhxXFExN6AKOG6N7dcP2CZTfDlhAnrEqv1yaa8d
# q6z2Nr41JmTamDu6GnszrYBbfowQHJ1S/rboYiXcag/PXfT+jlPP1uyFVk3v3byN
# pOORj7I5LFGc6XBpDco2LXCOMcg1KL3jtIckw+DJj361VI/c+gVVmG1oO5pGve2k
# rnopN6zL64NF50ZuyjLVwIYwXE8s4mKyzbnijYjklqwBSru+cakXW2dg3viSkR4d
# Pf0gz3N9QZpGdc3EXzTdEonW/aUgfX782Z5F37ZyL9t9X4C626p+Nuw2TPYrbqgS
# Uei/BQOj0XOmTTd0lBw0gg/wEPK3Rxjtp+iZfD9M269ewvPV2HM9Q07BMzlMjgK8
# QmguEOqEUUbi0b1qGFphAXPKZ6Je1yh2AuIzGHLXpyDwwvoSCtdjbwzJNmSLW6Cm
# gyFdXzB0kZSU2LlQ+QuJYfM2BjUYhEfb3BvR/bLUHMVr9lxSUV0S2yW6r1AFemzF
# ER1y7435UsSFF5PAPBXbGjfHCBUYP3irRbb1Hode2o+eFnJpxq57t7c+auIurQID
# AQABo4IB3TCCAdkwEgYJKwYBBAGCNxUBBAUCAwEAATAjBgkrBgEEAYI3FQIEFgQU
# KqdS/mTEmr6CkTxGNSnPEP8vBO4wHQYDVR0OBBYEFJ+nFV0AXmJdg/Tl0mWnG1M1
# GelyMFwGA1UdIARVMFMwUQYMKwYBBAGCN0yDfQEBMEEwPwYIKwYBBQUHAgEWM2h0
# dHA6Ly93d3cubWljcm9zb2Z0LmNvbS9wa2lvcHMvRG9jcy9SZXBvc2l0b3J5Lmh0
# bTATBgNVHSUEDDAKBggrBgEFBQcDCDAZBgkrBgEEAYI3FAIEDB4KAFMAdQBiAEMA
# QTALBgNVHQ8EBAMCAYYwDwYDVR0TAQH/BAUwAwEB/zAfBgNVHSMEGDAWgBTV9lbL
# j+iiXGJo0T2UkFvXzpoYxDBWBgNVHR8ETzBNMEugSaBHhkVodHRwOi8vY3JsLm1p
# Y3Jvc29mdC5jb20vcGtpL2NybC9wcm9kdWN0cy9NaWNSb29DZXJBdXRfMjAxMC0w
# Ni0yMy5jcmwwWgYIKwYBBQUHAQEETjBMMEoGCCsGAQUFBzAChj5odHRwOi8vd3d3
# Lm1pY3Jvc29mdC5jb20vcGtpL2NlcnRzL01pY1Jvb0NlckF1dF8yMDEwLTA2LTIz
# LmNydDANBgkqhkiG9w0BAQsFAAOCAgEAnVV9/Cqt4SwfZwExJFvhnnJL/Klv6lwU
# tj5OR2R4sQaTlz0xM7U518JxNj/aZGx80HU5bbsPMeTCj/ts0aGUGCLu6WZnOlNN
# 3Zi6th542DYunKmCVgADsAW+iehp4LoJ7nvfam++Kctu2D9IdQHZGN5tggz1bSNU
# 5HhTdSRXud2f8449xvNo32X2pFaq95W2KFUn0CS9QKC/GbYSEhFdPSfgQJY4rPf5
# KYnDvBewVIVCs/wMnosZiefwC2qBwoEZQhlSdYo2wh3DYXMuLGt7bj8sCXgU6ZGy
# qVvfSaN0DLzskYDSPeZKPmY7T7uG+jIa2Zb0j/aRAfbOxnT99kxybxCrdTDFNLB6
# 2FD+CljdQDzHVG2dY3RILLFORy3BFARxv2T5JL5zbcqOCb2zAVdJVGTZc9d/HltE
# AY5aGZFrDZ+kKNxnGSgkujhLmm77IVRrakURR6nxt67I6IleT53S0Ex2tVdUCbFp
# AUR+fKFhbHP+CrvsQWY9af3LwUFJfn6Tvsv4O+S3Fb+0zj6lMVGEvL8CwYKiexcd
# FYmNcP7ntdAoGokLjzbaukz5m/8K6TT4JDVnK+ANuOaMmdbhIurwJ0I9JZTmdHRb
# atGePu1+oDEzfbzL6Xu/OHBE0ZDxyKs6ijoIYn/ZcGNTTY3ugm2lBRDBcQZqELQd
# VTNYs6FwZvKhggNNMIICNQIBATCB+aGB0aSBzjCByzELMAkGA1UEBhMCVVMxEzAR
# BgNVBAgTCldhc2hpbmd0b24xEDAOBgNVBAcTB1JlZG1vbmQxHjAcBgNVBAoTFU1p
# Y3Jvc29mdCBDb3Jwb3JhdGlvbjElMCMGA1UECxMcTWljcm9zb2Z0IEFtZXJpY2Eg
# T3BlcmF0aW9uczEnMCUGA1UECxMeblNoaWVsZCBUU1MgRVNOOkEwMDAtMDVFMC1E
# OTQ3MSUwIwYDVQQDExxNaWNyb3NvZnQgVGltZS1TdGFtcCBTZXJ2aWNloiMKAQEw
# BwYFKw4DAhoDFQAJrD90ykHpo/0AGb7lmwvsCtqROaCBgzCBgKR+MHwxCzAJBgNV
# BAYTAlVTMRMwEQYDVQQIEwpXYXNoaW5ndG9uMRAwDgYDVQQHEwdSZWRtb25kMR4w
# HAYDVQQKExVNaWNyb3NvZnQgQ29ycG9yYXRpb24xJjAkBgNVBAMTHU1pY3Jvc29m
# dCBUaW1lLVN0YW1wIFBDQSAyMDEwMA0GCSqGSIb3DQEBCwUAAgUA7gM6YzAiGA8y
# MDI2MDcxNjExMDkyM1oYDzIwMjYwNzE3MTEwOTIzWjB0MDoGCisGAQQBhFkKBAEx
# LDAqMAoCBQDuAzpjAgEAMAcCAQACAgfmMAcCAQACAhPXMAoCBQDuBIvjAgEAMDYG
# CisGAQQBhFkKBAIxKDAmMAwGCisGAQQBhFkKAwKgCjAIAgEAAgMHoSChCjAIAgEA
# AgMBhqAwDQYJKoZIhvcNAQELBQADggEBAIxt7OnhME4pO6iwnELxDxZLO2/cEBrc
# jNcyxXNUARM576OOEJfPOpMN/cSsl8NX6h/vgoDoM+HcVL8X4uhSDxwMaWu7Q6Qv
# H+6gTFrjBYMel2DS2+GFKcvYEpMP00MzjzEnNlLVxLHJJnOPhb06wLaIUMoRwdm2
# dR+ebZTcoDt3E6l0Wwwg4YJEgV/bSyWnN8nj9wGYnfSMLnk2Ssb/Dn3/LBI/YhQ7
# exsRoiBB3S5dw0lMQZZ0NxiWH8kYTX38X2xhw8FDVp80qRZdA+8ZuOplcTKObXE4
# 5uwuRsKLhppix5yEd7EcmhepJFS0AhqBfmcyPUywnMvXflKrWl+xUYYxggQNMIIE
# CQIBATCBkzB8MQswCQYDVQQGEwJVUzETMBEGA1UECBMKV2FzaGluZ3RvbjEQMA4G
# A1UEBxMHUmVkbW9uZDEeMBwGA1UEChMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMSYw
# JAYDVQQDEx1NaWNyb3NvZnQgVGltZS1TdGFtcCBQQ0EgMjAxMAITMwAAAiu7AFD/
# TTuaoQABAAACKzANBglghkgBZQMEAgEFAKCCAUowGgYJKoZIhvcNAQkDMQ0GCyqG
# SIb3DQEJEAEEMC8GCSqGSIb3DQEJBDEiBCCQsncOVFQ0IwCpIvg7CHpaPvvGAnIx
# AZJIku9mJ/o7LTCB+gYLKoZIhvcNAQkQAi8xgeowgecwgeQwgb0EIHIOI/Q/kFft
# YA+M2OY+1Bx3ajBD6/WDAtPT2vFkv25SMIGYMIGApH4wfDELMAkGA1UEBhMCVVMx
# EzARBgNVBAgTCldhc2hpbmd0b24xEDAOBgNVBAcTB1JlZG1vbmQxHjAcBgNVBAoT
# FU1pY3Jvc29mdCBDb3Jwb3JhdGlvbjEmMCQGA1UEAxMdTWljcm9zb2Z0IFRpbWUt
# U3RhbXAgUENBIDIwMTACEzMAAAIruwBQ/007mqEAAQAAAiswIgQgBgBswn3YqUuj
# EHwV9IjvOFYXQf3Lj1fNk0GYsB4FLH0wDQYJKoZIhvcNAQELBQAEggIACwwIsGGw
# PkuA8X6e5rAPj1YyizCGtQKicGVin6ncD/V1RNuEVbQ+DyXR6o6uvhDuRMsZsH7l
# 4WwxcPO2S78845Rgz5rLIio1fM44zcFC+Go5oBCNLuCSnbm0qi+65KsiwebatKiw
# Wv6YP05NZp7ZS10YJ3ac7nCI+pqUL7VBQeQYy8BL0yZKfA4ai5TZbpnZ6/T9RoLy
# JOr0DGrHzwNk+3g22xyLMKB1S7BdNmaHkynuYnD0gv4m9jrQxMqceQXIwhyDHPKl
# uxEyWjTV7AcJEyIbQf5GHKOI0PwBz8UQc8d8MMvuLdz0YHpGOd50rl/TQP7rwuAz
# c2XTD7CeRE68cLtHot0EYKPC+1e8bgPO1NsB6SnFKOCvaEPsFt3veBymS8u9WsIS
# tUtzmx/HuTPhIMCahXYHroiTQ7LUU8/TYyOLEXExnZ0VreCux8qDaNArnY5A8O1H
# JH/LuGbrZNddHDQY8EEvc8IDMuB9O/zwtqv1zglzoJ6moV7dvIDFN88U/MFGG1Xr
# GmPriiHRKpOCsGhM3oHOGmv0FNk1E/G1oRgMchABuE3N7BmDtTvuU+7dJqkXADnu
# 96wcmmV4EfJ6ryLHBMLVMkuNGjESdDPJQyp/waQJfS0aFDiG23mbOQjI0wKc2rWB
# NORIhd/iGBtlt2lR4HZgJmY97SaD6piPZXE=
# SIG # End signature block