helpers/test/Invoke-WUGDiscoveryRunner.ps1

<#
.SYNOPSIS
    Master end-to-end discovery runner — orchestrates all discovery providers,
    generates real dashboards, and produces a WUG-ready monitor plan.
 
.DESCRIPTION
    Runs every registered discovery provider (AWS, Azure, Bigleaf, Docker,
    F5, Fortinet, GCP, HyperV, LoadMaster, Nutanix, OCI, Proxmox, VMware,
    WindowsAttributes, WindowsDiskIO) against live targets. For each provider it:
 
      1. Loads credentials from the DPAPI vault (prompts if missing)
      2. Runs Invoke-Discovery to query the target API
      3. Exports the discovery plan (JSON + CSV)
      4. Generates the provider's HTML dashboard
      5. Records pass/fail results using the same test framework
         as Invoke-WUGHelperTest.ps1
 
    The combined results are written to a unified HTML report plus a
    master JSON plan that contains every discovered item across all
    providers — enough data to create WUG monitors in bulk.
 
    "Smart selective" mode: if you pass any -Run* parameter, only those
    providers execute. Otherwise all providers run.
 
.PARAMETER RunAWS
    Include AWS provider. Default: all run.
.PARAMETER RunAzure
    Include Azure provider. Default: all run.
.PARAMETER RunBigleaf
    Include Bigleaf SD-WAN provider. Default: all run.
.PARAMETER RunDocker
    Include Docker provider. Default: all run.
.PARAMETER RunF5
    Include F5 BIG-IP provider. Default: all run.
.PARAMETER RunFortinet
    Include Fortinet FortiGate provider. Default: all run.
.PARAMETER RunGCP
    Include Google Cloud Platform provider. Default: all run.
.PARAMETER RunHyperV
    Include Hyper-V provider. Default: all run.
.PARAMETER RunLoadMaster
    Include Kemp LoadMaster provider. Default: all run.
.PARAMETER RunNutanix
    Include Nutanix AHV provider. Default: all run.
.PARAMETER RunOCI
    Include Oracle Cloud Infrastructure provider. Default: all run.
.PARAMETER RunProxmox
    Include Proxmox VE provider. Default: all run.
.PARAMETER RunVMware
    Include VMware vSphere provider. Default: all run.
.PARAMETER RunWindowsAttributes
    Include Windows Attributes provider (WMI hardware/OS info). Default: all run.
.PARAMETER RunWindowsDiskIO
    Include Windows Disk IO provider (WMI perf counters). Default: all run.
.PARAMETER OutputPath
    Directory for all output files (dashboards, plans, report).
    Defaults to $env:TEMP\DiscoveryRunner.
.PARAMETER SkipDashboard
    Skip HTML dashboard generation (plan + report only).
.PARAMETER NonInteractive
    Never prompt — skip providers whose vault credentials are missing.
 
.EXAMPLE
    .\Invoke-WUGDiscoveryRunner.ps1
    # Runs ALL providers, prompts for any missing credentials.
 
.EXAMPLE
    .\Invoke-WUGDiscoveryRunner.ps1 -RunProxmox 1 -RunHyperV 1
    # Runs only Proxmox and Hyper-V; everything else is skipped.
 
.EXAMPLE
    .\Invoke-WUGDiscoveryRunner.ps1 -NonInteractive -OutputPath C:\Reports
    # Runs all providers that have vault credentials — no prompts.
 
.NOTES
    Author : jason@wug.ninja
    Created : 2025-07-13
    Requires: PowerShell 5.1+, DiscoveryHelpers.ps1, DiscoveryProvider-*.ps1
              Provider-specific modules (Az, AWS.Tools, VMware.PowerCLI, etc.)
              are only needed for the providers you actually run.
#>

[CmdletBinding()]
param(
    [bool]$RunAWS,
    [bool]$RunAzure,
    [bool]$RunBigleaf,
    [bool]$RunDocker,
    [bool]$RunF5,
    [bool]$RunFortinet,
    [bool]$RunGCP,
    [bool]$RunHyperV,
    [bool]$RunLoadMaster,
    [bool]$RunNutanix,
    [bool]$RunOCI,
    [bool]$RunProxmox,
    [bool]$RunVMware,
    [bool]$RunWindowsAttributes,
    [bool]$RunWindowsDiskIO,
    [string]$OutputPath,
    [switch]$SkipDashboard,
    [switch]$NonInteractive
)

# ============================================================================
# region Smart Selective Mode
# ============================================================================
$runParams = @('RunAWS','RunAzure','RunBigleaf','RunDocker','RunF5','RunFortinet','RunGCP','RunHyperV','RunLoadMaster','RunNutanix','RunOCI','RunProxmox','RunVMware','RunWindowsAttributes','RunWindowsDiskIO')
$anyExplicit = $runParams | Where-Object { $PSBoundParameters.ContainsKey($_) }
if ($anyExplicit) {
    foreach ($p in $runParams) {
        if (-not $PSBoundParameters.ContainsKey($p)) {
            Set-Variable -Name $p -Value $false
        }
    }
} else {
    foreach ($p in $runParams) { Set-Variable -Name $p -Value $true }
}
# endregion

# ============================================================================
# region Output Directory
# ============================================================================
if (-not $OutputPath) {
    $OutputPath = Join-Path $env:TEMP 'DiscoveryRunner'
}
if (-not (Test-Path $OutputPath)) {
    New-Item -ItemType Directory -Path $OutputPath -Force | Out-Null
}
$timestamp = (Get-Date).ToString('yyyyMMdd_HHmmss')
# endregion

# ============================================================================
# region Dot-source Helpers
# ============================================================================
$scriptDir   = Split-Path $MyInvocation.MyCommand.Path -Parent
$discoveryDir = Join-Path (Split-Path $scriptDir -Parent) 'discovery'
$helpersRoot  = Split-Path $scriptDir -Parent

. (Join-Path $discoveryDir 'DiscoveryHelpers.ps1')

$providerScripts = @{
    AWS      = Join-Path $discoveryDir 'DiscoveryProvider-AWS.ps1'
    Azure    = Join-Path $discoveryDir 'DiscoveryProvider-Azure.ps1'
    Bigleaf  = Join-Path $discoveryDir 'DiscoveryProvider-Bigleaf.ps1'
    Docker   = Join-Path $discoveryDir 'DiscoveryProvider-Docker.ps1'
    F5       = Join-Path $discoveryDir 'DiscoveryProvider-F5.ps1'
    Fortinet = Join-Path $discoveryDir 'DiscoveryProvider-Fortinet.ps1'
    GCP      = Join-Path $discoveryDir 'DiscoveryProvider-GCP.ps1'
    HyperV      = Join-Path $discoveryDir 'DiscoveryProvider-HyperV.ps1'
    LoadMaster = Join-Path $discoveryDir 'DiscoveryProvider-LoadMaster.ps1'
    Nutanix    = Join-Path $discoveryDir 'DiscoveryProvider-Nutanix.ps1'
    OCI      = Join-Path $discoveryDir 'DiscoveryProvider-OCI.ps1'
    Proxmox  = Join-Path $discoveryDir 'DiscoveryProvider-Proxmox.ps1'
    VMware   = Join-Path $discoveryDir 'DiscoveryProvider-VMware.ps1'
    WindowsAttributes = Join-Path $discoveryDir 'DiscoveryProvider-Windows.ps1'
    WindowsDiskIO     = Join-Path $discoveryDir 'DiscoveryProvider-Windows.ps1'
}

$helperScripts = @{
    AWS      = Join-Path $helpersRoot 'aws\AWSHelpers.ps1'
    Azure    = Join-Path $helpersRoot 'azure\AzureHelpers.ps1'
    Bigleaf  = Join-Path $helpersRoot 'bigleaf\BigleafHelpers.ps1'
    Docker   = Join-Path $helpersRoot 'docker\DockerHelpers.ps1'
    F5       = Join-Path $helpersRoot 'f5\F5Helpers.ps1'
    Fortinet = Join-Path $helpersRoot 'fortinet\FortinetHelpers.ps1'
    GCP      = Join-Path $helpersRoot 'gcp\GCPHelpers.ps1'
    HyperV      = Join-Path $helpersRoot 'hyperv\HypervHelpers.ps1'
    LoadMaster = $null                                                  # No separate helper file
    Nutanix    = Join-Path $helpersRoot 'nutanix\NutanixHelpers.ps1'
    OCI      = Join-Path $helpersRoot 'oci\OCIHelpers.ps1'
    Proxmox  = Join-Path $helpersRoot 'proxmox\ProxmoxHelpers.ps1'
    VMware   = Join-Path $helpersRoot 'vmware\VMwareHelpers.ps1'
    WindowsAttributes = $null                                       # Uses DiscoveryProvider-Windows.ps1 directly
    WindowsDiskIO     = $null                                       # Uses DiscoveryProvider-Windows.ps1 directly
}
# endregion

# ============================================================================
# region Test Framework (matches Invoke-WUGHelperTest.ps1)
# ============================================================================
$script:TestResults = [System.Collections.Generic.List[PSCustomObject]]::new()

function Record-Test {
    param(
        [string]$Cmdlet,
        [string]$Endpoint,
        [string]$Status,
        [string]$Detail = ''
    )
    $script:TestResults.Add([PSCustomObject]@{
        Cmdlet   = $Cmdlet
        Endpoint = $Endpoint
        Status   = $Status
        Detail   = $Detail
    })
    $color = switch ($Status) { 'Pass' { 'Green' } 'Fail' { 'Red' } default { 'Yellow' } }
    Write-Host " [$Status] $Cmdlet ($Endpoint) $Detail" -ForegroundColor $color
}

