helpers/discovery/Setup-WindowsAttributes-Discovery.ps1

<#
.SYNOPSIS
    Discovers Windows system information via WMI and updates WhatsUp Gold
    device attributes with the collected data.
 
.DESCRIPTION
    This helper connects to Windows servers via WMI (Get-WmiObject) to collect
    system-level information such as OS version, service pack, hardware model,
    CPU, RAM, serial number, domain, last boot time, and installed roles/features.
    It then creates or updates device attributes in WhatsUp Gold using
    Set-WUGDeviceAttribute so that this information is always visible in the
    WUG console without manual data entry.
 
    Uses the shared Windows WMI credential vault (Windows.WMI.Credential.N)
    so credentials entered for Setup-WindowsDiskIO-Discovery are reused here
    and vice versa. Multi-credential fallback on access-denied errors.
 
    Attributes collected per device:
      - Windows.OSName (e.g. Microsoft Windows Server 2022 Standard)
      - Windows.OSVersion (e.g. 10.0.20348)
      - Windows.OSBuild (e.g. 20348)
      - Windows.ServicePack (e.g. 0)
      - Windows.Architecture (e.g. 64-bit)
      - Windows.Manufacturer (e.g. QEMU, Dell Inc.)
      - Windows.Model (e.g. Standard PC, PowerEdge R740)
      - Windows.SerialNumber (e.g. VMware-42 1a ...)
      - Windows.IsVirtualMachine (e.g. True)
      - Windows.BIOSVersion (e.g. Dell Inc. 2.14.1 (2025-08-20))
      - Windows.Motherboard (e.g. Dell Inc. 0CNCJW)
      - Windows.TotalMemoryGB (e.g. 16)
      - Windows.FreeMemoryGB (e.g. 8.42)
      - Windows.RAMSticks (e.g. 4)
      - Windows.RAMSpeedMHz (e.g. 3200)
      - Windows.CPUName (e.g. Intel Xeon E5-2680 v4)
      - Windows.CPUCores (e.g. 4)
      - Windows.CPULogical (e.g. 8)
      - Windows.CPUSpeedMHz (e.g. 2400)
      - Windows.GPUName (e.g. NVIDIA Tesla T4)
      - Windows.GPUMemoryMB (e.g. 16384)
      - Windows.GPUResolution (e.g. 1920x1080)
      - Windows.Domain (e.g. wugninja.local)
      - Windows.LastBootTime (e.g. 2026-04-01 08:32:15)
      - Windows.LastReboot (e.g. 0 months, 8 days, 3 hours ago)
      - Windows.InstallDate (e.g. 2025-11-12)
      - Windows.SystemType (e.g. x64-based PC)
      - Windows.LastLogonUser (e.g. DOMAIN\jsmith)
      - Windows.LastLogonTime (e.g. 2026-04-09 14:22:01)
      - Windows.DiskCount (e.g. 3)
      - Windows.TotalDiskGB (e.g. 1024)
      - Windows.DiskSummary (e.g. Samsung SSD 970 -- 477 GB (SSD) | ...)
      - Windows.NetworkAdapters (e.g. 2)
      - Windows.RunningServices (e.g. 87)
      - Windows.StoppedServices (e.g. 42)
      - Windows.InstalledSoftware (e.g. 134)
 
    Prerequisites:
      1. WhatsUpGoldPS module loaded and connected (Connect-WUGServer)
      2. WMI/DCOM or WinRM access to target Windows servers
      3. Credentials with admin access on target servers
 
.PARAMETER DeviceId
    One or more WUG device IDs to scan. When omitted, devices are resolved
    automatically from a Windows device group (see DeviceGroupSearch).
 
.PARAMETER Credential
    One or more PSCredentials for WMI access to the target Windows servers.
    Multiple credentials are tried in order per device. If omitted, resolved
    from the shared DPAPI vault (Windows.WMI.Credential.N).
 
.PARAMETER DeviceGroupSearch
    Search string used to find a WUG device group containing Windows devices.
    Default: 'Windows Infrastructure'.
 
.PARAMETER DeviceGroupId
    Explicit WUG device group ID. Overrides DeviceGroupSearch.
 
.PARAMETER WindowsOnly
    When enabled (default), only devices whose role or description contains
    'Windows' are scanned.
 
.PARAMETER AttributePrefix
    Prefix for all attribute names. Default: 'Windows'.
 
.PARAMETER IncludeAttribute
    Array of specific attribute keys to collect. Default: all.
    Use to limit which WMI classes are queried per device.
    Valid values: All, OSName, OSVersion, OSBuild, ServicePack, Architecture,
    Manufacturer, Model, SerialNumber, IsVirtualMachine, BIOSVersion,
    Motherboard, TotalMemoryGB, FreeMemoryGB, RAMSticks, RAMSpeedMHz,
    CPUName, CPUCores, CPULogical, CPUSpeedMHz, GPUName, GPUMemoryMB,
    GPUResolution, Domain, LastBootTime, LastReboot, InstallDate, SystemType,
    LastLogonUser, LastLogonTime, DiskCount, TotalDiskGB, DiskSummary,
    NetworkAdapters, RunningServices, StoppedServices, InstalledSoftware.
 
.PARAMETER Action
    What to do with discovery results. When specified, skips the interactive menu.
    Valid values: PushToWUG, ExportJSON, ExportCSV, ShowTable, Dashboard, DashboardAndPush, None.
    PushToWUG requires devices resolved from WhatsUp Gold (not standalone -Target mode).
 
.PARAMETER Target
    Windows host(s) to scan via WMI -- IP address or FQDN. Accepts multiple values.
    Enables standalone mode (no WhatsUp Gold connection needed for device resolution).
    PushToWUG action is not available in standalone -Target mode.
 
.PARAMETER DryRun
    Show what attributes would be set without making changes.
 
.PARAMETER NonInteractive
    Suppress interactive prompts. Uses cached vault credentials.
 
.PARAMETER WUGServer
    WhatsUp Gold server address. Default: resolved from vault.
 
.PARAMETER WUGCredential
    PSCredential for authenticating to WhatsUp Gold. When supplied, bypasses
    the vault-based WUG server resolution and connects directly.
 
.EXAMPLE
    .\Setup-WindowsAttributes-Discovery.ps1
 
    Interactive mode -- resolves creds from vault, scans all Windows devices,
    updates attributes in WUG.
 
.EXAMPLE
    .\Setup-WindowsAttributes-Discovery.ps1 -DeviceId 163,173 -NonInteractive
 
    Scans specific devices non-interactively using vault credentials.
 
.EXAMPLE
    .\Setup-WindowsAttributes-Discovery.ps1 -DryRun
 
    Preview what attributes would be set without making changes.
 
.EXAMPLE
    .\Setup-WindowsAttributes-Discovery.ps1 -IncludeAttribute OSName,TotalMemoryGB,CPUName
 
    Only collect OS name, RAM, and CPU info.
 
