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; failed GitHub wildcard test endpoints result in a WARNING unless another endpoint has a hard failure; 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 among the connectivity failures. The DNS subset is an ADDITIVE triage projection only: ordinary endpoint rows remain in $failed and drive FAILURE, while eligible GitHub wildcard probes remain in $warningFailures and drive WARNING. It lets a reviewer tell "name didn't resolve" apart from "resolved but unreachable" without changing either row's severity. 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. 5. Selected GitHub wildcard test failures roll up to WARNING. Rows whose Source is exactly 'Test for GitHub Wildcard' are manually defined probes used only to verify that a documented wildcard is allowed through the firewall or proxy; the individual probe endpoints are not separately documented as required endpoints. Only DNS-resolution failures and the known Layer7Response 'The underlying connection was closed: An unexpected error occurred on a send.' are downgraded. Any other failure remains a hard failure. #> [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-AzsSupportLog -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' } # DNS failures are identified before splitting hard failures from warning-only GitHub wildcard probes because # DNS resolution is one of the two conditions eligible for the warning downgrade (see DESIGN NOTES 2 and 5). $dnsErrorPattern = 'could not be resolved|No such host is known|name does not exist|temporary failure in name resolution|hostname resolution' $isDnsFailure = { param($Row) [string]::IsNullOrWhiteSpace([string]$Row.IPAddress) -or ([string]$Row.IPAddress -imatch $dnsErrorPattern) -or ([string]$Row.Layer7Response -imatch $dnsErrorPattern) } $isWarningOnlyFailure = { param($Row) $Row.PSObject.Properties['Source'] -and ([string]$Row.Source -ieq 'Test for GitHub Wildcard') -and ((& $isDnsFailure -Row $Row) -or ([string]$Row.Layer7Response -ieq 'The underlying connection was closed: An unexpected error occurred on a send.')) } # Bucket by resolved category (no row mutation). SSLInspected / PrivateLink have their own dedicated rules. $passed = @($rows | Where-Object { (& $categorise -Row $_) -ieq 'Success' }) $connectivityFailures = @($rows | Where-Object { (& $categorise -Row $_) -ieq 'ConnectivityFailure' }) $warningFailures = @($connectivityFailures | Where-Object { & $isWarningOnlyFailure -Row $_ }) $failed = @($connectivityFailures | Where-Object { -not (& $isWarningOnlyFailure -Row $_) }) $configGaps = @($rows | Where-Object { (& $categorise -Row $_) -ieq 'ConfigGap' }) $skipped = @($rows | Where-Object { (& $categorise -Row $_) -ieq 'Skipped' }) $sslInspected = @($rows | Where-Object { (& $categorise -Row $_) -ieq 'SSLInspected' }) $privateLink = @($rows | Where-Object { (& $categorise -Row $_) -ieq 'PrivateLink' }) # DNS-resolution sub-bucket: an additive triage projection over all connectivity failures. Warning-only # GitHub wildcard probes remain visible here even though they do not drive FAILURE. Detected via # Layer7Response / IPAddress resolver-error text or a blank IPAddress - see DESIGN NOTE 2. $dnsFailures = @($connectivityFailures | Where-Object { & $isDnsFailure -Row $_ }) $hardDnsFailures = @($failed | Where-Object { & $isDnsFailure -Row $_ }) $warningDnsFailures = @($warningFailures | Where-Object { & $isDnsFailure -Row $_ }) # 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-AzsSupportLog -Message ( "ResultCategory summary: Success={0}, ConnectivityFailure={1} (Warnings={2}, DnsResolutionFailures={3}), ConfigGap={4}, Skipped={5}, SSLInspected={6}, PrivateLink={7}" -f ` $passed.Count, $failed.Count, $warningFailures.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-AzsSupportLog -Level 'Informational' -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 ) } foreach ($row in $warningFailures) { Write-AzsSupportLog -Level 'Informational' -Message ( "WARNING: '{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 $hardDnsFailures) { Write-AzsSupportLog -Level 'Informational' -Message ( "DNS RESOLUTION FAILURE: '{0}' [{1}] could not be resolved. Layer7Response='{2}'" -f ` $row.URL, $row.Source, $row.Layer7Response ) } foreach ($row in $warningDnsFailures) { Write-AzsSupportLog -Level 'Informational' -Message ( $localizedData.DnsResolutionWarningMessage -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-AzsSupportLog -Level 'Informational' -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-AzsSupportLog -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-AzsSupportLog -Level 'Informational' -Message ( "CONFIG GAP: '{0}' skipped - {1}" -f $row.URL, $row.Note ) } # Decide status: any real failure -> FAILURE; GitHub wildcard test failures -> WARNING when no real failure is # present. 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. $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 } elseif ($warningFailures.Count -gt 0) { $insightRule.Status = [InsightStatus]::WARNING $insightRule.ErrorMessage = ($localizedData.GitHubWildcardWarningMessage -f $warningFailures.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-AzsSupportLog -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 WarningCount = $warningFailures.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 WarningEndpoints = $warningFailures | 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-AzsSupportLog -Level 'Exception' $insightRule.ScriptStackTrace = Get-FormattedException -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 # MIInJwYJKoZIhvcNAQcCoIInGDCCJxQCAQExDzANBglghkgBZQMEAgEFADB5Bgor # BgEEAYI3AgEEoGswaTA0BgorBgEEAYI3AgEeMCYCAwEAAAQQH8w7YFlLCE63JNLG # KX7zUQIBAAIBAAIBAAIBAAIBADAxMA0GCWCGSAFlAwQCAQUABCAF0szyZUGacNJg # W3mnyLrKckYQAzpUaVnD/4FylWpPdaCCDLowggX1MIID3aADAgECAhMzAAACHU0Z # 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 # 1cY2L4A7GTQG1h32HHAvfQESWP0xghnDMIIZvwIBATBuMFcxCzAJBgNVBAYTAlVT # MR4wHAYDVQQKExVNaWNyb3NvZnQgQ29ycG9yYXRpb24xKDAmBgNVBAMTH01pY3Jv # c29mdCBDb2RlIFNpZ25pbmcgUENBIDIwMjQCEzMAAAIdTRnITtcPV0gAAAAAAh0w # DQYJYIZIAWUDBAIBBQCggZAwGQYJKoZIhvcNAQkDMQwGCisGAQQBgjcCAQQwLwYJ # KoZIhvcNAQkEMSIEIAyyaZqNNR3wTFLqTCk9qlNud5wcy/1SZJ/GVtcQYfuSMEIG # CisGAQQBgjcCAQwxNDAyoBSAEgBNAGkAYwByAG8AcwBvAGYAdKEagBhodHRwOi8v # d3d3Lm1pY3Jvc29mdC5jb20wDQYJKoZIhvcNAQEBBQAEggEArCxbb0RDe9vqT6Dl # gogpfNiCfeZGEXKu+CVcYbswGqBuW+UgJfdhjgHKL8Rv4IZEJMtK9hKmuQ0mqcha # OkTYX37+iGwUERUEy30Uw91SyjwFhMydubwzVIDL9pyCDdx9z03YsQSzV6Z8UYCW # IEYN601S0xdKi3fojcZZEf2qlaS5uTIKOiVYIhTSorl+8lvxH52fbg49x3qSw5Ut # 2E1M3GrAXITl8sEziuJr62L3JV2GFAbaAWfdK9qZY7h/sUJP1i5i2cKwaWPNFwS0 # IhC4EcjTqf6XYnOhEvdahOzADvr2qKIpK5Aj1JyiMrjhdHYzk7mQJyb3j/Zeh+NN # OkojdaGCF5MwghePBgorBgEEAYI3AwMBMYIXfzCCF3sGCSqGSIb3DQEHAqCCF2ww # ghdoAgEDMQ8wDQYJYIZIAWUDBAIBBQAwggFRBgsqhkiG9w0BCRABBKCCAUAEggE8 # MIIBOAIBAQYKKwYBBAGEWQoDATAxMA0GCWCGSAFlAwQCAQUABCAjwouVUQdYwkpH # WWrwSN+ID0Zs4PnzWZJrO4Sw0fCWJQIGaoVZ8xgWGBIyMDI2MDkwMjE1MjY1NS45 # MlowBIACAfSggdGkgc4wgcsxCzAJBgNVBAYTAlVTMRMwEQYDVQQIEwpXYXNoaW5n # dG9uMRAwDgYDVQQHEwdSZWRtb25kMR4wHAYDVQQKExVNaWNyb3NvZnQgQ29ycG9y # YXRpb24xJTAjBgNVBAsTHE1pY3Jvc29mdCBBbWVyaWNhIE9wZXJhdGlvbnMxJzAl # BgNVBAsTHm5TaGllbGQgVFNTIEVTTjo5NjAwLTA1RTAtRDk0NzElMCMGA1UEAxMc # TWljcm9zb2Z0IFRpbWUtU3RhbXAgU2VydmljZaCCEeowggcgMIIFCKADAgECAhMz # AAACJjW0PmdDk/YfAAEAAAImMA0GCSqGSIb3DQEBCwUAMHwxCzAJBgNVBAYTAlVT # MRMwEQYDVQQIEwpXYXNoaW5ndG9uMRAwDgYDVQQHEwdSZWRtb25kMR4wHAYDVQQK # ExVNaWNyb3NvZnQgQ29ycG9yYXRpb24xJjAkBgNVBAMTHU1pY3Jvc29mdCBUaW1l # LVN0YW1wIFBDQSAyMDEwMB4XDTI2MDIxOTE5NDAwMloXDTI3MDUxNzE5NDAwMlow # gcsxCzAJBgNVBAYTAlVTMRMwEQYDVQQIEwpXYXNoaW5ndG9uMRAwDgYDVQQHEwdS # ZWRtb25kMR4wHAYDVQQKExVNaWNyb3NvZnQgQ29ycG9yYXRpb24xJTAjBgNVBAsT # HE1pY3Jvc29mdCBBbWVyaWNhIE9wZXJhdGlvbnMxJzAlBgNVBAsTHm5TaGllbGQg # VFNTIEVTTjo5NjAwLTA1RTAtRDk0NzElMCMGA1UEAxMcTWljcm9zb2Z0IFRpbWUt # U3RhbXAgU2VydmljZTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAL// # D5lkgvlEUWlUjwPdnK427wjNwAQ4PfQ4tiOHffuteNysiU5LOklzhl5TETKWLrHo # XrObg1Hx1s9v12IOn+E5TMdYbGIDVndFcoFv/gX+iPK83jdIQZapJ9VzcjcGWxhP # fl5xUAn2RV/3Rg6/b20WMkEFmRi+tP8PDDJEuxw7I/in73+XImMP5QuzdhcGFWt9 # n4xtAH4FgoupG8EpuP/BH1qQ2szFAg2gZoPmNk783+dKyYbY/XO/9y/iBKgwGdZ5 # AgGSN3YjnDUN5e6mna9KI2ZHmwDZmQErfKJBZom9HE4OWR+LIeT0yST9OthOOaM8 # JuF766qEc1HLxSVs69awKrS1G1TKQe/f0OCoB8k2sTw5K3zfmsHMOmutwCHCaB+G # hWLgAp6rCKRjSdRrjwrRDLzRdPh+IQDcTERk1pEWj02r8bBt+CoqoaZz3GEq5EVy # O25rgodm+cC+laAQVI4KSi9ez8FwueQQcz3FnyJRqDkLKE2pdhgT/PSlxd1ho0iR # DrwRaa68ubuD2ih9Xa86bkZU2iCGeRYbqcY+j8nASCYD2hJLQR+8VExY8D+ClK8X # eyECsoedoSlVJKLcM1vKK5iISz0qjQiRlzzEoV5BFqoZHGsH7av/sHdfzVOmz30q # EXCD7APzuh3bYXYxSDXHu3C3eBpWcWTQhkjBQ8IbAgMBAAGjggFJMIIBRTAdBgNV # HQ4EFgQUXeGf19gk3Zj9n0tVsE8jEDNcexAwHwYDVR0jBBgwFoAUn6cVXQBeYl2D # 9OXSZacbUzUZ6XIwXwYDVR0fBFgwVjBUoFKgUIZOaHR0cDovL3d3dy5taWNyb3Nv # ZnQuY29tL3BraW9wcy9jcmwvTWljcm9zb2Z0JTIwVGltZS1TdGFtcCUyMFBDQSUy # MDIwMTAoMSkuY3JsMGwGCCsGAQUFBwEBBGAwXjBcBggrBgEFBQcwAoZQaHR0cDov # L3d3dy5taWNyb3NvZnQuY29tL3BraW9wcy9jZXJ0cy9NaWNyb3NvZnQlMjBUaW1l # LVN0YW1wJTIwUENBJTIwMjAxMCgxKS5jcnQwDAYDVR0TAQH/BAIwADAWBgNVHSUB # Af8EDDAKBggrBgEFBQcDCDAOBgNVHQ8BAf8EBAMCB4AwDQYJKoZIhvcNAQELBQAD # ggIBADZTHS2v2xgKOyrQVHKJWnHXk66s1e/pCTuJf4CtU+XPfDi2qNxM2bV23e1O # 5rbAkykmE8fyftReGZP3x3kO7jguXhp2ex7hJB9WDdAvppGRceclSfzL2J+0H8/L # baf3GfA8V+PdCUM5KAu4eV673tTSIfZlqW5hptZcmKF2Jikrxw8cWWpk4CKi3T4Y # Px0/5Ey6+nG38XYuZh6WmhnCuKIU5SaXERRXvEkfJlmUOq6yR7K6rTUNO/3U3ioz # xx88+GX/alzgd4x/+d3Yei6J8lsNAU13hY+EvOfRLLe7VmHf5Le2NB2o353LDrRF # pX5FcKg4uAVncwCD8agOX5+9vmHL/VrvVy1fzARp3U9/p15/amp+XfAVz76GQXwN # ddNmh8k3hhVo3cifBsAZAMOQ0riWp5wKLHGZIrCJ0/KcZ4Tk6282grWmQuyb+LwX # VGMZzNn+RIXZUOSobzrqJD6NVsY5DoO7d7LIVwUpmgMngHmYQBL1pPZIqqWUt7Js # 5ugfqvruyJHkH/Yee7v4pi5hnLQERp20DqeAbhydJH0myuSGGwqZvXW6OrCAnI3H # 3YyygYbA2A3VojRAgPwKyMIXCl+YzOUDjjEcpi/eGaPF6oFLi5TmtB6ICdWCkl5p # UYqb+XM8O2emkZX7teFGnlvVnFP9ntfFz4jsfv+MK1ANmhplMIIHcTCCBVmgAwIB # AgITMwAAABXF52ueAptJmQAAAAAAFTANBgkqhkiG9w0BAQsFADCBiDELMAkGA1UE # BhMCVVMxEzARBgNVBAgTCldhc2hpbmd0b24xEDAOBgNVBAcTB1JlZG1vbmQxHjAc # BgNVBAoTFU1pY3Jvc29mdCBDb3Jwb3JhdGlvbjEyMDAGA1UEAxMpTWljcm9zb2Z0 # IFJvb3QgQ2VydGlmaWNhdGUgQXV0aG9yaXR5IDIwMTAwHhcNMjEwOTMwMTgyMjI1 # WhcNMzAwOTMwMTgzMjI1WjB8MQswCQYDVQQGEwJVUzETMBEGA1UECBMKV2FzaGlu # Z3RvbjEQMA4GA1UEBxMHUmVkbW9uZDEeMBwGA1UEChMVTWljcm9zb2Z0IENvcnBv # cmF0aW9uMSYwJAYDVQQDEx1NaWNyb3NvZnQgVGltZS1TdGFtcCBQQ0EgMjAxMDCC # AiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAOThpkzntHIhC3miy9ckeb0O # 1YLT/e6cBwfSqWxOdcjKNVf2AX9sSuDivbk+F2Az/1xPx2b3lVNxWuJ+Slr+uDZn # hUYjDLWNE893MsAQGOhgfWpSg0S3po5GawcU88V29YZQ3MFEyHFcUTE3oAo4bo3t # 1w/YJlN8OWECesSq/XJprx2rrPY2vjUmZNqYO7oaezOtgFt+jBAcnVL+tuhiJdxq # D89d9P6OU8/W7IVWTe/dvI2k45GPsjksUZzpcGkNyjYtcI4xyDUoveO0hyTD4MmP # frVUj9z6BVWYbWg7mka97aSueik3rMvrg0XnRm7KMtXAhjBcTyziYrLNueKNiOSW # rAFKu75xqRdbZ2De+JKRHh09/SDPc31BmkZ1zcRfNN0Sidb9pSB9fvzZnkXftnIv # 231fgLrbqn427DZM9ituqBJR6L8FA6PRc6ZNN3SUHDSCD/AQ8rdHGO2n6Jl8P0zb # r17C89XYcz1DTsEzOUyOArxCaC4Q6oRRRuLRvWoYWmEBc8pnol7XKHYC4jMYcten # IPDC+hIK12NvDMk2ZItboKaDIV1fMHSRlJTYuVD5C4lh8zYGNRiER9vcG9H9stQc # xWv2XFJRXRLbJbqvUAV6bMURHXLvjflSxIUXk8A8FdsaN8cIFRg/eKtFtvUeh17a # j54WcmnGrnu3tz5q4i6tAgMBAAGjggHdMIIB2TASBgkrBgEEAYI3FQEEBQIDAQAB # MCMGCSsGAQQBgjcVAgQWBBQqp1L+ZMSavoKRPEY1Kc8Q/y8E7jAdBgNVHQ4EFgQU # n6cVXQBeYl2D9OXSZacbUzUZ6XIwXAYDVR0gBFUwUzBRBgwrBgEEAYI3TIN9AQEw # QTA/BggrBgEFBQcCARYzaHR0cDovL3d3dy5taWNyb3NvZnQuY29tL3BraW9wcy9E # b2NzL1JlcG9zaXRvcnkuaHRtMBMGA1UdJQQMMAoGCCsGAQUFBwMIMBkGCSsGAQQB # gjcUAgQMHgoAUwB1AGIAQwBBMAsGA1UdDwQEAwIBhjAPBgNVHRMBAf8EBTADAQH/ # MB8GA1UdIwQYMBaAFNX2VsuP6KJcYmjRPZSQW9fOmhjEMFYGA1UdHwRPME0wS6BJ # oEeGRWh0dHA6Ly9jcmwubWljcm9zb2Z0LmNvbS9wa2kvY3JsL3Byb2R1Y3RzL01p # Y1Jvb0NlckF1dF8yMDEwLTA2LTIzLmNybDBaBggrBgEFBQcBAQROMEwwSgYIKwYB # BQUHMAKGPmh0dHA6Ly93d3cubWljcm9zb2Z0LmNvbS9wa2kvY2VydHMvTWljUm9v # Q2VyQXV0XzIwMTAtMDYtMjMuY3J0MA0GCSqGSIb3DQEBCwUAA4ICAQCdVX38Kq3h # LB9nATEkW+Geckv8qW/qXBS2Pk5HZHixBpOXPTEztTnXwnE2P9pkbHzQdTltuw8x # 5MKP+2zRoZQYIu7pZmc6U03dmLq2HnjYNi6cqYJWAAOwBb6J6Gngugnue99qb74p # y27YP0h1AdkY3m2CDPVtI1TkeFN1JFe53Z/zjj3G82jfZfakVqr3lbYoVSfQJL1A # oL8ZthISEV09J+BAljis9/kpicO8F7BUhUKz/AyeixmJ5/ALaoHCgRlCGVJ1ijbC # HcNhcy4sa3tuPywJeBTpkbKpW99Jo3QMvOyRgNI95ko+ZjtPu4b6MhrZlvSP9pEB # 9s7GdP32THJvEKt1MMU0sHrYUP4KWN1APMdUbZ1jdEgssU5HLcEUBHG/ZPkkvnNt # yo4JvbMBV0lUZNlz138eW0QBjloZkWsNn6Qo3GcZKCS6OEuabvshVGtqRRFHqfG3 # rsjoiV5PndLQTHa1V1QJsWkBRH58oWFsc/4Ku+xBZj1p/cvBQUl+fpO+y/g75LcV # v7TOPqUxUYS8vwLBgqJ7Fx0ViY1w/ue10CgaiQuPNtq6TPmb/wrpNPgkNWcr4A24 # 5oyZ1uEi6vAnQj0llOZ0dFtq0Z4+7X6gMTN9vMvpe784cETRkPHIqzqKOghif9lw # Y1NNje6CbaUFEMFxBmoQtB1VM1izoXBm8qGCA00wggI1AgEBMIH5oYHRpIHOMIHL # MQswCQYDVQQGEwJVUzETMBEGA1UECBMKV2FzaGluZ3RvbjEQMA4GA1UEBxMHUmVk # bW9uZDEeMBwGA1UEChMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMSUwIwYDVQQLExxN # aWNyb3NvZnQgQW1lcmljYSBPcGVyYXRpb25zMScwJQYDVQQLEx5uU2hpZWxkIFRT # UyBFU046OTYwMC0wNUUwLUQ5NDcxJTAjBgNVBAMTHE1pY3Jvc29mdCBUaW1lLVN0 # YW1wIFNlcnZpY2WiIwoBATAHBgUrDgMCGgMVAKL98zEW2Sqvtcxd2xHJZTSVIodn # oIGDMIGApH4wfDELMAkGA1UEBhMCVVMxEzARBgNVBAgTCldhc2hpbmd0b24xEDAO # BgNVBAcTB1JlZG1vbmQxHjAcBgNVBAoTFU1pY3Jvc29mdCBDb3Jwb3JhdGlvbjEm # MCQGA1UEAxMdTWljcm9zb2Z0IFRpbWUtU3RhbXAgUENBIDIwMTAwDQYJKoZIhvcN # AQELBQACBQDuQksqMCIYDzIwMjYwOTAyMDcxMzQ2WhgPMjAyNjA5MDMwNzEzNDZa # MHQwOgYKKwYBBAGEWQoEATEsMCowCgIFAO5CSyoCAQAwBwIBAAICATMwBwIBAAIC # EjEwCgIFAO5DnKoCAQAwNgYKKwYBBAGEWQoEAjEoMCYwDAYKKwYBBAGEWQoDAqAK # MAgCAQACAwehIKEKMAgCAQACAwGGoDANBgkqhkiG9w0BAQsFAAOCAQEAR5mQgfGF # K/owyLe3mcfFor3y8xwfdCOA2nhug3ttg/BvT2OozSnXGsFgFLiZXIkGiSGNn5P9 # nPvOWrQ5e6AyhDX1++ASRkr7qeg9O0AZfD0eaCDDGyPCIfzabAal636oazyPvg1b # xWzCDf7q9+03aazPJ3KOLBlkhI3O0tP1g/mB78mKkUzYwiGvyND96KesfisnL0JM # FiWOqmgWfCSimTs3KsirCr5cDDazE6f/jDS0p9fS/vdhX/MElzIxnoJqg978p0sa # VaQKoDFhG2TdFo9UeWwRs7nq61fqukLHybBRsnm+/ql5zf810B7bTCUzCwFQByAh # QZIZV2GfzRe0vjGCBA0wggQJAgEBMIGTMHwxCzAJBgNVBAYTAlVTMRMwEQYDVQQI # EwpXYXNoaW5ndG9uMRAwDgYDVQQHEwdSZWRtb25kMR4wHAYDVQQKExVNaWNyb3Nv # ZnQgQ29ycG9yYXRpb24xJjAkBgNVBAMTHU1pY3Jvc29mdCBUaW1lLVN0YW1wIFBD # QSAyMDEwAhMzAAACJjW0PmdDk/YfAAEAAAImMA0GCWCGSAFlAwQCAQUAoIIBSjAa # BgkqhkiG9w0BCQMxDQYLKoZIhvcNAQkQAQQwLwYJKoZIhvcNAQkEMSIEIHSub/i4 # AENdFZkO1klNDXuFanD0GEOaj8rYjpubyYRnMIH6BgsqhkiG9w0BCRACLzGB6jCB # 5zCB5DCBvQQgzDJcYWdM2xlEGuzoY38FtXSiRo0/dUFiosWNSwWduCowgZgwgYCk # fjB8MQswCQYDVQQGEwJVUzETMBEGA1UECBMKV2FzaGluZ3RvbjEQMA4GA1UEBxMH # UmVkbW9uZDEeMBwGA1UEChMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMSYwJAYDVQQD # Ex1NaWNyb3NvZnQgVGltZS1TdGFtcCBQQ0EgMjAxMAITMwAAAiY1tD5nQ5P2HwAB # AAACJjAiBCB1PCrjfAMl8DRpZt3j+OgXu1AQYpklBw6hEbBRwQGKUjANBgkqhkiG # 9w0BAQsFAASCAgChdlqDyqtt36eGdC1XqfKZvD1ScDE3X7g60x8ml7ssIZ98jNXI # YFADN2fj1NhAIGR1b04l4ckAu4ZbuAG0T+20BycJl7cDdwgmxVCsUZuroW7J0Ne1 # QJQW8nLKp4Ha2G68mYYrfAuhrlPdIqTZHjKL/leN/QB3ZxXIL8W2rH//zqtlZGbv # LQtXiQ9zhA7cQKA/ckmry4CRAzhdw7y10gPMOyKnzp2IDNFBTUb8d3JdRjexGRoP # 35v59enNiqKo168JOR53LmQrZ1foxGQB53/3SFAVv4qUoIneGupHOCZNwn3ZZJEy # IiO2dbqkSI9ahDYqvPSGL09WrwdUfYcnJV+rhcKckXgUC6W38OpMzSegppk9ntPz # aCZKPqPXLXp6odilb3I38sTqvw4NQ42ueKKRBd+F+UzLmCUNfSEf/Kt68sF8/DLV # r0khS9SKElC4MlxgIlDtV52+pmCMQN3ZqQoxKtYAh/W9KzFOkjgIsb/iwc8MRXkI # tnzvrP0ozdBBugX4EUpu878x1r58Y5mNpZBGPq67yCXa2Fb4fyQ8AwmwBS/ZCq2T # uNfMZiISWR3O+7eIbu2DpPf7MCtUYzUd6HeOT/gBmQAjN2bNdmS+XKWgVBLgDcOh # V1H2QbLAdXHkEoJ7O3j188kEEugPOqbpDjvuGHjh2OT0UEgPEtFL2CDq7w== # SIG # End signature block |