modules/AzStack.Insights/AzStack.Insights.Helper.psm1

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


# Analyzers and rules import this module standalone (Import-Module ..\AzStack.Insights.Helper.psm1),
# so it cannot rely on a parent module having already brought Write-AzsSupportLog into scope.
Import-Module -Name $PSScriptRoot\..\AzStack.Observability\AzStack.Observability.psm1

function Get-InsightModulePaths {
    <#
    .SYNOPSIS
        Returns the resolved paths for the module root and packages directory.
    .DESCRIPTION
        Derives paths from the module file's own $PSScriptRoot, which PowerShell binds
        at module load time and cannot be overwritten by script code. Prefer this over
        reading from a global variable, which can be overwritten at runtime.
    .OUTPUTS
        PSCustomObject with ModuleRoot and PackagesPath properties.
    #>

    [CmdletBinding()]
    [OutputType([PSCustomObject])]
    param()

    # $PSScriptRoot here is always the AzStack.Insights module directory
    # (modules\AzStack.Insights\), so two levels up reaches the version root.
    $moduleRoot   = (Get-Item -Path (Join-Path -Path $PSScriptRoot -ChildPath '..\..') -ErrorAction Stop).FullName
    $packagesPath = Join-Path -Path $moduleRoot -ChildPath 'packages'

    return [PSCustomObject]@{
        ModuleRoot   = $moduleRoot
        PackagesPath = $packagesPath
    }
}

function Get-ParsedFileVersion {
    <#
    .SYNOPSIS
        Returns a parsed System.Version value from a file's version metadata.
    .DESCRIPTION
        Reads FileVersion first, then ProductVersion, and extracts a numeric
        version token (for example, 1.2.3.4). Returns $null when no version can
        be parsed.
    .PARAMETER FilePath
        Path to the file whose version should be parsed.
    .OUTPUTS
        System.Version or $null.
    #>

    [CmdletBinding()]
    [OutputType([System.Version])]
    param(
        [Parameter(Mandatory = $true)]
        [string]$FilePath
    )

    $versionInfo = (Get-Item -Path $FilePath -ErrorAction Stop).VersionInfo
    $rawVersion = $versionInfo.FileVersion

    if ([string]::IsNullOrWhiteSpace($rawVersion)) {
        $rawVersion = $versionInfo.ProductVersion
    }

    if ([string]::IsNullOrWhiteSpace($rawVersion)) {
        return $null
    }

    $versionMatch = [System.Text.RegularExpressions.Regex]::Match($rawVersion, '\d+(\.\d+){1,3}')
    if (-not $versionMatch.Success) {
        return $null
    }

    try {
        return [System.Version]$versionMatch.Value
    }
    catch {
        return $null
    }
}

function Test-isFailoverCluster {
    [CmdletBinding()]
    param()
    # Detecting Failover Cluster based on the presence of the Get-Cluster cmdlet
    return ((Get-Command "Get-Cluster" -ErrorAction Ignore) -and (Get-Cluster -ErrorAction Ignore))
}

function Get-NodeFqdn {
    <#
    .SYNOPSIS
        Resolves the local node FQDN with resilient fallback behavior.
    .DESCRIPTION
        Uses Win32_ComputerSystem identity first, then DNS host lookup,
        then USERDNSDOMAIN, and finally falls back to computer name.
    #>

    [CmdletBinding()]
    param()

    $computerSystem = Get-CimInstance -ClassName Win32_ComputerSystem -ErrorAction SilentlyContinue
    if ($null -ne $computerSystem -and
        $computerSystem.PartOfDomain -and
        -not [string]::IsNullOrWhiteSpace($computerSystem.DNSHostName) -and
        -not [string]::IsNullOrWhiteSpace($computerSystem.Domain) -and
        $computerSystem.Domain -ne 'WORKGROUP') {
        return "$($computerSystem.DNSHostName).$($computerSystem.Domain)"
    }

    try {
        $hostName = [System.Net.Dns]::GetHostEntry($Env:ComputerName).HostName
        if (-not [string]::IsNullOrWhiteSpace($hostName) -and $hostName.Contains('.')) {
            return $hostName
        }
    }
    catch {
    }

    if (-not [string]::IsNullOrWhiteSpace($Env:USERDNSDOMAIN)) {
        return "$($Env:ComputerName).$($Env:USERDNSDOMAIN)"
    }

    return $Env:ComputerName
}

function Get-FirewallEndpoints {
    <#
    .SYNOPSIS
        Retrieves the required firewall endpoints for a specified Azure region.
 
    .DESCRIPTION
        This function returns a collection of firewall endpoints that need to be opened
        for Azure Local operations in the specified region. This comes from https://learn.microsoft.com/en-us/azure/azure-local/concepts/system-requirements-23h2?view=azloc-2601&tabs=azure-public
 
    .OUTPUTS
        Collection of PSObjects representing firewall endpoints.
 
    .EXAMPLE
        $endpoints = Get-FirewallEndpoints -Region 'eastus'
        Retrieves all firewall endpoints for the 'eastus' region and stores them in the $endpoints variable.
    #>


    [CmdletBinding()]
    param (
        [Parameter(Mandatory = $true)]
        [string]$Region
    )

    $endpointRootDir = Get-Item -Path "$PSScriptRoot\config\firewall_endpoints"
    try {
        switch ($Region) {
            'australiaeast' {
                $configData = Import-PowerShellDataFile -Path (Join-Path -Path $endpointRootDir.FullName -ChildPath "AustraliaEastEndpoints.psd1")
            }
            'canadacentral' {
                $configData = Import-PowerShellDataFile -Path (Join-Path -Path $endpointRootDir.FullName -ChildPath "CanadaCentralEndpoints.psd1")
            }
            'eastus' {
                $configData = Import-PowerShellDataFile -Path (Join-Path -Path $endpointRootDir.FullName -ChildPath "EastUSEndpoints.psd1")
            }
            'centralindia' {
                $configData = Import-PowerShellDataFile -Path (Join-Path -Path $endpointRootDir.FullName -ChildPath "IndiaCentralEndpoints.psd1")
            }
            'japaneast' {
                $configData = Import-PowerShellDataFile -Path (Join-Path -Path $endpointRootDir.FullName -ChildPath "JapanEastEndpoints.psd1")
            }
            'southcentralus' {
                $configData = Import-PowerShellDataFile -Path (Join-Path -Path $endpointRootDir.FullName -ChildPath "SouthCentralUSEndpoints.psd1")
            }
            'southeastasia' {
                $configData = Import-PowerShellDataFile -Path (Join-Path -Path $endpointRootDir.FullName -ChildPath "SoutheastAsiaEndpoints.psd1")
            }
            'westeurope' {
                $configData = Import-PowerShellDataFile -Path (Join-Path -Path $endpointRootDir.FullName -ChildPath "WestEuropeEndpoints.psd1")
            }
            'usgovvirginia' {
                $configData = Import-PowerShellDataFile -Path (Join-Path -Path $endpointRootDir.FullName -ChildPath "USGovVirginiaEndpoints.psd1")
            }
            default {
                return $null
            }
        }

        return $configData.Endpoints
    }
    catch {
        $message = "Failed to load firewall endpoints for region '$Region'. $($_.Exception.Message)"
        if (Get-Command -Name 'Write-AzsSupportLog' -ErrorAction SilentlyContinue) {
            try {
                Write-AzsSupportLog -Level 'Error' -Message $message
            }
            catch { }
        }
        else {
            Write-Verbose -Message $message
        }
        throw
    }
}