.EXAMPLE
    .\Setup-WindowsAttributes-Discovery.ps1 -Target '10.0.0.5','10.0.0.6' -Action Dashboard
 
    Standalone mode -- scans hosts directly and generates an HTML dashboard.
 
.EXAMPLE
    .\Setup-WindowsAttributes-Discovery.ps1 -Target '10.0.0.5' -Action ExportJSON -NonInteractive
 
    Scans a host and exports results to JSON. No WUG connection required.
 
.NOTES
    Author: Jason Alberino (jason@wug.ninja)
    Requires: WhatsUpGoldPS module, PowerShell 5.1+
    Encoding: UTF-8 with BOM
#>

[CmdletBinding(SupportsShouldProcess = $true)]
param(
    [int[]]$DeviceId,

    [PSCredential[]]$Credential,

    [string]$DeviceGroupSearch = 'Windows Infrastructure',

    [string]$DeviceGroupId,

    [bool]$WindowsOnly = $true,

    [string]$AttributePrefix = 'Windows',

    [ValidateSet('All', 'OSName', 'OSVersion', 'OSBuild', 'ServicePack', 'Architecture',
                 'Manufacturer', 'Model', 'SerialNumber', 'IsVirtualMachine', 'BIOSVersion',
                 'Motherboard', 'TotalMemoryGB', 'FreeMemoryGB', 'RAMSticks', 'RAMSpeedMHz',
                 'CPUName', 'CPUCores', 'CPULogical', 'CPUSpeedMHz', 'GPUName', 'GPUMemoryMB',
                 'GPUResolution', 'Domain', 'LastBootTime', 'LastReboot', 'InstallDate',
                 'SystemType', 'LastLogonUser', 'LastLogonTime', 'DiskCount', 'TotalDiskGB',
                 'DiskSummary', 'NetworkAdapters', 'RunningServices', 'StoppedServices',
                 'InstalledSoftware')]
    [string[]]$IncludeAttribute,

    [ValidateSet('PushToWUG', 'ExportJSON', 'ExportCSV', 'ShowTable', 'Dashboard', 'DashboardAndPush', 'None')]
    [string]$Action,

    [string[]]$Target,

    [switch]$DryRun,

    [switch]$NonInteractive,

    [string]$WUGServer,

    [PSCredential]$WUGCredential,

    [string]$OutputPath
)

# --- Load discovery helpers (vault, credential resolver) ----------------------
$scriptDir = Split-Path $MyInvocation.MyCommand.Path -Parent
$discoveryHelpersPath = Join-Path $scriptDir 'DiscoveryHelpers.ps1'
if (Test-Path $discoveryHelpersPath) {
    . $discoveryHelpersPath
}
else {
    Write-Error "DiscoveryHelpers.ps1 not found at '$discoveryHelpersPath'. Cannot continue."
    return
}

# Load dynamic dashboard generator (fallback)
$dynDashPath = Join-Path (Split-Path $scriptDir -Parent) 'reports\Export-DynamicDashboardHtml.ps1'
if (Test-Path $dynDashPath) { . $dynDashPath }

# Load provider-specific Windows dashboard functions
$winProviderPath = Join-Path $scriptDir 'DiscoveryProvider-Windows.ps1'
if (Test-Path $winProviderPath) { . $winProviderPath }

