packages/AzStackHci.DiagnosticSettings.0.6.9/Private/AzStackHci.WildcardAndSubdomain.Helpers.ps1

# ////////////////////////////////////////////////////////////////////////////
# Strict Mode v1 (PS 5.1 safe) - surfaces reads of uninitialised variables at runtime.
Set-StrictMode -Version 1.0

# Helper to perform DNS + optional TCP + Layer7 testing for a URL/port and return a result hashtable.
# Used by Expand-WildcardUrlsDynamically, Test-ManuallyDefinedSubdomains, and the extracted
# Invoke-Layer7EndpointTest helper (parallel fan-out) to avoid duplicated test logic.
#
# v0.6.7 (parallel branch): added -RequestMethod plumbing. Prior to this fix, the main
# Layer-7 sweep loop in Test-AzureLocalConnectivity called this helper without -RequestMethod,
# which meant Test-Layer7Connectivity always used its parameter default ('Auto' as of v0.6.7)
# regardless of what the user passed to the public function. The two later loops (redirected
# URLs + remaining URLs) were already plumbing -RequestMethod through directly. Adding the
# parameter here closes that hole.
Function Invoke-EndpointConnectivityTest {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory)][string]$Url,
        [Parameter(Mandatory)][int]$Port,
        [switch]$IncludeTCPConnectivityTests,
        [Parameter(Mandatory=$false)]
        [ValidateSet('Get','Head','Auto')]
        [string]$RequestMethod = 'Auto'
    )

    try {
        $DNSCheck = Get-DnsRecord -url (Get-DomainFromURL -url $Url).Domain
    } catch {
        Write-HostAzS "Error: DNS resolution failed for '$Url': $($_.Exception.Message)" -ForegroundColor Red
        return @{
            TCPStatus                        = "Failed"
            IPAddress                        = "DNS Error"
            Layer7Status                     = "Failed"
            Layer7Response                   = "DNS resolution error: $($_.Exception.Message)"
            Layer7ResponseTime               = "N/A"
            CertificateIssuer                = "N/A"
            CertificateSubject               = "N/A"
            CertificateThumbprint            = "N/A"
            CertificateIntermediateIssuer    = "N/A"
            CertificateIntermediateSubject   = "N/A"
            CertificateIntermediateThumbprint= "N/A"
            CertificateRootIssuer            = "N/A"
            CertificateRootSubject           = "N/A"
            CertificateRootThumbprint        = "N/A"
        }
    }

    if (($DNSCheck.DNSExists) -or ($script:Proxy.Enabled)) {
        if ($IncludeTCPConnectivityTests.IsPresent) {
            try {
                $tcpStatus, $ipAddress = Test-TCPConnectivity -url $Url -port $Port
            } catch {
                Write-HostAzS "Error: TCP connectivity test failed for '$Url': $($_.Exception.Message)" -ForegroundColor Red
                $tcpStatus = "Failed"
                $ipAddress = "TCP Error"
            }
        } else {
            $tcpStatus = "N/A"
            $ipAddress = $DNSCheck.IpAddress
        }
        try {
            $Layer7Results = Test-Layer7Connectivity -url $Url -port $Port -RequestMethod $RequestMethod
        } catch {
            Write-HostAzS "Error: Layer 7 connectivity test failed for '$Url': $($_.Exception.Message)" -ForegroundColor Red
            $Layer7Results = @{
                Layer7Status = "Failed"
                Layer7Response = "Layer 7 test error: $($_.Exception.Message)"
                Layer7ResponseTime = "N/A"
                CertificateIssuer = "N/A"
                CertificateSubject = "N/A"
                CertificateThumbprint = "N/A"
                CertificateIntermediateIssuer = "N/A"
                CertificateIntermediateSubject = "N/A"
                CertificateIntermediateThumbprint = "N/A"
                CertificateRootIssuer = "N/A"
                CertificateRootSubject = "N/A"
                CertificateRootThumbprint = "N/A"
            }
        }
        return @{
            TCPStatus                        = $tcpStatus
            IPAddress                        = $ipAddress
            Layer7Status                     = $Layer7Results.Layer7Status
            Layer7Response                   = $Layer7Results.Layer7Response
            Layer7ResponseTime               = $Layer7Results.Layer7ResponseTime
            CertificateIssuer                = $Layer7Results.CertificateIssuer
            CertificateSubject               = $Layer7Results.CertificateSubject
            CertificateThumbprint            = $Layer7Results.CertificateThumbprint
            CertificateIntermediateIssuer    = $Layer7Results.CertificateIntermediateIssuer
            CertificateIntermediateSubject   = $Layer7Results.CertificateIntermediateSubject
            CertificateIntermediateThumbprint= $Layer7Results.CertificateIntermediateThumbprint
            CertificateRootIssuer            = $Layer7Results.CertificateRootIssuer
            CertificateRootSubject           = $Layer7Results.CertificateRootSubject
            CertificateRootThumbprint        = $Layer7Results.CertificateRootThumbprint
        }
    } else {
        # DNS name does not exist
        return @{
            TCPStatus                        = if ($IncludeTCPConnectivityTests.IsPresent) { "Failed" } else { "N/A" }
            IPAddress                        = $DNSCheck.IpAddress
            Layer7Status                     = "Failed"
            Layer7Response                   = "N/A"
            Layer7ResponseTime               = "N/A"
            CertificateIssuer                = "N/A"
            CertificateSubject               = "N/A"
            CertificateThumbprint            = "N/A"
            CertificateIntermediateIssuer    = "N/A"
            CertificateIntermediateSubject   = "N/A"
            CertificateIntermediateThumbprint= "N/A"
            CertificateRootIssuer            = "N/A"
            CertificateRootSubject           = "N/A"
            CertificateRootThumbprint        = "N/A"
        }
    }
}