function Invoke-Test {
    param(
        [string]$Cmdlet,
        [string]$Endpoint,
        [scriptblock]$Test
    )
    try {
        $null = & $Test
        Record-Test -Cmdlet $Cmdlet -Endpoint $Endpoint -Status 'Pass'
    }
    catch {
        Record-Test -Cmdlet $Cmdlet -Endpoint $Endpoint -Status 'Fail' -Detail $_.Exception.Message
    }
}

function Export-TestResultsHtml {
    param(
        [Parameter(Mandatory)]$TestResults,
        [Parameter(Mandatory)][string]$OutputPath,
        [Parameter(Mandatory)][string]$TemplatePath,
        [string]$ReportTitle = 'Discovery Runner Results'
    )
    if (-not (Test-Path $TemplatePath)) {
        Write-Warning "Report template not found at $TemplatePath - skipping HTML report."
        return $false
    }
    $columns = @(
        @{ field = 'Cmdlet';   title = 'Cmdlet';   sortable = $true; searchable = $true }
        @{ field = 'Endpoint'; title = 'Endpoint'; sortable = $true; searchable = $true }
        @{ field = 'Status';   title = 'Status';   sortable = $true; searchable = $true; formatter = 'formatStatus' }
        @{ field = 'Detail';   title = 'Detail';   sortable = $true; searchable = $true }
    )
    $columnsJson = $columns | ConvertTo-Json -Depth 5 -Compress
    $dataRows = @($TestResults | Select-Object Cmdlet, Endpoint, Status, Detail)
    $dataJson = ConvertTo-Json -InputObject $dataRows -Depth 5 -Compress
    $tableConfig = @"
        columns: $columnsJson,
        data: $dataJson
"@

    $html = Get-Content -Path $TemplatePath -Raw
    $html = $html -replace 'replaceThisHere', $tableConfig
    $html = $html -replace 'ReplaceYourReportNameHere', $ReportTitle
    $html = $html -replace 'ReplaceUpdateTimeHere', (Get-Date).ToString('yyyy-MM-dd HH:mm:ss')
    $statusFn = @"
 
    function formatStatus(value) {
        if (value === 'Pass') return '<span class="badge bg-success">Pass</span>';
        if (value === 'Fail') return '<span class="badge bg-danger">Fail</span>';
        if (value === 'Skipped') return '<span class="badge bg-warning text-dark">Skipped</span>';
        return value;
    }
 
"@

    $html = $html.Replace(' function escapeHtml(text) {', "${statusFn} function escapeHtml(text) {")
    $html = $html -replace '(?s)return rows\.map\(row => \(\{.*?\}\)\);', 'return rows;'
    Set-Content -Path $OutputPath -Value $html -Encoding UTF8
    return $true
}
# endregion

# ============================================================================
# region Provider Configurations
# ============================================================================
# Each entry defines: targets, port/protocol, vault credential name,
# module dependencies, and dashboard export function name.
# Edit targets here to match your environment.

$ProviderConfig = [ordered]@{
    AWS = @{
        Toggle        = [ref]$RunAWS
        Targets       = @('all')                     # Scan all enabled regions
        Port          = $null                        # N/A for cloud
        Protocol      = $null
        VaultName     = 'AWS.Credential'
        CredType      = 'AWSKeys'                    # AccessKeyId|SecretAccessKey
        Modules       = @('AWS.Tools.EC2','AWS.Tools.RDS','AWS.Tools.ElasticLoadBalancingV2','AWS.Tools.CloudWatch','AWS.Tools.Common')
        HelperFile    = 'AWS'
        ProviderFile  = 'AWS'
        DashboardFunc = 'Export-AWSDiscoveryDashboardHtml'
        DashboardFile = 'AWS-Dashboard.html'
        GetDashData   = $null     # Set per-provider below
    }
    Azure = @{
        Toggle        = [ref]$RunAzure
        Targets       = @('azure')                   # Placeholder — Azure uses subscription, not IP
        Port          = $null
        Protocol      = $null
        VaultName     = $null                        # Dynamic: Azure.<TenantId>.ServicePrincipal
        CredType      = 'AzureSP'                    # TenantId|ApplicationId|ClientSecret
        Modules       = @('Az.Accounts','Az.Resources','Az.Monitor')
        HelperFile    = 'Azure'
        ProviderFile  = 'Azure'
        DashboardFunc = 'Export-AzureDiscoveryDashboardHtml'
        DashboardFile = 'Azure-Dashboard.html'
        GetDashData   = $null
    }
    Bigleaf = @{
        Toggle        = [ref]$RunBigleaf
        Targets       = @('bigleaf')                 # Uses Bigleaf API (cloud-based)
        Port          = $null
        Protocol      = $null
        VaultName     = 'Bigleaf.Credential'
        CredType      = 'PSCredential'
        Modules       = @()
        HelperFile    = 'Bigleaf'
        ProviderFile  = 'Bigleaf'
        DashboardFunc = 'Export-DynamicDashboardHtml'
        DashboardFile = 'Bigleaf-Dashboard.html'
        GetDashData   = $null
    }
    Docker = @{
        Toggle        = [ref]$RunDocker
        Targets       = @('docker1.corp.local')      # Docker host(s)
        Port          = 2375
        Protocol      = 'http'
        VaultName     = $null                        # Docker API typically unauthenticated or TLS
        CredType      = $null
        Modules       = @()
        HelperFile    = 'Docker'
        ProviderFile  = 'Docker'
        DashboardFunc = 'Export-DynamicDashboardHtml'
        DashboardFile = 'Docker-Dashboard.html'
        GetDashData   = $null
    }
    F5 = @{
        Toggle        = [ref]$RunF5
        Targets       = @('lb1.corp.local')
        Port          = 443
        Protocol      = 'https'
        VaultName     = 'F5.lb1.corp.local.Credential'
        CredType      = 'PSCredential'
        Modules       = @()
        HelperFile    = 'F5'
        ProviderFile  = 'F5'
        DashboardFunc = 'Export-F5DiscoveryDashboardHtml'
        DashboardFile = 'F5-Dashboard.html'
        GetDashData   = $null
    }
    Fortinet = @{
        Toggle        = [ref]$RunFortinet
        Targets       = @('fw1.corp.local')
        Port          = 443
        Protocol      = 'https'
        VaultName     = 'FortiGate.fw1.corp.local'
        CredType      = 'BearerToken'                # Single API token string
        Modules       = @()
        HelperFile    = 'Fortinet'
        ProviderFile  = 'Fortinet'
        DashboardFunc = 'Export-FortinetDiscoveryDashboardHtml'
        DashboardFile = 'Fortinet-Dashboard.html'
        GetDashData   = $null
    }
    GCP = @{
        Toggle        = [ref]$RunGCP
        Targets       = @('gcp')                     # Uses gcloud CLI / service account
        Port          = $null
        Protocol      = $null
        VaultName     = 'GCP.ServiceAccount'
        CredType      = 'FilePath'
        Modules       = @()                          # Requires gcloud CLI
        HelperFile    = 'GCP'
        ProviderFile  = 'GCP'
        DashboardFunc = 'Export-DynamicDashboardHtml'
        DashboardFile = 'GCP-Dashboard.html'
        GetDashData   = $null
    }
    HyperV = @{
        Toggle        = [ref]$RunHyperV
        Targets       = @('192.168.74.30')
        Port          = $null
        Protocol      = $null
        VaultName     = 'HyperV.192.168.74.30.Credential'
        CredType      = 'PSCredential'
        Modules       = @()
        HelperFile    = 'HyperV'
        ProviderFile  = 'HyperV'
        DashboardFunc = 'Export-HypervDiscoveryDashboardHtml'
        DashboardFile = 'HyperV-Dashboard.html'
        GetDashData   = $null
    }
    LoadMaster = @{
        Toggle        = [ref]$RunLoadMaster
        Targets       = @('loadmaster.corp.local')
        Port          = 443
        Protocol      = 'https'
        VaultName     = 'LoadMaster.loadmaster.corp.local.ApiKey'
        CredType      = 'BearerToken'                # API key (default auth)
        Modules       = @()
        HelperFile    = 'LoadMaster'
        ProviderFile  = 'LoadMaster'
        DashboardFunc = 'Export-LoadMasterDashboardHtml'
        DashboardFile = 'LoadMaster-Dashboard.html'
        GetDashData   = $null
    }
    Nutanix = @{
        Toggle        = [ref]$RunNutanix
        Targets       = @('nutanix-cluster.corp.local')
        Port          = 9440
        Protocol      = 'https'
        VaultName     = 'Nutanix.nutanix-cluster.corp.local.Credential'
        CredType      = 'PSCredential'
        Modules       = @()
        HelperFile    = 'Nutanix'
        ProviderFile  = 'Nutanix'
        DashboardFunc = 'Export-DynamicDashboardHtml'
        DashboardFile = 'Nutanix-Dashboard.html'
        GetDashData   = $null
    }
    OCI = @{
        Toggle        = [ref]$RunOCI
        Targets       = @('oci')                     # Uses OCI CLI config
        Port          = $null
        Protocol      = $null
        VaultName     = 'OCI.Config'
        CredType      = 'OCIConfig'
        Modules       = @()                          # Requires OCI CLI or SDK
        HelperFile    = 'OCI'
        ProviderFile  = 'OCI'
        DashboardFunc = 'Export-DynamicDashboardHtml'
        DashboardFile = 'OCI-Dashboard.html'
        GetDashData   = $null
    }
    Proxmox = @{
        Toggle        = [ref]$RunProxmox
        Targets       = @('192.168.1.30')
        Port          = 8006
        Protocol      = 'https'
        VaultName     = 'Proxmox.192.168.1.30.Token'
        CredType      = 'BearerToken'
        Modules       = @()
        HelperFile    = 'Proxmox'
        ProviderFile  = 'Proxmox'
        DashboardFunc = 'Export-ProxmoxDashboardHtml'
        DashboardFile = 'Proxmox-Dashboard.html'
        GetDashData   = $null
    }
    VMware = @{
        Toggle        = [ref]$RunVMware
        Targets       = @('vcenter.corp.local')
        Port          = 443
        Protocol      = 'https'
        VaultName     = 'VMware.vcenter.corp.local.Credential'
        CredType      = 'PSCredential'
        Modules       = @('VMware.PowerCLI')
        HelperFile    = 'VMware'
        ProviderFile  = 'VMware'
        DashboardFunc = 'Export-VMwareDiscoveryDashboardHtml'
        DashboardFile = 'VMware-Dashboard.html'
        GetDashData   = $null
    }
    WindowsAttributes = @{
        Toggle        = [ref]$RunWindowsAttributes
        Targets       = @('192.168.74.79')           # WUG server (queries WUG-managed devices via WMI)
        Port          = $null
        Protocol      = $null
        VaultName     = 'Windows.WMI.Credential.1'
        CredType      = 'PSCredential'
        Modules       = @()
        HelperFile    = 'WindowsAttributes'
        ProviderFile  = 'WindowsAttributes'
        DashboardFunc = 'Export-WindowsAttributesDashboardHtml'
        DashboardFile = 'WindowsAttributes-Dashboard.html'
        GetDashData   = $null
    }
    WindowsDiskIO = @{
        Toggle        = [ref]$RunWindowsDiskIO
        Targets       = @('192.168.74.79')           # WUG server (queries WUG-managed devices via WMI)
        Port          = $null
        Protocol      = $null
        VaultName     = 'Windows.WMI.Credential.1'
        CredType      = 'PSCredential'
        Modules       = @()
        HelperFile    = 'WindowsDiskIO'
        ProviderFile  = 'WindowsDiskIO'
        DashboardFunc = 'Export-WindowsDiskIODashboardHtml'
        DashboardFile = 'WindowsDiskIO-Dashboard.html'
        GetDashData   = $null
    }
}
# endregion