function Invoke-AzsInsightJobWithTimeout {
    <#
    .SYNOPSIS
        Runs a script block in a process-isolated job with a wall-clock timeout.
    .DESCRIPTION
        Uses a background process so a blocking filesystem or RPC operation cannot
        stall the Insights runspace. Job output is deserialized when it crosses the
        process boundary; callers should consume scalar properties rather than methods.
    .PARAMETER ScriptBlock
        Operation to run in the isolated process.
    .PARAMETER ArgumentList
        Arguments passed to the isolated script block.
    .PARAMETER TimeoutSeconds
        Maximum number of seconds to wait for the operation.
    .PARAMETER Activity
        Operation label used in timeout and failure messages.
    #>

    [CmdletBinding()]
    param (
        [Parameter(Mandatory = $true)]
        [scriptblock]$ScriptBlock,

        [Parameter(Mandatory = $false)]
        [object[]]$ArgumentList = @(),

        [Parameter(Mandatory = $true)]
        [ValidateRange(1, 3600)]
        [int]$TimeoutSeconds,

        [Parameter(Mandatory = $false)]
        [string]$Activity = 'Insight operation'
    )

    $job = $null
    $stopwatch = [System.Diagnostics.Stopwatch]::StartNew()
    try {
        $job = Start-Job -ScriptBlock $ScriptBlock -ArgumentList $ArgumentList -ErrorAction Stop
        $remainingSeconds = [int][Math]::Floor($TimeoutSeconds - $stopwatch.Elapsed.TotalSeconds)
        if ($remainingSeconds -lt 1) {
            throw (New-Object -TypeName System.TimeoutException -ArgumentList "$Activity exceeded the $TimeoutSeconds second timeout while starting the isolated process.")
        }

        $completedJob = Wait-Job -Job $job -Timeout $remainingSeconds -ErrorAction Stop

        if ($null -eq $completedJob) {
            throw (New-Object -TypeName System.TimeoutException -ArgumentList "$Activity exceeded the $TimeoutSeconds second timeout.")
        }

        if ($job.State -ne 'Completed') {
            $reason = $job.JobStateInfo.Reason
            if ($null -eq $reason) {
                $reason = $job.ChildJobs |
                    ForEach-Object { $_.JobStateInfo.Reason } |
                    Where-Object { $null -ne $_ } |
                    Select-Object -First 1
            }

            $reasonMessage = if ($null -ne $reason) {
                $reason.Message
            }
            else {
                "Job entered state '$($job.State)'."
            }

            throw "$Activity failed. $reasonMessage"
        }

        return Receive-Job -Job $job -ErrorAction Stop
    }
    finally {
        $stopwatch.Stop()
        if ($null -ne $job) {
            try {
                Remove-Job -Job $job -Force -ErrorAction Stop
            }
            catch {
                Write-Warning -Message "Failed to remove isolated job '$($job.Name)': $($_.Exception.Message)"
            }
        }
    }
}

function Test-AzsInsightPath {
    <#
    .SYNOPSIS
        Tests a path in a process-isolated job with a timeout.
    .PARAMETER Path
        Path to test.
    .PARAMETER TimeoutSeconds
        Maximum number of seconds to wait for Test-Path.
    .PARAMETER CallerName
        Label used in timeout and failure messages.
    #>

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

        [Parameter(Mandatory = $false)]
        [ValidateRange(1, 300)]
        [int]$TimeoutSeconds = 15,

        [Parameter(Mandatory = $false)]
        [string]$CallerName = 'MocArb'
    )

    $pathExists = Invoke-AzsInsightJobWithTimeout -ScriptBlock {
        param($PathToTest)
        Test-Path -LiteralPath $PathToTest -ErrorAction Stop
    } -ArgumentList @($Path) -TimeoutSeconds $TimeoutSeconds -Activity "[$env:COMPUTERNAME][$CallerName] Test-Path '$Path'"

    return [bool]$pathExists
}

function Get-MocConfigCached {
    <#
    .SYNOPSIS
        Returns MOC configuration, using the global cache when available.
    .DESCRIPTION
        Reads from $Global:MocArbCachedMocConfig when the variable exists (set
        by the MocArb analyzer). A cached $null is a negative cache entry that
        prevents each rule from repeating a failed operation. On cache miss,
        calls Get-MocConfig in a process-isolated job with a total retry budget.
    .PARAMETER CallerName
        Label used in diagnostic messages.
    .PARAMETER TimeoutSeconds
        Total wall-clock budget shared by all attempts and backoff delays.
    .PARAMETER MaximumAttempts
        Maximum attempts for quick transient failures. A blocking call consumes
        the remaining timeout budget and is not retried.
    .OUTPUTS
        The MOC configuration object, or $null if all attempts fail.
    #>

    [CmdletBinding()]
    param (
        [Parameter(Mandatory = $false)]
        [string]$CallerName = 'MocArb',

        [Parameter(Mandatory = $false)]
        [ValidateRange(1, 600)]
        [int]$TimeoutSeconds = 120,

        [Parameter(Mandatory = $false)]
        [ValidateRange(1, 10)]
        [int]$MaximumAttempts = 5
    )

    $cachedConfig = Get-Variable -Name 'MocArbCachedMocConfig' -Scope Global -ErrorAction Ignore
    if ($null -ne $cachedConfig) {
        return $cachedConfig.Value
    }

    Write-Verbose -Message "[$env:COMPUTERNAME][$CallerName] Cache miss, calling Get-MocConfig in an isolated process"

    try {
        $getMocConfigCommand = Get-Command -Name 'Get-MocConfig' -ErrorAction Stop
    }
    catch {
        Write-AzsSupportLog -Level 'Warning' -Message "[$env:COMPUTERNAME][$CallerName] Get-MocConfig is unavailable: $($_.Exception.Message)"
        return $null
    }

    $modulePath = if ($null -ne $getMocConfigCommand.Module) {
        $getMocConfigCommand.Module.Path
    }
    else {
        $null
    }
    $moduleName = $getMocConfigCommand.ModuleName
    $stopwatch = [System.Diagnostics.Stopwatch]::StartNew()
    $mocConfig = $null
    $lastFailure = $null
    $attemptsCompleted = 0

    for ($attempt = 1; $attempt -le $MaximumAttempts; $attempt++) {
        $remainingSeconds = [int][Math]::Floor($TimeoutSeconds - $stopwatch.Elapsed.TotalSeconds)
        if ($remainingSeconds -lt 1) {
            break
        }

        $attemptsCompleted = $attempt
        try {
            $mocConfig = Invoke-AzsInsightJobWithTimeout -ScriptBlock {
                param($ModulePath, $ModuleName)

                if (-not [string]::IsNullOrWhiteSpace($ModulePath)) {
                    Import-Module -Name $ModulePath -ErrorAction Stop
                }
                elseif (-not [string]::IsNullOrWhiteSpace($ModuleName)) {
                    Import-Module -Name $ModuleName -ErrorAction Stop
                }

                Get-MocConfig -ErrorAction Stop
            } -ArgumentList @($modulePath, $moduleName) -TimeoutSeconds $remainingSeconds -Activity "[$env:COMPUTERNAME][$CallerName] Get-MocConfig attempt $attempt"
        }
        catch [System.TimeoutException] {
            $lastFailure = $_.Exception.Message
            Write-AzsSupportLog -Level 'Warning' -Message $lastFailure
            break
        }
        catch {
            $lastFailure = $_.Exception.Message
            Write-AzsSupportLog -Level 'Warning' -Message "[$env:COMPUTERNAME][$CallerName] Get-MocConfig attempt $attempt failed: $lastFailure"
        }

        if ($null -ne $mocConfig) {
            $stopwatch.Stop()
            Write-Verbose -Message "[$env:COMPUTERNAME][$CallerName] Get-MocConfig succeeded on attempt $attempt after $([Math]::Round($stopwatch.Elapsed.TotalSeconds, 1)) seconds"
            return $mocConfig
        }

        if ([string]::IsNullOrWhiteSpace($lastFailure)) {
            $lastFailure = 'Get-MocConfig returned no configuration.'
        }

        if ($attempt -lt $MaximumAttempts) {
            $backoff = 2 * $attempt
            $remainingAfterAttempt = [int][Math]::Floor($TimeoutSeconds - $stopwatch.Elapsed.TotalSeconds)
            $sleepSeconds = [Math]::Min($backoff, $remainingAfterAttempt)
            if ($sleepSeconds -lt 1) {
                break
            }

            Write-AzsSupportLog -Level 'Warning' -Message "[$env:COMPUTERNAME][$CallerName] Get-MocConfig attempt $attempt returned no configuration, retrying in ${sleepSeconds}s"
            Start-Sleep -Seconds $sleepSeconds
        }
    }

    $stopwatch.Stop()
    if ([string]::IsNullOrWhiteSpace($lastFailure)) {
        $lastFailure = "The $TimeoutSeconds second retry budget was exhausted."
    }

    Write-AzsSupportLog -Level 'Warning' -Message "[$env:COMPUTERNAME][$CallerName] Get-MocConfig failed after $attemptsCompleted attempt(s) and $([Math]::Round($stopwatch.Elapsed.TotalSeconds, 1)) seconds. Last failure: $lastFailure"
    return $null
}