# ////////////////////////////////////////////////////////////////////////////
# Enhanced Function to expand wildcard URLs dynamically using pattern matching *.domain.com
#
# How this works:
# 1. Splits $script:Results into wildcard entries (*.foo.com) and non-wildcard entries (bar.foo.com)
# 2. For each wildcard, converts it to a regex (e.g. *.foo.com -> .*\.foo\.com)
# 3. Finds non-wildcard URLs that match the regex pattern
# 4. For each match: if already tested, adds a cross-reference Note; if new, runs connectivity tests
# 5. Updates the wildcard entry's Note field to show which specific URLs were tested for it
#
# This enables testing of wildcard firewall rules by verifying connectivity to known specific endpoints
# that fall under the wildcard pattern.
Function Expand-WildcardUrlsDynamically {

    begin {
        # Write-Verbose "Starting Expand-WildcardUrlsDynamically function"
    }

    process {
        # Split into wildcard and non-wildcard results
        [array]$wildcardResults = $script:Results | Where-Object { $_.IsWildcard -eq $true }
        [array]$nonWildcardResults = $script:Results | Where-Object { $_.IsWildcard -eq $false }

        [int]$wildcardCount = 0

        ForEach ($wildcard in $wildcardResults) {

            $wildcardCount++
            Write-Progress -Id 1 -ParentId 0 -Activity "Expanding Wildcard URLs" -Status "Wildcard $wildcardCount of $($wildcardResults.Count): $($wildcard.URL)" -PercentComplete (($wildcardCount / [math]::Max($wildcardResults.Count,1)) * 100)

            # Create a regex pattern from the wildcard URL
            # Escape dots first, then replace * with .* for proper regex matching
            $wildcardPattern = [regex]::Escape($wildcard.URL) -replace "\\\*", ".*"

            # Find matching non-wildcard URLs, for each wildcard URL that exists
            [array]$matchingUrls = @()
            $matchingUrls = $nonWildcardResults | Where-Object { $_.URL -match "^$wildcardPattern$" }

            Write-HostAzS "`n$wildcardCount of $($wildcardResults.Count): Processing $($wildcard.Source): $($wildcard.URL)"

            if($matchingUrls.Count -eq 0) {
                Write-HostAzS "`tInfo: No match found for $($wildcard.source), $($wildcard.URL)" -ForegroundColor Yellow
                if($wildcard.Note -notlike "Wildcard URL,*"){
                    # Update the note to include the matching URL
                    Write-Verbose "Updating Note of wildcard URL: $($wildcard.URL)"
                    ($script:Results | Where-Object { $PSItem.URL -eq $wildcard.URL } | Select-Object -First 1).Note = "Wildcard URL, not tested (as no non-wildcard) - $($wildcard.Note)"
                }
                # Continue to the next matching URL
                Continue

            } elseif ($matchingUrls.Count -gt 0) {
                # Matches found, expand the wildcard entry
                # Incremental counter for matching URLs
                [int]$counter = 0
                # Loop for each matching URL
                foreach ($match in $matchingUrls) {
                    $counter++
                    Write-HostAzS "`n`tProcessing match $counter of $($matchingUrls.count): $($match.URL)"
                    # Add URL based on test for either HTTP or HTTPS
                    if($match.Port -is [int]){

                        # If the URL already exists in the results, update the note
                        if($script:Results.URL -contains $match.URL){
                            # URL already exists in the results, skip
                            Write-HostAzS "`tEndpoint matches wildcard '$($wildcard.URL)', adding cross-reference in Notes column" -ForegroundColor Green
                            if(($match.Note -notlike "URL matches *") -and ($match.Note -notlike "Manually defined URL *")){
                                # Update the note to include the matching URL
                                Write-Verbose "Updating Note of matched URL: $($match.URL)"
                            ($script:Results | Where-Object { $PSItem.URL -eq $match.URL } | Select-Object -First 1).Note = "URL matches $($wildcard.Source), to test URL $($wildcard.URL) - $($match.Note)"
                            }
                            # Continue to the next matching URL
                            Continue

                        } elseif ($script:Results.URL -notcontains $match.URL){
                            # Specific URL matched to Wildcard URL does not exist in the results, unexpected, but will add it
                            Write-HostAzS "`tEndpoint matches wildcard '$($wildcard.URL)', adding to results" -ForegroundColor Green
                            $newEntry = $wildcard.PSObject.Copy()
                            $newEntry.URL = $match.URL
                            $newEntry.Port = $match.Port
                            $newEntry.ArcGateway = $match.ArcGateway
                            $newEntry.Source = "$($match.Source)"
                            $newEntry.Note = "URL matches $($match.Source), to test URL $($wildcard.URL) - $($match.Note)"
                            # Set IsWildcard to false, as this is no longer a wildcard URL, it is a specific URL to test a wildcard URL
                            $newEntry.IsWildcard = $false
                            if($ArcGatewayDeployment.IsPresent){
                                # Skip URLs with ArcGateway -eq $True
                                if($match.ArcGateway){
                                    # Skip URLs that support Arc Gateway
                                    Write-HostAzS "Skipped URL: '$($newEntry.URL)' as supported by Arc Gateway" -ForegroundColor Yellow
                                    $newEntry.TCPStatus = "Skipped"
                                    $newEntry.Note = "Skipped, URL is supported by Arc Gateway - $($match.Note)"
                                    $newEntry.IPAddress = "N/A"
                                    $newEntry.Layer7Status = "Skipped"
                                    $newEntry.Layer7Response = "N/A"
                                    $newEntry.Layer7ResponseTime = "N/A"
                                    $newEntry.CertificateIssuer = "N/A"
                                    $newEntry.CertificateSubject = "N/A"
                                    $newEntry.CertificateThumbprint = "N/A"
                                    $newEntry.IntermediateCertificateIssuer = "N/A"
                                    $newEntry.IntermediateCertificateSubject = "N/A"
                                    $newEntry.IntermediateCertificateThumbprint = "N/A"
                                    $newEntry.RootCertificateIssuer = "N/A"
                                    $newEntry.RootCertificateSubject = "N/A"
                                    $newEntry.RootCertificateThumbprint = "N/A"
                                } else {
                                    # Does not support Arc Gateway - run connectivity tests
                                    # $RequestMethod resolves from the Test-AzureLocalConnectivity call frame (dynamic scope), like $IncludeTCPConnectivityTests / $ArcGatewayDeployment above.
                                    $testResult = Invoke-EndpointConnectivityTest -Url $match.URL -Port $match.Port -IncludeTCPConnectivityTests:$IncludeTCPConnectivityTests.IsPresent -RequestMethod $RequestMethod
                                    $newEntry.TCPStatus = $testResult.TCPStatus
                                    $newEntry.IPAddress = $testResult.IPAddress
                                    $newEntry.Layer7Status = $testResult.Layer7Status
                                    $newEntry.Layer7Response = $testResult.Layer7Response
                                    $newEntry.Layer7ResponseTime = $testResult.Layer7ResponseTime
                                    $newEntry.CertificateIssuer = $testResult.CertificateIssuer
                                    $newEntry.CertificateSubject = $testResult.CertificateSubject
                                    $newEntry.CertificateThumbprint = $testResult.CertificateThumbprint
                                    $newEntry.IntermediateCertificateIssuer = $testResult.CertificateIntermediateIssuer
                                    $newEntry.IntermediateCertificateSubject = $testResult.CertificateIntermediateSubject
                                    $newEntry.IntermediateCertificateThumbprint = $testResult.CertificateIntermediateThumbprint
                                    $newEntry.RootCertificateIssuer = $testResult.CertificateRootIssuer
                                    $newEntry.RootCertificateSubject = $testResult.CertificateRootSubject
                                    $newEntry.RootCertificateThumbprint = $testResult.CertificateRootThumbprint
                                } # End of ArcGateway -eq $True check
                            } else {
                                # Else process URLs with ArcGateway -eq $False
                                # $RequestMethod resolves from the Test-AzureLocalConnectivity call frame (dynamic scope), like $IncludeTCPConnectivityTests / $ArcGatewayDeployment above.
                                $testResult = Invoke-EndpointConnectivityTest -Url $match.URL -Port $match.Port -IncludeTCPConnectivityTests:$IncludeTCPConnectivityTests.IsPresent -RequestMethod $RequestMethod
                                $newEntry.TCPStatus = $testResult.TCPStatus
                                $newEntry.IPAddress = $testResult.IPAddress
                                $newEntry.Layer7Status = $testResult.Layer7Status
                                $newEntry.Layer7Response = $testResult.Layer7Response
                                $newEntry.Layer7ResponseTime = $testResult.Layer7ResponseTime
                                $newEntry.CertificateIssuer = $testResult.CertificateIssuer
                                $newEntry.CertificateSubject = $testResult.CertificateSubject
                                $newEntry.CertificateThumbprint = $testResult.CertificateThumbprint
                                $newEntry.IntermediateCertificateIssuer = $testResult.CertificateIntermediateIssuer
                                $newEntry.IntermediateCertificateSubject = $testResult.CertificateIntermediateSubject
                                $newEntry.IntermediateCertificateThumbprint = $testResult.CertificateIntermediateThumbprint
                                $newEntry.RootCertificateIssuer = $testResult.CertificateRootIssuer
                                $newEntry.RootCertificateSubject = $testResult.CertificateRootSubject
                                $newEntry.RootCertificateThumbprint = $testResult.CertificateRootThumbprint
                            } # End of DNS check
                            # Add the new match to the expanded results
                            $script:Results.Add($newEntry) | Out-Null
                            Write-Verbose "Added: '$($newEntry.URL)' that matches Wildcard '$($wildcard.URL)'"
                        
                        } else {
                            # Unexpected condition, should not occur, as we use the Results array for the list of Wildcards, so the URL should always exist
                            Write-Warning "Unexpected: URL '$($match.URL)' matches Wildcard '$($wildcard.URL)', but conditions not met, skipping"
                            Continue
                        } # End of URL exists check

                    } else {
                        Write-Warning "Port not valid for url: '$($match.URL)', port: '$($match.Port)', note: '$($match.Source)' - skipping"
                    }

                } # End of matching URLs loop
            } else {
                # Unexpected condition, should not occur, as we use the Results array for the list of Wildcards, so the URL should always exist
                Write-Warning "Unexpected: Wildcard URL $($wildcard.URL) should match either zero or more results, but matches status unknown, skipping"
                Continue

            } # End of matching URLs check
        } # End of wildcard loop
        Write-Progress -Id 1 -Activity "Expanding Wildcard URLs" -Completed

    } # End of process block

    end {
        # Write-Debug "Completed Expand-WildcardUrlsDynamically function"
    }

} # End of Expand-WildcardUrlsDynamically function