# =============================================================================
# Attribute definitions: key -> WMI query info
# =============================================================================
# Simple entries: Class + Property (+ optional Transform)
# Compute entries: Compute scriptblock receives ($wmiCache, $devIP, $tryCred)
# and returns the value directly. Use for multi-object or registry queries.
$script:AttributeDefinitions = [ordered]@{
    # --- Identity / OS ---
    OSName        = @{ Class = 'Win32_OperatingSystem'; Property = 'Caption' }
    OSVersion     = @{ Class = 'Win32_OperatingSystem'; Property = 'Version' }
    OSBuild       = @{ Class = 'Win32_OperatingSystem'; Property = 'BuildNumber' }
    ServicePack   = @{ Class = 'Win32_OperatingSystem'; Property = 'ServicePackMajorVersion' }
    Architecture  = @{ Class = 'Win32_OperatingSystem'; Property = 'OSArchitecture' }
    Manufacturer  = @{ Class = 'Win32_ComputerSystem';  Property = 'Manufacturer' }
    Model         = @{ Class = 'Win32_ComputerSystem';  Property = 'Model' }
    SerialNumber  = @{ Class = 'Win32_BIOS';            Property = 'SerialNumber' }
    IsVirtualMachine = @{ Compute = {
        param($cache, $ip, $cred)
        $cs = $cache['Win32_ComputerSystem']
        if ($cs -is [System.Array]) { $cs = $cs[0] }
        $isVM = ($cs.Model -match 'Virtual|VMware|HVM|KVM|BHYVE' -or
                 $cs.Manufacturer -match 'VMware|Microsoft Corporation|Xen|QEMU|innotek|Parallels')
        "$isVM"
    }}
    BIOSVersion   = @{ Compute = {
        param($cache, $ip, $cred)
        $b = $cache['Win32_BIOS']
        if ($b -is [System.Array]) { $b = $b[0] }
        $dateStr = ''
        if ($b.ReleaseDate) {
            try { $dateStr = " ($([Management.ManagementDateTimeConverter]::ToDateTime($b.ReleaseDate).ToString('yyyy-MM-dd')))" } catch {}
        }
        "$($b.Manufacturer) $($b.SMBIOSBIOSVersion)$dateStr".Trim()
    }}
    Motherboard   = @{ Compute = {
        param($cache, $ip, $cred)
        $mb = $cache['Win32_BaseBoard']
        if ($mb -is [System.Array]) { $mb = $mb[0] }
        if ($mb) { "$($mb.Manufacturer) $($mb.Product)".Trim() } else { '' }
    }}
    # --- Memory ---
    TotalMemoryGB = @{ Class = 'Win32_ComputerSystem';  Property = 'TotalPhysicalMemory'; Transform = { param($v) [Math]::Round([double]$v / 1GB) } }
    FreeMemoryGB  = @{ Class = 'Win32_OperatingSystem';  Property = 'FreePhysicalMemory'; Transform = { param($v) [Math]::Round([double]$v / 1MB, 2) } }
    RAMSticks     = @{ Compute = {
        param($cache, $ip, $cred)
        $sticks = $cache['Win32_PhysicalMemory']
        if (-not $sticks) { return '0' }
        if ($sticks -is [System.Array]) { "$($sticks.Count)" } else { '1' }
    }}
    RAMSpeedMHz   = @{ Compute = {
        param($cache, $ip, $cred)
        $sticks = $cache['Win32_PhysicalMemory']
        if (-not $sticks) { return '' }
        $first = if ($sticks -is [System.Array]) { $sticks[0] } else { $sticks }
        if ($first.Speed) { "$($first.Speed)" } else { '' }
    }}
    # --- CPU ---
    CPUName       = @{ Class = 'Win32_Processor';       Property = 'Name' }
    CPUCores      = @{ Class = 'Win32_Processor';       Property = 'NumberOfCores' }
    CPULogical    = @{ Class = 'Win32_Processor';       Property = 'NumberOfLogicalProcessors' }
    CPUSpeedMHz   = @{ Class = 'Win32_Processor';       Property = 'MaxClockSpeed' }
    # --- GPU ---
    GPUName       = @{ Compute = {
        param($cache, $ip, $cred)
        $g = $cache['Win32_VideoController']
        if (-not $g) { return '' }
        if ($g -is [System.Array]) { $g = $g[0] }
        if ($g.Name) { "$($g.Name)".Trim() } else { '' }
    }}
    GPUMemoryMB   = @{ Compute = {
        param($cache, $ip, $cred)
        $g = $cache['Win32_VideoController']
        if (-not $g) { return '' }
        if ($g -is [System.Array]) { $g = $g[0] }
        if ($g.AdapterRAM) { "$([Math]::Round([double]$g.AdapterRAM / 1MB))" } else { '' }
    }}
    GPUResolution = @{ Compute = {
        param($cache, $ip, $cred)
        $g = $cache['Win32_VideoController']
        if (-not $g) { return '' }
        if ($g -is [System.Array]) { $g = $g[0] }
        if ($g.CurrentHorizontalResolution -and $g.CurrentVerticalResolution) {
            "$($g.CurrentHorizontalResolution)x$($g.CurrentVerticalResolution)"
        } else { '' }
    }}
    # --- Identity continued ---
    Domain        = @{ Class = 'Win32_ComputerSystem';  Property = 'Domain' }
    LastBootTime  = @{ Class = 'Win32_OperatingSystem';  Property = 'LastBootUpTime'; Transform = { param($v) if ($v) { try { [Management.ManagementDateTimeConverter]::ToDateTime($v).ToString('yyyy-MM-dd HH:mm:ss') } catch { "$v" } } else { '' } } }
    LastReboot    = @{ Class = 'Win32_OperatingSystem';  Property = 'LastBootUpTime'; Transform = {
        param($v)
        if ($v) {
            try {
                $bootDt = [Management.ManagementDateTimeConverter]::ToDateTime($v)
                $ts = [int]((Get-Date) - $bootDt).TotalSeconds
                $units = [ordered]@{ year = 31536000; month = 2592000; day = 86400; hour = 3600 }
                $parts = @()
                foreach ($u in $units.Keys) {
                    $n = [Math]::Floor($ts / $units[$u])
                    $ts = $ts % $units[$u]
                    if ($n -gt 0) { $parts += "$n $u$(if ($n -ne 1) { 's' })" }
                }
                if ($parts.Count -eq 0) { $parts += '0 hours' }
                ($parts -join ', ') + ' ago'
            } catch { "$v" }
        } else { '' }
    }}
    InstallDate   = @{ Class = 'Win32_OperatingSystem';  Property = 'InstallDate'; Transform = { param($v) if ($v) { try { [Management.ManagementDateTimeConverter]::ToDateTime($v).ToString('yyyy-MM-dd') } catch { "$v" } } else { '' } } }
    SystemType    = @{ Class = 'Win32_ComputerSystem';  Property = 'SystemType' }
    LastLogonUser = @{ Class = 'Win32_ComputerSystem';  Property = 'UserName' }
    LastLogonTime = @{ Class = 'Win32_OperatingSystem';  Property = 'LocalDateTime'; Transform = {
        param($v)
        (Get-Date).ToString('yyyy-MM-dd HH:mm:ss')
    }}
    # --- Disks ---
    DiskCount     = @{ Compute = {
        param($cache, $ip, $cred)
        $d = $cache['Win32_DiskDrive']
        if (-not $d) { return '0' }
        if ($d -is [System.Array]) { "$($d.Count)" } else { '1' }
    }}
    TotalDiskGB   = @{ Compute = {
        param($cache, $ip, $cred)
        $d = $cache['Win32_DiskDrive']
        if (-not $d) { return '0' }
        $arr = if ($d -is [System.Array]) { $d } else { @($d) }
        $total = ($arr | ForEach-Object { [double]$_.Size } | Measure-Object -Sum).Sum
        "$([Math]::Round($total / 1GB))"
    }}
    DiskSummary   = @{ Compute = {
        param($cache, $ip, $cred)
        $d = $cache['Win32_DiskDrive']
        if (-not $d) { return '' }
        $arr = if ($d -is [System.Array]) { $d } else { @($d) }
        $parts = $arr | ForEach-Object {
            $sizeGB = [Math]::Round([double]$_.Size / 1GB)
            $media  = if ($_.MediaType -match 'SSD|Solid') { 'SSD' } elseif ($_.MediaType -match 'Fixed') { 'HDD' } else { $_.MediaType }
            "$($_.Model) -- $sizeGB GB ($media)"
        }
        $parts -join ' | '
    }}
    # --- Network ---
    NetworkAdapters = @{ Compute = {
        param($cache, $ip, $cred)
        $n = $cache['Win32_NetworkAdapterConfiguration']
        if (-not $n) { return '0' }
        $arr = if ($n -is [System.Array]) { $n } else { @($n) }
        $enabled = @($arr | Where-Object { $_.IPEnabled })
        "$($enabled.Count)"
    }}
    # --- Services ---
    RunningServices = @{ Compute = {
        param($cache, $ip, $cred)
        $s = $cache['Win32_Service']
        if (-not $s) { return '0' }
        $arr = if ($s -is [System.Array]) { $s } else { @($s) }
        "$(@($arr | Where-Object { $_.State -eq 'Running' }).Count)"
    }}
    StoppedServices = @{ Compute = {
        param($cache, $ip, $cred)
        $s = $cache['Win32_Service']
        if (-not $s) { return '0' }
        $arr = if ($s -is [System.Array]) { $s } else { @($s) }
        "$(@($arr | Where-Object { $_.State -eq 'Stopped' }).Count)"
    }}
    # --- Software (registry-based, fast -- avoids Win32_Product) ---
    InstalledSoftware = @{ Compute = {
        param($cache, $ip, $cred)
        try {
            $isLocalConn = ($ip -eq 'localhost' -or $ip -eq '127.0.0.1' -or $ip -eq '::1' -or $ip -eq $env:COMPUTERNAME -or $ip -eq '.')
            if ($isLocalConn) {
                $wmiList = Get-WmiObject -List -Namespace root\default -ErrorAction Stop
            } else {
                $wmiList = Get-WmiObject -List -Namespace root\default -ComputerName $ip -Credential $cred -ErrorAction Stop
            }
            $reg = $wmiList | Where-Object { $_.Name -eq 'StdRegProv' }
            $HKLM = [UInt32]'0x80000002'
            $count = 0
            foreach ($regPath in @(
                'SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall',
                'SOFTWARE\WOW6432Node\Microsoft\Windows\CurrentVersion\Uninstall'
            )) {
                $subkeys = $reg.EnumKey($HKLM, $regPath)
                if ($subkeys.sNames) {
                    foreach ($key in $subkeys.sNames) {
                        $dn = $reg.GetStringValue($HKLM, "$regPath\$key", 'DisplayName')
                        if ($dn.sValue) { $count++ }
                    }
                }
            }
            "$count"
        }
        catch {
            Write-Verbose " Registry software count failed on ${ip}: $_"
            ''
        }
    }}
}