function Get-ArbControlPlaneVMs {
    <#
    .SYNOPSIS
        Returns local Hyper-V VM objects for the ARB control-plane VM.
    .DESCRIPTION
        Calls Get-VM -Name '*control-plan*' on the local node, then disambiguates
        when multiple VMs match (e.g., AKS workloads also create control-plane VMs).
        Uses three methods to identify the real ARB VM:
        1. Cluster group: ARB VM belongs to the '<clusterName>-arcbridge' cluster group.
           The cluster group name reliably contains 'arcbridge' even though the VM name does not.
        2. MOC kubeconfig IP: Matches the API server IP from the ARB kubeconfig to VM network adapters.
           Kubeconfigs under paths containing 'arcbridge' are preferred over AKS kubeconfigs.
        3. ARB hex-ID name pattern: matches '<hex>-control-plan[hex/random]-<hex>' VM names.
           Two real-world variants are seen in the field — '<hex>-control-plane-0-<hex>' (older
           clusters) and '<hex>-control-plan<5-char-suffix>-<hex>' (newer clusters, e.g. LH-21).
           Both are covered. AKS control-plane VMs use non-hex prefixes so this still excludes them.
        Falls back to returning all matches if disambiguation fails.
    .OUTPUTS
        Array of Hyper-V VM objects, or empty array if none found on this node.
    #>

    [CmdletBinding()]
    param ()

    $vms = @(Get-VM -Name '*control-plan*' -ErrorAction Ignore)

    if ($vms.Count -le 1) {
        return $vms
    }

    # Multiple VMs matched — AKS workloads can create additional control-plane VMs.
    Write-Verbose "[$env:COMPUTERNAME][Get-ArbControlPlaneVMs] Found $($vms.Count) VMs matching '*control-plan*', disambiguating ARB VM..."

    # Method 1: Cross-reference with the ARB failover cluster group (<clusterName>-arcbridge)
    # The cluster group name reliably contains 'arcbridge'; the VM name itself does not.
    try {
        $clusterName = Get-AzsSupportEceManagementClusterName -ErrorAction Stop
        $arbGroupName = "$clusterName-arcbridge"
        $arbGroup = Get-ClusterGroup -Name $arbGroupName -ErrorAction Ignore
        if ($arbGroup) {
            $arbGroupResources = @(Get-ClusterResource -InputObject $arbGroup -ErrorAction Ignore |
                Where-Object { $_.ResourceType -eq 'Virtual Machine' })
            if ($arbGroupResources.Count -gt 0) {
                $arbResourceNames = @($arbGroupResources | ForEach-Object { $_.Name })
                # Cluster resource names for VMs are typically 'Virtual Machine <VMName>'
                $matched = @($vms | Where-Object {
                    $vmName = $_.Name
                    $arbResourceNames -contains $vmName -or $arbResourceNames -contains "Virtual Machine $vmName"
                })
                if ($matched.Count -gt 0) {
                    Write-Verbose "[$env:COMPUTERNAME][Get-ArbControlPlaneVMs] Narrowed to $($matched.Count) VM(s) by cluster group '$arbGroupName'"
                    return $matched
                }
            }
        }
    }
    catch {
        Write-Warning "[$env:COMPUTERNAME][Get-ArbControlPlaneVMs] Cluster group cross-reference skipped: $_"
    }

    # Method 2: Match VM network adapter IPs against the ARB kubeconfig API server IP
    # Prefer kubeconfigs whose path contains 'arcbridge' to avoid picking up AKS kubeconfigs.
    $arbIp = $null
    # The kubeconfig IP is only a disambiguator here, so a discovery timeout must not fail the
    # VM lookup -- fall through to the name-pattern match in Method 3 instead.
    $kubeconfigs = @()
    try {
        $kubeconfigs = @(Get-ArbKubeconfigs -CallerName 'Get-ArbControlPlaneVMs')
    }
    catch [System.TimeoutException] {
        Write-Warning "[$env:COMPUTERNAME][Get-ArbControlPlaneVMs] Kubeconfig disambiguation skipped: $($_.Exception.Message)"
    }
    if ($kubeconfigs.Count -gt 0) {
        # Prefer kubeconfigs under arcbridge paths
        $arcbridgeKubeconfigs = @($kubeconfigs | Where-Object { $_.IsArcBridge })
        $orderedKubeconfigs = if ($arcbridgeKubeconfigs.Count -gt 0) { $arcbridgeKubeconfigs } else { $kubeconfigs }
        foreach ($kc in $orderedKubeconfigs) {
            if ($kc.ServerIp) {
                $arbIp = $kc.ServerIp
                break
            }
        }
    }

    if ($arbIp) {
        $matched = @($vms | Where-Object {
            $vmIps = @(
                $_ | Get-VMNetworkAdapter -ErrorAction Ignore |
                ForEach-Object { $_.IPAddresses } |
                Where-Object { $_ -match '^\d+\.' }
            )
            $vmIps -contains $arbIp
        })
        if ($matched.Count -gt 0) {
            Write-Verbose "[$env:COMPUTERNAME][Get-ArbControlPlaneVMs] Narrowed to $($matched.Count) VM(s) by MOC kubeconfig IP ($arbIp)"
            return $matched
        }
    }

    # ------------------------------------------------------------------------------------------
    # Method 3: ARB-vs-AKS disambiguation by VM name regex — READ BEFORE MODIFYING
    # ------------------------------------------------------------------------------------------
    # The regex below matches ONLY the Arc Resource Bridge (ARB) control-plane VM.
    # It DELIBERATELY EXCLUDES AKS workload-cluster control-plane VMs (which can co-exist).
    #
    # Discriminator: the leading prefix before '-control-plan' must be ALL hex chars [0-9a-f].
    # ARB VM prefix is a derived Azure resource identifier — all hex, typically ~45 chars:
    # caa5017c65b71e53681da3c40ae39f0807ad6c3f216b0-control-planzzq2l-92b1f66d (newer)
    # 013bc87fb2959d61230552d6d19dc6c9e7483f83cd66-control-plane-0-3e9345d2 (older)
    # AKS workload-cluster control-plane VM prefix contains non-hex letters (g–z), e.g.
    # 0002akscls001-control-plane-... ('k','s','c','l' are not hex → no match)
    #
    # If you are looking for AKS control-plane VMs you need a DIFFERENT regex/lookup —
    # this one will silently skip them.
    # ------------------------------------------------------------------------------------------
    $matched = @($vms | Where-Object { $_.Name -imatch '^[0-9a-f]+-control-plan[a-z0-9-]+-[0-9a-f]+$' })
    if ($matched.Count -gt 0) {
        Write-Verbose "[$env:COMPUTERNAME][Get-ArbControlPlaneVMs] Narrowed to $($matched.Count) VM(s) by ARB hex-ID name pattern"
        return $matched
    }

    Write-Warning "[$env:COMPUTERNAME][Get-ArbControlPlaneVMs] Could not disambiguate $($vms.Count) control-plane VMs. Returning all matches."
    return $vms
}

function Get-ArbControlPlaneTargets {
    <#
    .SYNOPSIS
        Discovers ARB control-plane VM IPv4 addresses.
    .DESCRIPTION
        Uses two methods to find ARB control-plane VM IPs:
        1. Local Hyper-V (Get-VM) for VMs matching '*control-plan*' on this node.
        2. Parses kubeconfig files in the MOC working directory and ClusterStorage
           for the Kubernetes API server IP (port 6443).
        Method 2 uses Get-MocConfigCached to locate the MOC working directory.
    .PARAMETER CallerName
        Label used in diagnostic messages.
    .OUTPUTS
        Array of unique IPv4 address strings, or empty array if none found.
    .NOTES
        Throws System.TimeoutException when Method 2 kubeconfig discovery does not
        complete. Callers must let that surface as UNKNOWN rather than treating it
        as an empty target list, because "discovery timed out" and "no ARB targets
        exist" require different operator action.
    #>

    [CmdletBinding()]
    param (
        [Parameter(Mandatory = $false)]
        [string]$CallerName = 'MocArb'
    )

    $arbTargets = @()

    # Method 1: Local Get-VM for ARB control-plane VMs on this node
    $arbVMs = Get-ArbControlPlaneVMs
    if ($arbVMs) {
        $arbTargets = @(
            $arbVMs |
            Get-VMNetworkAdapter -ErrorAction Ignore |
            ForEach-Object { $_.IPAddresses } |
            Where-Object { $_ } |
            Where-Object { $_ -match '^\d+\.' }
        )
    }

    # Method 2: Parse kubeconfig files for API server IP
    if ($arbTargets.Count -eq 0) {
        $kubeconfigs = @(Get-ArbKubeconfigs -CallerName $CallerName)

        # On clusters with AKS workloads, multiple kubeconfigs exist (ARB + AKS target clusters).
        # Prefer kubeconfigs whose path contains 'arcbridge' to avoid testing AKS endpoints.
        $arcbridgeKubeconfigs = @($kubeconfigs | Where-Object { $_.IsArcBridge })
        $orderedKubeconfigs = if ($arcbridgeKubeconfigs.Count -gt 0) { $arcbridgeKubeconfigs } else { $kubeconfigs }

        foreach ($kc in $orderedKubeconfigs) {
            if ($kc.ServerIp) {
                $arbTargets += $kc.ServerIp
            }
        }
        $arbTargets = @($arbTargets | Select-Object -Unique)
    }

    return $arbTargets
}