# ////////////////////////////////////////////////////////////////////////////
# Function to manually test known subdomains for wildcard URLs
Function Test-ManuallyDefinedSubdomains {

    Param (
    )

    begin {
        # Incremental counter for subdomains (separate from the main progress bar)
        [int]$urlCount = 0
        # Total number of manually defined subdomains for sub-progress tracking
        [int]$totalSubdomains = $script:MANUAL_SUBDOMAIN_COUNT
        # Use the constant subdomain list
        $manualSubdomains = $script:MANUAL_SUBDOMAINS
    }

    process {
        # Test each manual subdomain for each wildcard to validate connectivity.
        ForEach ($entry in $manualSubdomains) {
            $wildcard = $entry.Wildcard
            $subdomains = $entry.Subdomains

            # Check if the wildcard URL is supported by Arc Gateway, if so, skip the wildcard URL
            if($ArcGatewayDeployment.IsPresent -and ($PreArcGatewayRemoval | Where-Object { ($_.URL -eq $wildcard) -and ($_.ArcGateway -eq $true) })) {
                # Skip wildcard URLs that have already been tested
                Write-Verbose "Info: Wildcard '$wildcard', supports the Arc Gateway, skipping subdomain tests."
                Continue
            }

            # Check if the wildcard URL exists in the results, if not, skip testing subdomains
            if($script:Results.URL -notcontains $wildcard) {
                # Skip wildcard URLs that have already been tested
                Write-Verbose "Info: Wildcard '$wildcard', is not present in Results array, skipping subdomain tests."
                Continue
            }

            Write-HostAzS "`nTesting manually defined subdomains for wildcard $wildcard"

            # ForEach loop to process each subdomain
            ForEach ($subdomain in $subdomains) {
                # Test each subdomain for TCP and Layer 7 connectivity
                $urlCount++
                if ($totalSubdomains -gt 0) {
                    Write-Progress -Id 1 -ParentId 0 -Activity "Validating Wildcard Endpoints" -Status "Subdomain $urlCount of $($totalSubdomains): $subdomain" -PercentComplete (($urlCount / $totalSubdomains) * 100)
                }
                Write-HostAzS "`nWildcard validation $urlCount of $($totalSubdomains): Subdomain: $subdomain"
                # Test port 80 and 443 for each subdomain
                ForEach ($port in @(80, 443)) {
                    
                    # Exceptions to some URLs, that do not support port 80 or 443
                    # Skip port 443 for ctldl.windowsupdate.com, download.windowsupdate.com, fe2.update.microsoft.com and 1a.au.download.windowsupdate.com
                    if(($subdomain -in ("ctldl.windowsupdate.com","download.windowsupdate.com","fe2.update.microsoft.com","1a.au.download.windowsupdate.com")) -and ($port -eq $script:PORT_HTTPS)){
                        # Skip port 443 for ctldl.windowsupdate.com, download.windowsupdate.com, fe2.update.microsoft.com and 1a.au.download.windowsupdate.com
                        Write-HostAzS "Skipping port 443 for $subdomain" -ForegroundColor Yellow
                        Continue
                    }
                    # Skip port 80 for prod5.prod.hot.ingest.monitor.core.windows.net and edr-neu3.eu.endpoint.security.microsoft.com
                    if(($subdomain -in ("prod5.prod.hot.ingest.monitor.core.windows.net","edr-neu3.eu.endpoint.security.microsoft.com")) -and ($port -eq $script:PORT_HTTP)){
                        # Skip port 80 for prod5.prod.hot.ingest.monitor.core.windows.net and edr-neu3.eu.endpoint.security.microsoft.com
                        Write-HostAzS "Skipping port 80 for $subdomain" -ForegroundColor Yellow
                        Continue
                    }
                    
                    # Run connectivity tests using shared helper
                    # $RequestMethod resolves from the Test-AzureLocalConnectivity call frame (dynamic scope), like $IncludeTCPConnectivityTests / $ArcGatewayDeployment.
                    Write-HostAzS "Testing $subdomain on port $port"
                    $testResult = Invoke-EndpointConnectivityTest -Url $subdomain -Port $port -IncludeTCPConnectivityTests:$IncludeTCPConnectivityTests.IsPresent -RequestMethod $RequestMethod
                    $TCPstatus = $testResult.TCPStatus
                    $ipAddress = $testResult.IPAddress
                    $Layer7Status = $testResult.Layer7Status
                    $Layer7Response = $testResult.Layer7Response
                    $Layer7ResponseTime = $testResult.Layer7ResponseTime
                    $CertificateIssuer = $testResult.CertificateIssuer
                    $CertificateSubject = $testResult.CertificateSubject
                    $CertificateThumbprint = $testResult.CertificateThumbprint
                    $CertificateIntermediateIssuer = $testResult.CertificateIntermediateIssuer
                    $CertificateIntermediateSubject = $testResult.CertificateIntermediateSubject
                    $CertificateIntermediateThumbprint = $testResult.CertificateIntermediateThumbprint
                    $CertificateRootIssuer = $testResult.CertificateRootIssuer
                    $CertificateRootSubject = $testResult.CertificateRootSubject
                    $CertificateRootThumbprint = $testResult.CertificateRootThumbprint
                    
                    # Check if the URL already exists in the "allTestedUrls" results, only add new ones
                    if($script:Results.URL -notcontains $subdomain) {
                        # ArcGateway = $true, as all wildcards are "microsoft.com" or "windows.net", so should support ArcGateway
                        # MachineName (v0.6.7): identifies which node generated this row when bundles
                        # from multiple nodes are merged via -Scope Cluster.
                        $subdomainEntry = [PSCustomObject]@{
                            RowID = 0
                            MachineName = $env:COMPUTERNAME
                            URL = $subdomain
                            Port = $port
                            ArcGateway = $true
                            IsWildcard = $false
                            Source = "Test for $(($script:Results | Where-Object { ($_.URL -eq $wildcard) } | Select-Object -First 1).Source)"
                            Note = "Manually defined URL to test $Source Wildcard URL $wildcard"
                            TCPStatus = $TCPstatus
                            IPAddress = $ipAddress
                            Layer7Status = $Layer7Status
                            Layer7Response = $Layer7Response
                            Layer7ResponseTime = $Layer7ResponseTime
                            CertificateIssuer = $CertificateIssuer
                            CertificateSubject = $CertificateSubject
                            CertificateThumbprint = $CertificateThumbprint
                            IntermediateCertificateIssuer = $CertificateIntermediateIssuer
                            IntermediateCertificateSubject = $CertificateIntermediateSubject
                            IntermediateCertificateThumbprint = $CertificateIntermediateThumbprint
                            RootCertificateIssuer = $CertificateRootIssuer
                            RootCertificateSubject = $CertificateRootSubject
                            RootCertificateThumbprint = $CertificateRootThumbprint
                        }
                        # Add the subdomain entry to the results
                        Write-Debug "Info: Added $($subdomain) to the results array"
                        $script:Results.Add($subdomainEntry) | Out-Null
                        $MatchedWildcardURLs = ($script:Results | Where-Object { $PSItem.URL -eq $wildcard } )
                        ForEach($MatchedWildcardURL in $MatchedWildcardURLs) {
                            if($MatchedWildcardURL.Note -notlike "Wildcard URL,*"){
                                # Update the note to include the matching URL
                                Write-Verbose "Updating Note of wildcard URL: $($wildcard), with cross-reference to $($subdomain)"
                                $MatchedWildcardURL.Note = "Wildcard URL, tested by manually defined URL $($subdomain) - $($MatchedWildcardURL.Note)"
                            }
                        }

                    } else {
                        # URL already exists in the results, skip
                        Write-Verbose "Info: Url $($subdomain) already exists in the results, skipping"
                        $MatchedWildcardURLs = ($script:Results | Where-Object { $PSItem.URL -eq $wildcard } )
                        ForEach($MatchedWildcardURL in $MatchedWildcardURLs) {
                            if($MatchedWildcardURL.Note -notlike "Wildcard URL,*"){
                                # Update the note to include the matching URL
                                $MatchedWildcardURL.Note = "Wildcard URL, tested by manually defined URL $($subdomain) - $($MatchedWildcardURL.Note)"
                                Write-Verbose "Updated Note of wildcard URL: $($wildcard), with cross-reference to $($subdomain)"
                            }
                        }
                    }
                }
            } # End of ForEach per $port

        } # End of ForEach $subdomain

    } # End of Process block

    end {
        # Progress bar is managed by the caller (unified with main endpoint testing)
    }
} # End of Test-ManuallyDefinedSubdomains function


