Private/AzStackHci.Results.Helpers.ps1

# ////////////////////////////////////////////////////////////////////////////
# Function to process results
Function Publish-Results {
    param (
        [ValidateSet('HTML', 'CSV')]
        [string]$OutputFormat = 'HTML'
    )

    begin {
        # Write-Debug "Publish-Results: Beginning results processing and publishing"
    }

    process {
        # Assign Row IDs and reorder columns
        $rowIdCounter = 1
        foreach ($result in $script:Results) {
            $result.RowID = $rowIdCounter
            $rowIdCounter++
        }

        [System.Collections.ArrayList]$script:Results = @($script:Results | Select-Object RowID, URL, Port, ArcGateway, IsWildcard, Source, IPAddress, Layer7Status, Layer7Response, Layer7ResponseTime, Note, TCPStatus, CertificateIssuer, CertificateSubject, CertificateThumbprint, IntermediateCertificateIssuer, IntermediateCertificateSubject, IntermediateCertificateThumbprint, RootCertificateIssuer, RootCertificateSubject, RootCertificateThumbprint)

        # Sort the array by the properties Layer7Status (Failed first, then Success, then Skipped), Source, Url
        $statusOrder = @{ 'Failed' = 0; 'Success' = 1; 'Skipped' = 2 }
        [System.Collections.ArrayList]$script:Results = @($script:Results | Sort-Object -Property @{Expression={$statusOrder[$_.Layer7Status]}; Ascending=$true}, Source, Url)

        # Export results based on the OutputFormat parameter (HTML or CSV)
        try {
            switch ($OutputFormat) {
                'HTML' {
                    $htmlStyle = @"
<style>
    body { font-family: Segoe UI, Arial, sans-serif; margin: 20px; background-color: #f5f5f5; }
    h1 { color: #0078d4; border-bottom: 2px solid #0078d4; padding-bottom: 8px; }
    .summary { background: #fff; padding: 12px 20px; margin-bottom: 20px; border-left: 4px solid #0078d4; box-shadow: 0 1px 3px rgba(0,0,0,0.12); }
    .summary li { list-style: none; padding: 2px 0; }
    .summary ul { padding-left: 0; margin: 8px 0; }
    .table-wrapper { overflow-x: auto; max-width: 100%; box-shadow: 0 1px 3px rgba(0,0,0,0.12); }
    .top-scroll { overflow-x: auto; max-width: 100%; }
    .top-scroll div { height: 1px; }
    table { border-collapse: collapse; white-space: nowrap; background: #fff; }
    th { background-color: #0078d4; color: #fff; padding: 10px 8px; text-align: left; font-size: 13px; position: sticky; top: 0; }
    td { padding: 8px; border-bottom: 1px solid #e0e0e0; font-size: 13px; }
    tr:nth-child(even) { background-color: #f9f9f9; }
    tr:hover { background-color: #e8f4fd; }
    tr.status-failed { background-color: #fde7e9; }
    tr.status-success { background-color: #e6f4ea; }
    tr.status-skipped { background-color: #fff4ce; }
    h2 { color: #0078d4; margin-top: 24px; }
</style>
"@

                    $preContent = "<h1>Azure Local Connectivity Test Results</h1><!-- SUMMARY -->"
                    $htmlBody = $script:Results | ConvertTo-Html -Title 'Azure Local Connectivity Test Results' -Head $htmlStyle -PreContent $preContent
                    # Color-code rows based on Layer7Status
                    $htmlBody = $htmlBody -replace '<tr><td>([^<]*)</td><td>([^<]*)</td><td>([^<]*)</td><td>([^<]*)</td><td>([^<]*)</td><td>([^<]*)</td><td>([^<]*)</td><td>Failed</td>', '<tr class="status-failed"><td>$1</td><td>$2</td><td>$3</td><td>$4</td><td>$5</td><td>$6</td><td>$7</td><td>Failed</td>'
                    $htmlBody = $htmlBody -replace '<tr><td>([^<]*)</td><td>([^<]*)</td><td>([^<]*)</td><td>([^<]*)</td><td>([^<]*)</td><td>([^<]*)</td><td>([^<]*)</td><td>Success</td>', '<tr class="status-success"><td>$1</td><td>$2</td><td>$3</td><td>$4</td><td>$5</td><td>$6</td><td>$7</td><td>Success</td>'
                    $htmlBody = $htmlBody -replace '<tr><td>([^<]*)</td><td>([^<]*)</td><td>([^<]*)</td><td>([^<]*)</td><td>([^<]*)</td><td>([^<]*)</td><td>([^<]*)</td><td>Skipped</td>', '<tr class="status-skipped"><td>$1</td><td>$2</td><td>$3</td><td>$4</td><td>$5</td><td>$6</td><td>$7</td><td>Skipped</td>'
                    # Bold the Layer7Status cell text
                    $htmlBody = $htmlBody -replace '<td>(Failed|Success|Skipped)</td>', '<td><strong>$1</strong></td>'
                    # Wrap the table in a scrollable div with a synced top scrollbar
                    $htmlBody = $htmlBody -replace '<table>', '<div class="top-scroll" id="topScroll"><div></div></div><div class="table-wrapper" id="bottomScroll"><table>'
                    $htmlBody = $htmlBody -replace '</table>', '</table></div>'
                    # Add JavaScript to sync the top and bottom scrollbars and size the top scroll spacer to match the table width
                    $scrollScript = @"
<script>
document.addEventListener('DOMContentLoaded', function() {
    var top = document.getElementById('topScroll');
    var bottom = document.getElementById('bottomScroll');
    var table = bottom.querySelector('table');
    top.firstElementChild.style.width = table.scrollWidth + 'px';
    top.addEventListener('scroll', function() { bottom.scrollLeft = top.scrollLeft; });
    bottom.addEventListener('scroll', function() { top.scrollLeft = bottom.scrollLeft; });
});
</script>
"@

                    $htmlBody = $htmlBody -replace '</body>', "$scrollScript`n</body>"
                    $htmlBody | Set-Content -Path $script:OutputFile -Encoding UTF8
                }
                'CSV' {
                    $script:Results | Export-Csv -Path $script:OutputFile -NoTypeInformation
                }
            }
        } catch {
            Write-HostAzS "Failed to save test results to $($script:OutputFile)"
            Write-Error "Error: $($_.Exception.Message)"
        }

        # Always generate JSON output file (in addition to HTML/CSV)
        try {
            $script:Results | ConvertTo-Json -Depth 3 -Compress | Set-Content -Path $script:JsonOutputFile -Encoding UTF8
        } catch {
            Write-HostAzS "Failed to save JSON results to $($script:JsonOutputFile)"
            Write-Error "Error: $($_.Exception.Message)"
        }

        # // Calculate the number of successful, failed, and skipped URLs
        # Use TCP Status, if using TCP Connectivity Test switch
        [array]$successResults = @()
        if($IncludeTCPConnectivityTests.IsPresent){
            # Use TCPStatus for successful results
            [array]$successResults = $script:Results | Where-Object { $_.TCPStatus -eq "Success" }
        } else {
            # Otherwise default to Layer7Status
            [array]$successResults = $script:Results | Where-Object { $_.Layer7Status -eq "Success" }
        }
        # Failed URLs results
        [array]$failedResults = @()
        [array]$failedResults = $script:Results | Where-Object { $_.Layer7Status -eq "Failed" }
        # Skipped URLs results
        [array]$skippedResults = @()
        [array]$skippedResults = $script:Results | Where-Object { $_.TCPStatus -like "Skipped*" -or $_.Layer7Status -eq "Skipped" }

        # If the PassThru switch is not present, display the results
        if(-not($PassThru.IsPresent) -and -not($script:SilentMode)){

            # Console-display-only URL projection: reuse Get-DomainFromURL to strip scheme,
            # path and :port suffix so Format-Table -AutoSize doesn't wrap on long URLs.
            # The underlying $script:Results objects are UNCHANGED — CSV, HTML and JSON output
            # continue to emit the full URL exactly as tested. Wildcard URLs
            # (e.g. '*.blob.core.windows.net') are left as-is by Get-DomainFromURL.
            $UrlColumn = @{ Name = 'URL'; Expression = { (Get-DomainFromURL -url ([string]$_.URL)).Domain } }

            if($failedResults.Count -gt 0) {
                Write-HostAzS "`nThe following URLs failed:" -ForegroundColor Red
                if($IncludeTCPConnectivityTests.IsPresent){
                    $failedResults | Format-Table -Property RowID, Source, $UrlColumn, Port, TCPStatus, IpAddress, Layer7Response -AutoSize | Out-Host
                } else {
                    $failedResults | Format-Table -Property RowID, Source, $UrlColumn, Port, IpAddress, Layer7Response -AutoSize | Out-Host
                }

            } else {
                Write-HostAzS "`nNo URLs failed.`n" -ForegroundColor Green
            }

            if($successResults.Count -gt 0) {
                Write-HostAzS "The following URLs were successful:" -ForegroundColor Green
                if($IncludeTCPConnectivityTests.IsPresent){
                    $successResults | Format-Table -Property RowID, Source, $UrlColumn, Port, TCPStatus, IpAddress, Layer7Response -AutoSize | Out-Host
                } else {
                    $successResults | Format-Table -Property RowID, Source, $UrlColumn, Port, IpAddress, Layer7Response -AutoSize | Out-Host
                }
            } else {
                Write-HostAzS "No URLs were successful.`n"
            }

            if($skippedResults.Count -gt 0) {
                Write-HostAzS "The following URLs were skipped:"
                $skippedResults | Format-Table -Property RowID, Source, $UrlColumn, Port, Layer7Status, Note -AutoSize | Out-Host
            } else {
                Write-HostAzS "No URLs were skipped.`n" -ForegroundColor Green
            }

            # Display test results summary
            Write-HostAzS "`nTest results summary:"
            Write-HostAzS "---------------------------------`n"

            Write-HostAzS "Total URLs tested: $($script:Results.Count)"
            Write-HostAzS "Successful URLs: $($successResults.Count)" -ForegroundColor Green
            if($failedResults.Count -gt 0){
                Write-HostAzS "Failed URLs: $($failedResults.Count)" -ForegroundColor Red
            } else {
                Write-HostAzS "Failed URLs: $($failedResults.Count)"
            }
            Write-HostAzS "Skipped URLs: $($skippedResults.Count)`n" -ForegroundColor Yellow

            Write-HostAzS "The test result for each endpoint is shown above. For detailed output, including certificate information review the CSV file listed below."
            
        } elseif($PassThru.IsPresent -or $script:SilentMode) {
        
            # If PassThru or NoOutput switches are present, return the results as an array of objects instead of displaying in the console
            return $script:Results
        
        } else {
            # Not expected to hit this else block, but included for safety
        }

        Write-HostAzS "`nIMPORTANT: Only URLs with a Source of 'GitHub', 'Environment Checker' or '<OEM Name> SBE' are required on firewall / proxy outbound allow rules." -ForegroundColor Yellow -NoNewline
        Write-HostAzS " Any URLs with a Source of 'Redirect for ', 'Test for ' are only used for testing connectivity to the required endpoints using the automation in this module." -ForegroundColor Yellow

        Write-HostAzS "`nAzure Local product documentation for firewall requirements can be accessed using this URL from a device with a browser:`n`n`tMicrosoft documentation: 'https://learn.microsoft.com/azure/azure-local/concepts/firewall-requirements'`n" -ForegroundColor Green

    } # End of Process block

    end {
        # Write-Debug "Publish-Results: Results processing and publishing completed"
    }
} # End of Publish-Results


# ////////////////////////////////////////////////////////////////////////////

# Function to remove PII from the transcript file
# ////////////////////////////////////////////////////////////////////////////
Function Remove-PIIFromTranscriptFile {
    <#
    .SYNOPSIS
        Redact the transcript file by removing sensitive information.
    .DESCRIPTION
        This function reads a transcript file, removes sensitive information (Username and RunAs User values),
        and overwrites the file in place with the redacted content.
        Returns a PSCustomObject with Success, RedactionCount, and Error fields so callers can verify
        the file was redacted before uploading or sharing.
    .OUTPUTS
        System.Management.Automation.PSCustomObject with properties:
            Success (bool), RedactionCount (int), Error (string)
    #>

    [CmdletBinding()]
    [OutputType([PSCustomObject])]
    param (
        [Parameter(Mandatory=$true)]
        [string]$TranscriptFilePath
    )

    begin {
        # Write-Debug "Remove-PIIFromTranscriptFile: Beginning PII removal from '$TranscriptFilePath'"
        $result = [PSCustomObject]@{
            Success        = $false
            RedactionCount = 0
            Error          = $null
        }
    }

    process {
        # Read the transcript file content
        try {
            $transcriptContent = Get-Content -Path $TranscriptFilePath -ErrorAction Stop
        } catch {
            $result.Error = "Failed to read the transcript file: $($_.Exception.Message)"
            Write-HostAzS "Error: $($result.Error)" -ForegroundColor Red
            return $result
        }

        # Check if the transcript file was read successfully
        if($transcriptContent) {

            # Set the variable to the original transcript file contents
            $redactedContent = $transcriptContent

            # Count matches per pattern for verbose logging, then apply the redaction.
            $usernamePattern = '(?i)(Username: )([a-zA-Z0-9._-]+\\[a-zA-Z0-9._-]+)'
            $runAsPattern    = '(?i)(RunAs User: )([a-zA-Z0-9._-]+\\[a-zA-Z0-9._-]+)'
            $usernameMatches = [regex]::Matches(($redactedContent -join "`n"), $usernamePattern).Count
            $runAsMatches    = [regex]::Matches(($redactedContent -join "`n"), $runAsPattern).Count
            Write-Verbose "Remove-PIIFromTranscriptFile: Username matches=$usernameMatches, RunAs User matches=$runAsMatches"

            # Redact: Username: <domain>\<username> -> <REDACTED>
            $redactedContent = $redactedContent -replace $usernamePattern, '$1<REDACTED>'
            # Redact: RunAs User: <domain>\<username> -> <REDACTED>
            $redactedContent = $redactedContent -replace $runAsPattern, '$1<REDACTED>'

            $result.RedactionCount = $usernameMatches + $runAsMatches

            # Write the redacted content back to the same file
            try {
                Set-Content -Path $TranscriptFilePath -Value $redactedContent -ErrorAction Stop -Force
                $result.Success = $true
            } catch {
                $result.Error = "Failed to update transcript file: $($_.Exception.Message)"
                Write-HostAzS "Error: $($result.Error)" -ForegroundColor Red
            }
        } else {
            $result.Error = "Transcript file was empty or could not be read."
            Write-HostAzS "Error: $($result.Error)" -ForegroundColor Red
        }

        return $result
    }

    end {
        # Write-Debug "Remove-PIIFromTranscriptFile: PII removal completed"
    }
} # End Function Remove-PIIFromTranscriptFile

# SIG # Begin signature block
# MIIoVQYJKoZIhvcNAQcCoIIoRjCCKEICAQExDzANBglghkgBZQMEAgEFADB5Bgor
# BgEEAYI3AgEEoGswaTA0BgorBgEEAYI3AgEeMCYCAwEAAAQQH8w7YFlLCE63JNLG
# KX7zUQIBAAIBAAIBAAIBAAIBADAxMA0GCWCGSAFlAwQCAQUABCDezxEQ5ddbaoCo
# /wXYu5Are6LDeW7GOhDFWbxnVTPFz6CCDYUwggYDMIID66ADAgECAhMzAAAEhJji
# EuB4ozFdAAAAAASEMA0GCSqGSIb3DQEBCwUAMH4xCzAJBgNVBAYTAlVTMRMwEQYD
# VQQIEwpXYXNoaW5ndG9uMRAwDgYDVQQHEwdSZWRtb25kMR4wHAYDVQQKExVNaWNy
# b3NvZnQgQ29ycG9yYXRpb24xKDAmBgNVBAMTH01pY3Jvc29mdCBDb2RlIFNpZ25p
# bmcgUENBIDIwMTEwHhcNMjUwNjE5MTgyMTM1WhcNMjYwNjE3MTgyMTM1WjB0MQsw
# CQYDVQQGEwJVUzETMBEGA1UECBMKV2FzaGluZ3RvbjEQMA4GA1UEBxMHUmVkbW9u
# ZDEeMBwGA1UEChMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMR4wHAYDVQQDExVNaWNy
# b3NvZnQgQ29ycG9yYXRpb24wggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIB
# AQDtekqMKDnzfsyc1T1QpHfFtr+rkir8ldzLPKmMXbRDouVXAsvBfd6E82tPj4Yz
# aSluGDQoX3NpMKooKeVFjjNRq37yyT/h1QTLMB8dpmsZ/70UM+U/sYxvt1PWWxLj
# MNIXqzB8PjG6i7H2YFgk4YOhfGSekvnzW13dLAtfjD0wiwREPvCNlilRz7XoFde5
# KO01eFiWeteh48qUOqUaAkIznC4XB3sFd1LWUmupXHK05QfJSmnei9qZJBYTt8Zh
# ArGDh7nQn+Y1jOA3oBiCUJ4n1CMaWdDhrgdMuu026oWAbfC3prqkUn8LWp28H+2S
# LetNG5KQZZwvy3Zcn7+PQGl5AgMBAAGjggGCMIIBfjAfBgNVHSUEGDAWBgorBgEE
# AYI3TAgBBggrBgEFBQcDAzAdBgNVHQ4EFgQUBN/0b6Fh6nMdE4FAxYG9kWCpbYUw
# VAYDVR0RBE0wS6RJMEcxLTArBgNVBAsTJE1pY3Jvc29mdCBJcmVsYW5kIE9wZXJh
# dGlvbnMgTGltaXRlZDEWMBQGA1UEBRMNMjMwMDEyKzUwNTM2MjAfBgNVHSMEGDAW
# gBRIbmTlUAXTgqoXNzcitW2oynUClTBUBgNVHR8ETTBLMEmgR6BFhkNodHRwOi8v
# d3d3Lm1pY3Jvc29mdC5jb20vcGtpb3BzL2NybC9NaWNDb2RTaWdQQ0EyMDExXzIw
# MTEtMDctMDguY3JsMGEGCCsGAQUFBwEBBFUwUzBRBggrBgEFBQcwAoZFaHR0cDov
# L3d3dy5taWNyb3NvZnQuY29tL3BraW9wcy9jZXJ0cy9NaWNDb2RTaWdQQ0EyMDEx
# XzIwMTEtMDctMDguY3J0MAwGA1UdEwEB/wQCMAAwDQYJKoZIhvcNAQELBQADggIB
# AGLQps1XU4RTcoDIDLP6QG3NnRE3p/WSMp61Cs8Z+JUv3xJWGtBzYmCINmHVFv6i
# 8pYF/e79FNK6P1oKjduxqHSicBdg8Mj0k8kDFA/0eU26bPBRQUIaiWrhsDOrXWdL
# m7Zmu516oQoUWcINs4jBfjDEVV4bmgQYfe+4/MUJwQJ9h6mfE+kcCP4HlP4ChIQB
# UHoSymakcTBvZw+Qst7sbdt5KnQKkSEN01CzPG1awClCI6zLKf/vKIwnqHw/+Wvc
# Ar7gwKlWNmLwTNi807r9rWsXQep1Q8YMkIuGmZ0a1qCd3GuOkSRznz2/0ojeZVYh
# ZyohCQi1Bs+xfRkv/fy0HfV3mNyO22dFUvHzBZgqE5FbGjmUnrSr1x8lCrK+s4A+
# bOGp2IejOphWoZEPGOco/HEznZ5Lk6w6W+E2Jy3PHoFE0Y8TtkSE4/80Y2lBJhLj
# 27d8ueJ8IdQhSpL/WzTjjnuYH7Dx5o9pWdIGSaFNYuSqOYxrVW7N4AEQVRDZeqDc
# fqPG3O6r5SNsxXbd71DCIQURtUKss53ON+vrlV0rjiKBIdwvMNLQ9zK0jy77owDy
# XXoYkQxakN2uFIBO1UNAvCYXjs4rw3SRmBX9qiZ5ENxcn/pLMkiyb68QdwHUXz+1
# fI6ea3/jjpNPz6Dlc/RMcXIWeMMkhup/XEbwu73U+uz/MIIHejCCBWKgAwIBAgIK
# YQ6Q0gAAAAAAAzANBgkqhkiG9w0BAQsFADCBiDELMAkGA1UEBhMCVVMxEzARBgNV
# BAgTCldhc2hpbmd0b24xEDAOBgNVBAcTB1JlZG1vbmQxHjAcBgNVBAoTFU1pY3Jv
# c29mdCBDb3Jwb3JhdGlvbjEyMDAGA1UEAxMpTWljcm9zb2Z0IFJvb3QgQ2VydGlm
# aWNhdGUgQXV0aG9yaXR5IDIwMTEwHhcNMTEwNzA4MjA1OTA5WhcNMjYwNzA4MjEw
# OTA5WjB+MQswCQYDVQQGEwJVUzETMBEGA1UECBMKV2FzaGluZ3RvbjEQMA4GA1UE
# BxMHUmVkbW9uZDEeMBwGA1UEChMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMSgwJgYD
# VQQDEx9NaWNyb3NvZnQgQ29kZSBTaWduaW5nIFBDQSAyMDExMIICIjANBgkqhkiG
# 9w0BAQEFAAOCAg8AMIICCgKCAgEAq/D6chAcLq3YbqqCEE00uvK2WCGfQhsqa+la
# UKq4BjgaBEm6f8MMHt03a8YS2AvwOMKZBrDIOdUBFDFC04kNeWSHfpRgJGyvnkmc
# 6Whe0t+bU7IKLMOv2akrrnoJr9eWWcpgGgXpZnboMlImEi/nqwhQz7NEt13YxC4D
# dato88tt8zpcoRb0RrrgOGSsbmQ1eKagYw8t00CT+OPeBw3VXHmlSSnnDb6gE3e+
# lD3v++MrWhAfTVYoonpy4BI6t0le2O3tQ5GD2Xuye4Yb2T6xjF3oiU+EGvKhL1nk
# kDstrjNYxbc+/jLTswM9sbKvkjh+0p2ALPVOVpEhNSXDOW5kf1O6nA+tGSOEy/S6
# A4aN91/w0FK/jJSHvMAhdCVfGCi2zCcoOCWYOUo2z3yxkq4cI6epZuxhH2rhKEmd
# X4jiJV3TIUs+UsS1Vz8kA/DRelsv1SPjcF0PUUZ3s/gA4bysAoJf28AVs70b1FVL
# 5zmhD+kjSbwYuER8ReTBw3J64HLnJN+/RpnF78IcV9uDjexNSTCnq47f7Fufr/zd
# sGbiwZeBe+3W7UvnSSmnEyimp31ngOaKYnhfsi+E11ecXL93KCjx7W3DKI8sj0A3
# T8HhhUSJxAlMxdSlQy90lfdu+HggWCwTXWCVmj5PM4TasIgX3p5O9JawvEagbJjS
# 4NaIjAsCAwEAAaOCAe0wggHpMBAGCSsGAQQBgjcVAQQDAgEAMB0GA1UdDgQWBBRI
# bmTlUAXTgqoXNzcitW2oynUClTAZBgkrBgEEAYI3FAIEDB4KAFMAdQBiAEMAQTAL
# BgNVHQ8EBAMCAYYwDwYDVR0TAQH/BAUwAwEB/zAfBgNVHSMEGDAWgBRyLToCMZBD
# uRQFTuHqp8cx0SOJNDBaBgNVHR8EUzBRME+gTaBLhklodHRwOi8vY3JsLm1pY3Jv
# c29mdC5jb20vcGtpL2NybC9wcm9kdWN0cy9NaWNSb29DZXJBdXQyMDExXzIwMTFf
# MDNfMjIuY3JsMF4GCCsGAQUFBwEBBFIwUDBOBggrBgEFBQcwAoZCaHR0cDovL3d3
# dy5taWNyb3NvZnQuY29tL3BraS9jZXJ0cy9NaWNSb29DZXJBdXQyMDExXzIwMTFf
# MDNfMjIuY3J0MIGfBgNVHSAEgZcwgZQwgZEGCSsGAQQBgjcuAzCBgzA/BggrBgEF
# BQcCARYzaHR0cDovL3d3dy5taWNyb3NvZnQuY29tL3BraW9wcy9kb2NzL3ByaW1h
# cnljcHMuaHRtMEAGCCsGAQUFBwICMDQeMiAdAEwAZQBnAGEAbABfAHAAbwBsAGkA
# YwB5AF8AcwB0AGEAdABlAG0AZQBuAHQALiAdMA0GCSqGSIb3DQEBCwUAA4ICAQBn
# 8oalmOBUeRou09h0ZyKbC5YR4WOSmUKWfdJ5DJDBZV8uLD74w3LRbYP+vj/oCso7
# v0epo/Np22O/IjWll11lhJB9i0ZQVdgMknzSGksc8zxCi1LQsP1r4z4HLimb5j0b
# pdS1HXeUOeLpZMlEPXh6I/MTfaaQdION9MsmAkYqwooQu6SpBQyb7Wj6aC6VoCo/
# KmtYSWMfCWluWpiW5IP0wI/zRive/DvQvTXvbiWu5a8n7dDd8w6vmSiXmE0OPQvy
# CInWH8MyGOLwxS3OW560STkKxgrCxq2u5bLZ2xWIUUVYODJxJxp/sfQn+N4sOiBp
# mLJZiWhub6e3dMNABQamASooPoI/E01mC8CzTfXhj38cbxV9Rad25UAqZaPDXVJi
# hsMdYzaXht/a8/jyFqGaJ+HNpZfQ7l1jQeNbB5yHPgZ3BtEGsXUfFL5hYbXw3MYb
# BL7fQccOKO7eZS/sl/ahXJbYANahRr1Z85elCUtIEJmAH9AAKcWxm6U/RXceNcbS
# oqKfenoi+kiVH6v7RyOA9Z74v2u3S5fi63V4GuzqN5l5GEv/1rMjaHXmr/r8i+sL
# gOppO6/8MO0ETI7f33VtY5E90Z1WTk+/gFcioXgRMiF670EKsT/7qMykXcGhiJtX
# cVZOSEXAQsmbdlsKgEhr/Xmfwb1tbWrJUnMTDXpQzTGCGiYwghoiAgEBMIGVMH4x
# CzAJBgNVBAYTAlVTMRMwEQYDVQQIEwpXYXNoaW5ndG9uMRAwDgYDVQQHEwdSZWRt
# b25kMR4wHAYDVQQKExVNaWNyb3NvZnQgQ29ycG9yYXRpb24xKDAmBgNVBAMTH01p
# Y3Jvc29mdCBDb2RlIFNpZ25pbmcgUENBIDIwMTECEzMAAASEmOIS4HijMV0AAAAA
# BIQwDQYJYIZIAWUDBAIBBQCgga4wGQYJKoZIhvcNAQkDMQwGCisGAQQBgjcCAQQw
# HAYKKwYBBAGCNwIBCzEOMAwGCisGAQQBgjcCARUwLwYJKoZIhvcNAQkEMSIEIGeO
# wRzPCaLW7PfSXGQBp+8pdPTbcJn3qTtcIsYm4AEDMEIGCisGAQQBgjcCAQwxNDAy
# oBSAEgBNAGkAYwByAG8AcwBvAGYAdKEagBhodHRwOi8vd3d3Lm1pY3Jvc29mdC5j
# b20wDQYJKoZIhvcNAQEBBQAEggEAPBweclfg4OT7Q6VbpvsH7ZxJi2G6iZji4DwI
# 5/g7ui5hbQ5o4MqrvsRbvrB6GrtPnn68/YHJd1qexmqvaEvCnVAVhR/FlJHCJ9Yb
# 1a2pZ7vLjQOCgdqe209C0dDfKJXq58te7FqcrpW1zxPFKf0z4NjGEb9Y8pewnnbl
# 305cH89i2la64fao/bffb8gYcT3mRohbRqBi2Rn+VNMDDydHhsQEeHJfU+t+mH5K
# NRGKsn+4imaoLtp8/71miM3cmrHFM+IHSPTYSayOsFhhyWLKnkGFZUXL65ykzvyp
# gZUsuJkt27/QNqgCGd21SdGliIRYEB6OtvQHutRBXwOfpmyqh6GCF7AwghesBgor
# BgEEAYI3AwMBMYIXnDCCF5gGCSqGSIb3DQEHAqCCF4kwgheFAgEDMQ8wDQYJYIZI
# AWUDBAIBBQAwggFaBgsqhkiG9w0BCRABBKCCAUkEggFFMIIBQQIBAQYKKwYBBAGE
# WQoDATAxMA0GCWCGSAFlAwQCAQUABCBaJXwFH74Q0w1o7N0HliTKvEzco3qU5IpB
# vjl0LjzNYwIGabh8aoAbGBMyMDI2MDQyMDE3MTU1Ni4wOTlaMASAAgH0oIHZpIHW
# MIHTMQswCQYDVQQGEwJVUzETMBEGA1UECBMKV2FzaGluZ3RvbjEQMA4GA1UEBxMH
# UmVkbW9uZDEeMBwGA1UEChMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMS0wKwYDVQQL
# EyRNaWNyb3NvZnQgSXJlbGFuZCBPcGVyYXRpb25zIExpbWl0ZWQxJzAlBgNVBAsT
# Hm5TaGllbGQgVFNTIEVTTjo2QjA1LTA1RTAtRDk0NzElMCMGA1UEAxMcTWljcm9z
# b2Z0IFRpbWUtU3RhbXAgU2VydmljZaCCEf4wggcoMIIFEKADAgECAhMzAAACEUUY
# OZtDz/xsAAEAAAIRMA0GCSqGSIb3DQEBCwUAMHwxCzAJBgNVBAYTAlVTMRMwEQYD
# VQQIEwpXYXNoaW5ndG9uMRAwDgYDVQQHEwdSZWRtb25kMR4wHAYDVQQKExVNaWNy
# b3NvZnQgQ29ycG9yYXRpb24xJjAkBgNVBAMTHU1pY3Jvc29mdCBUaW1lLVN0YW1w
# IFBDQSAyMDEwMB4XDTI1MDgxNDE4NDgxM1oXDTI2MTExMzE4NDgxM1owgdMxCzAJ
# BgNVBAYTAlVTMRMwEQYDVQQIEwpXYXNoaW5ndG9uMRAwDgYDVQQHEwdSZWRtb25k
# MR4wHAYDVQQKExVNaWNyb3NvZnQgQ29ycG9yYXRpb24xLTArBgNVBAsTJE1pY3Jv
# c29mdCBJcmVsYW5kIE9wZXJhdGlvbnMgTGltaXRlZDEnMCUGA1UECxMeblNoaWVs
# ZCBUU1MgRVNOOjZCMDUtMDVFMC1EOTQ3MSUwIwYDVQQDExxNaWNyb3NvZnQgVGlt
# ZS1TdGFtcCBTZXJ2aWNlMIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEA
# z7m7MxAdL5Vayrk7jsMo3GnhN85ktHCZEvEcj4BIccHKd/NKC7uPvpX5dhO63W6V
# M5iCxklG8qQeVVrPaKvj8dYYJC7DNt4NN3XlVdC/voveJuPPhTJ/u7X+pYmV2qeh
# TVPOOB1/hpmt51SzgxZczMdnFl+X2e1PgutSA5CAh9/Xz5NW0CxnYVz8g0Vpxg+B
# q32amktRXr8m3BSEgUs8jgWRPVzPHEczpbhloGGEfHaROmHhVKIqN+JhMweEjU2N
# XM2W6hm32j/QH/I/KWqNNfYchHaG0xJljVTYoUKPpcQDuhH9dQKEgvGxj2U5/3Fq
# 1em4dO6Ih04m6R+ttxr6Y8oRJH9ZhZ3sciFBIvZh7E2YFXOjP4MGybSylQTPDEFA
# tHHgpkskeEUhsPDR9VvWWhekhQx3qXaAKh+AkLmz/hpE3e0y+RIKO2AREjULJAKg
# f+R9QnNvqMeMkz9PGrjsijqWGzB2k2JNyaUYKlbmQweOabsCioiY2fJbimjVyFAG
# k5AeYddUFxvJGgRVCH7BeBPKAq7MMOmSCTOMZ0Sw6zyNx4Uhh5Y0uJ0ZOoTKnB3K
# fdN/ba/eKHFeEhi3WqAfzTxiy0rMvhsfsXZK7zoclqaRvVl8Q48J174+eyriypY9
# HhU+ohgiYi4uQGDDVdTDeKDtoC/hD2Cn+ARzwE1rFfECAwEAAaOCAUkwggFFMB0G
# A1UdDgQWBBRifUUDwOnqIcvfb53+yV0EZn7OcDAfBgNVHSMEGDAWgBSfpxVdAF5i
# XYP05dJlpxtTNRnpcjBfBgNVHR8EWDBWMFSgUqBQhk5odHRwOi8vd3d3Lm1pY3Jv
# c29mdC5jb20vcGtpb3BzL2NybC9NaWNyb3NvZnQlMjBUaW1lLVN0YW1wJTIwUENB
# JTIwMjAxMCgxKS5jcmwwbAYIKwYBBQUHAQEEYDBeMFwGCCsGAQUFBzAChlBodHRw
# Oi8vd3d3Lm1pY3Jvc29mdC5jb20vcGtpb3BzL2NlcnRzL01pY3Jvc29mdCUyMFRp
# bWUtU3RhbXAlMjBQQ0ElMjAyMDEwKDEpLmNydDAMBgNVHRMBAf8EAjAAMBYGA1Ud
# JQEB/wQMMAoGCCsGAQUFBwMIMA4GA1UdDwEB/wQEAwIHgDANBgkqhkiG9w0BAQsF
# AAOCAgEApEKdnMeIIUiU6PatZ/qbrwiDzYUMKRczC4Bp/XY1S9NmHI+2c3dcpwH2
# SOmDfdvIIqt7mRrgvBPYOvJ9CtZS5eeIrsObC0b0ggKTv2wrTgWG+qktqNFEhQei
# pdURNLN68uHAm5edwBytd1kwy5r6B93klxDsldOmVWtw/ngj7knN09muCmwr17Jn
# sMFcoIN/H59s+1RYN7Vid4+7nj8FcvYy9rbZOMndBzsTiosF1M+aMIJX2k3EVFVs
# uDL7/R5ppI9Tg7eWQOWKMZHPdsA3ZqWzDuhJqTzoFSQShnZenC+xq/z9BhHPFFbU
# tfjAoG6EDPjSQJYXmogja8OEa19xwnh3wVufeP+ck+/0gxNi7g+kO6WaOm052F4s
# iD8xi6Uv75L7798lHvPThcxHHsgXqMY592d1wUof3tL/eDaQ0UhnYCU8yGkU2XJn
# ctONnBKAvURAvf2qiIWDj4Lpcm0zA7VuofuJR1Tpuyc5p1ja52bNZBBVqAOwyDhA
# mqWsJXAjYXnssC/fJkee314Fh+GIyMgvAPRScgqRZqV16dTBYvoe+w1n/wWs/yST
# UsxDw4T/AITcu5PAsLnCVpArDrFLRTFyut+eHUoG6UYZfj8/RsuQ42INse1pb/cP
# m7G2lcLJtkIKT80xvB1LiaNvPTBVEcmNSvFUM0xrXZXcYcxVXiYwggdxMIIFWaAD
# AgECAhMzAAAAFcXna54Cm0mZAAAAAAAVMA0GCSqGSIb3DQEBCwUAMIGIMQswCQYD
# VQQGEwJVUzETMBEGA1UECBMKV2FzaGluZ3RvbjEQMA4GA1UEBxMHUmVkbW9uZDEe
# MBwGA1UEChMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMTIwMAYDVQQDEylNaWNyb3Nv
# ZnQgUm9vdCBDZXJ0aWZpY2F0ZSBBdXRob3JpdHkgMjAxMDAeFw0yMTA5MzAxODIy
# MjVaFw0zMDA5MzAxODMyMjVaMHwxCzAJBgNVBAYTAlVTMRMwEQYDVQQIEwpXYXNo
# aW5ndG9uMRAwDgYDVQQHEwdSZWRtb25kMR4wHAYDVQQKExVNaWNyb3NvZnQgQ29y
# cG9yYXRpb24xJjAkBgNVBAMTHU1pY3Jvc29mdCBUaW1lLVN0YW1wIFBDQSAyMDEw
# MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEA5OGmTOe0ciELeaLL1yR5
# vQ7VgtP97pwHB9KpbE51yMo1V/YBf2xK4OK9uT4XYDP/XE/HZveVU3Fa4n5KWv64
# NmeFRiMMtY0Tz3cywBAY6GB9alKDRLemjkZrBxTzxXb1hlDcwUTIcVxRMTegCjhu
# je3XD9gmU3w5YQJ6xKr9cmmvHaus9ja+NSZk2pg7uhp7M62AW36MEBydUv626GIl
# 3GoPz130/o5Tz9bshVZN7928jaTjkY+yOSxRnOlwaQ3KNi1wjjHINSi947SHJMPg
# yY9+tVSP3PoFVZhtaDuaRr3tpK56KTesy+uDRedGbsoy1cCGMFxPLOJiss254o2I
# 5JasAUq7vnGpF1tnYN74kpEeHT39IM9zfUGaRnXNxF803RKJ1v2lIH1+/NmeRd+2
# ci/bfV+AutuqfjbsNkz2K26oElHovwUDo9Fzpk03dJQcNIIP8BDyt0cY7afomXw/
# TNuvXsLz1dhzPUNOwTM5TI4CvEJoLhDqhFFG4tG9ahhaYQFzymeiXtcodgLiMxhy
# 16cg8ML6EgrXY28MyTZki1ugpoMhXV8wdJGUlNi5UPkLiWHzNgY1GIRH29wb0f2y
# 1BzFa/ZcUlFdEtsluq9QBXpsxREdcu+N+VLEhReTwDwV2xo3xwgVGD94q0W29R6H
# XtqPnhZyacaue7e3PmriLq0CAwEAAaOCAd0wggHZMBIGCSsGAQQBgjcVAQQFAgMB
# AAEwIwYJKwYBBAGCNxUCBBYEFCqnUv5kxJq+gpE8RjUpzxD/LwTuMB0GA1UdDgQW
# BBSfpxVdAF5iXYP05dJlpxtTNRnpcjBcBgNVHSAEVTBTMFEGDCsGAQQBgjdMg30B
# ATBBMD8GCCsGAQUFBwIBFjNodHRwOi8vd3d3Lm1pY3Jvc29mdC5jb20vcGtpb3Bz
# L0RvY3MvUmVwb3NpdG9yeS5odG0wEwYDVR0lBAwwCgYIKwYBBQUHAwgwGQYJKwYB
# BAGCNxQCBAweCgBTAHUAYgBDAEEwCwYDVR0PBAQDAgGGMA8GA1UdEwEB/wQFMAMB
# Af8wHwYDVR0jBBgwFoAU1fZWy4/oolxiaNE9lJBb186aGMQwVgYDVR0fBE8wTTBL
# oEmgR4ZFaHR0cDovL2NybC5taWNyb3NvZnQuY29tL3BraS9jcmwvcHJvZHVjdHMv
# TWljUm9vQ2VyQXV0XzIwMTAtMDYtMjMuY3JsMFoGCCsGAQUFBwEBBE4wTDBKBggr
# BgEFBQcwAoY+aHR0cDovL3d3dy5taWNyb3NvZnQuY29tL3BraS9jZXJ0cy9NaWNS
# b29DZXJBdXRfMjAxMC0wNi0yMy5jcnQwDQYJKoZIhvcNAQELBQADggIBAJ1Vffwq
# reEsH2cBMSRb4Z5yS/ypb+pcFLY+TkdkeLEGk5c9MTO1OdfCcTY/2mRsfNB1OW27
# DzHkwo/7bNGhlBgi7ulmZzpTTd2YurYeeNg2LpypglYAA7AFvonoaeC6Ce5732pv
# vinLbtg/SHUB2RjebYIM9W0jVOR4U3UkV7ndn/OOPcbzaN9l9qRWqveVtihVJ9Ak
# vUCgvxm2EhIRXT0n4ECWOKz3+SmJw7wXsFSFQrP8DJ6LGYnn8AtqgcKBGUIZUnWK
# NsIdw2FzLixre24/LAl4FOmRsqlb30mjdAy87JGA0j3mSj5mO0+7hvoyGtmW9I/2
# kQH2zsZ0/fZMcm8Qq3UwxTSwethQ/gpY3UA8x1RtnWN0SCyxTkctwRQEcb9k+SS+
# c23Kjgm9swFXSVRk2XPXfx5bRAGOWhmRaw2fpCjcZxkoJLo4S5pu+yFUa2pFEUep
# 8beuyOiJXk+d0tBMdrVXVAmxaQFEfnyhYWxz/gq77EFmPWn9y8FBSX5+k77L+Dvk
# txW/tM4+pTFRhLy/AsGConsXHRWJjXD+57XQKBqJC4822rpM+Zv/Cuk0+CQ1Zyvg
# DbjmjJnW4SLq8CdCPSWU5nR0W2rRnj7tfqAxM328y+l7vzhwRNGQ8cirOoo6CGJ/
# 2XBjU02N7oJtpQUQwXEGahC0HVUzWLOhcGbyoYIDWTCCAkECAQEwggEBoYHZpIHW
# MIHTMQswCQYDVQQGEwJVUzETMBEGA1UECBMKV2FzaGluZ3RvbjEQMA4GA1UEBxMH
# UmVkbW9uZDEeMBwGA1UEChMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMS0wKwYDVQQL
# EyRNaWNyb3NvZnQgSXJlbGFuZCBPcGVyYXRpb25zIExpbWl0ZWQxJzAlBgNVBAsT
# Hm5TaGllbGQgVFNTIEVTTjo2QjA1LTA1RTAtRDk0NzElMCMGA1UEAxMcTWljcm9z
# b2Z0IFRpbWUtU3RhbXAgU2VydmljZaIjCgEBMAcGBSsOAwIaAxUAKyp8q2VdgAq1
# VGkzd7PZwV6zNc2ggYMwgYCkfjB8MQswCQYDVQQGEwJVUzETMBEGA1UECBMKV2Fz
# aGluZ3RvbjEQMA4GA1UEBxMHUmVkbW9uZDEeMBwGA1UEChMVTWljcm9zb2Z0IENv
# cnBvcmF0aW9uMSYwJAYDVQQDEx1NaWNyb3NvZnQgVGltZS1TdGFtcCBQQ0EgMjAx
# MDANBgkqhkiG9w0BAQsFAAIFAO2Qc8EwIhgPMjAyNjA0MjAwOTQzMjlaGA8yMDI2
# MDQyMTA5NDMyOVowdzA9BgorBgEEAYRZCgQBMS8wLTAKAgUA7ZBzwQIBADAKAgEA
# AgIKQQIB/zAHAgEAAgISdDAKAgUA7ZHFQQIBADA2BgorBgEEAYRZCgQCMSgwJjAM
# BgorBgEEAYRZCgMCoAowCAIBAAIDB6EgoQowCAIBAAIDAYagMA0GCSqGSIb3DQEB
# CwUAA4IBAQARmmIjG0OToUwclMH7G/KuzRTRc6kvkq7S2U1uEVBlhhFK7Ft5yRem
# hWCYLaPka5SzIeVk4DLn0mSXnARKJHdH7tUADd3SUWtmI5Js09AUZk6aYrPT+jrl
# Nls0vIm9Ir+1JeSq7fW3l1JxNy8F/nZPIwOIe9KsQfM4Wr27R+jTJFGyIwXf4Olf
# E0fABPfKcYoh6WreyfhAIxtTJ43pcZYP0aWueJOfCajCZyCEOwVzQad0ZipJBUBc
# O3urBG/sGXTW0e9Ksu5T9uPIojifbNeMN0jI3+9cnA5etp4SY7Ef5Wxd/TezzdMM
# ebWuEUGyb5fFqO3q8OP/AvCIgsyMY1lzMYIEDTCCBAkCAQEwgZMwfDELMAkGA1UE
# BhMCVVMxEzARBgNVBAgTCldhc2hpbmd0b24xEDAOBgNVBAcTB1JlZG1vbmQxHjAc
# BgNVBAoTFU1pY3Jvc29mdCBDb3Jwb3JhdGlvbjEmMCQGA1UEAxMdTWljcm9zb2Z0
# IFRpbWUtU3RhbXAgUENBIDIwMTACEzMAAAIRRRg5m0PP/GwAAQAAAhEwDQYJYIZI
# AWUDBAIBBQCgggFKMBoGCSqGSIb3DQEJAzENBgsqhkiG9w0BCRABBDAvBgkqhkiG
# 9w0BCQQxIgQgH9FUOFpNloapl0LHAEpVz2MI+k8CrDmn9oD0V/e0GjQwgfoGCyqG
# SIb3DQEJEAIvMYHqMIHnMIHkMIG9BCAsrTOpmu+HTq1aXFwvlhjF8p2nUCNNCEX/
# OWLHNDMmtzCBmDCBgKR+MHwxCzAJBgNVBAYTAlVTMRMwEQYDVQQIEwpXYXNoaW5n
# dG9uMRAwDgYDVQQHEwdSZWRtb25kMR4wHAYDVQQKExVNaWNyb3NvZnQgQ29ycG9y
# YXRpb24xJjAkBgNVBAMTHU1pY3Jvc29mdCBUaW1lLVN0YW1wIFBDQSAyMDEwAhMz
# AAACEUUYOZtDz/xsAAEAAAIRMCIEICwp9mc/PZOG+dnQzhSFGbNnnMuROrJ/6rxk
# +wtk5drDMA0GCSqGSIb3DQEBCwUABIICAF6rjvMie72b7/Z7X0U3oy05Gfjwam/y
# Y/LBboe7FmIaOT3ru6cb20urXKqwJxcZ/bnCYWPm1G4yHddF6hysr590+/il3hq9
# ASxCC/dgiA+qlBPLpKHXQKd206eUJwg1AKoO5bMQlfpWtbrdl5/LVBsmzvp1mYr1
# ETDt11ognJeGf3MMFJxEoZ2weReMCPt2dlEZ5F6Gg6WwPm40zoaL68eRjvRfAD5t
# m5uGmU2DTm/0EpLtuV0AId7NSDtH0wFVzFX0xjeqRspehhJht0AAjDbTTnfhVSd1
# s8ZGidBAe1GPVE9h+Bvg743Al3v7FoI/2GhBjipAMIYtoa2x8csESsXJ9BiEWN+D
# AmpDtQLbdtDwxVv8YEgvVcW+dhgBX85l4z1N3KCqRz6et0ZI8Jq0FhmUFbtm/U/o
# JSMS5oZDoV4xulgOc3orTEt38MAi9T49mJNdjkKTEyT+J47IAE3FVPhmAZ3uczAH
# yZfscVc94tKGcqNOTa/YHl9JoWeOcBHUC8g3ei0hc6Rx8dXIoubaUqFZert8EHdM
# 9GxR9ZrDK/2VQLWr2tSWcfHMILI8vsCN7wTlDsEbseafky+vlXQjoQ8DNZQ1YZF+
# 0SiknhuucT1GwLL15jcgEgiPPjMDEsMBR7hzBXcAEqQfp0FuBBA/sCO4EF4TPiP9
# /BBvAZwwxwM+
# SIG # End signature block