function Get-ArbKubeconfigs {
    <#
    .SYNOPSIS
        Finds ARB kubeconfig files on the local node.
    .DESCRIPTION
        Searches the MOC working directory and ClusterStorage for kubeconfig files
        that contain a Kubernetes API server endpoint (port 6443). Filesystem
        enumeration and content reads run inside a process-isolated timeout. When
        the MocArb analyzer initializes its global cache envelope, success or an
        empty result is cached for the remainder of that analyzer run.
 
        A discovery timeout that yields no endpoints is NOT reported as an empty
        result: an empty array means "no ARB kubeconfig exists on this node",
        whereas a timeout means "discovery did not finish, so targets are
        unknown". Conflating the two hides the degraded-ClusterStorage failure
        mode this timeout exists to surface. The timeout is recorded in the cache
        envelope and re-thrown on subsequent calls, so later rules inherit the
        distinction without repeating the scan.
    .PARAMETER CallerName
        Label used in diagnostic messages.
    .PARAMETER TimeoutSeconds
        Total wall-clock budget shared by all search paths.
    .OUTPUTS
        Objects with FullName, ServerIp, and IsArcBridge properties.
    #>

    [CmdletBinding()]
    param (
        [Parameter(Mandatory = $false)]
        [string]$CallerName = 'MocArb',

        [Parameter(Mandatory = $false)]
        [ValidateRange(1, 300)]
        [int]$TimeoutSeconds = 30
    )

    $timeoutMessage = "[$env:COMPUTERNAME][$CallerName] ARB kubeconfig discovery did not complete within the allowed time; control-plane targets could not be determined."

    $cacheVariable = Get-Variable -Name 'MocArbCachedKubeconfigEndpoints' -Scope Global -ErrorAction Ignore
    if ($null -ne $cacheVariable -and $null -ne $cacheVariable.Value -and $cacheVariable.Value.IsInitialized) {
        # Replay a cached timeout as a timeout rather than as an empty result, so every rule in
        # the analyzer run reports "targets unknown" instead of "no ARB found".
        if ($cacheVariable.Value.TimedOut -and @($cacheVariable.Value.Items).Count -eq 0) {
            throw (New-Object -TypeName System.TimeoutException -ArgumentList $timeoutMessage)
        }
        return @($cacheVariable.Value.Items)
    }

    $searchPaths = @()
    $mocCfg = Get-MocConfigCached -CallerName $CallerName
    if ($mocCfg -and $mocCfg.WorkingDir) {
        $searchPaths += $mocCfg.WorkingDir
    }
    $searchPaths += 'C:\ClusterStorage'
    $searchPaths = @($searchPaths | Where-Object { $_ } | Select-Object -Unique)

    $found = @()
    $discoveryTimedOut = $false
    $stopwatch = [System.Diagnostics.Stopwatch]::StartNew()
    foreach ($searchPath in $searchPaths) {
        $remainingSeconds = [int][Math]::Floor($TimeoutSeconds - $stopwatch.Elapsed.TotalSeconds)
        if ($remainingSeconds -lt 1) {
            # The budget was consumed by an earlier search path that found nothing; the
            # remaining paths are unsearched, so this is a timeout and not an empty result.
            $discoveryTimedOut = $true
            break
        }

        try {
            $found += @(Invoke-AzsInsightJobWithTimeout -ScriptBlock {
                param($SearchPath)

                if (-not (Test-Path -LiteralPath $SearchPath -ErrorAction Stop)) {
                    return
                }

                $enumerationErrors = @()
                $kubeconfigs = @(Get-ChildItem -LiteralPath $SearchPath -Filter 'kubeconfig' -Recurse -Depth 10 -ErrorAction SilentlyContinue -ErrorVariable enumerationErrors)
                foreach ($enumerationError in $enumerationErrors) {
                    Write-Warning -Message "Kubeconfig enumeration under '$SearchPath' was partially unavailable: $($enumerationError.Exception.Message)"
                }

                foreach ($kubeconfig in $kubeconfigs) {
                    try {
                        $content = Get-Content -LiteralPath $kubeconfig.FullName -Raw -ErrorAction Stop
                    }
                    catch {
                        Write-Warning -Message "Unable to read kubeconfig '$($kubeconfig.FullName)': $($_.Exception.Message)"
                        continue
                    }

                    if ($content -match 'server:\s*https?://([0-9]+\.[0-9]+\.[0-9]+\.[0-9]+):6443') {
                        [PSCustomObject]@{
                            FullName    = $kubeconfig.FullName
                            ServerIp    = $matches[1]
                            IsArcBridge = $kubeconfig.FullName -like '*arcbridge*'
                        }
                    }
                }
            } -ArgumentList @($searchPath) -TimeoutSeconds $remainingSeconds -Activity "[$env:COMPUTERNAME][$CallerName] kubeconfig search in '$searchPath'")
        }
        catch [System.TimeoutException] {
            Write-AzsSupportLog -Level 'Warning' -Message $_.Exception.Message
            $discoveryTimedOut = $true
            break
        }
        catch {
            Write-AzsSupportLog -Level 'Warning' -Message "[$env:COMPUTERNAME][$CallerName] Kubeconfig search failed in '$searchPath': $($_.Exception.Message)"
            continue
        }

        if ($found.Count -gt 0) { break }
    }

    $stopwatch.Stop()
    $found = @($found | Where-Object { $null -ne $_ })
    if ($null -ne $cacheVariable) {
        # Cache before throwing so the remaining rules inherit the timeout without re-scanning.
        $Global:MocArbCachedKubeconfigEndpoints = [PSCustomObject]@{
            IsInitialized = $true
            Items         = $found
            TimedOut      = $discoveryTimedOut
        }
    }

    # A partial result is still usable, so only a timeout that produced nothing is fatal.
    if ($discoveryTimedOut -and $found.Count -eq 0) {
        throw (New-Object -TypeName System.TimeoutException -ArgumentList $timeoutMessage)
    }

    return $found
}