# Resolve which attributes to collect
if ($IncludeAttribute -and $IncludeAttribute -contains 'All') {
    $activeAttributes = $script:AttributeDefinitions
}
elseif ($IncludeAttribute) {
    $activeAttributes = [ordered]@{}
    foreach ($key in $IncludeAttribute) {
        if ($script:AttributeDefinitions.Contains($key)) {
            $activeAttributes[$key] = $script:AttributeDefinitions[$key]
        }
    }
}
else {
    $activeAttributes = $script:AttributeDefinitions
}

if ($activeAttributes.Count -eq 0) {
    Write-Error "No valid attributes selected. Exiting."
    return
}

# Figure out which WMI classes we actually need to query
# Simple attrs declare a Class; Compute attrs may reference classes implicitly.
# We build the list from simple attrs, then add the extra classes that Compute
# entries depend on (declared in the definitions via naming convention).
$requiredClasses = [System.Collections.Generic.List[string]]::new()
foreach ($def in $activeAttributes.Values) {
    if ($def.Class -and -not $requiredClasses.Contains($def.Class)) {
        $requiredClasses.Add($def.Class)
    }
}
# Extra classes needed by Compute attributes when they are active
$computeClassDeps = @{
    IsVirtualMachine  = @('Win32_ComputerSystem')
    BIOSVersion       = @('Win32_BIOS')
    Motherboard       = @('Win32_BaseBoard')
    RAMSticks         = @('Win32_PhysicalMemory')
    RAMSpeedMHz       = @('Win32_PhysicalMemory')
    GPUName           = @('Win32_VideoController')
    GPUMemoryMB       = @('Win32_VideoController')
    GPUResolution     = @('Win32_VideoController')
    DiskCount         = @('Win32_DiskDrive')
    TotalDiskGB       = @('Win32_DiskDrive')
    DiskSummary       = @('Win32_DiskDrive')
    NetworkAdapters   = @('Win32_NetworkAdapterConfiguration')
    RunningServices   = @('Win32_Service')
    StoppedServices   = @('Win32_Service')
}
foreach ($key in $activeAttributes.Keys) {
    if ($computeClassDeps.ContainsKey($key)) {
        foreach ($cls in $computeClassDeps[$key]) {
            if (-not $requiredClasses.Contains($cls)) {
                $requiredClasses.Add($cls)
            }
        }
    }
}

# =============================================================================
# Preflight: WUG connection (only needed when resolving devices from WUG or pushing)
# =============================================================================
$needsWUG = (-not $Target) -or ($Action -eq 'PushToWUG') -or ($Action -eq 'DashboardAndPush')
if ($needsWUG) {
if (-not $global:WUGBearerHeaders -or -not $global:WhatsUpServerBaseURI) {
    if ($WUGCredential) {
        # Direct credential supplied -- bypass vault resolution
        $wugUri = if ($WUGServer) { $WUGServer } else { 'https://localhost:9644' }
        try {
            Connect-WUGServer -serverUri $wugUri -Credential $WUGCredential -IgnoreSSLErrors
            Write-Host "Connected to WhatsUp Gold." -ForegroundColor Green
        }
        catch {
            Write-Error "Failed to connect to WhatsUp Gold: $_"
            return
        }
    }
    else {
    Write-Host "Not connected to WhatsUp Gold. Resolving from vault..." -ForegroundColor Yellow
    $wugSplat = @{ Name = 'WUG.Server'; CredType = 'WUGServer'; ProviderLabel = 'WhatsUp Gold' }
    if ($NonInteractive) { $wugSplat.NonInteractive = $true }
    $wugResolved = Resolve-DiscoveryCredential @wugSplat
    if (-not $wugResolved) {
        Write-Error "No WhatsUp Gold server credentials available. Run Connect-WUGServer or configure the vault."
        return
    }
    $wugConnSplat = @{}
    if ($wugResolved -is [hashtable]) {
        $wugConnSplat.Server = $wugResolved.Server
        if ($wugResolved.Port)     { $wugConnSplat.Port = $wugResolved.Port }
        if ($wugResolved.Protocol) { $wugConnSplat.Protocol = $wugResolved.Protocol }
        if ($wugResolved.Credential) { $wugConnSplat.Credential = $wugResolved.Credential }
        if ($wugResolved.IgnoreSSL) { $wugConnSplat.IgnoreSSL = $true }
    }
    elseif ($WUGServer) {
        $wugConnSplat.Server = $WUGServer
    }
    try {
        Connect-WUGServer @wugConnSplat
        Write-Host "Connected to WhatsUp Gold." -ForegroundColor Green
    }
    catch {
        Write-Error "Failed to connect to WhatsUp Gold: $_"
        return
    }
    }
}
} # end needsWUG

# =============================================================================
# Resolve WMI credentials (shared Windows vault, multi-credential support)
# =============================================================================
$credentialList = [System.Collections.Generic.List[PSCredential]]::new()

if ($Credential) {
    foreach ($c in $Credential) { $credentialList.Add($c) }
}
else {
    $credIndex = 1
    $wmiVaultName = "Windows.WMI.Credential.$credIndex"
    $credSplat = @{ Name = $wmiVaultName; CredType = 'PSCredential'; ProviderLabel = "Windows WMI #$credIndex" }
    if ($NonInteractive) { $credSplat.NonInteractive = $true }
    $firstCred = Resolve-DiscoveryCredential @credSplat
    if (-not $firstCred) {
        Write-Error "No WMI credentials available. Provide -Credential or configure the vault."
        return
    }
    $credentialList.Add($firstCred)

    if (-not $NonInteractive) {
        while ($true) {
            Write-Host ""
            Write-Host " Credential #$($credentialList.Count) loaded: $($credentialList[-1].UserName)" -ForegroundColor Gray
            Write-Host " Add another credential to try if this one fails on some servers?" -ForegroundColor Cyan
            $addMore = Read-Host -Prompt " [Y]es / [N]o (default: N)"
            if ($addMore -notmatch '^[Yy]') { break }

            $credIndex++
            $wmiVaultName = "Windows.WMI.Credential.$credIndex"
            $credSplat = @{ Name = $wmiVaultName; CredType = 'PSCredential'; ProviderLabel = "Windows WMI #$credIndex" }
            $nextCred = Resolve-DiscoveryCredential @credSplat
            if ($nextCred) {
                $credentialList.Add($nextCred)
            }
            else {
                Write-Host " Skipped." -ForegroundColor Yellow
                break
            }
        }
    }
    else {
        while ($true) {
            $credIndex++
            $wmiVaultName = "Windows.WMI.Credential.$credIndex"
            $credSplat = @{ Name = $wmiVaultName; CredType = 'PSCredential'; NonInteractive = $true }
            $nextCred = Resolve-DiscoveryCredential @credSplat
            if ($nextCred) {
                $credentialList.Add($nextCred)
            }
            else {
                break
            }
        }
    }
}