# ============================================================================
# region Credential Resolver (delegates to shared Resolve-DiscoveryCredential)
# ============================================================================
function Resolve-ProviderCredential {
    param(
        [string]$ProviderName,
        [string]$VaultName,
        [string]$CredType,
        [bool]$AllowPrompt = $true
    )

    $resolveParams = @{ ProviderLabel = $ProviderName; CredType = $CredType }

    if ($VaultName) {
        $resolveParams['Name'] = $VaultName
    } elseif ($ProviderName -eq 'Azure') {
        $resolveParams['Name'] = 'Azure'
    } else {
        if ($AllowPrompt) {
            Write-Warning " No vault name configured for $ProviderName."
            $VaultName = Read-Host -Prompt " Vault credential name for $ProviderName"
            if ([string]::IsNullOrWhiteSpace($VaultName)) { return $null }
            $resolveParams['Name'] = $VaultName
        } else {
            return $null
        }
    }

    if (-not $AllowPrompt) { $resolveParams['NonInteractive'] = $true }

    return (Resolve-DiscoveryCredential @resolveParams)
}
# endregion

# ============================================================================
# region Module Check
# ============================================================================
function Test-ProviderModules {
    param([string[]]$Modules)
    foreach ($mod in $Modules) {
        if (-not (Get-Module -ListAvailable -Name $mod -ErrorAction SilentlyContinue)) {
            return $false
        }
    }
    return $true
}
# endregion

# ============================================================================
# region MAIN — Banner
# ============================================================================
$bannerProviders = @()
foreach ($name in $ProviderConfig.Keys) {
    if ($ProviderConfig[$name].Toggle.Value) { $bannerProviders += $name }
}

Write-Host ''
Write-Host ' ============================================================' -ForegroundColor DarkCyan
Write-Host ' WhatsUpGoldPS Discovery Runner &#x1F977;' -ForegroundColor Cyan
Write-Host ' ============================================================' -ForegroundColor DarkCyan
Write-Host " Providers : $($bannerProviders -join ', ')" -ForegroundColor White
Write-Host " Output : $OutputPath" -ForegroundColor White
Write-Host " Timestamp : $timestamp" -ForegroundColor White
Write-Host ' ============================================================' -ForegroundColor DarkCyan
Write-Host ''
# endregion

# ============================================================================
# region MAIN — Provider Loop
# ============================================================================
$masterPlan = [System.Collections.Generic.List[PSCustomObject]]::new()
$providerSummary = [System.Collections.Generic.List[PSCustomObject]]::new()

