helpers/discovery/Setup-AWS-Discovery.ps1
|
<# .SYNOPSIS AWS Cloud Discovery — scan all regions, generate dashboard, push to WUG. .DESCRIPTION Discovers AWS resources (EC2 instances, RDS databases, Load Balancers) across ALL enabled regions by default — just like Azure discovery scans all subscriptions and resource groups automatically. ===================================================================== QUICK START (3 steps) ===================================================================== Step 1 — Run interactively ONCE to save your AWS credentials: .\Setup-AWS-Discovery.ps1 Step 2 — Schedule it (runs daily at 2 AM, no prompts): .\Register-DiscoveryScheduledTask.ps1 -Mode Provider ` -Provider AWS -Action Dashboard -TriggerType Daily Step 3 — Open the dashboard: Start-Process "$env:LOCALAPPDATA\WhatsUpGoldPS\DiscoveryHelpers\Output\AWS-Dashboard.html" That's it. The script scans every AWS region your IAM key can access, discovers all EC2/RDS/ELB resources, and generates a searchable HTML dashboard with sortable tables, summary cards, and CSV/JSON export. ===================================================================== WHAT IT DOES ===================================================================== 1. Authenticates with your AWS Access Key + Secret Key (DPAPI vault) 2. Enumerates ALL enabled AWS regions (or specific ones you choose) 3. In each region, discovers: - EC2 instances (name, IP, state, type, platform, VPC, AZ) - RDS instances (endpoint, engine, class, state, AZ) - Elastic Load Balancers (DNS, type, state, VPC, AZs) 4. Presents an action menu: [1] Push to WhatsUp Gold (create devices + monitors + attributes) [2] Export JSON [3] Export CSV [4] Show table [5] Generate HTML dashboard [6] Exit Zero external dependencies when using REST API mode (default). AWS.Tools PowerShell modules are optional for module-based collection. ===================================================================== WHATSUP GOLD INTEGRATION ===================================================================== Option [1] PushToWUG does the following automatically: - Creates a WUG device for each AWS resource (by IP) - Sets rich device attributes: AWS.Region, AWS.InstanceType, AWS.State, AWS.VpcId, AWS.Platform, AWS.AZ, etc. - Creates REST API active + performance monitors - Use these attributes in WUG dashboards, reports, and groups You can also use the Cloud Resource Monitor in WUG and point it at these attributes for cloud-native monitoring. ===================================================================== CREDENTIAL SECURITY ===================================================================== - AWS Access Key + Secret Key stored in DPAPI vault (encrypted to your Windows user account + machine — cannot be read by other users or on other machines) - No credentials in plaintext, ever — not in scripts, logs, or history - First run prompts with masked input, subsequent runs auto-load - Optional AES-256 double encryption via Set-DiscoveryVaultPassword .PARAMETER Region AWS region(s) to scan. Default: 'all' (every enabled region). Examples: -Region 'all' # Scan everything (default) -Region 'us-east-1' # Single region -Region 'us-east-1','eu-west-1' # Multiple specific regions When set to 'all', the script uses the AWS DescribeRegions API to enumerate every region your IAM key has access to, then scans each one. .PARAMETER UseRestApi Collection method. Default: $true (REST API, zero dependencies). Pass -UseRestApi:$false to use AWS.Tools PowerShell modules instead. .PARAMETER Action What to do after discovery. Skips the interactive menu when specified. Valid: PushToWUG, ExportJSON, ExportCSV, ShowTable, Dashboard, None. Non-interactive default: Dashboard. .PARAMETER WUGServer WhatsUp Gold server address for PushToWUG. Default: 192.168.74.74. .PARAMETER WUGCredential PSCredential for WhatsUp Gold admin login (non-interactive WUG push). .PARAMETER OutputPath Output directory for dashboards and exports. Non-interactive default: %LOCALAPPDATA%\WhatsUpGoldPS\DiscoveryHelpers\Output. .PARAMETER NonInteractive Suppress all prompts. Uses cached vault credentials and defaults. Ideal for scheduled task execution via Register-DiscoveryScheduledTask.ps1. .EXAMPLE .\Setup-AWS-Discovery.ps1 # Interactive mode — scans all regions, prompts for action. .EXAMPLE .\Setup-AWS-Discovery.ps1 -Action Dashboard # Scan all regions, generate dashboard, no other prompts. .EXAMPLE .\Setup-AWS-Discovery.ps1 -Region 'us-east-1','us-west-2' -Action Dashboard # Scan only two regions, generate dashboard. .EXAMPLE .\Setup-AWS-Discovery.ps1 -Action PushToWUG -NonInteractive # Scheduled mode — scan all regions, push to WUG, zero prompts. .EXAMPLE .\Setup-AWS-Discovery.ps1 -Action Dashboard -NonInteractive # Scheduled mode — scan all regions, generate dashboard only. .NOTES Author : jason@wug.ninja Requires: PowerShell 5.1+ REST API mode has ZERO external module dependencies. Module mode requires: AWS.Tools.EC2, AWS.Tools.RDS, AWS.Tools.ElasticLoadBalancingV2, AWS.Tools.CloudWatch. WhatsUpGoldPS module is only needed for PushToWUG. .LINK https://github.com/jayyx2/WhatsUpGoldPS #> [CmdletBinding()] param( [Alias('Target')] [string[]]$Region = @('all'), [switch]$UseRestApi, [ValidateSet('PushToWUG', 'ExportJSON', 'ExportCSV', 'ShowTable', 'Dashboard', 'DashboardAndPush', 'None')] [string]$Action, [string]$WUGServer = '192.168.74.74', [PSCredential]$WUGCredential, [string]$OutputPath, # --- Opt-in cost, quota and waste analysis ------------------------------- [switch]$IncludeCostAndQuota, [hashtable]$UnitPrice, [string]$CostReportPath, [switch]$NonInteractive ) # --- Output directory (persistent default for scheduled runs) ----------------- if (-not $OutputPath) { if ($NonInteractive) { $OutputPath = Join-Path $env:LOCALAPPDATA 'WhatsUpGoldPS\DiscoveryHelpers\Output' } else { $OutputPath = $env:TEMP } } if (-not (Test-Path $OutputPath)) { New-Item -ItemType Directory -Path $OutputPath -Force | Out-Null } $OutputDir = $OutputPath # --- Configuration ----------------------------------------------------------- # Default to REST API if not explicitly set if (-not $PSBoundParameters.ContainsKey('UseRestApi')) { $UseRestApi = $true } # --- Load helpers (works from any directory) ---------------------------------- $scriptDir = Split-Path $MyInvocation.MyCommand.Path -Parent . (Join-Path $scriptDir 'DiscoveryHelpers.ps1') . (Join-Path $scriptDir 'DiscoveryProvider-AWS.ps1') # ============================================================================== # STEP 1: Gather target info # ============================================================================== Write-Host "=== AWS Discovery ===" -ForegroundColor Cyan Write-Host "" # --- Collection method --------------------------------------------------------- if (-not $PSBoundParameters.ContainsKey('UseRestApi') -and -not $NonInteractive) { Write-Host "AWS data collection method:" -ForegroundColor Cyan Write-Host " [1] REST API direct (zero external dependencies, SigV4 signing)" -ForegroundColor White Write-Host " [2] AWS.Tools PowerShell modules (requires AWS.Tools.EC2, etc.)" -ForegroundColor White Write-Host "" $methodChoice = Read-Host -Prompt "Choice [1/2, default: 1]" $UseRestApi = ($methodChoice -ne '2') } if ($UseRestApi) { Write-Host "Using REST API mode (no AWS.Tools modules needed)." -ForegroundColor Green } else { Write-Host "Using AWS.Tools module mode." -ForegroundColor Green # --- Check for AWS.Tools modules ----------------------------------------------- if (-not (Get-Module -ListAvailable -Name AWS.Tools.EC2 -ErrorAction SilentlyContinue)) { Write-Warning "AWS.Tools.EC2 module not found. Install with:" Write-Host " Install-Module -Name AWS.Tools.Installer -Scope CurrentUser -Force" -ForegroundColor Yellow Write-Host " Install-AWSToolsModule EC2, RDS, ElasticLoadBalancingV2, CloudWatch -CleanUp" -ForegroundColor Yellow if ($NonInteractive) { Write-Error "AWS.Tools modules not found and cannot install in non-interactive mode." return } Write-Host "" $installChoice = Read-Host -Prompt "Attempt to install now? [y/N]" if ($installChoice -eq 'y' -or $installChoice -eq 'Y') { try { Install-Module -Name AWS.Tools.Installer -Scope CurrentUser -Force -ErrorAction Stop Install-AWSToolsModule EC2, RDS, ElasticLoadBalancingV2, CloudWatch, Common -CleanUp -ErrorAction Stop Write-Host "AWS.Tools modules installed successfully." -ForegroundColor Green } catch { Write-Error "Failed to install AWS.Tools modules: $_" return } } else { Write-Host "Cannot proceed without AWS.Tools modules. Exiting." -ForegroundColor Red return } } } # --- Resolve region(s) --------------------------------------------------------- $AWSRegions = @($Region) # Interactive region selection (if not specified via parameter) if (-not $NonInteractive -and -not $PSBoundParameters.ContainsKey('Region')) { Write-Host "" Write-Host "AWS region scope:" -ForegroundColor Cyan Write-Host " [1] All regions (scans every enabled region — recommended)" -ForegroundColor White Write-Host " [2] Specific region(s) (e.g. us-east-1, eu-west-1)" -ForegroundColor White Write-Host "" $regionChoice = Read-Host -Prompt "Choice [1/2, default: 1]" if ($regionChoice -eq '2') { $regionInput = Read-Host -Prompt "Enter region(s), comma-separated" if (-not [string]::IsNullOrWhiteSpace($regionInput)) { $AWSRegions = @($regionInput -split '\s*,\s*' | ForEach-Object { $_.Trim() } | Where-Object { $_ }) } } else { $AWSRegions = @('all') } } if ($AWSRegions.Count -eq 0) { $AWSRegions = @('all') } $scanLabel = if ($AWSRegions -contains 'all') { 'ALL enabled regions' } else { $AWSRegions -join ', ' } Write-Host "Regions: $scanLabel" -ForegroundColor Cyan Write-Host "" # ============================================================================== # STEP 2: Credentials (DPAPI vault — encrypted, cached) # ============================================================================== $credSplat = @{ Name = 'AWS.Credential'; CredType = 'AWSKeys'; ProviderLabel = 'AWS' } if ($NonInteractive) { $credSplat.NonInteractive = $true } elseif ($Action) { $credSplat.AutoUse = $true } $AWSCred = Resolve-DiscoveryCredential @credSplat if (-not $AWSCred) { Write-Error 'No AWS credentials. Exiting.' return } # Handle both PSCredential format (UserName=AccessKey, Password=SecureString of SecretKey) # and Hashtable format (AccessKey, SecretKey) from Fields-based vault storage if ($AWSCred -is [PSCredential]) { $awsAccessKey = $AWSCred.UserName $bstrAws = [System.Runtime.InteropServices.Marshal]::SecureStringToBSTR($AWSCred.Password) try { $plainAwsSK = [System.Runtime.InteropServices.Marshal]::PtrToStringAuto($bstrAws) } finally { [System.Runtime.InteropServices.Marshal]::ZeroFreeBSTR($bstrAws) } } elseif ($AWSCred -is [hashtable]) { # Fields-based vault storage: AccessKey, SecretKey (plaintext strings) $awsAccessKey = $AWSCred.AccessKey $plainAwsSK = $AWSCred.SecretKey } else { Write-Warning "No valid AWS credential available." return } # ============================================================================== # STEP 3: Discover — authenticate and enumerate AWS resources # ============================================================================== Write-Host "" Write-Host "Scanning AWS: $scanLabel..." -ForegroundColor Cyan # First non-'all' region for initial connection; provider resolves 'all' dynamically $connectRegion = ($AWSRegions | Where-Object { $_ -ne 'all' } | Select-Object -First 1) if (-not $connectRegion) { $connectRegion = 'us-east-1' } $plan = Invoke-Discovery -ProviderName 'AWS' ` -Target $AWSRegions ` -Credential @{ AccessKey = $awsAccessKey; SecretKey = $plainAwsSK; Region = $connectRegion; UseRestApi = $UseRestApi } if (-not $plan -or $plan.Count -eq 0) { Write-Warning "No items discovered. Check AWS credentials and region accessibility." if ($Action -ne 'Dashboard' -and $Action -ne 'DashboardAndPush') { return } $plan = @() } # ============================================================================== # STEP 3b: Cost, quota and waste analysis (opt-in) # ============================================================================== $costFindings = @() if ($IncludeCostAndQuota) { $costHelpers = Join-Path $scriptDir 'CloudCostHelpers.ps1' if (-not (Test-Path $costHelpers)) { Write-Warning "CloudCostHelpers.ps1 not found; skipping cost and quota analysis." } else { . $costHelpers Write-Host "" Write-Host "Analyzing AWS cost, quota and waste..." -ForegroundColor Cyan $costRegions = @($plan | ForEach-Object { $_.Attributes['AWS.Region'] } | Where-Object { $_ } | Sort-Object -Unique) if ($costRegions.Count -eq 0) { $costRegions = @($connectRegion) } foreach ($costRegion in $costRegions) { Write-Host " Region $costRegion..." -ForegroundColor DarkGray try { $costSplat = @{ Region = $costRegion; AccountName = "AWS $costRegion" } if ($UnitPrice) { $costSplat['UnitPrice'] = $UnitPrice } $costFindings += @(Get-AWSCostQuotaFinding @costSplat) } catch { Write-Warning " Cost analysis failed for ${costRegion}: $($_.Exception.Message)" } } if ($costFindings.Count -gt 0) { Get-CloudCostSummary -Finding $costFindings | Format-Table -AutoSize | Out-String -Width 160 | Write-Host $costFindings | Where-Object { $_.Status -eq 'Fail' -or $_.Status -eq 'Warn' } | Select-Object Category, Check, Status, Value, Resource | Format-Table -AutoSize | Out-String -Width 160 | Write-Host if (-not $CostReportPath) { $CostReportPath = Join-Path $OutputDir 'AWS-Cost-Quota.html' } try { Export-CloudCostDashboard -Finding $costFindings -OutputPath $CostReportPath -ReportTitle 'AWS Cost and Quota' | Out-Null Write-Host " Cost report: $CostReportPath" -ForegroundColor Green } catch { Write-Warning " Could not render cost dashboard: $($_.Exception.Message)" } } else { Write-Host " No cost or quota findings returned." -ForegroundColor DarkGray } } } # ============================================================================== # STEP 4: Show the plan # ============================================================================== $devicePlan = [ordered]@{} foreach ($item in $plan) { $resName = $item.Attributes['AWS.ResourceName'] $region = $item.Attributes['AWS.Region'] $key = "resource:${region}:$resName" if (-not $devicePlan.Contains($key)) { $devicePlan[$key] = @{ Name = $resName IP = $item.Attributes['AWS.IPAddress'] Type = $item.Attributes['AWS.DeviceType'] Region = $region State = $item.Attributes['AWS.State'] Attrs = $item.Attributes Items = [System.Collections.ArrayList]@() } } [void]$devicePlan[$key].Items.Add($item) } $withIP = @($devicePlan.Values | Where-Object { $_.IP }) $withoutIP = @($devicePlan.Values | Where-Object { -not $_.IP }) $ec2Count = @($devicePlan.Values | Where-Object { $_.Type -eq 'EC2' }).Count $rdsCount = @($devicePlan.Values | Where-Object { $_.Type -eq 'RDS' }).Count $elbCount = @($devicePlan.Values | Where-Object { $_.Type -eq 'ELB' }).Count # Discover actual regions from the plan data $discoveredRegions = @($devicePlan.Values | Select-Object -ExpandProperty Region -Unique | Sort-Object) $activeTemplates = @($plan | Where-Object { $_.ItemType -eq 'ActiveMonitor' } | Select-Object -ExpandProperty Name -Unique) $perfTemplates = @($plan | Where-Object { $_.ItemType -eq 'PerformanceMonitor' } | Select-Object -ExpandProperty Name -Unique) Write-Host "" Write-Host "Discovery complete!" -ForegroundColor Green Write-Host " Total Resources: $($devicePlan.Count)" -ForegroundColor White Write-Host " EC2 Instances: $ec2Count" -ForegroundColor White Write-Host " RDS Instances: $rdsCount" -ForegroundColor White Write-Host " Load Balancers: $elbCount" -ForegroundColor White Write-Host " Resources (with IP): $($withIP.Count)" -ForegroundColor White Write-Host " Resources (no IP): $($withoutIP.Count)" -ForegroundColor White Write-Host " Regions with resources:$($discoveredRegions.Count) ($($discoveredRegions -join ', '))" -ForegroundColor White Write-Host "" Write-Host " Active monitor templates: $($activeTemplates.Count) ($(@($plan | Where-Object { $_.ItemType -eq 'ActiveMonitor' }).Count) assignments)" -ForegroundColor White Write-Host " Perf monitor templates: $($perfTemplates.Count) ($(@($plan | Where-Object { $_.ItemType -eq 'PerformanceMonitor' }).Count) assignments)" -ForegroundColor White Write-Host " Total plan items: $($plan.Count)" -ForegroundColor White Write-Host "" $devicePlan.Values | Sort-Object @{E={$_.Type}}, @{E={$_.Name}} | Select-Object -First 50 | ForEach-Object { [PSCustomObject]@{ Resource = $_.Name Type = $_.Type IP = if ($_.IP) { $_.IP } else { 'N/A' } Region = $_.Region State = $_.State Monitors = $_.Items.Count }} | Format-Table -AutoSize if ($devicePlan.Count -gt 50) { Write-Host " ... and $($devicePlan.Count - 50) more resources (use option [4] to see all)" -ForegroundColor Gray } # ============================================================================== # STEP 5: Export or push to WUG # ============================================================================== # --- Map Action parameter to menu choice number --- $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' # Default to Dashboard for non-interactive } if (-not $choice) { Write-Host "What would you like to do?" -ForegroundColor Cyan Write-Host " [1] Push monitors to WhatsUp Gold (creates devices + monitors)" Write-Host " [2] Export plan to JSON file" Write-Host " [3] Export plan to CSV file" Write-Host " [4] Show full plan table" Write-Host " [5] Generate AWS HTML dashboard (live data)" Write-Host " [6] Exit (do nothing)" 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) } foreach ($currentChoice in $actionsToRun) { switch ($currentChoice) { '1' { Write-Host "Loading WhatsUpGoldPS module..." -ForegroundColor Cyan try { $repoRoot = Split-Path (Split-Path $PSScriptRoot -Parent) -Parent $repoPsd1 = Join-Path $repoRoot 'WhatsUpGoldPS.psd1' if (Test-Path $repoPsd1) { Import-Module $repoPsd1 -Force -ErrorAction Stop } else { Import-Module WhatsUpGoldPS -ErrorAction Stop } } catch { Write-Error "Could not load WhatsUpGoldPS module. Is it installed? $_" return } # Dot-source internal helper so scripts can call Get-WUGAPIResponse directly $apiResponsePath = Join-Path $PSScriptRoot '..\..\functions\Get-WUGAPIResponse.ps1' if (Test-Path $apiResponsePath) { . $apiResponsePath } if ($WUGCredential) { Connect-WUGServer -serverUri $WUGServer -Credential $WUGCredential -IgnoreSSLErrors } else { Connect-WUGServer -AutoConnect -IgnoreSSLErrors } # ================================================================== # Phase 1: Create AWS credential in WUG library # ================================================================== Write-Host "" Write-Host "Phase 1: AWS Credential in WUG..." -ForegroundColor Cyan # Use pre-extracted credentials (supports both PSCredential and Hashtable formats) $awsCredName = "AWS - $awsAccessKey" $wugAwsCredId = $null # Search for existing AWS credential try { $existingCreds = @(Get-WUGCredential -Type aws) $matchCred = $existingCreds | Where-Object { $_.name -eq $awsCredName } | Select-Object -First 1 if ($matchCred) { $wugAwsCredId = $matchCred.id Write-Host " Found existing AWS credential: $awsCredName (ID: $wugAwsCredId)" -ForegroundColor Green } } catch { Write-Verbose "AWS credential search error: $_" } if (-not $wugAwsCredId) { try { $newCred = Add-WUGCredential -Name $awsCredName -Type aws ` -AwsAccessKeyID $awsAccessKey ` -AwsSecureAccessKey $plainAwsSK if ($newCred -and $newCred.id) { $wugAwsCredId = $newCred.id Write-Host " Created AWS credential: $awsCredName (ID: $wugAwsCredId)" -ForegroundColor Green } elseif ($newCred -and $newCred.resourceId) { $wugAwsCredId = $newCred.resourceId Write-Host " Created AWS credential: $awsCredName (ID: $wugAwsCredId)" -ForegroundColor Green } else { # Re-search after potential duplicate creation $existingCreds = @(Get-WUGCredential -Type aws) $matchCred = $existingCreds | Where-Object { $_.name -like "*$awsAccessKey*" } | Select-Object -First 1 if ($matchCred) { $wugAwsCredId = $matchCred.id Write-Host " Found AWS credential after create: $($matchCred.name) (ID: $wugAwsCredId)" -ForegroundColor Green } } } catch { Write-Warning "Failed to create AWS credential: $_" # Try re-search try { $existingCreds = @(Get-WUGCredential -Type aws) $matchCred = $existingCreds | Where-Object { $_.name -like '*AWS*' } | Select-Object -First 1 if ($matchCred) { $wugAwsCredId = $matchCred.id Write-Host " Using existing AWS credential: $($matchCred.name) (ID: $wugAwsCredId)" -ForegroundColor Yellow } } catch { } } } # Clear sensitive variable (already extracted to $plainAwsSK which is script-scoped) if (-not $wugAwsCredId) { Write-Warning "No AWS credential available in WUG. CloudWatch monitors will not authenticate." } # ================================================================== # Phase 2: Identify existing vs new devices # ================================================================== Write-Host "" Write-Host "Phase 2: Identifying devices..." -ForegroundColor Cyan $wugDeviceMap = @{} # devicePlan key -> WUG device ID $existingKeys = @() $newKeys = @() foreach ($key in $devicePlan.Keys) { $dev = $devicePlan[$key] $existingDevice = $null # Search by name first (handles cloud resources with/without IP) try { $searchResults = @(Get-WUGDevice -SearchValue $dev.Name) if ($searchResults.Count -gt 0) { $existingDevice = $searchResults | Where-Object { $_.displayName -like "*$($dev.Name)*" -or $_.hostName -eq $dev.Name } | Select-Object -First 1 if (-not $existingDevice -and $searchResults.Count -eq 1) { $existingDevice = $searchResults[0] } } } catch { Write-Verbose "Name search for '$($dev.Name)' returned error: $_" } # If no match by name and device has IP, search by IP if (-not $existingDevice -and $dev.IP) { try { $searchResults = @(Get-WUGDevice -SearchValue $dev.IP) if ($searchResults.Count -gt 0) { $existingDevice = $searchResults | Where-Object { $_.networkAddress -eq $dev.IP -or $_.hostName -eq $dev.IP } | Select-Object -First 1 if (-not $existingDevice -and $searchResults.Count -eq 1) { $existingDevice = $searchResults[0] } } } catch { Write-Verbose "IP search for '$($dev.IP)' returned error: $_" } } if ($existingDevice) { $wugDeviceMap[$key] = $existingDevice.id $existingKeys += $key Write-Host " Existing: $($existingDevice.displayName) (ID: $($existingDevice.id))" -ForegroundColor DarkGray } else { $newKeys += $key } } Write-Host " Found $($existingKeys.Count) existing, $($newKeys.Count) new" -ForegroundColor Cyan # ================================================================== # Phase 3: Create new devices via Add-WUGDeviceTemplate # ================================================================== Write-Host "" Write-Host "Phase 3: Creating $($newKeys.Count) new devices..." -ForegroundColor Cyan $devicesCreated = 0 foreach ($key in $newKeys) { $dev = $devicePlan[$key] $addIP = if ($dev.IP) { $dev.IP } else { '0.0.0.0' } $displayName = "$($dev.Name) ($($dev.Type))" # Collect active monitor names for this device $activeMonNames = @($dev.Items | Where-Object { $_.ItemType -eq 'ActiveMonitor' } | Select-Object -ExpandProperty Name) # Build attributes array for template $attrArray = @() foreach ($attrName in $dev.Attrs.Keys) { $attrArray += @{ name = $attrName; value = "$($dev.Attrs[$attrName])" } } # Build template params $templateParams = @{ IpOrName = $addIP DisplayName = $displayName Brand = 'AWS' Attributes = $attrArray } # Attach AWS credential if available if ($wugAwsCredId) { $templateParams.CredentialAWS = $awsCredName } # Attach active monitors (Ping) if any exist if ($activeMonNames.Count -gt 0) { $templateParams.ActiveMonitors = $activeMonNames } else { $templateParams.NoDefaultActiveMonitor = $true } Write-Host " Adding: $displayName ($addIP)..." -ForegroundColor Yellow try { $templateResult = Add-WUGDeviceTemplate @templateParams if ($templateResult) { # Extract device ID from result $newDeviceId = $null if ($templateResult.id) { $newDeviceId = $templateResult.id } elseif ($templateResult.resourceId) { $newDeviceId = $templateResult.resourceId } else { # Search for newly created device Start-Sleep -Milliseconds 1500 $newSearch = @(Get-WUGDevice -SearchValue $addIP) if ($addIP -eq '0.0.0.0') { $newSearch = @(Get-WUGDevice -SearchValue $dev.Name) } $newDevice = $newSearch | Select-Object -First 1 if ($newDevice) { $newDeviceId = $newDevice.id } } if ($newDeviceId) { $wugDeviceMap[$key] = $newDeviceId $devicesCreated++ Write-Host " Created: ID $newDeviceId" -ForegroundColor Green } else { Write-Warning " Created device but could not retrieve ID for: $displayName" } } } catch { Write-Warning " Failed to create device '$displayName': $_" # Fallback: try basic Add-WUGDevice if ($addIP -ne '0.0.0.0') { try { Add-WUGDevice -IpOrName $addIP -GroupId 0 | Out-Null Start-Sleep -Seconds 2 $fallbackDev = @(Get-WUGDevice -SearchValue $addIP) | Select-Object -First 1 if ($fallbackDev) { $wugDeviceMap[$key] = $fallbackDev.id $devicesCreated++ Write-Host " Fallback created: $($fallbackDev.displayName) (ID: $($fallbackDev.id))" -ForegroundColor Yellow } } catch { Write-Warning " Fallback also failed for '$addIP': $_" } } } } Write-Host " Devices created: $devicesCreated" -ForegroundColor Cyan # ================================================================== # Phase 4: Update existing devices (credentials + attributes) # ================================================================== if ($existingKeys.Count -gt 0) { Write-Host "" Write-Host "Phase 4: Updating $($existingKeys.Count) existing devices..." -ForegroundColor Cyan foreach ($key in $existingKeys) { $devId = $wugDeviceMap[$key] $dev = $devicePlan[$key] # Assign AWS credential if available if ($wugAwsCredId) { try { Set-WUGDeviceCredential -DeviceId $devId -CredentialId $wugAwsCredId -Assign | Out-Null Write-Verbose " Assigned AWS credential to device $devId" } catch { Write-Verbose " Credential assign error for device $devId`: $_" } } # Update attributes foreach ($attrName in $dev.Attrs.Keys) { try { Set-WUGDeviceAttribute -DeviceId $devId -Name $attrName -Value $dev.Attrs[$attrName] | Out-Null } catch { Write-Verbose " Attribute set error for $attrName on device $devId`: $_" } } } Write-Host " Updated attributes and credentials on $($existingKeys.Count) devices" -ForegroundColor Green } # ================================================================== # Phase 5: Set DeviceId on plan items + sync monitors # ================================================================== Write-Host "" Write-Host "Phase 5: Syncing monitors..." -ForegroundColor Cyan foreach ($key in $devicePlan.Keys) { if (-not $wugDeviceMap.ContainsKey($key)) { continue } $wugId = $wugDeviceMap[$key] foreach ($item in $devicePlan[$key].Items) { $item.DeviceId = $wugId } } $result = Invoke-WUGDiscoverySync -Plan $plan ` -PollingIntervalSeconds 300 ` -PerfPollingIntervalMinutes 5 # ================================================================== # Summary # ================================================================== Write-Host "" Write-Host "Push complete!" -ForegroundColor Green Write-Host " Devices in WUG: $($wugDeviceMap.Count) ($devicesCreated new, $($existingKeys.Count) existing)" -ForegroundColor White Write-Host " AWS credential: $(if ($wugAwsCredId) { $awsCredName } else { 'NONE' })" -ForegroundColor White Write-Host " Active monitors created: $($result.ActiveCreated)" -ForegroundColor White Write-Host " Performance monitors created: $($result.PerfCreated)" -ForegroundColor White Write-Host " Assigned to devices: $($result.Assigned)" -ForegroundColor White Write-Host " Skipped (already exist): $($result.Skipped)" -ForegroundColor White Write-Host " Attributes set: $($result.AttrsUpdated)" -ForegroundColor White if ($result.Failed -gt 0) { Write-Host " Failed: $($result.Failed)" -ForegroundColor Red } Write-Host "" Write-Host "Done! AWS devices + CloudWatch monitors pushed to WhatsUp Gold." -ForegroundColor Green } '2' { $jsonPath = Join-Path $OutputDir 'aws-discovery-plan.json' $plan | Export-DiscoveryPlan -Format JSON -Path $jsonPath -IncludeParams Write-Host "Exported to: $jsonPath" -ForegroundColor Green } '3' { $csvPath = Join-Path $OutputDir 'aws-discovery-plan.csv' $plan | Export-DiscoveryPlan -Format CSV -Path $csvPath Write-Host "Exported to: $csvPath" -ForegroundColor Green } '4' { $plan | Export-DiscoveryPlan -Format Table } '5' { # ---------------------------------------------------------------- # Generate AWS HTML Dashboard from discovery data # ---------------------------------------------------------------- Write-Host "" Write-Host "Building AWS dashboard from discovery data..." -ForegroundColor Cyan # Build dashboard rows from the already-collected $devicePlan $dashboardRows = @() foreach ($key in $devicePlan.Keys) { $dev = $devicePlan[$key] $a = $dev.Attrs # Count monitors by type $pingCount = @($dev.Items | Where-Object { $_.ItemType -eq 'ActiveMonitor' }).Count $cwCount = @($dev.Items | Where-Object { $_.ItemType -eq 'PerformanceMonitor' }).Count $cwMetricNames = @($dev.Items | Where-Object { $_.ItemType -eq 'PerformanceMonitor' } | ForEach-Object { $_.MonitorParams['CloudWatchMetric'] } | Where-Object { $_ }) -join ', ' $dashboardRows += [PSCustomObject]@{ ResourceType = $dev.Type Name = $dev.Name State = $dev.State IPAddress = if ($dev.IP) { $dev.IP } else { 'N/A' } PrivateIP = if ($a['AWS.PrivateIP']) { $a['AWS.PrivateIP'] } else { '' } Region = $dev.Region AvailabilityZone = if ($a['AWS.AZ']) { $a['AWS.AZ'] } else { '' } InstanceType = if ($a['AWS.InstanceType']) { $a['AWS.InstanceType'] } else { '' } Platform = if ($a['AWS.Platform']) { $a['AWS.Platform'] } else { '' } VpcId = if ($a['AWS.VpcId']) { $a['AWS.VpcId'] } else { '' } InstanceId = if ($a['AWS.InstanceId']) { $a['AWS.InstanceId'] } else { '' } PingMonitor = if ($pingCount -gt 0) { 'Yes' } else { 'No' } CloudWatchCount = $cwCount CloudWatchMetrics = $cwMetricNames } } if (-not $dashboardRows -or $dashboardRows.Count -eq 0) { Write-Host " No resources discovered. Generating empty dashboard." -ForegroundColor Yellow $dashboardRows = @() } $dashReportTitle = "AWS Dashboard" $dashTempPath = Join-Path $OutputDir 'AWS-Dashboard.html' $null = Export-AWSDiscoveryDashboardHtml ` -DashboardData $dashboardRows ` -OutputPath $dashTempPath ` -ReportTitle $dashReportTitle Write-Host "" Write-Host "Dashboard generated: $dashTempPath" -ForegroundColor Green if ($dashboardRows.Count -gt 0) { $dEC2 = @($dashboardRows | Where-Object { $_.ResourceType -eq 'EC2' }).Count $dRDS = @($dashboardRows | Where-Object { $_.ResourceType -eq 'RDS' }).Count $dELB = @($dashboardRows | Where-Object { $_.ResourceType -eq 'ELB' }).Count $dRunning = @($dashboardRows | Where-Object { $_.State -eq 'running' -or $_.State -eq 'active' -or $_.State -eq 'available' }).Count Write-Host " EC2: $dEC2 | RDS: $dRDS | ELB: $dELB | Active: $dRunning" -ForegroundColor White } else { Write-Host " No resources found across any region." -ForegroundColor Yellow } $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 'AWS-Dashboard.html' try { Copy-Item -Path $dashTempPath -Destination $wugDashPath -Force Write-Host "Copied to WUG: $wugDashPath" -ForegroundColor Green Write-Host " Access via WUG web UI: /NmConsole/dashboards/AWS-Dashboard.html" -ForegroundColor Cyan } catch { Write-Warning "Could not copy to NmConsole (run as admin?): $_" Write-Host " Manual copy: Copy-Item '$dashTempPath' '$wugDashPath'" -ForegroundColor Yellow } Deploy-DashboardWebConfig -Path $wugDashDir } else { Write-Host "" Write-Host "WUG NmConsole directory not found locally." -ForegroundColor Yellow Write-Host "Copy the file to your WUG server:" -ForegroundColor Yellow Write-Host " Copy-Item '$dashTempPath' '<WUG_Install>\Html\NmConsole\dashboards\AWS-Dashboard.html'" -ForegroundColor Cyan } } default { Write-Host "No action taken." -ForegroundColor Gray } } } # end foreach actionsToRun Write-Host "" Write-Host "Re-run anytime to discover new AWS resources." -ForegroundColor Cyan # SIG # Begin signature block # MIIr+wYJKoZIhvcNAQcCoIIr7DCCK+gCAQExDzANBglghkgBZQMEAgEFADB5Bgor # BgEEAYI3AgEEoGswaTA0BgorBgEEAYI3AgEeMCYCAwEAAAQQH8w7YFlLCE63JNLG # KX7zUQIBAAIBAAIBAAIBAAIBADAxMA0GCWCGSAFlAwQCAQUABCD+YgDw1i10Oaa5 # JMqLI2kkaNzR9A8tqfScO+T8B/oiKqCCJQ0wggVvMIIEV6ADAgECAhBI/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 # kPZPubdcMIIG7TCCBNWgAwIBAgIQCE/cM09+RU7bww+P+ZIYNTANBgkqhkiG9w0B # AQsFADBpMQswCQYDVQQGEwJVUzEXMBUGA1UEChMORGlnaUNlcnQsIEluYy4xQTA/ # BgNVBAMTOERpZ2lDZXJ0IFRydXN0ZWQgRzQgVGltZVN0YW1waW5nIFJTQTQwOTYg # U0hBMjU2IDIwMjUgQ0ExMB4XDTI2MDgwNTAwMDAwMFoXDTM3MTEwNDIzNTk1OVow # YzELMAkGA1UEBhMCVVMxFzAVBgNVBAoTDkRpZ2lDZXJ0LCBJbmMuMTswOQYDVQQD # EzJEaWdpQ2VydCBTSEEyNTYgUlNBNDA5NiBUaW1lc3RhbXAgUmVzcG9uZGVyIDIw # MjYgMTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBALZ7pvLJ/s1K+NSb # TGWz/TjGMPh8CQ6RucZCLv5anHzWJjF/NWJrFIhy24fcpKXlgRiky4WAawDfU3YP # 0BMxt9l3Dm5oCG5Z69AqEN1kgHg2epx+l+lZBcmJCcN0ASURML5uFIS80sZsDwO3 # BSkUxDjLJhBI+qiZP3aixAC/qEGLjsBNlLol9VZ7pfGEXiMlneJIC5/YKuizVzNF # KZZEeoy/0B8Zm+nzKBgSWG52lCO1w+nCg6XpCtklTJXeIg283hw7TmmsZXR+SMbj # brEOvZ3fP2VxIgeR28Y90ZStd3F9VuA5RVynb/whITPAo9b75Zr4Ta6Mj3URm26Q # ZYMn/FnbuTegcoRcFEZ9FOqM5T6MTdtr/n74lIT/ug0eeOzmZ6QTFg33otX+bFRs # IolvykE1jive4PuESaT8zzVeFWDAMDtozNgLctkGD1ZjkEyZtJrLl5ya0m5doH/S # cpaZCZVl6pNUOCybMc/kxC6EAmSJY24L0yYKD1Nkddsnb/ItVKi/2nXpQNMu1PT5 # prW83vV8d67WowuUs0HdY4H8AMLGvdL/WHEj3ZnqMqAQQP9u3Ai9t+5eQ02GDwy0 # ODjdzi0xlp70W+ow63/0++YDEX1M0iwgUHwbrJvfpklkZQvw3+kv3vUPItdwrocz # k9icflf55W1zOEKAcJVAIXpcMCU9AgMBAAGjggGVMIIBkTAMBgNVHRMBAf8EAjAA # MB0GA1UdDgQWBBQUyWOKMC7USvtulPPm40B+9ezN4jAfBgNVHSMEGDAWgBTvb1NK # 6eQGfHrK4pBW9i/USezLTjAOBgNVHQ8BAf8EBAMCB4AwFgYDVR0lAQH/BAwwCgYI # KwYBBQUHAwgwgZUGCCsGAQUFBwEBBIGIMIGFMCQGCCsGAQUFBzABhhhodHRwOi8v # b2NzcC5kaWdpY2VydC5jb20wXQYIKwYBBQUHMAKGUWh0dHA6Ly9jYWNlcnRzLmRp # Z2ljZXJ0LmNvbS9EaWdpQ2VydFRydXN0ZWRHNFRpbWVTdGFtcGluZ1JTQTQwOTZT # SEEyNTYyMDI1Q0ExLmNydDBfBgNVHR8EWDBWMFSgUqBQhk5odHRwOi8vY3JsMy5k # aWdpY2VydC5jb20vRGlnaUNlcnRUcnVzdGVkRzRUaW1lU3RhbXBpbmdSU0E0MDk2 # U0hBMjU2MjAyNUNBMS5jcmwwIAYDVR0gBBkwFzAIBgZngQwBBAIwCwYJYIZIAYb9 # bAcBMA0GCSqGSIb3DQEBCwUAA4ICAQCNxTphHp1SCt+ZrAmAfn0oQLFr0mLywSLa # DXQIENoyKqxrFbJblzCVP/pkXmwXOdrOpWygLzlT12os5ipDCy35RBCg2UMeApEt # rfGhz45F4Wt4WGdNdIbRWt3YTYJmpR+b7lr4d7Uwn+H600u4D7RnOGf8Wj4UNgAd # ZkfHhHv1mx9EVh71SJelcEN/oORSjXzdjfw1iZH9d8Nh/thn6hH23d+VsPAr6GAY # yzSA02nXD1nYLI7Ijmiv+xLCiYC41DSFYL3GhTiy0PxpawPtGRyaBVGzq+UiTfM8 # pD7KVyF5aQyWP4KhVGUUTnmm/RlYJoW3TiXA/+t0YcT2oRVBm3JETjajHug2AL+v # 5jhtKVnd3D0rbHXEu27o+Q8p4sEWPMqKDB+qbceb6T/6WcwTwXmQ9lOCLLYcsQeS # WmvKqzpAec9etE14jOQAzLKWdE3w/TCaKtLRaRT7LCkRYVnhA2D73FLje1O5b3HR # 5eHs0NzU/+xX7NbEdcofy0W3Wdwd1XOqtlpg/JgwtKfZM5dqO94lbUveOiJBI+xZ # EbGRsMNbXmMREUTgu+Oca7Y73MPWcslIx2VhkSKSXjDbD6rgg39H5Mh7QfieAIjW # agkJNt68Yfim6cjEzVSiLSeZfdkr5dtFPTW6jATlWJdYeeDRGCyatf8R1hSjzSvd # N8yWQPT9gzGCBkQwggZAAgEBMGgwVDELMAkGA1UEBhMCR0IxGDAWBgNVBAoTD1Nl # Y3RpZ28gTGltaXRlZDErMCkGA1UEAxMiU2VjdGlnbyBQdWJsaWMgQ29kZSBTaWdu # aW5nIENBIFIzNgIQB5zg5NEUf4XNOXPPdi036zANBglghkgBZQMEAgEFAKCBhDAY # BgorBgEEAYI3AgEMMQowCKACgAChAoAAMBkGCSqGSIb3DQEJAzEMBgorBgEEAYI3 # AgEEMBwGCisGAQQBgjcCAQsxDjAMBgorBgEEAYI3AgEVMC8GCSqGSIb3DQEJBDEi # BCDBu6scXorH0p2sypSJowJ876ksQrOdLL1t9Ck3Nm/VPzANBgkqhkiG9w0BAQEF # AASCAgCOB+zYIBIvGarb6+AIxG0tR8ghC+gXgGjnkotDKTIUCXoO2GhsRrW1o5pD # 1ydCa10mwmaIJJMjQHgVzBqxWKNxd4UGO0FQ4GW5dZglxNnI3kLl3prvNQQsKqs9 # 0Tq3Mr2SCgWU8wMe6y1Aa3rz/rdZT7wwNr5+SElxfkbLrC9mX3vW8wIN3cIYNr6O # 3Aqmco3z/xDCeCoZqvpvdOaY2XsTOhudyiVK+QJlGwFDSSrmLMxesO7O2bM+Aejv # rtkWf820ucudG1CzTcoMIS4Gc2VEMiRAqH/0NcOD8IQ499gtHpWW1+W5V1TQLdKK # FA6mYmCeHJ6BebooBru9LpSgkYQRI93efxJW05spjGRTIRIMtPh4roiIapE/Yz3F # +x+TWJv5KaksczxxZohCkhoaP3vaQIU3mAPS8UcNX+h9yU1wQJOxftXL2u/fpGum # SjKJEQ5sb3x76MShnqB1KK34PX/nKDvJ/2nnaEuGYql+p8ZNASuPhSdx5BOWWKTf # E8acx0GbfCy0ecQt7iePBuRdvLdk+Uhohs4zhaDu1h/S7S8NUBjYfqyZOl4Pv0eW # QYZvoEHNLunonWG3O7cInbR60tklmBMCxRt3vaf22Dc2HyvtA7JPKCkscqX5Ou53 # Vned80mu3VLC+I9X9t37wycddzJV0cbNvoQaywFak7PhvOH6Z6GCAyYwggMiBgkq # hkiG9w0BCQYxggMTMIIDDwIBATB9MGkxCzAJBgNVBAYTAlVTMRcwFQYDVQQKEw5E # aWdpQ2VydCwgSW5jLjFBMD8GA1UEAxM4RGlnaUNlcnQgVHJ1c3RlZCBHNCBUaW1l # U3RhbXBpbmcgUlNBNDA5NiBTSEEyNTYgMjAyNSBDQTECEAhP3DNPfkVO28MPj/mS # GDUwDQYJYIZIAWUDBAIBBQCgaTAYBgkqhkiG9w0BCQMxCwYJKoZIhvcNAQcBMBwG # CSqGSIb3DQEJBTEPFw0yNjA5MjkyMTEzMDdaMC8GCSqGSIb3DQEJBDEiBCAebgQw # /kAa8Ns46yH8Vo47rW0h/DQrE/B4eb8t+Vqo8jANBgkqhkiG9w0BAQEFAASCAgAU # igHzj6xqdsyYvNDZtAWUvnwbZI9TNY8KzLBzE0sWMpXFcseqdLZ+/84pAUgAru5Z # aDKntX7qLZ91zziOQP4y/DSea1Vf/5KjP0yHrn0bD3plq+sXpWd2wKrCL8ik7Dgr # 7N+vTNgkBVV64Nb4hTK3Fdnq0wTk+zPHx12TgilCzHajsJf4K1X4bbnaWqFHK822 # RplZH74/khmtxuJsCpMvPy00Z95ZupdN92Obz8I58dT4CgEx/hBlGZZ5SviRSP8q # GB2M85R6Io11B7yfC7lwge1ht1tXQFCHBKPWlVn6k1f/Dhr5Wrpa/oCO08pts2bN # YhRW4q4Ht7tr7VkJbbT0Z2Y9qtbN0dhmwl+Vd4I+Bor2Pz61QdJgjlODGx2nqOMk # 52eUKQydObQcYgmOFQKPT2nkeIoFGxdZikd9XkaEvQdJashKs7lZ5/dEwu4OfcVK # BRRUdDmlXPt9BeybCvQGaOis1dC5bxc8rH54VIqd0FV6pRypyia07d9f6XoXhT82 # Fypb0yeieB5oOOzeYzp2c9pjbDcMtD+sq1+RnFUd7vxgvzRLH6mm1xR0dJEXV8Rx # 0sjdU/IF0yNgl3fe5ByexZbINcUZ0Epc2jI8Txtxphw6fDgIuYbu51frigNrUlax # cBuUI39dYVerSoTa/QYnyloHGSW1o7/FQyxgg9D95Q== # SIG # End signature block |