Write-Host ""
Write-Host " Credentials to try ($($credentialList.Count)):" -ForegroundColor Cyan
for ($ci = 0; $ci -lt $credentialList.Count; $ci++) {
    Write-Host " [$($ci + 1)] $($credentialList[$ci].UserName)" -ForegroundColor Gray
}

# =============================================================================
# STEP 1: Resolve target devices (smart Windows group detection)
# =============================================================================
Write-Host ""
Write-Host "=== Windows Attributes Discovery ===" -ForegroundColor Cyan
Write-Host ""

$standaloneMode = $false
if ($Target) {
    # Standalone mode: scan specified hosts directly (no WUG device lookup)
    $standaloneMode = $true
    Write-Host "Standalone mode: scanning $($Target.Count) specified host(s)..." -ForegroundColor Cyan
    $devices = @()
    $targetId = 0
    foreach ($t in $Target) {
        $targetId++
        $devices += [PSCustomObject]@{
            id             = "target-$targetId"
            displayName    = $t
            hostName       = $t
            networkAddress = $t
            role           = 'Windows'
        }
    }
}
elseif ($DeviceId) {
    Write-Host "Fetching $($DeviceId.Count) specified device(s) from WhatsUp Gold..." -ForegroundColor Cyan
    $devices = @()
    foreach ($dId in $DeviceId) {
        try {
            $dev = Get-WUGDevice -DeviceId $dId
            if ($dev) { $devices += $dev }
        }
        catch {
            Write-Warning "Could not retrieve device ID ${dId}: $_"
        }
    }
}
else {
    $resolvedGroupId = $null

    if ($DeviceGroupId) {
        $resolvedGroupId = $DeviceGroupId
        Write-Host "Using explicit device group ID: $resolvedGroupId" -ForegroundColor Cyan
    }
    elseif ($DeviceGroupSearch) {
        Write-Host "Searching for device group matching '$DeviceGroupSearch'..." -ForegroundColor Cyan
        try {
            $matchingGroups = @(Get-WUGDeviceGroup -SearchValue $DeviceGroupSearch)
            if ($matchingGroups.Count -eq 1) {
                $resolvedGroupId = $matchingGroups[0].id
                Write-Host " Found group: '$($matchingGroups[0].name)' (ID: $resolvedGroupId)" -ForegroundColor Green
            }
            elseif ($matchingGroups.Count -gt 1) {
                Write-Host " Found $($matchingGroups.Count) matching groups:" -ForegroundColor Cyan
                for ($gi = 0; $gi -lt $matchingGroups.Count; $gi++) {
                    $g = $matchingGroups[$gi]
                    $devCount = if ($g.deviceCount) { $g.deviceCount } else { '?' }
                    Write-Host " [$($gi + 1)] $($g.name) (ID: $($g.id), Devices: $devCount)" -ForegroundColor White
                }
                if (-not $NonInteractive) {
                    Write-Host ""
                    $groupChoice = Read-Host -Prompt " Select group number [default: 1]"
                    if (-not $groupChoice) { $groupChoice = '1' }
                    $groupIdx = [int]$groupChoice - 1
                    if ($groupIdx -ge 0 -and $groupIdx -lt $matchingGroups.Count) {
                        $resolvedGroupId = $matchingGroups[$groupIdx].id
                        Write-Host " Selected: '$($matchingGroups[$groupIdx].name)' (ID: $resolvedGroupId)" -ForegroundColor Green
                    }
                    else {
                        Write-Warning "Invalid selection. Using first match."
                        $resolvedGroupId = $matchingGroups[0].id
                    }
                }
                else {
                    $resolvedGroupId = $matchingGroups[0].id
                    Write-Host " Auto-selected: '$($matchingGroups[0].name)' (ID: $resolvedGroupId)" -ForegroundColor Gray
                }
            }
            else {
                Write-Host " No groups matching '$DeviceGroupSearch'. Falling back to all devices." -ForegroundColor Yellow
            }
        }
        catch {
            Write-Warning "Failed to search device groups: $_. Falling back to all devices."
        }
    }

    if (-not $resolvedGroupId) { $resolvedGroupId = '-1' }
    Write-Host "Fetching devices from group $resolvedGroupId..." -ForegroundColor Cyan
    $devices = @(Get-WUGDevice -DeviceGroupID $resolvedGroupId)

    if ($WindowsOnly -and $devices.Count -gt 0) {
        $preCount = $devices.Count
        $devices = @($devices | Where-Object {
            ($_.role -and $_.role -match 'Windows') -or
            ($_.description -and $_.description -match 'Windows')
        })
        $filtered = $preCount - $devices.Count
        if ($filtered -gt 0) {
            Write-Host " Filtered to Windows devices: $($devices.Count) of $preCount (skipped $filtered non-Windows)." -ForegroundColor Gray
        }
    }
}

if (-not $devices -or $devices.Count -eq 0) {
    Write-Error "No devices found. Exiting."
    return
}

Write-Host "Found $($devices.Count) device(s)." -ForegroundColor Green
Write-Host "Attributes to collect: $($activeAttributes.Count) ($($activeAttributes.Keys -join ', '))" -ForegroundColor Gray
Write-Host "WMI classes needed: $($requiredClasses -join ', ')" -ForegroundColor Gray
Write-Host ""

# =============================================================================
# STEP 2: Scan each device via WMI and collect attribute data
# =============================================================================
$deviceAttrs  = @{}   # devId -> hashtable of attrKey -> value
$deviceInfo   = @{}   # devId -> @{ Name; IP }
$scannedCount = 0
$failedDevices = 0