foreach ($providerName in $ProviderConfig.Keys) {
    $cfg = $ProviderConfig[$providerName]
    if (-not $cfg.Toggle.Value) { continue }

    Write-Host "--- $providerName ---" -ForegroundColor Cyan

    # ── Step 1: Check modules ────────────────────────────────────────────
    if ($cfg.Modules.Count -gt 0) {
        $modsOk = Test-ProviderModules -Modules $cfg.Modules
        if (-not $modsOk) {
            $missing = $cfg.Modules | Where-Object { -not (Get-Module -ListAvailable -Name $_ -ErrorAction SilentlyContinue) }
            Record-Test -Cmdlet "$providerName Module Check" -Endpoint 'Prerequisites' `
                -Status 'Skipped' -Detail "Missing modules: $($missing -join ', ')"
            $providerSummary.Add([PSCustomObject]@{
                Provider = $providerName; Status = 'Skipped'; Items = 0
                Detail = "Missing: $($missing -join ', ')"
            })
            Write-Host ''
            continue
        }
    }
    Record-Test -Cmdlet "$providerName Module Check" -Endpoint 'Prerequisites' -Status 'Pass'

    # ── Step 2: Load helper + provider scripts ───────────────────────────
    $helperPath   = $helperScripts[$cfg.HelperFile]
    $providerPath = $providerScripts[$cfg.ProviderFile]

    $loadOk = $true
    foreach ($p in @($helperPath, $providerPath)) {
        if ($p -and (Test-Path $p)) {
            try { . $p }
            catch {
                Record-Test -Cmdlet "$providerName Load Scripts" -Endpoint $p -Status 'Fail' -Detail $_.Exception.Message
                $loadOk = $false
            }
        }
        elseif ($p) {
            Record-Test -Cmdlet "$providerName Load Scripts" -Endpoint $p -Status 'Fail' -Detail 'File not found'
            $loadOk = $false
        }
    }
    if (-not $loadOk) {
        $providerSummary.Add([PSCustomObject]@{
            Provider = $providerName; Status = 'Failed'; Items = 0; Detail = 'Script load error'
        })
        Write-Host ''
        continue
    }
    Record-Test -Cmdlet "$providerName Load Scripts" -Endpoint 'Discovery' -Status 'Pass'

    # ── Step 3: Resolve credentials ──────────────────────────────────────
    $cred = Resolve-ProviderCredential -ProviderName $providerName `
        -VaultName $cfg.VaultName `
        -CredType $cfg.CredType `
        -AllowPrompt (-not $NonInteractive)

    if (-not $cred) {
        Record-Test -Cmdlet "$providerName Credentials" -Endpoint 'Vault' `
            -Status 'Skipped' -Detail 'No credential available'
        $providerSummary.Add([PSCustomObject]@{
            Provider = $providerName; Status = 'Skipped'; Items = 0; Detail = 'No credential'
        })
        Write-Host ''
        continue
    }
    Record-Test -Cmdlet "$providerName Credentials" -Endpoint 'Vault' -Status 'Pass'

    # ── Step 4: Run discovery ────────────────────────────────────────────
    $plan = $null
    $discoveryParams = @{
        ProviderName = $providerName
        Target       = $cfg.Targets
    }
    if ($cfg.Port)     { $discoveryParams.ApiPort     = $cfg.Port }
    if ($cfg.Protocol) { $discoveryParams.ApiProtocol = $cfg.Protocol }

    # Provider-specific credential passing
    switch ($providerName) {
        'AWS' {
            # AWS needs Connect-AWSProfile, not Invoke-Discovery directly
            $bstr = [System.Runtime.InteropServices.Marshal]::SecureStringToBSTR($cred.Password)
            try { $plainSK = [System.Runtime.InteropServices.Marshal]::PtrToStringAuto($bstr) }
            finally { [System.Runtime.InteropServices.Marshal]::ZeroFreeBSTR($bstr) }
            try {
                Connect-AWSProfile -AccessKey $cred.UserName -SecretKey $plainSK -Region $cfg.Targets[0]
                $discoveryParams.AttributeValue = 'true'
            }
            catch {
                Record-Test -Cmdlet "$providerName Connect" -Endpoint $cfg.Targets[0] `
                    -Status 'Fail' -Detail $_.Exception.Message
                $providerSummary.Add([PSCustomObject]@{
                    Provider = $providerName; Status = 'Failed'; Items = 0; Detail = 'Connect failed'
                })
                Write-Host ''
                continue
            }
        }
        'Azure' {
            # Parse TenantId|AppId from credential
            $parts = $cred.UserName -split '\|', 2
            $tenantId = $parts[0]
            $appId    = $parts[1]
            $bstr = [System.Runtime.InteropServices.Marshal]::SecureStringToBSTR($cred.Password)
            try { $clientSecret = [System.Runtime.InteropServices.Marshal]::PtrToStringAuto($bstr) }
            finally { [System.Runtime.InteropServices.Marshal]::ZeroFreeBSTR($bstr) }
            try {
                $secSecret = ConvertTo-SecureString $clientSecret -AsPlainText -Force
                $azCred = [PSCredential]::new($appId, $secSecret)
                Connect-AzAccount -ServicePrincipal -Credential $azCred -Tenant $tenantId -ErrorAction Stop | Out-Null
                $discoveryParams.AttributeValue = $tenantId
            }
            catch {
                Record-Test -Cmdlet "$providerName Connect" -Endpoint "Tenant:$tenantId" `
                    -Status 'Fail' -Detail $_.Exception.Message
                $providerSummary.Add([PSCustomObject]@{
                    Provider = $providerName; Status = 'Failed'; Items = 0; Detail = 'Auth failed'
                })
                Write-Host ''
                continue
            }
        }
        'Fortinet' {
            $discoveryParams.Credential = @{ ApiToken = $cred }
        }
        'LoadMaster' {
            $discoveryParams.Credential = @{ ApiKey = $cred }
        }
        'Proxmox' {
            $discoveryParams.Credential = @{ ApiToken = $cred }
        }
        'HyperV' {
            $hvCred = $cred
            if ($cred -is [string] -and $cred -match '\|') {
                $parts = $cred -split '\|', 2
                $secPwd = ConvertTo-SecureString $parts[1] -AsPlainText -Force
                $hvCred = [PSCredential]::new($parts[0], $secPwd)
            }
            $discoveryParams.Credential = @{
                Username     = $hvCred.UserName
                Password     = $hvCred.GetNetworkCredential().Password
                PSCredential = $hvCred
            }
        }
        'F5' {
            $f5Cred = $cred
            if ($cred -is [string] -and $cred -match '\|') {
                $parts = $cred -split '\|', 2
                $secPwd = ConvertTo-SecureString $parts[1] -AsPlainText -Force
                $f5Cred = [PSCredential]::new($parts[0], $secPwd)
            }
            $discoveryParams.Credential = @{
                Username     = $f5Cred.UserName
                Password     = $f5Cred.GetNetworkCredential().Password
                PSCredential = $f5Cred
            }
        }
        'VMware' {
            $vmCred = $cred
            if ($cred -is [string] -and $cred -match '\|') {
                $parts = $cred -split '\|', 2
                $secPwd = ConvertTo-SecureString $parts[1] -AsPlainText -Force
                $vmCred = [PSCredential]::new($parts[0], $secPwd)
            }
            $discoveryParams.Credential = @{
                Username     = $vmCred.UserName
                Password     = $vmCred.GetNetworkCredential().Password
                PSCredential = $vmCred
            }
        }
        { $_ -in 'WindowsAttributes', 'WindowsDiskIO' } {
            # Windows providers use WMI — pass PSCredential directly
            $winCred = $cred
            if ($cred -is [string] -and $cred -match '\|') {
                $parts = $cred -split '\|', 2
                $secPwd = ConvertTo-SecureString $parts[1] -AsPlainText -Force
                $winCred = [PSCredential]::new($parts[0], $secPwd)
            }
            $discoveryParams.Credential = @{
                Username     = $winCred.UserName
                Password     = $winCred.GetNetworkCredential().Password
                PSCredential = $winCred
            }
        }
    }

    Invoke-Test -Cmdlet "$providerName Discovery" -Endpoint ($cfg.Targets -join ', ') -Test {
        $script:currentPlan = Invoke-Discovery @discoveryParams
        if (-not $script:currentPlan -or $script:currentPlan.Count -eq 0) {
            throw "No items discovered"
        }
    }
    $plan = $script:currentPlan

    if (-not $plan -or $plan.Count -eq 0) {
        $providerSummary.Add([PSCustomObject]@{
            Provider = $providerName; Status = 'Failed'; Items = 0; Detail = 'No items discovered'
        })
        Write-Host ''
        continue
    }

    $activeCount = @($plan | Where-Object { $_.ItemType -eq 'ActiveMonitor' }).Count
    $perfCount   = @($plan | Where-Object { $_.ItemType -eq 'PerformanceMonitor' }).Count
    Write-Host " Discovered $($plan.Count) items ($activeCount active, $perfCount perf)" -ForegroundColor Green

    # Add to master plan
    foreach ($item in $plan) { $masterPlan.Add($item) }

    # ── Step 5: Export plan (JSON + CSV) ─────────────────────────────────
    $planJsonPath = Join-Path $OutputPath "${providerName}-plan-${timestamp}.json"
    $planCsvPath  = Join-Path $OutputPath "${providerName}-plan-${timestamp}.csv"

    Invoke-Test -Cmdlet "$providerName Export JSON" -Endpoint $planJsonPath -Test {
        $plan | Export-DiscoveryPlan -Format JSON -Path $planJsonPath -IncludeParams
    }
    Invoke-Test -Cmdlet "$providerName Export CSV" -Endpoint $planCsvPath -Test {
        $plan | Export-DiscoveryPlan -Format CSV -Path $planCsvPath
    }

    # ── Step 6: Generate dashboard ───────────────────────────────────────
    if (-not $SkipDashboard -and $cfg.DashboardFunc) {
        $dashPath = Join-Path $OutputPath $cfg.DashboardFile
        $dashFunc = $cfg.DashboardFunc

        Invoke-Test -Cmdlet "$providerName Dashboard" -Endpoint $dashPath -Test {
            if (-not (Get-Command -Name $dashFunc -ErrorAction SilentlyContinue)) {
                throw "Dashboard function '$dashFunc' not available"
            }

            # Build dashboard data from the plan's device attributes
            $dashData = @()
            $deviceGroups = $plan | Group-Object -Property DeviceName

            foreach ($group in $deviceGroups) {
                $first = $group.Group[0]
                $attrs = $first.Attributes
                if (-not $attrs) { $attrs = @{} }

                # Generic row — providers with custom dashboard funcs may need specific shapes
                $row = [PSCustomObject]@{
                    DeviceName     = $first.DeviceName
                    DeviceIP       = $first.DeviceIP
                    Provider       = $providerName
                    ActiveMonitors = @($group.Group | Where-Object { $_.ItemType -eq 'ActiveMonitor' }).Count
                    PerfMonitors   = @($group.Group | Where-Object { $_.ItemType -eq 'PerformanceMonitor' }).Count
                    TotalItems     = $group.Group.Count
                }
                $dashData += $row
            }

            & $dashFunc -DashboardData $dashData -OutputPath $dashPath -ReportTitle "$providerName Dashboard"
        }
    }
    elseif ($SkipDashboard) {
        Record-Test -Cmdlet "$providerName Dashboard" -Endpoint '(skipped)' -Status 'Skipped' -Detail '-SkipDashboard'
    }
    else {
        Record-Test -Cmdlet "$providerName Dashboard" -Endpoint '(none)' -Status 'Skipped' -Detail 'No dashboard function'
    }

    $providerSummary.Add([PSCustomObject]@{
        Provider = $providerName; Status = 'OK'; Items = $plan.Count
        Detail = "$activeCount active, $perfCount perf"
    })

    Write-Host ''
}
# endregion

# ============================================================================
# region Master Plan Export
# ============================================================================
Write-Host '--- Master Plan ---' -ForegroundColor Cyan

if ($masterPlan.Count -gt 0) {
    $masterJsonPath = Join-Path $OutputPath "MasterPlan-${timestamp}.json"
    $masterCsvPath  = Join-Path $OutputPath "MasterPlan-${timestamp}.csv"

    Invoke-Test -Cmdlet 'Master Plan JSON' -Endpoint $masterJsonPath -Test {
        @($masterPlan) | Export-DiscoveryPlan -Format JSON -Path $masterJsonPath -IncludeParams
    }
    Invoke-Test -Cmdlet 'Master Plan CSV' -Endpoint $masterCsvPath -Test {
        @($masterPlan) | Export-DiscoveryPlan -Format CSV -Path $masterCsvPath
    }

    Write-Host ''
    Write-Host " Total discovered items: $($masterPlan.Count)" -ForegroundColor Green

    # Quick WUG monitor summary
    $monitorSummary = $masterPlan | Group-Object -Property { "$($_.ProviderName)/$($_.ItemType)" } |
        Sort-Object Name |
        ForEach-Object {
            [PSCustomObject]@{
                Category = $_.Name
                Count    = $_.Count
            }
        }
    Write-Host ''
    Write-Host ' WUG Monitor Blueprint:' -ForegroundColor Cyan
    foreach ($s in $monitorSummary) {
        Write-Host " $($s.Category): $($s.Count)" -ForegroundColor White
    }
}
else {
    Record-Test -Cmdlet 'Master Plan' -Endpoint '(empty)' -Status 'Skipped' -Detail 'No items discovered'
}
# endregion

# ============================================================================
# region Provider Summary Table
# ============================================================================
Write-Host ''
Write-Host '--- Provider Summary ---' -ForegroundColor Cyan
$providerSummary | Format-Table -AutoSize
# endregion

# ============================================================================
# region HTML Test Report
# ============================================================================
$templatePath = Join-Path $scriptDir 'Test-Dashboard-Template.html'
$reportPath   = Join-Path $OutputPath "DiscoveryRunner-Report-${timestamp}.html"

$exported = Export-TestResultsHtml -TestResults $script:TestResults `
    -OutputPath $reportPath `
    -TemplatePath $templatePath `
    -ReportTitle "Discovery Runner — $timestamp"

if ($exported) {
    Write-Host "Test report: $reportPath" -ForegroundColor Green
}
# endregion

# ============================================================================
# region Final Summary
# ============================================================================
$pass    = @($script:TestResults | Where-Object { $_.Status -eq 'Pass' }).Count
$fail    = @($script:TestResults | Where-Object { $_.Status -eq 'Fail' }).Count
$skipped = @($script:TestResults | Where-Object { $_.Status -eq 'Skipped' }).Count

Write-Host ''
Write-Host ' ============================================================' -ForegroundColor DarkCyan
Write-Host ' Discovery Runner Complete' -ForegroundColor Cyan
Write-Host " Pass: $pass | Fail: $fail | Skipped: $skipped" -ForegroundColor $(if ($fail -gt 0) { 'Red' } else { 'Green' })
Write-Host " Master plan items: $($masterPlan.Count)" -ForegroundColor White
Write-Host " Output: $OutputPath" -ForegroundColor White
Write-Host ' ============================================================' -ForegroundColor DarkCyan
Write-Host ''
# endregion

# SIG # Begin signature block
# MIIr+wYJKoZIhvcNAQcCoIIr7DCCK+gCAQExDzANBglghkgBZQMEAgEFADB5Bgor
# BgEEAYI3AgEEoGswaTA0BgorBgEEAYI3AgEeMCYCAwEAAAQQH8w7YFlLCE63JNLG
# KX7zUQIBAAIBAAIBAAIBAAIBADAxMA0GCWCGSAFlAwQCAQUABCCyqF8susyMCcps
# aQfnjE15DMJeEPjjOcl665+R4Xy9KKCCJQ0wggVvMIIEV6ADAgECAhBI/JO0YFWU
# jTanyYqJ1pQWMA0GCSqGSIb3DQEBDAUAMHsxCzAJBgNVBAYTAkdCMRswGQYDVQQI
# DBJHcmVhdGVyIE1hbmNoZXN0ZXIxEDAOBgNVBAcMB1NhbGZvcmQxGjAYBgNVBAoM
# EUNvbW9kbyBDQSBMaW1pdGVkMSEwHwYDVQQDDBhBQUEgQ2VydGlmaWNhdGUgU2Vy
# dmljZXMwHhcNMjEwNTI1MDAwMDAwWhcNMjgxMjMxMjM1OTU5WjBWMQswCQYDVQQG
# EwJHQjEYMBYGA1UEChMPU2VjdGlnbyBMaW1pdGVkMS0wKwYDVQQDEyRTZWN0aWdv
# IFB1YmxpYyBDb2RlIFNpZ25pbmcgUm9vdCBSNDYwggIiMA0GCSqGSIb3DQEBAQUA
# A4ICDwAwggIKAoICAQCN55QSIgQkdC7/FiMCkoq2rjaFrEfUI5ErPtx94jGgUW+s
# hJHjUoq14pbe0IdjJImK/+8Skzt9u7aKvb0Ffyeba2XTpQxpsbxJOZrxbW6q5KCD
# J9qaDStQ6Utbs7hkNqR+Sj2pcaths3OzPAsM79szV+W+NDfjlxtd/R8SPYIDdub7
# P2bSlDFp+m2zNKzBenjcklDyZMeqLQSrw2rq4C+np9xu1+j/2iGrQL+57g2extme
# me/G3h+pDHazJyCh1rr9gOcB0u/rgimVcI3/uxXP/tEPNqIuTzKQdEZrRzUTdwUz
# T2MuuC3hv2WnBGsY2HH6zAjybYmZELGt2z4s5KoYsMYHAXVn3m3pY2MeNn9pib6q
# RT5uWl+PoVvLnTCGMOgDs0DGDQ84zWeoU4j6uDBl+m/H5x2xg3RpPqzEaDux5mcz
# mrYI4IAFSEDu9oJkRqj1c7AGlfJsZZ+/VVscnFcax3hGfHCqlBuCF6yH6bbJDoEc
# QNYWFyn8XJwYK+pF9e+91WdPKF4F7pBMeufG9ND8+s0+MkYTIDaKBOq3qgdGnA2T
# OglmmVhcKaO5DKYwODzQRjY1fJy67sPV+Qp2+n4FG0DKkjXp1XrRtX8ArqmQqsV/
# AZwQsRb8zG4Y3G9i/qZQp7h7uJ0VP/4gDHXIIloTlRmQAOka1cKG8eOO7F/05QID
# AQABo4IBEjCCAQ4wHwYDVR0jBBgwFoAUoBEKIz6W8Qfs4q8p74Klf9AwpLQwHQYD
# VR0OBBYEFDLrkpr/NZZILyhAQnAgNpFcF4XmMA4GA1UdDwEB/wQEAwIBhjAPBgNV
# HRMBAf8EBTADAQH/MBMGA1UdJQQMMAoGCCsGAQUFBwMDMBsGA1UdIAQUMBIwBgYE
# VR0gADAIBgZngQwBBAEwQwYDVR0fBDwwOjA4oDagNIYyaHR0cDovL2NybC5jb21v
# ZG9jYS5jb20vQUFBQ2VydGlmaWNhdGVTZXJ2aWNlcy5jcmwwNAYIKwYBBQUHAQEE
# KDAmMCQGCCsGAQUFBzABhhhodHRwOi8vb2NzcC5jb21vZG9jYS5jb20wDQYJKoZI
# hvcNAQEMBQADggEBABK/oe+LdJqYRLhpRrWrJAoMpIpnuDqBv0WKfVIHqI0fTiGF
# OaNrXi0ghr8QuK55O1PNtPvYRL4G2VxjZ9RAFodEhnIq1jIV9RKDwvnhXRFAZ/ZC
# J3LFI+ICOBpMIOLbAffNRk8monxmwFE2tokCVMf8WPtsAO7+mKYulaEMUykfb9gZ
# pk+e96wJ6l2CxouvgKe9gUhShDHaMuwV5KZMPWw5c9QLhTkg4IUaaOGnSDip0TYl
# d8GNGRbFiExmfS9jzpjoad+sPKhdnckcW67Y8y90z7h+9teDnRGWYpquRRPaf9xH
# +9/DUp/mBlXpnYzyOmJRvOwkDynUWICE5EV7WtgwggWNMIIEdaADAgECAhAOmxiO
# +dAt5+/bUOIIQBhaMA0GCSqGSIb3DQEBDAUAMGUxCzAJBgNVBAYTAlVTMRUwEwYD
# VQQKEwxEaWdpQ2VydCBJbmMxGTAXBgNVBAsTEHd3dy5kaWdpY2VydC5jb20xJDAi
# BgNVBAMTG0RpZ2lDZXJ0IEFzc3VyZWQgSUQgUm9vdCBDQTAeFw0yMjA4MDEwMDAw
# MDBaFw0zMTExMDkyMzU5NTlaMGIxCzAJBgNVBAYTAlVTMRUwEwYDVQQKEwxEaWdp
# Q2VydCBJbmMxGTAXBgNVBAsTEHd3dy5kaWdpY2VydC5jb20xITAfBgNVBAMTGERp
# Z2lDZXJ0IFRydXN0ZWQgUm9vdCBHNDCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCC
# AgoCggIBAL/mkHNo3rvkXUo8MCIwaTPswqclLskhPfKK2FnC4SmnPVirdprNrnsb
# hA3EMB/zG6Q4FutWxpdtHauyefLKEdLkX9YFPFIPUh/GnhWlfr6fqVcWWVVyr2iT
# cMKyunWZanMylNEQRBAu34LzB4TmdDttceItDBvuINXJIB1jKS3O7F5OyJP4IWGb
# NOsFxl7sWxq868nPzaw0QF+xembud8hIqGZXV59UWI4MK7dPpzDZVu7Ke13jrclP
# XuU15zHL2pNe3I6PgNq2kZhAkHnDeMe2scS1ahg4AxCN2NQ3pC4FfYj1gj4QkXCr
# VYJBMtfbBHMqbpEBfCFM1LyuGwN1XXhm2ToxRJozQL8I11pJpMLmqaBn3aQnvKFP
# ObURWBf3JFxGj2T3wWmIdph2PVldQnaHiZdpekjw4KISG2aadMreSx7nDmOu5tTv
# kpI6nj3cAORFJYm2mkQZK37AlLTSYW3rM9nF30sEAMx9HJXDj/chsrIRt7t/8tWM
# cCxBYKqxYxhElRp2Yn72gLD76GSmM9GJB+G9t+ZDpBi4pncB4Q+UDCEdslQpJYls
# 5Q5SUUd0viastkF13nqsX40/ybzTQRESW+UQUOsxxcpyFiIJ33xMdT9j7CFfxCBR
# a2+xq4aLT8LWRV+dIPyhHsXAj6KxfgommfXkaS+YHS312amyHeUbAgMBAAGjggE6
# MIIBNjAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQWBBTs1+OC0nFdZEzfLmc/57qY
# rhwPTzAfBgNVHSMEGDAWgBRF66Kv9JLLgjEtUYunpyGd823IDzAOBgNVHQ8BAf8E
# BAMCAYYweQYIKwYBBQUHAQEEbTBrMCQGCCsGAQUFBzABhhhodHRwOi8vb2NzcC5k
# aWdpY2VydC5jb20wQwYIKwYBBQUHMAKGN2h0dHA6Ly9jYWNlcnRzLmRpZ2ljZXJ0
# LmNvbS9EaWdpQ2VydEFzc3VyZWRJRFJvb3RDQS5jcnQwRQYDVR0fBD4wPDA6oDig
# NoY0aHR0cDovL2NybDMuZGlnaWNlcnQuY29tL0RpZ2lDZXJ0QXNzdXJlZElEUm9v
# dENBLmNybDARBgNVHSAECjAIMAYGBFUdIAAwDQYJKoZIhvcNAQEMBQADggEBAHCg
# v0NcVec4X6CjdBs9thbX979XB72arKGHLOyFXqkauyL4hxppVCLtpIh3bb0aFPQT
# SnovLbc47/T/gLn4offyct4kvFIDyE7QKt76LVbP+fT3rDB6mouyXtTP0UNEm0Mh
# 65ZyoUi0mcudT6cGAxN3J0TU53/oWajwvy8LpunyNDzs9wPHh6jSTEAZNUZqaVSw
# uKFWjuyk1T3osdz9HNj0d1pcVIxv76FQPfx2CWiEn2/K2yCNNWAcAgPLILCsWKAO
# QGPFmCLBsln1VWvPJ6tsds5vIy30fnFqI2si/xK4VC0nftg62fC2h5b9W9FcrBjD
# TZ9ztwGpn1eqXijiuZQwggYaMIIEAqADAgECAhBiHW0MUgGeO5B5FSCJIRwKMA0G
# CSqGSIb3DQEBDAUAMFYxCzAJBgNVBAYTAkdCMRgwFgYDVQQKEw9TZWN0aWdvIExp
# bWl0ZWQxLTArBgNVBAMTJFNlY3RpZ28gUHVibGljIENvZGUgU2lnbmluZyBSb290
# IFI0NjAeFw0yMTAzMjIwMDAwMDBaFw0zNjAzMjEyMzU5NTlaMFQxCzAJBgNVBAYT
# AkdCMRgwFgYDVQQKEw9TZWN0aWdvIExpbWl0ZWQxKzApBgNVBAMTIlNlY3RpZ28g
# UHVibGljIENvZGUgU2lnbmluZyBDQSBSMzYwggGiMA0GCSqGSIb3DQEBAQUAA4IB
# jwAwggGKAoIBgQCbK51T+jU/jmAGQ2rAz/V/9shTUxjIztNsfvxYB5UXeWUzCxEe
# AEZGbEN4QMgCsJLZUKhWThj/yPqy0iSZhXkZ6Pg2A2NVDgFigOMYzB2OKhdqfWGV
# oYW3haT29PSTahYkwmMv0b/83nbeECbiMXhSOtbam+/36F09fy1tsB8je/RV0mIk
# 8XL/tfCK6cPuYHE215wzrK0h1SWHTxPbPuYkRdkP05ZwmRmTnAO5/arnY83jeNzh
# P06ShdnRqtZlV59+8yv+KIhE5ILMqgOZYAENHNX9SJDm+qxp4VqpB3MV/h53yl41
# aHU5pledi9lCBbH9JeIkNFICiVHNkRmq4TpxtwfvjsUedyz8rNyfQJy/aOs5b4s+
# ac7IH60B+Ja7TVM+EKv1WuTGwcLmoU3FpOFMbmPj8pz44MPZ1f9+YEQIQty/NQd/
# 2yGgW+ufflcZ/ZE9o1M7a5Jnqf2i2/uMSWymR8r2oQBMdlyh2n5HirY4jKnFH/9g
# Rvd+QOfdRrJZb1sCAwEAAaOCAWQwggFgMB8GA1UdIwQYMBaAFDLrkpr/NZZILyhA
# QnAgNpFcF4XmMB0GA1UdDgQWBBQPKssghyi47G9IritUpimqF6TNDDAOBgNVHQ8B
# Af8EBAMCAYYwEgYDVR0TAQH/BAgwBgEB/wIBADATBgNVHSUEDDAKBggrBgEFBQcD
# AzAbBgNVHSAEFDASMAYGBFUdIAAwCAYGZ4EMAQQBMEsGA1UdHwREMEIwQKA+oDyG
# Omh0dHA6Ly9jcmwuc2VjdGlnby5jb20vU2VjdGlnb1B1YmxpY0NvZGVTaWduaW5n
# Um9vdFI0Ni5jcmwwewYIKwYBBQUHAQEEbzBtMEYGCCsGAQUFBzAChjpodHRwOi8v
# Y3J0LnNlY3RpZ28uY29tL1NlY3RpZ29QdWJsaWNDb2RlU2lnbmluZ1Jvb3RSNDYu
# cDdjMCMGCCsGAQUFBzABhhdodHRwOi8vb2NzcC5zZWN0aWdvLmNvbTANBgkqhkiG
# 9w0BAQwFAAOCAgEABv+C4XdjNm57oRUgmxP/BP6YdURhw1aVcdGRP4Wh60BAscjW
# 4HL9hcpkOTz5jUug2oeunbYAowbFC2AKK+cMcXIBD0ZdOaWTsyNyBBsMLHqafvIh
# rCymlaS98+QpoBCyKppP0OcxYEdU0hpsaqBBIZOtBajjcw5+w/KeFvPYfLF/ldYp
# mlG+vd0xqlqd099iChnyIMvY5HexjO2AmtsbpVn0OhNcWbWDRF/3sBp6fWXhz7Dc
# ML4iTAWS+MVXeNLj1lJziVKEoroGs9Mlizg0bUMbOalOhOfCipnx8CaLZeVme5yE
# Lg09Jlo8BMe80jO37PU8ejfkP9/uPak7VLwELKxAMcJszkyeiaerlphwoKx1uHRz
# NyE6bxuSKcutisqmKL5OTunAvtONEoteSiabkPVSZ2z76mKnzAfZxCl/3dq3dUNw
# 4rg3sTCggkHSRqTqlLMS7gjrhTqBmzu1L90Y1KWN/Y5JKdGvspbOrTfOXyXvmPL6
# E52z1NZJ6ctuMFBQZH3pwWvqURR8AgQdULUvrxjUYbHHj95Ejza63zdrEcxWLDX6
# xWls/GDnVNueKjWUH3fTv1Y8Wdho698YADR7TNx8X8z2Bev6SivBBOHY+uqiirZt
# g0y9ShQoPzmCcn63Syatatvx157YK9hlcPmVoa1oDE5/L9Uo2bC5a4CH2RwwggY+
# MIIEpqADAgECAhAHnODk0RR/hc05c892LTfrMA0GCSqGSIb3DQEBDAUAMFQxCzAJ
# BgNVBAYTAkdCMRgwFgYDVQQKEw9TZWN0aWdvIExpbWl0ZWQxKzApBgNVBAMTIlNl
# Y3RpZ28gUHVibGljIENvZGUgU2lnbmluZyBDQSBSMzYwHhcNMjYwMjA5MDAwMDAw
# WhcNMjkwNDIxMjM1OTU5WjBVMQswCQYDVQQGEwJVUzEUMBIGA1UECAwLQ29ubmVj
# dGljdXQxFzAVBgNVBAoMDkphc29uIEFsYmVyaW5vMRcwFQYDVQQDDA5KYXNvbiBB
# bGJlcmlubzCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAPN6aN4B1yYW
# kI5b5TBj3I0VV/peETrHb6EY4BHGxt8Ap+eT+WpEpJyEtRYPxEmNJL3A38Bkg7mw
# zPE3/1NK570ZBCuBjSAn4mSDIgIuXZnvyBO9W1OQs5d67MlJLUAEufl18tOr3ST1
# DeO9gSjQSAE5Nql0QDxPnm93OZBon+Fz3CmE+z3MwAe2h4KdtRAnCqwM+/V7iBdb
# w+JOxolpx+7RVjGyProTENIG3pe/hKvPb501lf8uBAADLdjZr5ip8vIWbf857Yw1
# Bu10nVI7HW3eE8Cl5//d1ribHlzTzQLfttW+k+DaFsKZBBL56l4YAlIVRsrOiE1k
# dHYYx6IGrEA809R7+TZA9DzGqyFiv9qmJAbL4fDwetDeyIq+Oztz1LvEdy8Rcd0J
# BY+J4S0eDEFIA3X0N8VcLeAwabKb9AjulKXwUeqCJLvN79CJ90UTZb2+I+tamj0d
# n+IKMEsJ4v4Ggx72sxFr9+6XziodtTg5Luf2xd6+PhhamOxF2px9LObhBLLEMyRs
# CHZIzVZOFKu9BpHQH7ufGB+Sa80Tli0/6LEyn9+bMYWi2ttn6lLOPThXMiQaooRU
# q6q2u3+F4SaPlxVFLI7OJVMhar6nW6joBvELTJPmANSMjDSRFDfHRCdGbZsL/keE
# LJNy+jZctF6VvxQEjFM8/bazu6qYhrA7AgMBAAGjggGJMIIBhTAfBgNVHSMEGDAW
# gBQPKssghyi47G9IritUpimqF6TNDDAdBgNVHQ4EFgQU6YF0o0D5AVhKHbVocr8G
# aSIBibAwDgYDVR0PAQH/BAQDAgeAMAwGA1UdEwEB/wQCMAAwEwYDVR0lBAwwCgYI
# KwYBBQUHAwMwSgYDVR0gBEMwQTA1BgwrBgEEAbIxAQIBAwIwJTAjBggrBgEFBQcC
# ARYXaHR0cHM6Ly9zZWN0aWdvLmNvbS9DUFMwCAYGZ4EMAQQBMEkGA1UdHwRCMEAw
# PqA8oDqGOGh0dHA6Ly9jcmwuc2VjdGlnby5jb20vU2VjdGlnb1B1YmxpY0NvZGVT
# aWduaW5nQ0FSMzYuY3JsMHkGCCsGAQUFBwEBBG0wazBEBggrBgEFBQcwAoY4aHR0
# cDovL2NydC5zZWN0aWdvLmNvbS9TZWN0aWdvUHVibGljQ29kZVNpZ25pbmdDQVIz
# Ni5jcnQwIwYIKwYBBQUHMAGGF2h0dHA6Ly9vY3NwLnNlY3RpZ28uY29tMA0GCSqG
# SIb3DQEBDAUAA4IBgQAEIsm4xnOd/tZMVrKwi3doAXvCwOA/RYQnFJD7R/bSQRu3
# wXEK4o9SIefye18B/q4fhBkhNAJuEvTQAGfqbbpxow03J5PrDTp1WPCWbXKX8Oz9
# vGWJFyJxRGftkdzZ57JE00synEMS8XCwLO9P32MyR9Z9URrpiLPJ9rQjfHMb1BUd
# vaNayomm7aWLAnD+X7jm6o8sNT5An1cwEAob7obWDM6sX93wphwJNBJAstH9Ozs6
# LwISOX6sKS7CKm9N3Kp8hOUue0ZHAtZdFl6o5u12wy+zzieGEI50fKnN77FfNKFO
# WKlS6OJwlArcbFegB5K89LcE5iNSmaM3VMB2ADV1FEcjGSHw4lTg1Wx+WMAMdl/7
# nbvfFxJ9uu5tNiT54B0s+lZO/HztwXYQUczdsFon3pjsNrsk9ZlalBi5SHkIu+F6
# g7tWiEv3rtVApmJRnLkUr2Xq2a4nbslUCt4jKs5UX4V1nSX8OM++AXoyVGO+iTj7
# z+pl6XE9Gw/Td6WKKKswgga0MIIEnKADAgECAhANx6xXBf8hmS5AQyIMOkmGMA0G
# CSqGSIb3DQEBCwUAMGIxCzAJBgNVBAYTAlVTMRUwEwYDVQQKEwxEaWdpQ2VydCBJ
# bmMxGTAXBgNVBAsTEHd3dy5kaWdpY2VydC5jb20xITAfBgNVBAMTGERpZ2lDZXJ0
# IFRydXN0ZWQgUm9vdCBHNDAeFw0yNTA1MDcwMDAwMDBaFw0zODAxMTQyMzU5NTla
# MGkxCzAJBgNVBAYTAlVTMRcwFQYDVQQKEw5EaWdpQ2VydCwgSW5jLjFBMD8GA1UE
# AxM4RGlnaUNlcnQgVHJ1c3RlZCBHNCBUaW1lU3RhbXBpbmcgUlNBNDA5NiBTSEEy
# NTYgMjAyNSBDQTEwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQC0eDHT
# CphBcr48RsAcrHXbo0ZodLRRF51NrY0NlLWZloMsVO1DahGPNRcybEKq+RuwOnPh
# of6pvF4uGjwjqNjfEvUi6wuim5bap+0lgloM2zX4kftn5B1IpYzTqpyFQ/4Bt0mA
# xAHeHYNnQxqXmRinvuNgxVBdJkf77S2uPoCj7GH8BLuxBG5AvftBdsOECS1UkxBv
# MgEdgkFiDNYiOTx4OtiFcMSkqTtF2hfQz3zQSku2Ws3IfDReb6e3mmdglTcaarps
# 0wjUjsZvkgFkriK9tUKJm/s80FiocSk1VYLZlDwFt+cVFBURJg6zMUjZa/zbCclF
# 83bRVFLeGkuAhHiGPMvSGmhgaTzVyhYn4p0+8y9oHRaQT/aofEnS5xLrfxnGpTXi
# UOeSLsJygoLPp66bkDX1ZlAeSpQl92QOMeRxykvq6gbylsXQskBBBnGy3tW/AMOM
# CZIVNSaz7BX8VtYGqLt9MmeOreGPRdtBx3yGOP+rx3rKWDEJlIqLXvJWnY0v5ydP
# pOjL6s36czwzsucuoKs7Yk/ehb//Wx+5kMqIMRvUBDx6z1ev+7psNOdgJMoiwOrU
# G2ZdSoQbU2rMkpLiQ6bGRinZbI4OLu9BMIFm1UUl9VnePs6BaaeEWvjJSjNm2qA+
# sdFUeEY0qVjPKOWug/G6X5uAiynM7Bu2ayBjUwIDAQABo4IBXTCCAVkwEgYDVR0T
# AQH/BAgwBgEB/wIBADAdBgNVHQ4EFgQU729TSunkBnx6yuKQVvYv1Ensy04wHwYD
# VR0jBBgwFoAU7NfjgtJxXWRM3y5nP+e6mK4cD08wDgYDVR0PAQH/BAQDAgGGMBMG
# A1UdJQQMMAoGCCsGAQUFBwMIMHcGCCsGAQUFBwEBBGswaTAkBggrBgEFBQcwAYYY
# aHR0cDovL29jc3AuZGlnaWNlcnQuY29tMEEGCCsGAQUFBzAChjVodHRwOi8vY2Fj
# ZXJ0cy5kaWdpY2VydC5jb20vRGlnaUNlcnRUcnVzdGVkUm9vdEc0LmNydDBDBgNV
# HR8EPDA6MDigNqA0hjJodHRwOi8vY3JsMy5kaWdpY2VydC5jb20vRGlnaUNlcnRU
# cnVzdGVkUm9vdEc0LmNybDAgBgNVHSAEGTAXMAgGBmeBDAEEAjALBglghkgBhv1s
# BwEwDQYJKoZIhvcNAQELBQADggIBABfO+xaAHP4HPRF2cTC9vgvItTSmf83Qh8WI
# GjB/T8ObXAZz8OjuhUxjaaFdleMM0lBryPTQM2qEJPe36zwbSI/mS83afsl3YTj+
# IQhQE7jU/kXjjytJgnn0hvrV6hqWGd3rLAUt6vJy9lMDPjTLxLgXf9r5nWMQwr8M
# yb9rEVKChHyfpzee5kH0F8HABBgr0UdqirZ7bowe9Vj2AIMD8liyrukZ2iA/wdG2
# th9y1IsA0QF8dTXqvcnTmpfeQh35k5zOCPmSNq1UH410ANVko43+Cdmu4y81hjaj
# V/gxdEkMx1NKU4uHQcKfZxAvBAKqMVuqte69M9J6A47OvgRaPs+2ykgcGV00TYr2
# Lr3ty9qIijanrUR3anzEwlvzZiiyfTPjLbnFRsjsYg39OlV8cipDoq7+qNNjqFze
# GxcytL5TTLL4ZaoBdqbhOhZ3ZRDUphPvSRmMThi0vw9vODRzW6AxnJll38F0cuJG
# 7uEBYTptMSbhdhGQDpOXgpIUsWTjd6xpR6oaQf/DJbg3s6KCLPAlZ66RzIg9sC+N
# Jpud/v4+7RWsWCiKi9EOLLHfMR2ZyJ/+xhCx9yHbxtl5TPau1j/1MIDpMPx0LckT
# etiSuEtQvLsNz3Qbp7wGWqbIiOWCnb5WqxL3/BAPvIXKUjPSxyZsq8WhbaM2tszW
# kPZPubdcMIIG7TCCBNWgAwIBAgIQCoDvGEuN8QWC0cR2p5V0aDANBgkqhkiG9w0B
# AQsFADBpMQswCQYDVQQGEwJVUzEXMBUGA1UEChMORGlnaUNlcnQsIEluYy4xQTA/
# BgNVBAMTOERpZ2lDZXJ0IFRydXN0ZWQgRzQgVGltZVN0YW1waW5nIFJTQTQwOTYg
# U0hBMjU2IDIwMjUgQ0ExMB4XDTI1MDYwNDAwMDAwMFoXDTM2MDkwMzIzNTk1OVow
# YzELMAkGA1UEBhMCVVMxFzAVBgNVBAoTDkRpZ2lDZXJ0LCBJbmMuMTswOQYDVQQD
# EzJEaWdpQ2VydCBTSEEyNTYgUlNBNDA5NiBUaW1lc3RhbXAgUmVzcG9uZGVyIDIw
# MjUgMTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBANBGrC0Sxp7Q6q5g
# VrMrV7pvUf+GcAoB38o3zBlCMGMyqJnfFNZx+wvA69HFTBdwbHwBSOeLpvPnZ8ZN
# +vo8dE2/pPvOx/Vj8TchTySA2R4QKpVD7dvNZh6wW2R6kSu9RJt/4QhguSssp3qo
# me7MrxVyfQO9sMx6ZAWjFDYOzDi8SOhPUWlLnh00Cll8pjrUcCV3K3E0zz09ldQ/
# /nBZZREr4h/GI6Dxb2UoyrN0ijtUDVHRXdmncOOMA3CoB/iUSROUINDT98oksouT
# MYFOnHoRh6+86Ltc5zjPKHW5KqCvpSduSwhwUmotuQhcg9tw2YD3w6ySSSu+3qU8
# DD+nigNJFmt6LAHvH3KSuNLoZLc1Hf2JNMVL4Q1OpbybpMe46YceNA0LfNsnqcnp
# JeItK/DhKbPxTTuGoX7wJNdoRORVbPR1VVnDuSeHVZlc4seAO+6d2sC26/PQPdP5
# 1ho1zBp+xUIZkpSFA8vWdoUoHLWnqWU3dCCyFG1roSrgHjSHlq8xymLnjCbSLZ49
# kPmk8iyyizNDIXj//cOgrY7rlRyTlaCCfw7aSUROwnu7zER6EaJ+AliL7ojTdS5P
# WPsWeupWs7NpChUk555K096V1hE0yZIXe+giAwW00aHzrDchIc2bQhpp0IoKRR7Y
# ufAkprxMiXAJQ1XCmnCfgPf8+3mnAgMBAAGjggGVMIIBkTAMBgNVHRMBAf8EAjAA
# MB0GA1UdDgQWBBTkO/zyMe39/dfzkXFjGVBDz2GM6DAfBgNVHSMEGDAWgBTvb1NK
# 6eQGfHrK4pBW9i/USezLTjAOBgNVHQ8BAf8EBAMCB4AwFgYDVR0lAQH/BAwwCgYI
# KwYBBQUHAwgwgZUGCCsGAQUFBwEBBIGIMIGFMCQGCCsGAQUFBzABhhhodHRwOi8v
# b2NzcC5kaWdpY2VydC5jb20wXQYIKwYBBQUHMAKGUWh0dHA6Ly9jYWNlcnRzLmRp
# Z2ljZXJ0LmNvbS9EaWdpQ2VydFRydXN0ZWRHNFRpbWVTdGFtcGluZ1JTQTQwOTZT
# SEEyNTYyMDI1Q0ExLmNydDBfBgNVHR8EWDBWMFSgUqBQhk5odHRwOi8vY3JsMy5k
# aWdpY2VydC5jb20vRGlnaUNlcnRUcnVzdGVkRzRUaW1lU3RhbXBpbmdSU0E0MDk2
# U0hBMjU2MjAyNUNBMS5jcmwwIAYDVR0gBBkwFzAIBgZngQwBBAIwCwYJYIZIAYb9
# bAcBMA0GCSqGSIb3DQEBCwUAA4ICAQBlKq3xHCcEua5gQezRCESeY0ByIfjk9iJP
# 2zWLpQq1b4URGnwWBdEZD9gBq9fNaNmFj6Eh8/YmRDfxT7C0k8FUFqNh+tshgb4O
# 6Lgjg8K8elC4+oWCqnU/ML9lFfim8/9yJmZSe2F8AQ/UdKFOtj7YMTmqPO9mzskg
# iC3QYIUP2S3HQvHG1FDu+WUqW4daIqToXFE/JQ/EABgfZXLWU0ziTN6R3ygQBHMU
# BaB5bdrPbF6MRYs03h4obEMnxYOX8VBRKe1uNnzQVTeLni2nHkX/QqvXnNb+YkDF
# kxUGtMTaiLR9wjxUxu2hECZpqyU1d0IbX6Wq8/gVutDojBIFeRlqAcuEVT0cKsb+
# zJNEsuEB7O7/cuvTQasnM9AWcIQfVjnzrvwiCZ85EE8LUkqRhoS3Y50OHgaY7T/l
# wd6UArb+BOVAkg2oOvol/DJgddJ35XTxfUlQ+8Hggt8l2Yv7roancJIFcbojBcxl
# RcGG0LIhp6GvReQGgMgYxQbV1S3CrWqZzBt1R9xJgKf47CdxVRd/ndUlQ05oxYy2
# zRWVFjF7mcr4C34Mj3ocCVccAvlKV9jEnstrniLvUxxVZE/rptb7IRE2lskKPIJg
# baP5t2nGj/ULLi49xTcBZU8atufk+EMF/cWuiC7POGT75qaL6vdCvHlshtjdNXOC
# IUjsarfNZzGCBkQwggZAAgEBMGgwVDELMAkGA1UEBhMCR0IxGDAWBgNVBAoTD1Nl
# Y3RpZ28gTGltaXRlZDErMCkGA1UEAxMiU2VjdGlnbyBQdWJsaWMgQ29kZSBTaWdu
# aW5nIENBIFIzNgIQB5zg5NEUf4XNOXPPdi036zANBglghkgBZQMEAgEFAKCBhDAY
# BgorBgEEAYI3AgEMMQowCKACgAChAoAAMBkGCSqGSIb3DQEJAzEMBgorBgEEAYI3
# AgEEMBwGCisGAQQBgjcCAQsxDjAMBgorBgEEAYI3AgEVMC8GCSqGSIb3DQEJBDEi
# BCDi1Uz19mnGRdG6TFDt395F02e6/TpfPrVvzGcQ+RcT+DANBgkqhkiG9w0BAQEF
# AASCAgDEQwdHknZWlgr6v7esHO6BUGd7a+np5yPBf6rUaJDhlkrNkpzFfRPydN2X
# zisQHeIMk51YxSWDWZTDW8x4XFoKO4pPq/8j8bk/0wIt5bjHfBz36QM2yHSk90gX
# pGZmwHzjmLyh8NWHhOcXIjTe+pVtd2q8Sl/Hx3ytCv4uctxpALrRlP4ytdx7uyyB
# Lon49A6yNHx3pe8BgVBPznQOYT0ljIkTqMpP8Eis+RSdqkFEb+2bP3rkkyWnS+io
# wJPARWLcayGMGNBAmQ6MkSd9yuCrRD9cAcCxkSDqeAU3xNpsXJpmyy9+JTbD4oH1
# DNwJcKpMLINwSSKc+LjKtq3QuenXogwn9MKmr1qwlfAIbVkNAi8SXRkSjj7t7BHy
# IbGf6EsZIM/19871YM+ZPo7ueXhS5aY+NrS49AVJfh+lJoAejGnJrjMsgzwIcWYZ
# jhtf6c6s7rYnDqq/+NLpG0vaazzVwBX4Z3820uO3YejdOcP67l/KVR8uWM2zR983
# Q1sr+HTaPjj97E05o9mRafmDnJXwQx+5uaChV3rg9Ve7RSDfWec6tmU8bsgqoNKF
# nOfZNj95LqV8hF9ml46JZdQJJntuASYdXrgHWOJXnxdemE+t20KPlSBZzUrc2wcO
# G0pV8SBcEqrCjf+X64d+Jv8sSyItNa/9fDXhSuEwpA8ej5z+yKGCAyYwggMiBgkq
# hkiG9w0BCQYxggMTMIIDDwIBATB9MGkxCzAJBgNVBAYTAlVTMRcwFQYDVQQKEw5E
# aWdpQ2VydCwgSW5jLjFBMD8GA1UEAxM4RGlnaUNlcnQgVHJ1c3RlZCBHNCBUaW1l
# U3RhbXBpbmcgUlNBNDA5NiBTSEEyNTYgMjAyNSBDQTECEAqA7xhLjfEFgtHEdqeV
# dGgwDQYJYIZIAWUDBAIBBQCgaTAYBgkqhkiG9w0BCQMxCwYJKoZIhvcNAQcBMBwG
# CSqGSIb3DQEJBTEPFw0yNjA1MjgxMzQxNDZaMC8GCSqGSIb3DQEJBDEiBCDn4XE9
# 6vNMFOwHau0WCqboz5q48FdnwuVWhI2bE+AqKDANBgkqhkiG9w0BAQEFAASCAgCe
# ItF7OKHMNgLupatw6Z6pEVjnIAjlZO0n2znqq31/crV3MuFjVnYzTuluTXNg7YWX
# 9tUQSBmU9+Yapzg799dB3wecpLm2C7TD4Tov9k8OsTSeRr57/HLXjuQBxcm7wxvz
# CS9Bpz7f7Neh5xwtVy5ppSaDYt6Xw3iSVBSkfiN8ms0cdH/Eg9uEPQ2d/hfrqChM
# uou9eDLQBRNeYYBtgpuOJg7tjjKDSuPJsXn1YZx2MMQ0JA/d0mostC8s9YuHIYeQ
# SehWE00/0p9xPHrclMXbBsrlGtOAVg9LZoJklFZNlQ8Q3kezGL8I5YjAmmiAC7Bd
# 5RXJNENglASlHZYt76wlL4Pizx6Q+TCdmxaocGWKYXFSu/rNlr1MfC31cu4EM4Zh
# aOD9zqAswNLKNEyt4jBZiuWjQBN9Z7cVngvsrSjk3Ov30IV+cM73b7G2rz2Z0rtW
# GaYWZ6U0+WWokDZpfqgiTvtn1mvEn6iWUtKK5heRkJ9camjqC4W++w6k0D2oPeoy
# /H7PkCXMVIw9C7KVOeUI6vS/MYooknfsWrIfXGbCyYyAQ3HI2Ohp0pi9oUiIo2Z6
# nLA2sj1TcEyGRQzAy4UdYL6w3Vy9H3TxTMs1Va1T3B4usRo7ve2tjPa5jB9j7Z4X
# KgRoR9g3QW6cLwdhr/C+M5ZgSow7AQP8lZIBVy7qOw==
# SIG # End signature block