# ////////////////////////////////////////////////////////////////////////////
# Function to parse endpoints from markdown lines
Function Get-EndpointsFromMarkdown {
    <#
    .SYNOPSIS
        Parse endpoints from markdown lines.
    .DESCRIPTION
        This function parses the provided markdown lines to extract endpoint URLs.
    #>

    [CmdletBinding()]
    param (
        [Parameter(Mandatory=$true)]
        [AllowEmptyCollection()]
        [string[]]$InputMarkdown,

        [Parameter(Mandatory=$true)]
        [ValidateNotNullOrEmpty()]
        [string]$Source
    )

    begin {
        # Write-Debug "Get-EndpointsFromMarkdown: Beginning endpoint parsing from markdown lines"
        # Initialize counter to store the number of parsed endpoints
        [int]$parsedEndpoints = 0
        
        # Validate input is not empty
        if (-not $InputMarkdown -or $InputMarkdown.Count -eq 0) {
            Write-Warning "No markdown content provided to parse in function: 'Get-EndpointsFromMarkdown'. Returning 0 endpoints."
            return $parsedEndpoints
        }
    }

    process {
        # Parse markdown table rows to extract endpoint URLs.
        # Expected format: | RowNum | ... | URL | Ports | Note | ArcGateway | ...
        # The regex matches lines starting with | followed by a number (the row ID column).
        # Columns are split on '|' and indexed: [3]=URL, [4]=Ports, [5]=Note, [6]=ArcGateway.
        ForEach ($line in $InputMarkdown) {
            if ($line -match "^\|\s*(\d+)\s*\|") {
                # $rowId = [int]($line -replace "^\|\s*(\d+)\s*\|.*", '$1')
                $columns = $line -split "\|"
                # Validate column count before accessing indices (need at least 7 columns: empty, rowId, desc, URL, Ports, Note, ArcGateway)
                if ($columns.Count -lt 7) {
                    Write-Warning "Skipping malformed markdown row (expected at least 7 columns, found $($columns.Count)): $line"
                    Continue
                }
                $url = $columns[3].Trim()
                # Sanitize URL: only allow FQDN characters (alphanumeric, hyphens, dots, wildcards, forward slashes, colons for port, underscores)
                # This prevents injection of unexpected values from crafted markdown files.
                if ($url -notmatch '^[a-zA-Z0-9\*\.\-\:/_]+$') {
                    Write-Warning "Skipping invalid URL from markdown: '$url' - contains unexpected characters"
                    Continue
                }
                $ports = $columns[4].Trim() -split ','
                $note = $columns[5].Trim()
                $ArcGatewayText = $columns[6].Trim()
                if(($ArcGatewayText.Length -ge 2) -and ($ArcGatewayText.Substring(0,2) -eq "No")){
                    # "No"
                    [bool]$ArcGateway = $false
                } elseif(($ArcGatewayText.Length -ge 3) -and ($ArcGatewayText.Substring(0,3) -eq "Yes")){
                    # "Yes"
                    [bool]$ArcGateway = $true
                } else {
                    # Unknown
                    Write-Warning "Unknown ArcGateway status for url: '$url' : $ArcGatewayText"
                    [bool]$ArcGateway = $false
                }
                # Use only the domain name from the URL, remove any absolute path from the endpoint when checking if it exists
                $urlcheck = (Get-DomainFromURL -url $url).Domain
                foreach ($port in $ports) {
                    $isWildcard = $url.Contains("*")
                    if($script:Results | Where-Object { ((Get-DomainFromURL -url $_.URL).Domain -eq $urlcheck) -and ($_.Port -eq $port) }) {
                        Write-Debug "$($Source): Skipping '$url' with port '$port', as it already exists in the results array."
                        # If the ArcGateway value is different, update it
                        $script:Results | Where-Object { ((Get-DomainFromURL -url $_.URL).Domain -like $urlcheck) -and ($_.Port -eq $port) } | ForEach-Object {
                            $_.ArcGateway = $ArcGateway
                        }
                        Continue
                    } else {
                        Write-Debug "$($Source): Adding '$urlcheck' with port '$port' to results array for processing."
                        # MachineName (v0.6.7): identifies which node generated this row when bundles
                        # from multiple nodes are merged via -Scope Cluster. This is the primary URL
                        # ingestion path so the bulk of result rows in any run originate here.
                        $script:Results.Add([PSCustomObject]@{
                            RowID = 0
                            MachineName = $env:COMPUTERNAME
                            URL = $url
                            Port = [int]$port
                            ArcGateway = $ArcGateway
                            IsWildcard = $isWildcard
                            Source = if ($isWildcard) { "$Source Wildcard" } else { "$Source" }
                            Note = $note
                            TCPStatus = ""
                            IPAddress = ""
                            Layer7Status = ""
                            Layer7Response = ""
                            Layer7ResponseTime = ""
                            CertificateIssuer = ""
                            CertificateSubject = ""
                            CertificateThumbprint = ""
                            IntermediateCertificateIssuer = ""
                            IntermediateCertificateSubject = ""
                            IntermediateCertificateThumbprint = ""
                            RootCertificateIssuer = ""
                            RootCertificateSubject = ""
                            RootCertificateThumbprint = ""
                        }) | Out-Null
                        # Increment the parsed endpoints counter
                        $parsedEndpoints++
                    }
                }
            }
            
        }
    } # End of process block

    end {
        # Write-Debug "Get-EndpointsFromMarkdown: Endpoint parsing completed"
        Return $parsedEndpoints

    }
} # End Function Get-EndpointsFromMarkdown