$scanIndex = 0
foreach ($dev in $devices) {
    $scanIndex++
    $devId   = $dev.id
    $devName = if ($dev.displayName) { $dev.displayName } else { $dev.hostName }
    $devIP   = $dev.networkAddress

    if (-not $devIP) {
        Write-Warning "Device '$devName' (ID: $devId) has no network address. Skipping."
        continue
    }

    $deviceInfo[$devId] = @{ Name = $devName; IP = $devIP }

    $pct = [Math]::Round(($scanIndex / $devices.Count) * 100)
    Write-Progress -Activity "Scanning devices via WMI" `
        -Status "$devName ($devIP) [$scanIndex of $($devices.Count)]" `
        -PercentComplete $pct

    Write-Host " [$scanIndex/$($devices.Count)] $devName ($devIP)..." -ForegroundColor Gray -NoNewline

    # Try each credential
    $scanSuccess = $false
    for ($ci = 0; $ci -lt $credentialList.Count; $ci++) {
        $tryCred = $credentialList[$ci]
        if ($credentialList.Count -gt 1 -and $ci -gt 0) {
            Write-Host "" -NoNewline
            Write-Host " Retrying with credential #$($ci + 1) ($($tryCred.UserName))..." -ForegroundColor DarkGray -NoNewline
        }

        try {
            # Query each required WMI class once per device, cache results
            # WMI does not allow explicit credentials for local connections
            $isLocal = ($devIP -eq 'localhost' -or $devIP -eq '127.0.0.1' -or $devIP -eq '::1' -or $devIP -eq $env:COMPUTERNAME -or $devIP -eq '.')
            $wmiCache = @{}
            foreach ($cls in $requiredClasses) {
                if ($isLocal) {
                    $wmiCache[$cls] = Get-WmiObject -Class $cls -ErrorAction Stop
                } else {
                    $wmiCache[$cls] = Get-WmiObject -Class $cls -ComputerName $devIP -Credential $tryCred -ErrorAction Stop
                }
            }

            # Extract attribute values
            $attrs = [ordered]@{}
            foreach ($key in $activeAttributes.Keys) {
                $def = $activeAttributes[$key]

                if ($def.Compute) {
                    # Compute-style: pass full cache + connection info + local flag
                    try {
                        $val = & $def.Compute $wmiCache $devIP $tryCred $isLocal
                    }
                    catch {
                        Write-Verbose " Compute failed for '$key' on ${devIP}: $_"
                        $val = ''
                    }
                }
                else {
                    # Simple Class+Property style
                    $wmiObj = $wmiCache[$def.Class]
                    if (-not $wmiObj) { $attrs[$key] = ''; continue }

                    # Some classes return arrays (e.g. Win32_Processor) -- take first
                    $obj = if ($wmiObj -is [System.Array]) { $wmiObj[0] } else { $wmiObj }

                    $rawVal = $obj.($def.Property)
                    if ($def.Transform) {
                        $val = & $def.Transform $rawVal
                    }
                    else {
                        $val = if ($null -ne $rawVal) { "$rawVal".Trim() } else { '' }
                    }
                }
                $attrs[$key] = $val
            }

            $deviceAttrs[$devId] = $attrs
            $scannedCount++
            $credLabel = if ($credentialList.Count -gt 1) { " [cred #$($ci + 1)]" } else { '' }
            Write-Host " OK ($($attrs.Count) attrs)$credLabel" -ForegroundColor Green
            $scanSuccess = $true
            break
        }
        catch [System.UnauthorizedAccessException] {
            if ($ci -lt ($credentialList.Count - 1)) {
                Write-Verbose "Credential #$($ci + 1) access denied on ${devIP}. Trying next."
                continue
            }
            Write-Host " access denied (all $($credentialList.Count) credential(s) failed)." -ForegroundColor Red
            $failedDevices++
        }
        catch [System.Runtime.InteropServices.COMException] {
            $comMsg = $_.Exception.Message
            if ($comMsg -match 'Access is denied|0x80070005') {
                if ($ci -lt ($credentialList.Count - 1)) {
                    Write-Verbose "Credential #$($ci + 1) COM access denied on ${devIP}. Trying next."
                    continue
                }
                Write-Host " access denied (all $($credentialList.Count) credential(s) failed)." -ForegroundColor Red
            }
            else {
                Write-Host " WMI/RPC unavailable." -ForegroundColor Red
            }
            $failedDevices++
        }
        catch {
            Write-Host " failed: $($_.Exception.Message)" -ForegroundColor Red
            $failedDevices++
            break
        }
    }
}

Write-Progress -Activity "Scanning devices via WMI" -Completed

if ($deviceAttrs.Count -eq 0) {
    Write-Warning "No device attributes collected. Nothing to do."
    return
}

# =============================================================================
# Summary
# =============================================================================
Write-Host ""
Write-Host "--- Scan Summary ---" -ForegroundColor Cyan
Write-Host " Devices scanned OK: $scannedCount" -ForegroundColor White
Write-Host " Devices failed: $failedDevices" -ForegroundColor $(if ($failedDevices -gt 0) { 'Red' } else { 'Gray' })
Write-Host " Attributes per device: $($activeAttributes.Count)" -ForegroundColor White
Write-Host ""

# Show collected data
foreach ($devId in ($deviceAttrs.Keys | Sort-Object)) {
    $info = $deviceInfo[$devId]
    Write-Host " $($info.Name) ($($info.IP)):" -ForegroundColor White
    foreach ($key in $deviceAttrs[$devId].Keys) {
        $attrName = "${AttributePrefix}.${key}"
        $attrVal  = $deviceAttrs[$devId][$key]
        Write-Host " $attrName = $attrVal" -ForegroundColor Gray
    }
}
Write-Host ""

if ($DryRun) {
    Write-Host "[DRY RUN] No changes made." -ForegroundColor Yellow
    return
}

# =============================================================================
# STEP 3: Action routing (menu or -Action parameter)
# =============================================================================
if ($OutputPath) {
    $OutputDir = $OutputPath
} else {
    $OutputDir = Join-Path $env:LOCALAPPDATA 'WhatsUpGoldPS\DiscoveryHelpers\Output'
}
if (-not (Test-Path $OutputDir)) { New-Item -ItemType Directory -Path $OutputDir -Force | Out-Null }

$choice = $null
if ($Action) {
    switch ($Action) {
        'PushToWUG'        { $choice = '1' }
        'ExportJSON'       { $choice = '2' }
        'ExportCSV'        { $choice = '3' }
        'ShowTable'        { $choice = '4' }
        'Dashboard'        { $choice = '5' }
        'None'             { $choice = '6' }
        'DashboardAndPush' { $choice = '7' }
    }
}

if (-not $choice -and $NonInteractive) { $choice = '5' }

if (-not $choice) {
    Write-Host "What would you like to do with the collected attributes?" -ForegroundColor Cyan
    if ($standaloneMode) {
        Write-Host " [1] Push to WhatsUp Gold (not available in standalone mode)" -ForegroundColor DarkGray
    }
    else {
        Write-Host " [1] Push attributes to WhatsUp Gold"
    }
    Write-Host " [2] Export to JSON file"
    Write-Host " [3] Export to CSV file"
    Write-Host " [4] Show full table in console"
    Write-Host " [5] Generate HTML dashboard (inventory report)"
    Write-Host " [6] Exit (do nothing)"
    if (-not $standaloneMode) {
        Write-Host " [7] Dashboard + Push to WUG"
    }
    Write-Host ""
    $choice = Read-Host -Prompt "Choice [1-7]"
}

# Handle DashboardAndPush: run Dashboard then PushToWUG sequentially
if ($choice -eq '7') {
    $actionsToRun = @('5', '1')
} else {
    $actionsToRun = @($choice)
}