function Get-EffectiveVlanId {
    <#
    .SYNOPSIS
        Resolves the effective VLAN ID applied to a Hyper-V virtual network adapter.
    .DESCRIPTION
        A Hyper-V vNIC can carry its VLAN in one of two places depending on how the
        port is programmed:
          - Access mode: the VLAN is in 'AccessVlanId' (Get-VMNetworkAdapterVlan) and
            the isolation 'DefaultIsolationID' reads 0.
          - VLAN isolation mode: the VLAN is in 'DefaultIsolationID'
            (Get-VMNetworkAdapterIsolation) and 'AccessVlanId' reads 0. SDN / ARC-SDN
            managed ports (including the ARB control-plane VM NIC) run in this mode.
        Reading only one source produces a false mismatch when the port uses the other.
        This helper applies the same precedence the virtual switch applies, so an
        access-mode port and an isolation-mode port carrying the same VLAN resolve to
        the same effective value.
    .PARAMETER VlanInfo
        The object returned by Get-VMNetworkAdapterVlan for the adapter. Optional.
    .PARAMETER IsolationInfo
        The object returned by Get-VMNetworkAdapterIsolation for the adapter. Optional.
    .OUTPUTS
        PSCustomObject with EffectiveVlanId, Source, and the raw diagnostic fields
        (OperationMode, AccessVlanId, IsolationMode, DefaultIsolationID). EffectiveVlanId
        is an int when the VLAN can be resolved, or $null with Source 'Unknown' when the
        adapter VLAN could not be determined (no usable isolation data and AccessVlanId 0).
    #>

    [CmdletBinding()]
    param (
        [Parameter()]
        [object] $VlanInfo,

        [Parameter()]
        [object] $IsolationInfo
    )

    $operationMode = $null
    $accessVlanId = $null
    $isolationMode = $null
    $defaultIsolationId = $null

    if ($null -ne $VlanInfo) {
        $operationMode = $VlanInfo.OperationMode
        $accessVlanId = $VlanInfo.AccessVlanId
    }
    if ($null -ne $IsolationInfo) {
        $isolationMode = $IsolationInfo.IsolationMode
        $defaultIsolationId = $IsolationInfo.DefaultIsolationID
    }

    # Precedence: a port in VLAN isolation mode carries its VLAN in DefaultIsolationID
    # (AccessVlanId reads 0 there). Otherwise an access-mode port carries it in
    # AccessVlanId. If neither is set and isolation data was readable the port is
    # genuinely untagged (0). If isolation data could NOT be read, an AccessVlanId of 0
    # is ambiguous (it cannot distinguish an untagged port from a VLAN-isolation port
    # whose tag lives in DefaultIsolationID), so the effective VLAN is left unresolved
    # (null) and the caller keeps this adapter out of the drift comparison.
    if (($null -ne $isolationMode) -and ("$isolationMode" -eq 'Vlan') -and ($null -ne $defaultIsolationId) -and ([int]$defaultIsolationId -gt 0)) {
        $effectiveVlanId = [int]$defaultIsolationId
        $source = 'DefaultIsolationID'
    }
    elseif (($null -ne $accessVlanId) -and ([int]$accessVlanId -gt 0)) {
        $effectiveVlanId = [int]$accessVlanId
        $source = 'AccessVlanId'
    }
    elseif ($null -ne $IsolationInfo) {
        $effectiveVlanId = 0
        $source = 'Untagged'
    }
    else {
        $effectiveVlanId = $null
        $source = 'Unknown'
    }

    return [PSCustomObject]@{
        EffectiveVlanId    = $effectiveVlanId
        Source             = $source
        OperationMode      = $operationMode
        AccessVlanId       = $accessVlanId
        IsolationMode      = $isolationMode
        DefaultIsolationID = $defaultIsolationId
    }
}

function Get-CimReferenceKeyValue {
    <#
    .SYNOPSIS
        Extracts a key property value from a CIM reference-typed property.
    .DESCRIPTION
        Reference-typed properties on a CimInstance (for example the 'Parent' and
        'HostResource' properties of Msvm_EthernetPortAllocationSettingData) may be
        surfaced either as a Microsoft.Management.Infrastructure.CimInstance whose key
        properties are populated, or as a WMI object-path string of the form
        '...InstanceID="Microsoft:GUID\\GUID"...'. This helper returns the requested key
        value regardless of which representation is used, so callers do not have to
        branch on the shape of the reference.
    .PARAMETER Reference
        The reference-typed property value (CimInstance or object-path string).
    .PARAMETER KeyName
        The name of the key property to extract (for example 'InstanceID' or 'Name').
    .OUTPUTS
        The string value of the requested key, or $null when it cannot be resolved.
    #>

    [CmdletBinding()]
    [OutputType([string])]
    param (
        [Parameter()]
        [object] $Reference,

        [Parameter(Mandatory = $true)]
        [string] $KeyName
    )

    if ($null -eq $Reference) {
        return $null
    }

    if ($Reference -is [Microsoft.Management.Infrastructure.CimInstance]) {
        $property = $Reference.CimInstanceProperties[$KeyName]
        if ($null -ne $property) {
            return [string]$property.Value
        }
        return $null
    }

    # Fall back to parsing an object-path string such as ...KeyName="value"...
    # Anchor the key name on a word boundary so it cannot match as a substring of a
    # different key. For example, Msvm_VirtualEthernetSwitch is a two-key class and its
    # real HostResource reference is:
    # Msvm_VirtualEthernetSwitch.CreationClassName="Msvm_VirtualEthernetSwitch",Name="<switch-guid>"
    # An unanchored 'Name="([^"]*)"' would match inside CreationClassName and return the
    # class name instead of the GUID; the '\b' anchor forces a match on the standalone key.
    $match = [regex]::Match([string]$Reference, '\b' + [regex]::Escape($KeyName) + '="([^"]*)"')
    if ($match.Success) {
        # Backslashes inside a WMI object-path key value are escaped (doubled). The real
        # Parent reference emitted by Hyper-V is, for example,
        # Msvm_SyntheticEthernetPortSettingData.InstanceID="Microsoft:<guid>\\<portGuid>"
        # Unescape the doubled backslash so the returned value matches the single-backslash
        # form the referenced instance reports through its own InstanceID key property
        # (otherwise the correlating hashtable lookup in Get-VMNetworkAdapterFromCim misses
        # and every adapter is silently skipped).
        return $match.Groups[1].Value -replace '\\\\', '\'
    }

    return $null
}