# SIG # Begin signature block
# MIInUAYJKoZIhvcNAQcCoIInQTCCJz0CAQExDzANBglghkgBZQMEAgEFADB5Bgor
# BgEEAYI3AgEEoGswaTA0BgorBgEEAYI3AgEeMCYCAwEAAAQQH8w7YFlLCE63JNLG
# KX7zUQIBAAIBAAIBAAIBAAIBADAxMA0GCWCGSAFlAwQCAQUABCBSJmyb4UnCL0Km
# 9PUd2YwWBNrhm/xTTP5564gjSh3SCqCCDMkwggYEMIID7KADAgECAhMzAAACHPrN
# 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
# CisGAQQBgjcCAQQwLwYJKoZIhvcNAQkEMSIEIDcXkV3hxA49lQuvujEMnOJkpktg
# ryBHGcT3BKjMz7aIMEIGCisGAQQBgjcCAQwxNDAyoBSAEgBNAGkAYwByAG8AcwBv
# AGYAdKEagBhodHRwOi8vd3d3Lm1pY3Jvc29mdC5jb20wDQYJKoZIhvcNAQEBBQAE
# ggEA0FIL2hlzSDz6A+WWOBJl3xuahEw9RJxZ453HIBnneetEwJdQlO7KME6q+Owq
# Ea+j9ya//QYoe72DZwP9WnW9zY8RWxhg9W7cH0V89VInMVAQtYqC0FBSXuvW0szu
# ou04L/CB/ZsyOVv8qTPwokeUQp9vKDq1qxLdA2Zpiuv5iSY+yH1tzcputmS+GKjP
# tyZZxBSOvKsE+dnW4y3BE+1IMYFr0YRM0h+YP7KIr7aTh7eaOPgnyzYNgTqtiykN
# XZYluHbb8pQpJhQqGRCSYUFuD+njqAI8AjlQN2EY28nNhEolFqfGVq0Ol+g+Qhwf
# hSzPOUzLbGZ52jrRX3WkYowWSaGCF60wghepBgorBgEEAYI3AwMBMYIXmTCCF5UG
# CSqGSIb3DQEHAqCCF4YwgheCAgEDMQ8wDQYJYIZIAWUDBAIBBQAwggFaBgsqhkiG
# 9w0BCRABBKCCAUkEggFFMIIBQQIBAQYKKwYBBAGEWQoDATAxMA0GCWCGSAFlAwQC
# AQUABCDIrFdrRsUOF7XrBbN9KwyFRPWZvmQV7gBBTTinunPQSAIGaoj0VJ6BGBMy
# MDI2MDkwMjE1MjczMi41NzJaMASAAgH0oIHZpIHWMIHTMQswCQYDVQQGEwJVUzET
# MBEGA1UECBMKV2FzaGluZ3RvbjEQMA4GA1UEBxMHUmVkbW9uZDEeMBwGA1UEChMV
# TWljcm9zb2Z0IENvcnBvcmF0aW9uMS0wKwYDVQQLEyRNaWNyb3NvZnQgSXJlbGFu
# ZCBPcGVyYXRpb25zIExpbWl0ZWQxJzAlBgNVBAsTHm5TaGllbGQgVFNTIEVTTjo0
# QzFBLTA1RTAtRDk0NzElMCMGA1UEAxMcTWljcm9zb2Z0IFRpbWUtU3RhbXAgU2Vy
# dmljZaCCEfswggcoMIIFEKADAgECAhMzAAACGCXZkgXi5+XkAAEAAAIYMA0GCSqG
# SIb3DQEBCwUAMHwxCzAJBgNVBAYTAlVTMRMwEQYDVQQIEwpXYXNoaW5ndG9uMRAw
# DgYDVQQHEwdSZWRtb25kMR4wHAYDVQQKExVNaWNyb3NvZnQgQ29ycG9yYXRpb24x
# JjAkBgNVBAMTHU1pY3Jvc29mdCBUaW1lLVN0YW1wIFBDQSAyMDEwMB4XDTI1MDgx
# NDE4NDgyNVoXDTI2MTExMzE4NDgyNVowgdMxCzAJBgNVBAYTAlVTMRMwEQYDVQQI
# EwpXYXNoaW5ndG9uMRAwDgYDVQQHEwdSZWRtb25kMR4wHAYDVQQKExVNaWNyb3Nv
# ZnQgQ29ycG9yYXRpb24xLTArBgNVBAsTJE1pY3Jvc29mdCBJcmVsYW5kIE9wZXJh
# dGlvbnMgTGltaXRlZDEnMCUGA1UECxMeblNoaWVsZCBUU1MgRVNOOjRDMUEtMDVF
# MC1EOTQ3MSUwIwYDVQQDExxNaWNyb3NvZnQgVGltZS1TdGFtcCBTZXJ2aWNlMIIC
# IjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAsdzo6uuQJqAfxLnvEBfIvj6k
# nK+p6bnMXEFZ/QjPOFywlcjDfzI8Dg1nzDlxm7/pqbvjWhyvazKmFyO6qbPwClfR
# nI57h5OCixgpOOCGJJQIZSTiMgui3B8DPiFtJPcfzRt3FsnxjLXwBIjGgnjGfmQl
# 7zejA1WoYL/qBmQhw/FDFTWebxfo4m0RCCOxf2qwj31aOjc2aYUePtLMXHsXKPFH
# 0tp5SKIF/9tJxRSg0NYEvQqVilje8aQkPd3qzAux2Mc5HMSK4NMTtVVCYAWDUZ4p
# +6iDI9t5BNCBIsf5ooFNUWtxCqnpFYiLYkHfFfxhVUBZ8LGGxYsA36snD65s2Hf4
# t86k0e8WelH/usfhYqOM3z2yaI8rg08631IkwqUzyQoEPqMsHgBem1xpmOGSIUnV
# vTsAv+lmECL2RqrcOZlZax8K0aiij8h6UkWBN2IA/ikackTSGVRBQmWWZuLFWV/T
# 4xuNzscC0X7xo4fetgpsqaEA0jY/QevkTvLv4OlNN9eOL8LNh7Vm0R65P7oabOQD
# qtUFAwCgjgPJ0iV/jQCaMAcO3SYpG5wSAYiJkk4XLjNSlNxU2Idjs1sORhl7s7LC
# 6hOb7bVAHVwON74GxfFNiEIA6BfudANjpQJ0nUc/ppEXpT4pgDBHsYtV8OyKSjKs
# IxOdFR7fIJIjDc8DvUkCAwEAAaOCAUkwggFFMB0GA1UdDgQWBBQkLqHEXDobY7dH
# uoQCBa4sX7aL0TAfBgNVHSMEGDAWgBSfpxVdAF5iXYP05dJlpxtTNRnpcjBfBgNV
# HR8EWDBWMFSgUqBQhk5odHRwOi8vd3d3Lm1pY3Jvc29mdC5jb20vcGtpb3BzL2Ny
# bC9NaWNyb3NvZnQlMjBUaW1lLVN0YW1wJTIwUENBJTIwMjAxMCgxKS5jcmwwbAYI
# KwYBBQUHAQEEYDBeMFwGCCsGAQUFBzAChlBodHRwOi8vd3d3Lm1pY3Jvc29mdC5j
# b20vcGtpb3BzL2NlcnRzL01pY3Jvc29mdCUyMFRpbWUtU3RhbXAlMjBQQ0ElMjAy
# MDEwKDEpLmNydDAMBgNVHRMBAf8EAjAAMBYGA1UdJQEB/wQMMAoGCCsGAQUFBwMI
# MA4GA1UdDwEB/wQEAwIHgDANBgkqhkiG9w0BAQsFAAOCAgEAnkjRhjwPgdoIpvt4
# YioT/j0LWuBxF3ARBKXDENggraKvC0oRPwbjAmsXnPEmtuo5MD8uJ9Xw9eYrxqqk
# K4DF9snZMrHMfooxCa++1irLz8YoozC4tci+a4N37Sbke1pt1xs9qZtvkPgZGWn5
# BcwVfmAwSZLHi2CuZ06Y0/X+t6fNBnrbMVovNaDX4WPdyI9GEzxfIggDsck2Ipo4
# VXL/Arcz7p2F7bEZGRuyxjgMC+woCkDJaH/yk/wcZpAsixe4POdN0DW6Zb35O3Dg
# 3+a6prANMc3WIdvfKDl75P0aqcQbQAR7b0f4gH4NMkUct0Wm4GN5KhsE1YK7V/wA
# qDKmK4jx3zLz3a8Hsxa9HB3GyitlmC5sDhOl4QTGN5kRi6oCoV4hK+kIFgnkWjHh
# SRNomz36QnbCSG/BHLEm2GRU9u3/I4zUd9E1AC97IJEGfwb+0NWb3QEcrkypdGdW
# wl0LEObhrQR9B1V7+edcyNmsX0p2BX0rFpd1PkXJSbxf8IcEiw/bkNgagZE+VlDt
# xXeruLdo5k3lGOv7rPYuOEaoZYxDvZtpHP9P36wmW4INjR6NInn2UM+krP/xeLnR
# bDBkm9RslnoDhVraliKDH62BxhcgL9tiRgOHlcI0wqvVWLdv8yW8rxkawOlhCRqT
# 3EKECW8ktUAPwNbBULkT+oWcvBcwggdxMIIFWaADAgECAhMzAAAAFcXna54Cm0mZ
# 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
# ZCBPcGVyYXRpb25zIExpbWl0ZWQxJzAlBgNVBAsTHm5TaGllbGQgVFNTIEVTTjo0
# QzFBLTA1RTAtRDk0NzElMCMGA1UEAxMcTWljcm9zb2Z0IFRpbWUtU3RhbXAgU2Vy
# dmljZaIjCgEBMAcGBSsOAwIaAxUAnWtGrXWiuNE8QrKfm4CtGr57z+mggYMwgYCk
# fjB8MQswCQYDVQQGEwJVUzETMBEGA1UECBMKV2FzaGluZ3RvbjEQMA4GA1UEBxMH
# UmVkbW9uZDEeMBwGA1UEChMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMSYwJAYDVQQD
# Ex1NaWNyb3NvZnQgVGltZS1TdGFtcCBQQ0EgMjAxMDANBgkqhkiG9w0BAQsFAAIF
# AO5CmugwIhgPMjAyNjA5MDIxMjU0MDBaGA8yMDI2MDkwMzEyNTQwMFowdDA6Bgor
# BgEEAYRZCgQBMSwwKjAKAgUA7kKa6AIBADAHAgEAAgIIkzAHAgEAAgIUGjAKAgUA
# 7kPsaAIBADA2BgorBgEEAYRZCgQCMSgwJjAMBgorBgEEAYRZCgMCoAowCAIBAAID
# B6EgoQowCAIBAAIDAYagMA0GCSqGSIb3DQEBCwUAA4IBAQAtKO8Nr/tEJkLIOTg8
# qYwHcEmx401uyqvOosg8mIOqDTfm5uzhc+2LU+JoVBJ1kguqQHAlNkoFPqQw+Zj8
# HIKNo7J7H7tDPRsbMMufG8GcCVaT83agUMI2g8O6ik2+uOUUSxlAg10Vhop0vf6E
# XwplZH44ga1tBm7g+16zizQ64YCQYN8jBGMAtRry3SQy1UOIMidSM1EuBQJ8kKrL
# bKMqPAisp2y7PATXNVO1zFY8y3l3MWia3aYCk1LCfUsj8tq/5uQDAzUMm6w75jtd
# QlEWhtqYKRGaLnEDo7QADRDiT11jOTSVldo6svSIJxtpXel/IupdNFlpv9Lcu2l6
# KE/TMYIEDTCCBAkCAQEwgZMwfDELMAkGA1UEBhMCVVMxEzARBgNVBAgTCldhc2hp
# bmd0b24xEDAOBgNVBAcTB1JlZG1vbmQxHjAcBgNVBAoTFU1pY3Jvc29mdCBDb3Jw
# b3JhdGlvbjEmMCQGA1UEAxMdTWljcm9zb2Z0IFRpbWUtU3RhbXAgUENBIDIwMTAC
# EzMAAAIYJdmSBeLn5eQAAQAAAhgwDQYJYIZIAWUDBAIBBQCgggFKMBoGCSqGSIb3
# DQEJAzENBgsqhkiG9w0BCRABBDAvBgkqhkiG9w0BCQQxIgQg1QRgkZn8x1eTGSRf
# ccRBBnoPbNL+SQ5WYp/nHWLHYlowgfoGCyqGSIb3DQEJEAIvMYHqMIHnMIHkMIG9
# BCCZE9yJuOTItIwWaES6lzGKK1XcSoz1ynRzaOVzx9eFajCBmDCBgKR+MHwxCzAJ
# BgNVBAYTAlVTMRMwEQYDVQQIEwpXYXNoaW5ndG9uMRAwDgYDVQQHEwdSZWRtb25k
# MR4wHAYDVQQKExVNaWNyb3NvZnQgQ29ycG9yYXRpb24xJjAkBgNVBAMTHU1pY3Jv
# c29mdCBUaW1lLVN0YW1wIFBDQSAyMDEwAhMzAAACGCXZkgXi5+XkAAEAAAIYMCIE
# IJW5KRYAKY8qTBA2nV0v2HB6E6/mhCPOjaUI7gjQArffMA0GCSqGSIb3DQEBCwUA
# BIICAHufhp/f7RF7GnmizSvBBi49IY9MOVHKlXUon7USQ7b82k/hjuPoxoOEMneH
# rPAoUH3QXQphzq3Yp70UThjGpSBbibRsR43zInuR2oxjkp0Bf5j9k6soSNAALkGm
# tUP2uPbtdIRqrjEcs1HFQ4IZVSybymHS1gs3irB93Y8Mpy67XWch/5QwrzguRAJj
# MwY/pFpoL4W3TkbTXFqRqbZrSX4FVs0Ayp2UUaUc0oFFtpNMT7ZVXLwnV67vaPi8
# zeFKYbbSfoqTdqyPaXeBv9GmuZCiSsgxnAs7QDpJw/bqe2v5Tjde14tJc9VfE9Ks
# ezHh+xI3pIoDBOjHGVqHUsN1wETz7YAUmaOdBqeUEFc5W/Hn8mhB0h4cu5rA+zUw
# x4gweqlSOwZrJuSI18nFx0//hg5kzt//U4cU88dnDTn7Dx1mDzJQg9lMEm9QyWzI
# 5QzW6sxyy5WKubXXUHCHjLWyJRdGeecfSKSShag6d4CHczlpFwj7S3nesTBHU2RP
# FHQhS+vx2s6vlodOS4JN0JZVbMFnBGPW1figyOgxG4JUAmOcv1NoPEOaEmbYUFN3
# xWUFc0vmFcaZTb+89OJI0T8r6KHOx6/xcaUfnKgMVcXULV2REVLnLCWU/ZYTZoL/
# 0cm6yyBrXFa/qBTxMkTBr3s5TvmpAsMBI4bSENyDLgusyciu
# SIG # End signature block