# Build export-friendly data for JSON/CSV/Table/Dashboard
# Attribute keys that hold numeric values (cast for dashboard thresholds)
$numericAttrs = @('TotalMemoryGB', 'FreeMemoryGB', 'RAMSticks', 'RAMSpeedMHz',
    'CPUCores', 'CPULogical', 'CPUSpeedMHz', 'GPUMemoryMB',
    'DiskCount', 'TotalDiskGB', 'NetworkAdapters',
    'RunningServices', 'StoppedServices', 'InstalledSoftware')

$exportData = @()
foreach ($devId in ($deviceAttrs.Keys | Sort-Object)) {
    $info  = $deviceInfo[$devId]
    $attrs = $deviceAttrs[$devId]
    $row   = [ordered]@{
        DeviceId = $devId
        Host     = $info.Name
        IP       = $info.IP
    }
    foreach ($key in $attrs.Keys) {
        $val = $attrs[$key]
        if ($numericAttrs -contains $key -and $val -match '^\d+(\.\d+)?$') {
            $row["${AttributePrefix}.${key}"] = [double]$val
        }
        else {
            $row["${AttributePrefix}.${key}"] = $val
        }
    }
    $exportData += [PSCustomObject]$row
}

foreach ($currentChoice in $actionsToRun) {
switch ($currentChoice) {
    '1' {
        # PushToWUG
        if ($standaloneMode) {
            Write-Warning "Push to WUG requires devices resolved from WhatsUp Gold. Omit -Target or use -DeviceId."
            continue
        }

        Write-Host "Updating device attributes in WhatsUp Gold..." -ForegroundColor Cyan

        $updatedCount = 0
        $errorCount   = 0
        $totalAttrs   = 0

        $devIndex = 0
        foreach ($devId in ($deviceAttrs.Keys | Sort-Object)) {
            $devIndex++
            $info  = $deviceInfo[$devId]
            $attrs = $deviceAttrs[$devId]

            $pct = [Math]::Round(($devIndex / $deviceAttrs.Count) * 100)
            Write-Progress -Activity "Updating device attributes" `
                -Status "$($info.Name) [$devIndex of $($deviceAttrs.Count)]" `
                -PercentComplete $pct

            foreach ($key in $attrs.Keys) {
                $attrName = "${AttributePrefix}.${key}"
                $attrVal  = $attrs[$key]
                $totalAttrs++

                if ([string]::IsNullOrEmpty($attrVal)) {
                    Write-Verbose " Skipping empty attribute '$attrName' on $($info.Name)"
                    continue
                }

                if (-not $PSCmdlet.ShouldProcess("$($info.Name) (ID: $devId)", "Set attribute '$attrName' = '$attrVal'")) {
                    continue
                }

                try {
                    Set-WUGDeviceAttribute -DeviceId $devId -Name $attrName -Value $attrVal -Confirm:$false | Out-Null
                    $updatedCount++
                    Write-Verbose " Set $attrName = $attrVal on $($info.Name)"
                }
                catch {
                    Write-Warning " Failed to set '$attrName' on $($info.Name) (ID: ${devId}): $_"
                    $errorCount++
                }
            }

            # Stamp last-run timestamp per device
            $lastRunName = "${AttributePrefix}.LastRun"
            $lastRunVal  = (Get-Date).ToString('o')
            if ($PSCmdlet.ShouldProcess("$($info.Name) (ID: $devId)", "Set attribute '$lastRunName' = '$lastRunVal'")) {
                try {
                    Set-WUGDeviceAttribute -DeviceId $devId -Name $lastRunName -Value $lastRunVal -Confirm:$false | Out-Null
                    Write-Verbose " Set $lastRunName = $lastRunVal on $($info.Name)"
                }
                catch {
                    Write-Warning " Failed to set '$lastRunName' on $($info.Name) (ID: ${devId}): $_"
                }
            }
        }

        Write-Progress -Activity "Updating device attributes" -Completed

        Write-Host ""
        Write-Host "=== Windows Attributes Push Complete ===" -ForegroundColor Cyan
        Write-Host " Devices processed: $($deviceAttrs.Count)" -ForegroundColor Green
        Write-Host " Attributes updated: $updatedCount" -ForegroundColor Green
        Write-Host " Attributes skipped: $($totalAttrs - $updatedCount - $errorCount)" -ForegroundColor Gray
        Write-Host " Errors: $errorCount" -ForegroundColor $(if ($errorCount -gt 0) { 'Red' } else { 'Gray' })
        Write-Host ""
    }
    '2' {
        # ExportJSON
        $jsonPath = Join-Path $OutputDir "WindowsAttributes-$(Get-Date -Format 'yyyyMMdd-HHmmss').json"
        $exportData | ConvertTo-Json -Depth 5 | Set-Content -Path $jsonPath -Encoding UTF8
        Write-Host "Exported to: $jsonPath" -ForegroundColor Green
    }
    '3' {
        # ExportCSV
        $csvPath = Join-Path $OutputDir "WindowsAttributes-$(Get-Date -Format 'yyyyMMdd-HHmmss').csv"
        $exportData | Export-Csv -Path $csvPath -NoTypeInformation -Encoding UTF8
        Write-Host "Exported to: $csvPath" -ForegroundColor Green
    }
    '4' {
        # ShowTable
        $exportData | Format-Table -AutoSize
    }
    '5' {
        # Dashboard - generate HTML inventory report
        $dashPath = Join-Path $OutputDir 'WindowsAttributes-Dashboard.html'

        if (Get-Command -Name 'Export-WindowsAttributesDashboardHtml' -ErrorAction SilentlyContinue) {
            Export-WindowsAttributesDashboardHtml -DashboardData $exportData `
                -OutputPath $dashPath `
                -ReportTitle 'Windows Attributes Inventory'
        }
        elseif (Get-Command -Name 'Export-DynamicDashboardHtml' -ErrorAction SilentlyContinue) {
            $thresholds = @(
                @{ Field = 'Windows.FreeMemoryGB'; Warning = 4; Critical = 2; Invert = $true }
                @{ Field = 'Windows.StoppedServices'; Warning = 50; Critical = 80 }
            )
            Export-DynamicDashboardHtml -Data $exportData `
                -OutputPath $dashPath `
                -ReportTitle 'Windows Attributes Inventory' `
                -CardField @('Windows.IsVirtualMachine', 'Windows.Domain') `
                -ThresholdField $thresholds `
                -ExportPrefix 'WindowsAttributes'
        }
        else {
            Write-Warning "No dashboard function available. Exporting as JSON instead."
            $jsonPath = Join-Path $OutputDir "WindowsAttributes-$(Get-Date -Format 'yyyyMMdd-HHmmss').json"
            $exportData | ConvertTo-Json -Depth 5 | Set-Content -Path $jsonPath -Encoding UTF8
            Write-Host "JSON exported: $jsonPath" -ForegroundColor Green
            break
        }

        Write-Host "Dashboard generated: $dashPath" -ForegroundColor Green
        Write-Host " Devices: $($deviceAttrs.Count) | Attributes: $($activeAttributes.Count)" -ForegroundColor Gray

        # Try to copy to WUG NmConsole
        $nmConsolePaths = @(
            "${env:ProgramFiles(x86)}\Ipswitch\WhatsUp\Html\NmConsole"
            "${env:ProgramFiles}\Ipswitch\WhatsUp\Html\NmConsole"
        )
        $nmConsolePath = $nmConsolePaths | Where-Object { Test-Path $_ } | Select-Object -First 1
        if ($nmConsolePath) {
            $wugDashDir = Join-Path $nmConsolePath 'dashboards'
            if (-not (Test-Path $wugDashDir)) { New-Item -ItemType Directory -Path $wugDashDir -Force | Out-Null }
            $wugDashPath = Join-Path $wugDashDir 'WindowsAttributes-Dashboard.html'
            try {
                Copy-Item -Path $dashPath -Destination $wugDashPath -Force
                Write-Host "Copied to WUG: $wugDashPath" -ForegroundColor Green
                Write-Host " Access via WUG web UI: /NmConsole/dashboards/WindowsAttributes-Dashboard.html" -ForegroundColor Cyan
            }
            catch {
                Write-Warning "Could not copy to NmConsole (run as admin?): $_"
            }
            Deploy-DashboardWebConfig -Path $wugDashDir
        }
    }
    '6' {
        Write-Host "No action taken." -ForegroundColor Gray
    }
    default {
        Write-Warning "Invalid choice '$currentChoice'."
    }
}
} # end foreach actionsToRun

Write-Host ""
Write-Host "=== Windows Attributes Discovery Complete ===" -ForegroundColor Cyan
Write-Host ""

# SIG # Begin signature block
# MIIr+wYJKoZIhvcNAQcCoIIr7DCCK+gCAQExDzANBglghkgBZQMEAgEFADB5Bgor
# BgEEAYI3AgEEoGswaTA0BgorBgEEAYI3AgEeMCYCAwEAAAQQH8w7YFlLCE63JNLG
# KX7zUQIBAAIBAAIBAAIBAAIBADAxMA0GCWCGSAFlAwQCAQUABCDPEz1uaSdCItOI
# RyM/rmC8G8w8OaxMXcYDy6nvM6qzcKCCJQ0wggVvMIIEV6ADAgECAhBI/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
# BCCZJMi7UzERfc4pki7KFffSmmnFfvHnDJi1Bhtd6EcfADANBgkqhkiG9w0BAQEF
# AASCAgBHG8EIvvSds1B5W0igpb2MQlBtIhBCVjQSm9tnpf8/j3vJ7TQyfn93btkC
# kKxVE5TO5B6JuzA93MRkcr0qFycAybxayKp0WKIJqYsoOomj7XSj5dHdsxZt7x9r
# SlnD3GASMAr7QBv12NdPKE01n0U4LUvv9TiHnQ6izZlys67JZOkFZbF2YuTIAFFB
# jLH6Ck+BfJZOaeoPgfG51qQTEzz4KCXuBiUA/WqgmdD9Qs+StyYZEZtGQhI8NuDq
# zoY3XfpUWfZU7LXTVKAMNGXzjrKkNPOcmnq2Mijc3h2oa4PAKAg8fAdVhhZGCOxq
# aumHfcAl0lzN8Pl6c3/Ga50n0s8bP3lY0xKH1t/RelZ/95GmNc35Cu3oDfnuMHgn
# Grt/741WShTw5kEcib76MCoZIqLQL6Szmegc9uv5W+e0vHxOk0gH32gbF0lyDZpP
# KzFfre7TksCSZm8SzH2w2rYkJcN6I1NFJzzQ/Sx2LqRDQdBLez5iagRe3Lg04rqP
# sP8AEq73+/5kOGqs/Iq2Sd14p2xbkvqiByVssHNjDMi2I8x7ebHMp9SXEdgkbzGd
# pd8UBGR2ba+uReZIZbrXFGkEoGIU0RLMnbWxnYCe2T6MTt/gkW1rE6JXzpNmIHYd
# jZB1AP3LymaWE+alYxwX6UPuoKMNwGaC2coIt2DupEgIRo08xaGCAyYwggMiBgkq
# hkiG9w0BCQYxggMTMIIDDwIBATB9MGkxCzAJBgNVBAYTAlVTMRcwFQYDVQQKEw5E
# aWdpQ2VydCwgSW5jLjFBMD8GA1UEAxM4RGlnaUNlcnQgVHJ1c3RlZCBHNCBUaW1l
# U3RhbXBpbmcgUlNBNDA5NiBTSEEyNTYgMjAyNSBDQTECEAqA7xhLjfEFgtHEdqeV
# dGgwDQYJYIZIAWUDBAIBBQCgaTAYBgkqhkiG9w0BCQMxCwYJKoZIhvcNAQcBMBwG
# CSqGSIb3DQEJBTEPFw0yNjA1MjgyMDE2MTdaMC8GCSqGSIb3DQEJBDEiBCC7hWh/
# hkglfPtdpqCTc+/z611RJyhuKKI4Gy/dlCNsOTANBgkqhkiG9w0BAQEFAASCAgBL
# Zqs9ZUy0E/S1BiRRxvVKASDKRCMeUSAXMo8VpPy+H9ykdGdWnbs4c83AwYIwaAAJ
# TuNdBSvUZgMtFhACCi35D0s5yrtpO0gbodcgxlI5TVlc6rzPbQWaFAYfIrx8hDJG
# frLAuTJSiwMAuBY6PKT33tfPvkLhBs/lZI9Kw9Pu4BVnBSI8v4XEW73FUW1tfWfa
# IBEyQCKQwsMhyH3X+6Xjza+4bgA+XI9fIiEFmQL0BFFjBRMcEfaWm6lMZ/oR4WSC
# LMlYWa2r3Hg2NgFMRpv7XE4Bk/r2QWp5TQG7GX1FEsefWUHfge9IqhAHj6ievdCP
# AYw3lYp07wbUoSIpFylndVAhLysLzHjw8fVvC05iHqS2b+A5x0CCNYhT64/zGgeq
# 0vF5XS3ppwVeEQJhedMbAGvouv+Kis7LkP8Nq9GypLRgtyhRNJvjo7wStmLHpbc2
# cu39+q5J0pidwfev0QmUTvXbLD+UMbI1FR3dQzsS1Ghusv/GYjqDlAYQteCKBQuW
# 8tTqQ3UoWoUjO47rIL8Btfc7LVmG1MbAnjtlz40QsmvlU12OZl+wcDXvi7QgPVXa
# FSECiCEzMCs4a17NGE5TIIEU77HIznx/PBXh0wqNo5hOm0Bd3GyCPknCYXhdKrst
# 89yeylcG52AfsoZlxBVxGPW6TdFlffIBRYKSkTgzCw==
# SIG # End signature block