function Get-VMNetworkAdapterFromCim {
    <#
    .SYNOPSIS
        Collects Hyper-V VM network adapters using CIM cmdlets instead of Get-VMNetworkAdapter.
    .DESCRIPTION
        Get-VMNetworkAdapter is expensive on hosts with many VMs because it performs a large
        number of per-adapter WMI association traversals. This helper issues a small, fixed
        number of bulk Get-CimInstance queries against the root\virtualization\v2 namespace and
        correlates the results in memory, which scales far better on dense hosts.
 
        Only adapters that are connected to a virtual switch are returned (the connection is
        represented by Msvm_EthernetPortAllocationSettingData). Disconnected adapters have no
        switch association and are therefore not relevant to switch-scoped checks.
 
        The returned objects mirror the subset of Get-VMNetworkAdapter properties that the
        switch insight rules consume, so those rules do not need to change.
    .OUTPUTS
        PSCustomObject collection, each with: Name, VMName, IsManagementOs, SwitchName,
        SwitchId, MacAddress, and MacAddressSpoofing ('On' or 'Off').
    #>

    [CmdletBinding()]
    [OutputType([System.Management.Automation.PSObject[]])]
    param ()

    $namespace = 'root\virtualization\v2'
    $cimParams = @{ Namespace = $namespace; ErrorAction = 'Stop' }

    try {
        # Virtual switches: correlate by Name (the switch GUID, which matches Get-VMSwitch.Id).
        $switchByGuid = @{}
        foreach ($vSwitch in @(Get-CimInstance @cimParams -ClassName 'Msvm_VirtualEthernetSwitch')) {
            if (-not [string]::IsNullOrEmpty($vSwitch.Name)) {
                $switchByGuid[$vSwitch.Name] = $vSwitch
            }
        }

        # The host ("Hosting Computer System") owns the management OS (host) vNICs; any adapter
        # whose owning system GUID equals the host GUID is a management OS adapter.
        $hostSystemGuid = $null
        $vmNameByGuid = @{}
        foreach ($system in @(Get-CimInstance @cimParams -ClassName 'Msvm_ComputerSystem')) {
            if (-not [string]::IsNullOrEmpty($system.Name)) {
                $vmNameByGuid[$system.Name] = $system.ElementName
            }
            if ($system.Caption -ieq 'Hosting Computer System') {
                $hostSystemGuid = $system.Name
            }
        }

        # Port setting data describes each adapter (name, MAC). Both synthetic and emulated NICs.
        $portByInstanceId = @{}
        foreach ($portClass in @('Msvm_SyntheticEthernetPortSettingData', 'Msvm_EmulatedEthernetPortSettingData')) {
            foreach ($port in @(Get-CimInstance @cimParams -ClassName $portClass)) {
                if (-not [string]::IsNullOrEmpty($port.InstanceID)) {
                    $portByInstanceId[$port.InstanceID] = $port
                }
            }
        }

        # Security setting data carries AllowMacSpoofing. Its InstanceID is the owning connection's
        # InstanceID plus a feature suffix, so index by the connection prefix for correlation.
        $securityByConnectionId = @{}
        foreach ($security in @(Get-CimInstance @cimParams -ClassName 'Msvm_EthernetSwitchPortSecuritySettingData')) {
            if ([string]::IsNullOrEmpty($security.InstanceID)) {
                continue
            }
            # InstanceID form: '<connection InstanceID>\<featureGuid>'. Strip the last segment.
            $connectionId = $security.InstanceID -replace '\\[^\\]*$', ''
            if (-not [string]::IsNullOrEmpty($connectionId)) {
                $securityByConnectionId[$connectionId] = $security
            }
        }

        $adapters = [System.Collections.Generic.List[object]]::new()

        # Each connection (allocation) ties an adapter port to a virtual switch.
        foreach ($connection in @(Get-CimInstance @cimParams -ClassName 'Msvm_EthernetPortAllocationSettingData')) {
            # Resolve the switch this connection targets from its HostResource references.
            $vSwitch = $null
            foreach ($hostResource in @($connection.HostResource)) {
                $switchGuid = Get-CimReferenceKeyValue -Reference $hostResource -KeyName 'Name'
                if (-not [string]::IsNullOrEmpty($switchGuid) -and $switchByGuid.ContainsKey($switchGuid)) {
                    $vSwitch = $switchByGuid[$switchGuid]
                    break
                }
            }

            # Skip connections that are not attached to a known virtual switch.
            if ($null -eq $vSwitch) {
                continue
            }

            # Resolve the adapter port backing this connection.
            $portInstanceId = Get-CimReferenceKeyValue -Reference $connection.Parent -KeyName 'InstanceID'
            if ([string]::IsNullOrEmpty($portInstanceId) -or -not $portByInstanceId.ContainsKey($portInstanceId)) {
                continue
            }
            $port = $portByInstanceId[$portInstanceId]

            # MAC spoofing state comes from the security feature setting on this connection.
            $macAddressSpoofing = 'Off'
            if ($securityByConnectionId.ContainsKey($connection.InstanceID)) {
                $security = $securityByConnectionId[$connection.InstanceID]
                if ($security.AllowMacSpoofing) {
                    $macAddressSpoofing = 'On'
                }
            }

            # The owning system GUID is the first path segment of the port InstanceID
            # ('Microsoft:<systemGuid>\<portGuid>').
            $systemGuid = $null
            $instanceCore = $portInstanceId -replace '^Microsoft:', ''
            if ($instanceCore.Contains('\')) {
                $systemGuid = $instanceCore.Substring(0, $instanceCore.IndexOf('\'))
            }

            $isManagementOs = (-not [string]::IsNullOrEmpty($hostSystemGuid)) -and ($systemGuid -eq $hostSystemGuid)
            $vmName = if ($isManagementOs) { $null } elseif ($null -ne $systemGuid -and $vmNameByGuid.ContainsKey($systemGuid)) { $vmNameByGuid[$systemGuid] } else { $null }

            $adapters.Add([PSCustomObject]@{
                Name               = $port.ElementName
                VMName             = $vmName
                IsManagementOs     = $isManagementOs
                SwitchName         = $vSwitch.ElementName
                SwitchId           = $vSwitch.Name
                MacAddress         = $port.Address
                MacAddressSpoofing = $macAddressSpoofing
            })
        }

        return $adapters.ToArray()
    }
    catch {
        # Surface CIM failures as a non-terminating error so callers using
        # -ErrorAction SilentlyContinue gracefully skip (returning no adapters)
        # instead of the whole analyzer going UNKNOWN on the terminating error.
        Write-Error -ErrorRecord $_
        return @()
    }
}

# SIG # Begin signature block
# MIInRAYJKoZIhvcNAQcCoIInNTCCJzECAQExDzANBglghkgBZQMEAgEFADB5Bgor
# BgEEAYI3AgEEoGswaTA0BgorBgEEAYI3AgEeMCYCAwEAAAQQH8w7YFlLCE63JNLG
# KX7zUQIBAAIBAAIBAAIBAAIBADAxMA0GCWCGSAFlAwQCAQUABCBDnyFfIUCsUay4
# iCWDAMqjxXf4CdLl8CTp5+piu6ElSKCCDLowggX1MIID3aADAgECAhMzAAACHU0Z
# 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
# 1cY2L4A7GTQG1h32HHAvfQESWP0xghngMIIZ3AIBATBuMFcxCzAJBgNVBAYTAlVT
# MR4wHAYDVQQKExVNaWNyb3NvZnQgQ29ycG9yYXRpb24xKDAmBgNVBAMTH01pY3Jv
# c29mdCBDb2RlIFNpZ25pbmcgUENBIDIwMjQCEzMAAAIdTRnITtcPV0gAAAAAAh0w
# DQYJYIZIAWUDBAIBBQCggZAwGQYJKoZIhvcNAQkDMQwGCisGAQQBgjcCAQQwLwYJ
# KoZIhvcNAQkEMSIEIBZeplhe/pn3EOzgVE1UOnIdqJrKvj9OwjRkxLd0dGErMEIG
# CisGAQQBgjcCAQwxNDAyoBSAEgBNAGkAYwByAG8AcwBvAGYAdKEagBhodHRwOi8v
# d3d3Lm1pY3Jvc29mdC5jb20wDQYJKoZIhvcNAQEBBQAEggEAlmavH+MSMVuwIqdG
# CTXFzxJQnvaZTqdVShlZVFEeljmjxB0/wliVPGvJUSFhpmt4JX8Ne1ZdqKbzg5sj
# 4JpzGVvpRS9U1tJ3lED2u1oa/IJHtSTWmmSix2lRuhJiJwZkLg9EdepB2g9v6D+Q
# LPJULAO47iG03u31Ai9NXEQKakVu8XWsB/eDZKmz54ifJVcIZsm12nycciGSn4iP
# 3jtO6cvtPoCeS5OF8xggsoF89/mEcDE5HZ25kMuy13Qylvvd+nwprPGTxyApFlUq
# L7bEwDhaTb7tTLuWkvSGRIe+UANHkWoLqQbsfvacpSgXfWJWK3E03zuO98bMwj78
# G7/aCKGCF7AwghesBgorBgEEAYI3AwMBMYIXnDCCF5gGCSqGSIb3DQEHAqCCF4kw
# gheFAgEDMQ8wDQYJYIZIAWUDBAIBBQAwggFaBgsqhkiG9w0BCRABBKCCAUkEggFF
# MIIBQQIBAQYKKwYBBAGEWQoDATAxMA0GCWCGSAFlAwQCAQUABCDp8NtTSH9eiJ6B
# eIzlj3hN3ORQnSfqxt6KyZRkPpsFmQIGaolQ9UupGBMyMDI2MDkwMjE1MjYwOS41
# NDJaMASAAgH0oIHZpIHWMIHTMQswCQYDVQQGEwJVUzETMBEGA1UECBMKV2FzaGlu
# Z3RvbjEQMA4GA1UEBxMHUmVkbW9uZDEeMBwGA1UEChMVTWljcm9zb2Z0IENvcnBv
# cmF0aW9uMS0wKwYDVQQLEyRNaWNyb3NvZnQgSXJlbGFuZCBPcGVyYXRpb25zIExp
# bWl0ZWQxJzAlBgNVBAsTHm5TaGllbGQgVFNTIEVTTjo1MjFBLTA1RTAtRDk0NzEl
# MCMGA1UEAxMcTWljcm9zb2Z0IFRpbWUtU3RhbXAgU2VydmljZaCCEf4wggcoMIIF
# EKADAgECAhMzAAACF3H7LqWvAR3qAAEAAAIXMA0GCSqGSIb3DQEBCwUAMHwxCzAJ
# BgNVBAYTAlVTMRMwEQYDVQQIEwpXYXNoaW5ndG9uMRAwDgYDVQQHEwdSZWRtb25k
# MR4wHAYDVQQKExVNaWNyb3NvZnQgQ29ycG9yYXRpb24xJjAkBgNVBAMTHU1pY3Jv
# c29mdCBUaW1lLVN0YW1wIFBDQSAyMDEwMB4XDTI1MDgxNDE4NDgyM1oXDTI2MTEx
# MzE4NDgyM1owgdMxCzAJBgNVBAYTAlVTMRMwEQYDVQQIEwpXYXNoaW5ndG9uMRAw
# DgYDVQQHEwdSZWRtb25kMR4wHAYDVQQKExVNaWNyb3NvZnQgQ29ycG9yYXRpb24x
# LTArBgNVBAsTJE1pY3Jvc29mdCBJcmVsYW5kIE9wZXJhdGlvbnMgTGltaXRlZDEn
# MCUGA1UECxMeblNoaWVsZCBUU1MgRVNOOjUyMUEtMDVFMC1EOTQ3MSUwIwYDVQQD
# ExxNaWNyb3NvZnQgVGltZS1TdGFtcCBTZXJ2aWNlMIICIjANBgkqhkiG9w0BAQEF
# AAOCAg8AMIICCgKCAgEAwM82sEw+39vYR7iGCIFDnYNhRM+BzF2AYiq5dUpZpJFP
# RjCcipQ6RUbI+RAYNRApExx5ygrXbaWtuwvqsqAVSWbU/W6fecujjILkPqn9pngt
# WRkfQgbYgvaXALl6PY2yOH9f72MD+6AyxQenSpAMdUzY/Qk/jtjsHdFXVBe+tshl
# IkSJ3GZw8VVKqTg3GZElztwbJWNtrhBEvhf6anxMegQMJP7tO8/BJ7ITs4/AV3D2
# bv8eHk81Y+fOmQ8mQ61WLq2wItvlzIT5bzelK9LvEycf5x1lXxAwEw5a7dpS+CKT
# anhtv+Q2mwebAybjf9io4k48stTaq1rtcrOiDwddqVm1S9e8h1TszXFzjLLvE9Em
# jnNfIewsY+RChUaHnY4FFwwJEnEv/JS76oHT0oGdy7+J60fGOl7A1UoUyAkhpb2B
# ja+SwSIiHbQ4FDyJiLlZ6drZZ84MoJ852JSxM0hBjGO6FZlPO8iuNyk680Di8Vnb
# SNpIdJN+DhlepeTUMBDHqCmd0mVWRWZPm1pvgty93asNt/Ng6o4m2dnooWOdM3yK
# sJaWjyHqic9gfTrZBM+PCXqeTaO1oEiaQ+h4w0nHVdV+XSvI2m1yN4iibqjm5HPa
# AO3OJ+OmNLftNVmr4Z6U2T6pIcLBysoKcDUvCqycXj4C/+n1KFBpDGdDMw9gmu8C
# AwEAAaOCAUkwggFFMB0GA1UdDgQWBBRQrN9jlwNOoeE5ZQqnF5x8S1bJQzAfBgNV
# HSMEGDAWgBSfpxVdAF5iXYP05dJlpxtTNRnpcjBfBgNVHR8EWDBWMFSgUqBQhk5o
# dHRwOi8vd3d3Lm1pY3Jvc29mdC5jb20vcGtpb3BzL2NybC9NaWNyb3NvZnQlMjBU
# aW1lLVN0YW1wJTIwUENBJTIwMjAxMCgxKS5jcmwwbAYIKwYBBQUHAQEEYDBeMFwG
# CCsGAQUFBzAChlBodHRwOi8vd3d3Lm1pY3Jvc29mdC5jb20vcGtpb3BzL2NlcnRz
# L01pY3Jvc29mdCUyMFRpbWUtU3RhbXAlMjBQQ0ElMjAyMDEwKDEpLmNydDAMBgNV
# HRMBAf8EAjAAMBYGA1UdJQEB/wQMMAoGCCsGAQUFBwMIMA4GA1UdDwEB/wQEAwIH
# gDANBgkqhkiG9w0BAQsFAAOCAgEARmgFdhB7xIAIHEEg5I/5S+gx67aR6RiW8ZAw
# tE3mz8o0dyn+pIP+lidNR1IKQQ0r+RjYgI9cZ6mbvAyvh3e2q/BV8rjHE3ud9PyY
# yq32euFgdZ3vX4b5QXePWlpBAYrdziR27rHz6WwpH5dZsSypbXDBbQkWkNl6g82y
# Ty3AbBbKDXBdzxZsEauaOplatK7Er4dhglKBex8JQ2dMSkSZweCNDXqd9r/9W2Vd
# RZsDJKP/Xc4UyQlVsboBotKtYESXFkjwR1HVsH+Q0C69/N5CP/Tq3YgI1ub4b9+3
# MJFKWhJXCcJGFZkcLwUmYwoFg1XLo7DLJdGjrIH1jsI2NFXJFQHef6AdRe1ERvYQ
# eqtyrBvxIvR+P/83FNYyzx04inUT9TF2AwTOuqCC6Z67oNwR4pEEJyAIEREvkdhj
# jfWcgsk/nGTlfahvNY/SOHrNRKo49KDlccNzRCJQyQ+D59r7/qebNSyQPTfwI9++
# jEY0Q/UWKVNLhio55GYBseJ99s7NzkdxOr9Uftp597HEovbA69qGlZ3OpUE3H1RB
# GDVp/FvM2uXTum8LrMkPXx5Ap/kbPASsC9ju9oMCe2IEXO2SeD1aD3IqvAOdHFKH
# g1vpbPUQSWb6g2xfBV30wFcqaPYgzcbxPWPyZqK+S8l7zw64aO5hmJ7eQwoMfTu0
# Vay6r48wggdxMIIFWaADAgECAhMzAAAAFcXna54Cm0mZAAAAAAAVMA0GCSqGSIb3
# DQEBCwUAMIGIMQswCQYDVQQGEwJVUzETMBEGA1UECBMKV2FzaGluZ3RvbjEQMA4G
# A1UEBxMHUmVkbW9uZDEeMBwGA1UEChMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMTIw
# MAYDVQQDEylNaWNyb3NvZnQgUm9vdCBDZXJ0aWZpY2F0ZSBBdXRob3JpdHkgMjAx
# MDAeFw0yMTA5MzAxODIyMjVaFw0zMDA5MzAxODMyMjVaMHwxCzAJBgNVBAYTAlVT
# MRMwEQYDVQQIEwpXYXNoaW5ndG9uMRAwDgYDVQQHEwdSZWRtb25kMR4wHAYDVQQK
# ExVNaWNyb3NvZnQgQ29ycG9yYXRpb24xJjAkBgNVBAMTHU1pY3Jvc29mdCBUaW1l
# LVN0YW1wIFBDQSAyMDEwMIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEA
# 5OGmTOe0ciELeaLL1yR5vQ7VgtP97pwHB9KpbE51yMo1V/YBf2xK4OK9uT4XYDP/
# XE/HZveVU3Fa4n5KWv64NmeFRiMMtY0Tz3cywBAY6GB9alKDRLemjkZrBxTzxXb1
# hlDcwUTIcVxRMTegCjhuje3XD9gmU3w5YQJ6xKr9cmmvHaus9ja+NSZk2pg7uhp7
# M62AW36MEBydUv626GIl3GoPz130/o5Tz9bshVZN7928jaTjkY+yOSxRnOlwaQ3K
# Ni1wjjHINSi947SHJMPgyY9+tVSP3PoFVZhtaDuaRr3tpK56KTesy+uDRedGbsoy
# 1cCGMFxPLOJiss254o2I5JasAUq7vnGpF1tnYN74kpEeHT39IM9zfUGaRnXNxF80
# 3RKJ1v2lIH1+/NmeRd+2ci/bfV+AutuqfjbsNkz2K26oElHovwUDo9Fzpk03dJQc
# NIIP8BDyt0cY7afomXw/TNuvXsLz1dhzPUNOwTM5TI4CvEJoLhDqhFFG4tG9ahha
# YQFzymeiXtcodgLiMxhy16cg8ML6EgrXY28MyTZki1ugpoMhXV8wdJGUlNi5UPkL
# iWHzNgY1GIRH29wb0f2y1BzFa/ZcUlFdEtsluq9QBXpsxREdcu+N+VLEhReTwDwV
# 2xo3xwgVGD94q0W29R6HXtqPnhZyacaue7e3PmriLq0CAwEAAaOCAd0wggHZMBIG
# CSsGAQQBgjcVAQQFAgMBAAEwIwYJKwYBBAGCNxUCBBYEFCqnUv5kxJq+gpE8RjUp
# zxD/LwTuMB0GA1UdDgQWBBSfpxVdAF5iXYP05dJlpxtTNRnpcjBcBgNVHSAEVTBT
# MFEGDCsGAQQBgjdMg30BATBBMD8GCCsGAQUFBwIBFjNodHRwOi8vd3d3Lm1pY3Jv
# c29mdC5jb20vcGtpb3BzL0RvY3MvUmVwb3NpdG9yeS5odG0wEwYDVR0lBAwwCgYI
# KwYBBQUHAwgwGQYJKwYBBAGCNxQCBAweCgBTAHUAYgBDAEEwCwYDVR0PBAQDAgGG
# MA8GA1UdEwEB/wQFMAMBAf8wHwYDVR0jBBgwFoAU1fZWy4/oolxiaNE9lJBb186a
# GMQwVgYDVR0fBE8wTTBLoEmgR4ZFaHR0cDovL2NybC5taWNyb3NvZnQuY29tL3Br
# aS9jcmwvcHJvZHVjdHMvTWljUm9vQ2VyQXV0XzIwMTAtMDYtMjMuY3JsMFoGCCsG
# AQUFBwEBBE4wTDBKBggrBgEFBQcwAoY+aHR0cDovL3d3dy5taWNyb3NvZnQuY29t
# L3BraS9jZXJ0cy9NaWNSb29DZXJBdXRfMjAxMC0wNi0yMy5jcnQwDQYJKoZIhvcN
# AQELBQADggIBAJ1VffwqreEsH2cBMSRb4Z5yS/ypb+pcFLY+TkdkeLEGk5c9MTO1
# OdfCcTY/2mRsfNB1OW27DzHkwo/7bNGhlBgi7ulmZzpTTd2YurYeeNg2LpypglYA
# A7AFvonoaeC6Ce5732pvvinLbtg/SHUB2RjebYIM9W0jVOR4U3UkV7ndn/OOPcbz
# aN9l9qRWqveVtihVJ9AkvUCgvxm2EhIRXT0n4ECWOKz3+SmJw7wXsFSFQrP8DJ6L
# GYnn8AtqgcKBGUIZUnWKNsIdw2FzLixre24/LAl4FOmRsqlb30mjdAy87JGA0j3m
# Sj5mO0+7hvoyGtmW9I/2kQH2zsZ0/fZMcm8Qq3UwxTSwethQ/gpY3UA8x1RtnWN0
# SCyxTkctwRQEcb9k+SS+c23Kjgm9swFXSVRk2XPXfx5bRAGOWhmRaw2fpCjcZxko
# JLo4S5pu+yFUa2pFEUep8beuyOiJXk+d0tBMdrVXVAmxaQFEfnyhYWxz/gq77EFm
# PWn9y8FBSX5+k77L+DvktxW/tM4+pTFRhLy/AsGConsXHRWJjXD+57XQKBqJC482
# 2rpM+Zv/Cuk0+CQ1ZyvgDbjmjJnW4SLq8CdCPSWU5nR0W2rRnj7tfqAxM328y+l7
# vzhwRNGQ8cirOoo6CGJ/2XBjU02N7oJtpQUQwXEGahC0HVUzWLOhcGbyoYIDWTCC
# AkECAQEwggEBoYHZpIHWMIHTMQswCQYDVQQGEwJVUzETMBEGA1UECBMKV2FzaGlu
# Z3RvbjEQMA4GA1UEBxMHUmVkbW9uZDEeMBwGA1UEChMVTWljcm9zb2Z0IENvcnBv
# cmF0aW9uMS0wKwYDVQQLEyRNaWNyb3NvZnQgSXJlbGFuZCBPcGVyYXRpb25zIExp
# bWl0ZWQxJzAlBgNVBAsTHm5TaGllbGQgVFNTIEVTTjo1MjFBLTA1RTAtRDk0NzEl
# MCMGA1UEAxMcTWljcm9zb2Z0IFRpbWUtU3RhbXAgU2VydmljZaIjCgEBMAcGBSsO
# AwIaAxUAabKAFaKt2haUdqkHfFYzAzfgSMuggYMwgYCkfjB8MQswCQYDVQQGEwJV
# UzETMBEGA1UECBMKV2FzaGluZ3RvbjEQMA4GA1UEBxMHUmVkbW9uZDEeMBwGA1UE
# ChMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMSYwJAYDVQQDEx1NaWNyb3NvZnQgVGlt
# ZS1TdGFtcCBQQ0EgMjAxMDANBgkqhkiG9w0BAQsFAAIFAO5CTsswIhgPMjAyNjA5
# MDIwNzI5MTVaGA8yMDI2MDkwMzA3MjkxNVowdzA9BgorBgEEAYRZCgQBMS8wLTAK
# AgUA7kJOywIBADAKAgEAAgIcIwIB/zAHAgEAAgITJTAKAgUA7kOgSwIBADA2Bgor
# BgEEAYRZCgQCMSgwJjAMBgorBgEEAYRZCgMCoAowCAIBAAIDB6EgoQowCAIBAAID
# AYagMA0GCSqGSIb3DQEBCwUAA4IBAQBLqnQ0V+8Vt+f0zPH/LnPXwpafW3K5JZ1P
# 5NyzU6SRAEn49LMOWAN6QL3cqY7kWlRUb3VNtzmd+smlWIZNkLN6sRlIIrzCPFvG
# 4jFjqnrScow3Xak0GHKNbK/+r7OiftLIOV4HpSa4FF4zynbawOte3/dxJ/EHSvUg
# VH8cTIZ91U07Wo+GN980d7h9rBm1qYuP/bbhGD7BtRbJg/Qs92Mwz8W7m0Udl9El
# DabAYuRbifRyI6cyvpxudG0DeMuFlwG13VsTkrbnthNwYaPxNthfLPPMj+tcF83w
# +IKL7Cl9AmFm5HbsgbMegLxYvOCEe9d9m6ngccbPLvBaizKpeaoGMYIEDTCCBAkC
# AQEwgZMwfDELMAkGA1UEBhMCVVMxEzARBgNVBAgTCldhc2hpbmd0b24xEDAOBgNV
# BAcTB1JlZG1vbmQxHjAcBgNVBAoTFU1pY3Jvc29mdCBDb3Jwb3JhdGlvbjEmMCQG
# A1UEAxMdTWljcm9zb2Z0IFRpbWUtU3RhbXAgUENBIDIwMTACEzMAAAIXcfsupa8B
# HeoAAQAAAhcwDQYJYIZIAWUDBAIBBQCgggFKMBoGCSqGSIb3DQEJAzENBgsqhkiG
# 9w0BCRABBDAvBgkqhkiG9w0BCQQxIgQgV35tx97bHzars5VdZhPkw/6HmNPRadn6
# Z+GP1bhqtygwgfoGCyqGSIb3DQEJEAIvMYHqMIHnMIHkMIG9BCDQ8lBgPl23yZ0S
# zUSt5phOIegHPywrkNwevxe2k+RaWzCBmDCBgKR+MHwxCzAJBgNVBAYTAlVTMRMw
# EQYDVQQIEwpXYXNoaW5ndG9uMRAwDgYDVQQHEwdSZWRtb25kMR4wHAYDVQQKExVN
# aWNyb3NvZnQgQ29ycG9yYXRpb24xJjAkBgNVBAMTHU1pY3Jvc29mdCBUaW1lLVN0
# YW1wIFBDQSAyMDEwAhMzAAACF3H7LqWvAR3qAAEAAAIXMCIEIKj6hTyZodonOU1g
# CWasxwxGKzcwRjJ44RpBNsnK6/C8MA0GCSqGSIb3DQEBCwUABIICAIkxahDrsX7y
# DIUElu2S6Cwd11XbkwHN1j46jJtJU3W2op2fkETMvgSfu+PVf+kVE28HGeE5cffv
# w/6PbKA51RUswVMd3Zh6bGnIvv2S3qwMfB7EKS26Ybk2Z21dSTSU9ba7N5OJIpID
# 73f8fOHoptysAmddRBRhVUZ/ujhCWDgslTrdVID8w3au+CWmOhvuFSEzV00UaSRN
# G43LBnK0Ytj1yVGzEhrsCXFZxCg+lmH9jhrHZ1QoOcddCSqY0XnPmCPQ5N9bA2z/
# nze4arUm+SOHkNzNJVAc9Kk5wRbYYDjNIldGIW9baVwNXhEzMcHsgwuaCbL05Xs1
# Vg6c5Pj2UfhGGNPzY7KqoXNWGpNN4M2MJYzFpF7N2xQYRmmUUPusT8U9LDTBW1lr
# ZXDRBZsW3AtpSaYxu7UWga7QN2Snii3vGm7FOiWAjuXmhpN47v1SdBn/fi1wlO2A
# x4Pmi2Gj+Mqj4lHZDH51ZRXG4E7iisGm5P+ALaC1mmDy+0wVzXSfd6mDbvowRV74
# lDqDEKbV4zHpuSeQW6cQUc1vdkE/NSU344nsyUd09c72FOi6Ceiq2H7dBg4pgLjs
# eOo5Df6oNTzPh33R6bpUGMaNa3p9DK6+5AnRwOeB9ywYmhqOL/HRuZL+AgeRT+EX
# sM4Jru/d5sQ90DYFXyB3qX5VG8uFo4Om
# SIG # End signature block