helpers/aws/AWSHelpers.ps1
|
# ============================================================================= # AWS Helpers for WhatsUpGoldPS # Requires the AWS.Tools PowerShell modules. Install them first: # Install-Module -Name AWS.Tools.Installer -Scope CurrentUser -Force # Install-AWSToolsModule EC2, CloudWatch, RDS, ElasticLoadBalancingV2, S3, ECS, Lambda, ResourceGroupsTaggingAPI -CleanUp # Or install individually: # Install-Module -Name AWS.Tools.Common, AWS.Tools.EC2, AWS.Tools.CloudWatch, # AWS.Tools.RDS, AWS.Tools.ElasticLoadBalancingV2, # AWS.Tools.ResourceGroupsTaggingAPI -Scope CurrentUser -Force # ============================================================================= function Connect-AWSProfile { <# .SYNOPSIS Configures AWS credentials for the current session. .DESCRIPTION Sets up AWS credentials using an Access Key and Secret Key pair. Optionally sets the default region. Validates connectivity by calling Get-EC2Region. .PARAMETER AccessKey The AWS IAM access key ID. .PARAMETER SecretKey The AWS IAM secret access key. .PARAMETER Region The default AWS region (e.g. us-east-1). Defaults to us-east-1. .PARAMETER ProfileName Use a stored AWS credential profile instead of keys. .EXAMPLE Connect-AWSProfile -AccessKey "AKIAIOSFODNN7EXAMPLE" -SecretKey "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY" -Region "us-east-1" Authenticates to AWS using an IAM access key pair in the us-east-1 region. .EXAMPLE Connect-AWSProfile -ProfileName "MyStoredProfile" -Region "eu-west-1" Authenticates using a previously stored credential profile and sets the default region to eu-west-1. #> param( [Parameter(ParameterSetName = "Keys", Mandatory)][string]$AccessKey, [Parameter(ParameterSetName = "Keys", Mandatory)][string]$SecretKey, [Parameter(ParameterSetName = "Profile", Mandatory)][string]$ProfileName, [string]$Region = "us-east-1" ) if ($PSCmdlet.ParameterSetName -eq "Keys") { Set-AWSCredential -AccessKey $AccessKey -SecretKey $SecretKey -StoreAs "WhatsUpGoldPS_Session" Initialize-AWSDefaultConfiguration -ProfileName "WhatsUpGoldPS_Session" -Region $Region } else { Initialize-AWSDefaultConfiguration -ProfileName $ProfileName -Region $Region } # Validate connectivity try { Get-EC2Region -Region $Region -ErrorAction Stop | Out-Null Write-Verbose "Connected to AWS region $Region" } catch { throw "Failed to connect to AWS: $($_.Exception.Message)" } } function Get-AWSRegionList { <# .SYNOPSIS Returns all enabled AWS regions. .DESCRIPTION Wraps Get-EC2Region and returns a simplified collection. .EXAMPLE Get-AWSRegionList Returns all enabled AWS regions with their endpoint URLs. .EXAMPLE Get-AWSRegionList | Where-Object { $_.RegionName -like "us-*" } Returns only US-based AWS regions. #> $regions = Get-EC2Region -ErrorAction Stop foreach ($r in $regions) { [PSCustomObject]@{ RegionName = "$($r.RegionName)" Endpoint = "$($r.Endpoint)" } } } function Get-AWSEC2Instances { <# .SYNOPSIS Returns all EC2 instances in the specified region. .DESCRIPTION Wraps Get-EC2Instance and returns a detailed collection of instance objects with key properties suitable for WUG device creation. .PARAMETER Region The AWS region to query. If omitted, uses the default region. .EXAMPLE Get-AWSEC2Instances Returns all EC2 instances in the default region. .EXAMPLE Get-AWSEC2Instances -Region "us-west-2" Returns all EC2 instances in the us-west-2 region. .EXAMPLE Get-AWSEC2Instances -Region "eu-west-1" | Where-Object { $_.State -eq "running" } Returns only running EC2 instances in the eu-west-1 region. #> param( [string]$Region ) $splat = @{ ErrorAction = "Stop" } if ($Region) { $splat["Region"] = $Region } $reservations = Get-EC2Instance @splat foreach ($reservation in $reservations) { foreach ($inst in $reservation.Instances) { # Name tag $nameTag = ($inst.Tags | Where-Object { $_.Key -eq "Name" }).Value if (-not $nameTag) { $nameTag = $inst.InstanceId } # All tags as string $tagsStr = if ($inst.Tags) { ($inst.Tags | ForEach-Object { "$($_.Key)=$($_.Value)" }) -join "; " } else { "" } # Security groups $sgNames = ($inst.SecurityGroups | ForEach-Object { $_.GroupName }) -join ", " $sgIds = ($inst.SecurityGroups | ForEach-Object { $_.GroupId }) -join ", " # Block devices $diskCount = @($inst.BlockDeviceMappings).Count $rootDevice = "$($inst.RootDeviceType)" # Network $publicIP = if ($inst.PublicIpAddress) { "$($inst.PublicIpAddress)" } else { "N/A" } $privateIP = if ($inst.PrivateIpAddress) { "$($inst.PrivateIpAddress)" } else { "N/A" } [PSCustomObject]@{ InstanceId = "$($inst.InstanceId)" Name = $nameTag InstanceType = "$($inst.InstanceType)" State = "$($inst.State.Name)" Region = if ($Region) { $Region } else { (Get-DefaultAWSRegion).Region } AvailabilityZone = "$($inst.Placement.AvailabilityZone)" VpcId = "$($inst.VpcId)" SubnetId = "$($inst.SubnetId)" PublicIP = $publicIP PrivateIP = $privateIP PublicDnsName = if ($inst.PublicDnsName) { "$($inst.PublicDnsName)" } else { "N/A" } PrivateDnsName = if ($inst.PrivateDnsName) { "$($inst.PrivateDnsName)" } else { "N/A" } Platform = if ($inst.PlatformDetails) { "$($inst.PlatformDetails)" } else { "Linux/UNIX" } Architecture = "$($inst.Architecture)" ImageId = "$($inst.ImageId)" KeyName = if ($inst.KeyName) { "$($inst.KeyName)" } else { "N/A" } LaunchTime = "$($inst.LaunchTime.ToString('yyyy-MM-dd HH:mm:ss'))" SecurityGroups = $sgNames SecurityGroupIds = $sgIds DiskCount = "$diskCount" RootDeviceType = $rootDevice Tags = $tagsStr IAMRole = if ($inst.IamInstanceProfile) { "$($inst.IamInstanceProfile.Arn)" } else { "N/A" } Monitoring = "$($inst.Monitoring.State)" } } } } function Get-AWSRDSInstances { <# .SYNOPSIS Returns all RDS database instances in the specified region. .PARAMETER Region The AWS region to query. .EXAMPLE Get-AWSRDSInstances Returns all RDS database instances in the default region. .EXAMPLE Get-AWSRDSInstances -Region "us-east-1" Returns all RDS instances in us-east-1 with endpoint, engine, and status details. .EXAMPLE Get-AWSRDSInstances | Where-Object { $_.Engine -eq "mysql" } Returns only MySQL RDS instances. #> param( [string]$Region ) $splat = @{ ErrorAction = "Stop" } if ($Region) { $splat["Region"] = $Region } $instances = Get-RDSDBInstance @splat foreach ($db in $instances) { $endpoint = if ($db.Endpoint) { "$($db.Endpoint.Address)" } else { "N/A" } $port = if ($db.Endpoint) { "$($db.Endpoint.Port)" } else { "N/A" } [PSCustomObject]@{ DBInstanceId = "$($db.DBInstanceIdentifier)" Engine = "$($db.Engine)" EngineVersion = "$($db.EngineVersion)" DBInstanceClass = "$($db.DBInstanceClass)" Status = "$($db.DBInstanceStatus)" Endpoint = $endpoint Port = $port Region = if ($Region) { $Region } else { (Get-DefaultAWSRegion).Region } AvailabilityZone = "$($db.AvailabilityZone)" MultiAZ = "$($db.MultiAZ)" StorageType = "$($db.StorageType)" AllocatedStorageGB = "$($db.AllocatedStorage)" VpcId = if ($db.DBSubnetGroup) { "$($db.DBSubnetGroup.VpcId)" } else { "N/A" } PubliclyAccessible = "$($db.PubliclyAccessible)" StorageEncrypted = "$($db.StorageEncrypted)" DBName = if ($db.DBName) { "$($db.DBName)" } else { "N/A" } MasterUsername = "$($db.MasterUsername)" BackupRetention = "$($db.BackupRetentionPeriod)" ARN = "$($db.DBInstanceArn)" } } } function Get-AWSLoadBalancers { <# .SYNOPSIS Returns all ELBv2 (ALB/NLB) load balancers in the specified region. .PARAMETER Region The AWS region to query. .EXAMPLE Get-AWSLoadBalancers Returns all ALB and NLB load balancers in the default region. .EXAMPLE Get-AWSLoadBalancers -Region "us-west-2" Returns all load balancers in us-west-2 with DNS names, type, and state. .EXAMPLE Get-AWSLoadBalancers | Where-Object { $_.Type -eq "application" } Returns only Application Load Balancers. #> param( [string]$Region ) $splat = @{ ErrorAction = "Stop" } if ($Region) { $splat["Region"] = $Region } $lbs = Get-ELB2LoadBalancer @splat foreach ($lb in $lbs) { $dnsName = if ($lb.DNSName) { "$($lb.DNSName)" } else { "N/A" } $azs = ($lb.AvailabilityZones | ForEach-Object { $_.ZoneName }) -join ", " [PSCustomObject]@{ LoadBalancerName = "$($lb.LoadBalancerName)" Type = "$($lb.Type)" Scheme = "$($lb.Scheme)" State = "$($lb.State.Code)" DNSName = $dnsName Region = if ($Region) { $Region } else { (Get-DefaultAWSRegion).Region } VpcId = "$($lb.VpcId)" AvailabilityZones = $azs ARN = "$($lb.LoadBalancerArn)" } } } function Get-AWSCloudWatchMetrics { <# .SYNOPSIS Returns recent CloudWatch metric data for an AWS resource. .DESCRIPTION Queries CloudWatch for the specified namespace/dimensions and returns the latest data points for common metrics. .PARAMETER Namespace The CloudWatch namespace (e.g. AWS/EC2, AWS/RDS). .PARAMETER DimensionName The dimension name (e.g. InstanceId, DBInstanceIdentifier). .PARAMETER DimensionValue The dimension value (the resource ID). .PARAMETER MetricNames Array of metric names to retrieve. If omitted, uses defaults per namespace. .PARAMETER Region The AWS region. .EXAMPLE Get-AWSCloudWatchMetrics -Namespace "AWS/EC2" -DimensionName "InstanceId" -DimensionValue "i-0123456789abcdef0" Returns default EC2 metrics (CPUUtilization, NetworkIn, etc.) for the specified instance. .EXAMPLE Get-AWSCloudWatchMetrics -Namespace "AWS/RDS" -DimensionName "DBInstanceIdentifier" -DimensionValue "mydb" -Region "us-east-1" Returns default RDS metrics for the specified database in us-east-1. .EXAMPLE Get-AWSCloudWatchMetrics -Namespace "AWS/EC2" -DimensionName "InstanceId" -DimensionValue "i-0123456789abcdef0" -MetricNames @("CPUUtilization") Returns only the CPUUtilization metric for the specified EC2 instance. #> param( [Parameter(Mandatory)][string]$Namespace, [Parameter(Mandatory)][string]$DimensionName, [Parameter(Mandatory)][string]$DimensionValue, [string[]]$MetricNames, [string]$Region ) # Default metrics per namespace if (-not $MetricNames) { $MetricNames = switch ($Namespace) { "AWS/EC2" { @("CPUUtilization", "NetworkIn", "NetworkOut", "DiskReadOps", "DiskWriteOps", "StatusCheckFailed", "StatusCheckFailed_Instance", "StatusCheckFailed_System") } "AWS/RDS" { @("CPUUtilization", "FreeableMemory", "ReadIOPS", "WriteIOPS", "DatabaseConnections", "FreeStorageSpace") } "AWS/ELB" { @("RequestCount", "HealthyHostCount", "UnHealthyHostCount", "Latency") } "AWS/ApplicationELB" { @("RequestCount", "TargetResponseTime", "HealthyHostCount", "UnHealthyHostCount", "HTTPCode_ELB_5XX_Count") } "AWS/NetworkELB" { @("ActiveFlowCount", "NewFlowCount", "ProcessedBytes", "HealthyHostCount", "UnHealthyHostCount") } default { @() } } } $dimension = [Amazon.CloudWatch.Model.Dimension]::new() $dimension.Name = $DimensionName $dimension.Value = $DimensionValue $splat = @{ ErrorAction = "Stop" } if ($Region) { $splat["Region"] = $Region } $results = @() foreach ($metricName in $MetricNames) { try { $data = Get-CWMetricStatistic ` -Namespace $Namespace ` -MetricName $metricName ` -Dimension $dimension ` -StartTime (Get-Date).AddHours(-1) ` -EndTime (Get-Date) ` -Period 300 ` -Statistic "Average" ` @splat $lastValue = "N/A" if ($data.Datapoints) { $latest = $data.Datapoints | Sort-Object Timestamp | Select-Object -Last 1 if ($null -ne $latest.Average) { $lastValue = "$([math]::Round($latest.Average, 4))" } } $results += [PSCustomObject]@{ MetricName = $metricName Namespace = $Namespace LastValue = $lastValue Unit = if ($data.Datapoints) { "$($data.Datapoints[0].Unit)" } else { "N/A" } } } catch { Write-Verbose "Could not retrieve $metricName from $Namespace : $($_.Exception.Message)" } } return $results } function Resolve-AWSResourceIP { <# .SYNOPSIS Resolves an IP address for an AWS resource. .DESCRIPTION For EC2 instances returns the public IP (preferred) or private IP. For RDS instances resolves the endpoint FQDN via DNS. For load balancers resolves the DNS name. .PARAMETER ResourceType The type of resource: EC2, RDS, or ELB. .PARAMETER Resource The resource object from the corresponding Get-AWS* function. .EXAMPLE $instances = Get-AWSEC2Instances $ip = Resolve-AWSResourceIP -ResourceType "EC2" -Resource $instances[0] Resolves the IP address for the first EC2 instance (prefers public IP). .EXAMPLE $rds = Get-AWSRDSInstances Resolve-AWSResourceIP -ResourceType "RDS" -Resource $rds[0] Resolves the IP address for an RDS instance by performing DNS lookup on its endpoint. .EXAMPLE $elbs = Get-AWSLoadBalancers Resolve-AWSResourceIP -ResourceType "ELB" -Resource $elbs[0] Resolves the IP address for a load balancer by performing DNS lookup on its DNS name. #> param( [Parameter(Mandatory)][ValidateSet("EC2", "RDS", "ELB")][string]$ResourceType, [Parameter(Mandatory)]$Resource ) $ip = $null switch ($ResourceType) { "EC2" { if ($Resource.PublicIP -and $Resource.PublicIP -ne "N/A") { $ip = $Resource.PublicIP } elseif ($Resource.PrivateIP -and $Resource.PrivateIP -ne "N/A") { $ip = $Resource.PrivateIP } } "RDS" { if ($Resource.Endpoint -and $Resource.Endpoint -ne "N/A") { try { $resolved = [System.Net.Dns]::GetHostAddresses($Resource.Endpoint) | Select-Object -First 1 if ($resolved) { $ip = $resolved.IPAddressToString } } catch { Write-Verbose "Could not resolve RDS endpoint $($Resource.Endpoint): $($_.Exception.Message)" } } } "ELB" { if ($Resource.DNSName -and $Resource.DNSName -ne "N/A") { try { $resolved = [System.Net.Dns]::GetHostAddresses($Resource.DNSName) | Select-Object -First 1 if ($resolved) { $ip = $resolved.IPAddressToString } } catch { Write-Verbose "Could not resolve ELB DNS $($Resource.DNSName): $($_.Exception.Message)" } } } } return $ip } function Get-AWSDashboard { <# .SYNOPSIS Builds a unified dashboard view of AWS EC2, RDS, and ELB resources. .DESCRIPTION Queries each specified region for EC2 instances, RDS databases, and ELBv2 load balancers then returns a flat collection suitable for Bootstrap Table display. Each row represents a resource with resolved IP addresses, instance type, platform, VPC, and monitoring status. .PARAMETER Regions Array of AWS region names to query (e.g. "us-east-1","eu-west-1"). Defaults to the current default region if omitted. .PARAMETER IncludeRDS Include RDS instances in the results. Defaults to $true. .PARAMETER IncludeELB Include ELBv2 load balancers in the results. Defaults to $true. .EXAMPLE Get-AWSDashboard Returns all EC2, RDS, and ELB resources in the current default region. .EXAMPLE Get-AWSDashboard -Regions "us-east-1","us-west-2" -IncludeRDS $false Returns EC2 and ELB resources across two regions. .EXAMPLE Connect-AWSProfile -ProfileName "prod" $data = Get-AWSDashboard -Regions "us-east-1","eu-west-1" Export-AWSDashboardHtml -DashboardData $data -OutputPath "C:\Reports\aws.html" Start-Process "C:\Reports\aws.html" End-to-end: authenticate, gather data across regions, export HTML, and open in browser. .OUTPUTS PSCustomObject[] Each object contains resource details: ResourceType, Name, State, IPAddress, PrivateIP, Region, AvailabilityZone, InstanceType, Platform, VpcId, DiskCount, LaunchTime, Monitoring, Tags. .NOTES Author : jason@wug.ninja Version : 1.0.0 Date : 2025-07-15 Requires: PowerShell 5.1+, AWS.Tools PowerShell modules (AWS.Tools.EC2, AWS.Tools.RDS, AWS.Tools.ElasticLoadBalancingV2). .LINK https://github.com/jayyx2/WhatsUpGoldPS #> param( [string[]]$Regions, [bool]$IncludeRDS = $true, [bool]$IncludeELB = $true ) if (-not $Regions) { $Regions = @((Get-DefaultAWSRegion).Region) } $results = @() foreach ($region in $Regions) { # EC2 try { $instances = Get-AWSEC2Instances -Region $region foreach ($inst in $instances) { $ip = Resolve-AWSResourceIP -ResourceType "EC2" -Resource $inst $results += [PSCustomObject]@{ ResourceType = "EC2" Name = $inst.Name State = $inst.State IPAddress = if ($ip) { $ip } else { "N/A" } PrivateIP = $inst.PrivateIP Region = $inst.Region AvailabilityZone = $inst.AvailabilityZone InstanceType = $inst.InstanceType Platform = $inst.Platform VpcId = $inst.VpcId DiskCount = $inst.DiskCount LaunchTime = $inst.LaunchTime Monitoring = $inst.Monitoring Tags = $inst.Tags } } } catch { Write-Warning "EC2 query failed for ${region}: $($_.Exception.Message)" } # RDS if ($IncludeRDS) { try { $rdsInstances = Get-AWSRDSInstances -Region $region foreach ($db in $rdsInstances) { $ip = Resolve-AWSResourceIP -ResourceType "RDS" -Resource $db $results += [PSCustomObject]@{ ResourceType = "RDS" Name = $db.DBInstanceId State = $db.Status IPAddress = if ($ip) { $ip } else { "N/A" } PrivateIP = "N/A" Region = $db.Region AvailabilityZone = $db.AvailabilityZone InstanceType = $db.DBInstanceClass Platform = "$($db.Engine) $($db.EngineVersion)" VpcId = $db.VpcId DiskCount = $db.AllocatedStorageGB LaunchTime = "N/A" Monitoring = "N/A" Tags = "" } } } catch { Write-Warning "RDS query failed for ${region}: $($_.Exception.Message)" } } # ELB if ($IncludeELB) { try { $lbs = Get-AWSLoadBalancers -Region $region foreach ($lb in $lbs) { $ip = Resolve-AWSResourceIP -ResourceType "ELB" -Resource $lb $results += [PSCustomObject]@{ ResourceType = "ELB" Name = $lb.LoadBalancerName State = $lb.State IPAddress = if ($ip) { $ip } else { "N/A" } PrivateIP = "N/A" Region = $lb.Region AvailabilityZone = $lb.AvailabilityZones InstanceType = "$($lb.Type)/$($lb.Scheme)" Platform = "ELBv2" VpcId = $lb.VpcId DiskCount = "N/A" LaunchTime = "N/A" Monitoring = "N/A" Tags = "" } } } catch { Write-Warning "ELB query failed for ${region}: $($_.Exception.Message)" } } } return $results } function Export-AWSDashboardHtml { <# .SYNOPSIS Renders AWS dashboard data into a self-contained HTML file. .DESCRIPTION Takes the output of Get-AWSDashboard and generates a Bootstrap-based HTML report with sortable, searchable, and exportable tables. The report uses Bootstrap 5 and Bootstrap-Table for interactive filtering, sorting, column toggling, and CSV/JSON export. .PARAMETER DashboardData Array of PSCustomObject from Get-AWSDashboard containing EC2, RDS, and ELB details. .PARAMETER OutputPath File path for the output HTML file. Parent directory must exist. .PARAMETER ReportTitle Title shown in the report header. Defaults to "AWS Dashboard". .PARAMETER TemplatePath Optional path to a custom HTML template. If omitted, uses the AWS-Dashboard-Template.html in the same directory as this script. .EXAMPLE $data = Get-AWSDashboard -Regions "us-east-1" Export-AWSDashboardHtml -DashboardData $data -OutputPath "C:\Reports\aws.html" Exports the dashboard data to an HTML file using the default template. .EXAMPLE Export-AWSDashboardHtml -DashboardData $data -OutputPath "$env:TEMP\aws.html" -ReportTitle "Production AWS" Exports with a custom report title. .EXAMPLE Connect-AWSProfile -ProfileName "prod" $data = Get-AWSDashboard -Regions "us-east-1" Export-AWSDashboardHtml -DashboardData $data -OutputPath "C:\Reports\aws.html" Start-Process "C:\Reports\aws.html" Full pipeline: authenticate, gather, export, and open the report in a browser. .OUTPUTS System.Void Writes an HTML file to the path specified by OutputPath. .NOTES Author : jason@wug.ninja Version : 1.0.0 Date : 2025-07-15 Requires: PowerShell 5.1+, AWS-Dashboard-Template.html in the script directory. .LINK https://github.com/jayyx2/WhatsUpGoldPS #> [CmdletBinding()] param( [Parameter(Mandatory)]$DashboardData, [Parameter(Mandatory)][string]$OutputPath, [string]$ReportTitle = "AWS Dashboard", [string]$TemplatePath ) if (-not $TemplatePath) { $TemplatePath = Join-Path $PSScriptRoot "AWS-Dashboard-Template.html" } if (-not (Test-Path $TemplatePath)) { throw "HTML template not found at $TemplatePath" } $firstObj = $DashboardData | Select-Object -First 1 $columns = @() foreach ($prop in $firstObj.PSObject.Properties) { $col = @{ field = $prop.Name title = ($prop.Name -creplace '([A-Z])', ' $1').Trim() sortable = $true searchable = $true } if ($prop.Name -eq 'State') { $col.formatter = 'formatState' } $columns += $col } $columnsJson = $columns | ConvertTo-Json -Depth 5 -Compress $dataJson = $DashboardData | ConvertTo-Json -Depth 5 -Compress $tableConfig = @" columns: $columnsJson, data: $dataJson "@ $html = Get-Content -Path $TemplatePath -Raw $html = $html -replace 'replaceThisHere', $tableConfig $html = $html -replace 'ReplaceYourReportNameHere', $ReportTitle $html = $html -replace 'ReplaceUpdateTimeHere', (Get-Date).ToString("yyyy-MM-dd HH:mm:ss") Set-Content -Path $OutputPath -Value $html -Encoding UTF8 Write-Verbose "AWS Dashboard HTML written to $OutputPath" } # SIG # Begin signature block # MIIVlwYJKoZIhvcNAQcCoIIViDCCFYQCAQExDzANBglghkgBZQMEAgEFADB5Bgor # BgEEAYI3AgEEoGswaTA0BgorBgEEAYI3AgEeMCYCAwEAAAQQH8w7YFlLCE63JNLG # KX7zUQIBAAIBAAIBAAIBAAIBADAxMA0GCWCGSAFlAwQCAQUABCA+JRiCgIUhFf/z # Hbt1vS8bMCMV1eTmoBLC3zOg18gxY6CCEdMwggVvMIIEV6ADAgECAhBI/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/mBlXpnYzyOmJRvOwkDynUWICE5EV7WtgwggYaMIIEAqADAgECAhBiHW0M # UgGeO5B5FSCJIRwKMA0GCSqGSIb3DQEBDAUAMFYxCzAJBgNVBAYTAkdCMRgwFgYD # VQQKEw9TZWN0aWdvIExpbWl0ZWQxLTArBgNVBAMTJFNlY3RpZ28gUHVibGljIENv # ZGUgU2lnbmluZyBSb290IFI0NjAeFw0yMTAzMjIwMDAwMDBaFw0zNjAzMjEyMzU5 # NTlaMFQxCzAJBgNVBAYTAkdCMRgwFgYDVQQKEw9TZWN0aWdvIExpbWl0ZWQxKzAp # BgNVBAMTIlNlY3RpZ28gUHVibGljIENvZGUgU2lnbmluZyBDQSBSMzYwggGiMA0G # CSqGSIb3DQEBAQUAA4IBjwAwggGKAoIBgQCbK51T+jU/jmAGQ2rAz/V/9shTUxjI # ztNsfvxYB5UXeWUzCxEeAEZGbEN4QMgCsJLZUKhWThj/yPqy0iSZhXkZ6Pg2A2NV # DgFigOMYzB2OKhdqfWGVoYW3haT29PSTahYkwmMv0b/83nbeECbiMXhSOtbam+/3 # 6F09fy1tsB8je/RV0mIk8XL/tfCK6cPuYHE215wzrK0h1SWHTxPbPuYkRdkP05Zw # mRmTnAO5/arnY83jeNzhP06ShdnRqtZlV59+8yv+KIhE5ILMqgOZYAENHNX9SJDm # +qxp4VqpB3MV/h53yl41aHU5pledi9lCBbH9JeIkNFICiVHNkRmq4TpxtwfvjsUe # dyz8rNyfQJy/aOs5b4s+ac7IH60B+Ja7TVM+EKv1WuTGwcLmoU3FpOFMbmPj8pz4 # 4MPZ1f9+YEQIQty/NQd/2yGgW+ufflcZ/ZE9o1M7a5Jnqf2i2/uMSWymR8r2oQBM # dlyh2n5HirY4jKnFH/9gRvd+QOfdRrJZb1sCAwEAAaOCAWQwggFgMB8GA1UdIwQY # MBaAFDLrkpr/NZZILyhAQnAgNpFcF4XmMB0GA1UdDgQWBBQPKssghyi47G9IritU # pimqF6TNDDAOBgNVHQ8BAf8EBAMCAYYwEgYDVR0TAQH/BAgwBgEB/wIBADATBgNV # HSUEDDAKBggrBgEFBQcDAzAbBgNVHSAEFDASMAYGBFUdIAAwCAYGZ4EMAQQBMEsG # A1UdHwREMEIwQKA+oDyGOmh0dHA6Ly9jcmwuc2VjdGlnby5jb20vU2VjdGlnb1B1 # YmxpY0NvZGVTaWduaW5nUm9vdFI0Ni5jcmwwewYIKwYBBQUHAQEEbzBtMEYGCCsG # AQUFBzAChjpodHRwOi8vY3J0LnNlY3RpZ28uY29tL1NlY3RpZ29QdWJsaWNDb2Rl # U2lnbmluZ1Jvb3RSNDYucDdjMCMGCCsGAQUFBzABhhdodHRwOi8vb2NzcC5zZWN0 # aWdvLmNvbTANBgkqhkiG9w0BAQwFAAOCAgEABv+C4XdjNm57oRUgmxP/BP6YdURh # w1aVcdGRP4Wh60BAscjW4HL9hcpkOTz5jUug2oeunbYAowbFC2AKK+cMcXIBD0Zd # OaWTsyNyBBsMLHqafvIhrCymlaS98+QpoBCyKppP0OcxYEdU0hpsaqBBIZOtBajj # cw5+w/KeFvPYfLF/ldYpmlG+vd0xqlqd099iChnyIMvY5HexjO2AmtsbpVn0OhNc # WbWDRF/3sBp6fWXhz7DcML4iTAWS+MVXeNLj1lJziVKEoroGs9Mlizg0bUMbOalO # hOfCipnx8CaLZeVme5yELg09Jlo8BMe80jO37PU8ejfkP9/uPak7VLwELKxAMcJs # zkyeiaerlphwoKx1uHRzNyE6bxuSKcutisqmKL5OTunAvtONEoteSiabkPVSZ2z7 # 6mKnzAfZxCl/3dq3dUNw4rg3sTCggkHSRqTqlLMS7gjrhTqBmzu1L90Y1KWN/Y5J # KdGvspbOrTfOXyXvmPL6E52z1NZJ6ctuMFBQZH3pwWvqURR8AgQdULUvrxjUYbHH # j95Ejza63zdrEcxWLDX6xWls/GDnVNueKjWUH3fTv1Y8Wdho698YADR7TNx8X8z2 # Bev6SivBBOHY+uqiirZtg0y9ShQoPzmCcn63Syatatvx157YK9hlcPmVoa1oDE5/ # L9Uo2bC5a4CH2RwwggY+MIIEpqADAgECAhAHnODk0RR/hc05c892LTfrMA0GCSqG # SIb3DQEBDAUAMFQxCzAJBgNVBAYTAkdCMRgwFgYDVQQKEw9TZWN0aWdvIExpbWl0 # ZWQxKzApBgNVBAMTIlNlY3RpZ28gUHVibGljIENvZGUgU2lnbmluZyBDQSBSMzYw # HhcNMjYwMjA5MDAwMDAwWhcNMjkwNDIxMjM1OTU5WjBVMQswCQYDVQQGEwJVUzEU # MBIGA1UECAwLQ29ubmVjdGljdXQxFzAVBgNVBAoMDkphc29uIEFsYmVyaW5vMRcw # FQYDVQQDDA5KYXNvbiBBbGJlcmlubzCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCC # AgoCggIBAPN6aN4B1yYWkI5b5TBj3I0VV/peETrHb6EY4BHGxt8Ap+eT+WpEpJyE # tRYPxEmNJL3A38Bkg7mwzPE3/1NK570ZBCuBjSAn4mSDIgIuXZnvyBO9W1OQs5d6 # 7MlJLUAEufl18tOr3ST1DeO9gSjQSAE5Nql0QDxPnm93OZBon+Fz3CmE+z3MwAe2 # h4KdtRAnCqwM+/V7iBdbw+JOxolpx+7RVjGyProTENIG3pe/hKvPb501lf8uBAAD # LdjZr5ip8vIWbf857Yw1Bu10nVI7HW3eE8Cl5//d1ribHlzTzQLfttW+k+DaFsKZ # BBL56l4YAlIVRsrOiE1kdHYYx6IGrEA809R7+TZA9DzGqyFiv9qmJAbL4fDwetDe # yIq+Oztz1LvEdy8Rcd0JBY+J4S0eDEFIA3X0N8VcLeAwabKb9AjulKXwUeqCJLvN # 79CJ90UTZb2+I+tamj0dn+IKMEsJ4v4Ggx72sxFr9+6XziodtTg5Luf2xd6+Phha # mOxF2px9LObhBLLEMyRsCHZIzVZOFKu9BpHQH7ufGB+Sa80Tli0/6LEyn9+bMYWi # 2ttn6lLOPThXMiQaooRUq6q2u3+F4SaPlxVFLI7OJVMhar6nW6joBvELTJPmANSM # jDSRFDfHRCdGbZsL/keELJNy+jZctF6VvxQEjFM8/bazu6qYhrA7AgMBAAGjggGJ # MIIBhTAfBgNVHSMEGDAWgBQPKssghyi47G9IritUpimqF6TNDDAdBgNVHQ4EFgQU # 6YF0o0D5AVhKHbVocr8GaSIBibAwDgYDVR0PAQH/BAQDAgeAMAwGA1UdEwEB/wQC # MAAwEwYDVR0lBAwwCgYIKwYBBQUHAwMwSgYDVR0gBEMwQTA1BgwrBgEEAbIxAQIB # AwIwJTAjBggrBgEFBQcCARYXaHR0cHM6Ly9zZWN0aWdvLmNvbS9DUFMwCAYGZ4EM # AQQBMEkGA1UdHwRCMEAwPqA8oDqGOGh0dHA6Ly9jcmwuc2VjdGlnby5jb20vU2Vj # dGlnb1B1YmxpY0NvZGVTaWduaW5nQ0FSMzYuY3JsMHkGCCsGAQUFBwEBBG0wazBE # BggrBgEFBQcwAoY4aHR0cDovL2NydC5zZWN0aWdvLmNvbS9TZWN0aWdvUHVibGlj # Q29kZVNpZ25pbmdDQVIzNi5jcnQwIwYIKwYBBQUHMAGGF2h0dHA6Ly9vY3NwLnNl # Y3RpZ28uY29tMA0GCSqGSIb3DQEBDAUAA4IBgQAEIsm4xnOd/tZMVrKwi3doAXvC # wOA/RYQnFJD7R/bSQRu3wXEK4o9SIefye18B/q4fhBkhNAJuEvTQAGfqbbpxow03 # J5PrDTp1WPCWbXKX8Oz9vGWJFyJxRGftkdzZ57JE00synEMS8XCwLO9P32MyR9Z9 # URrpiLPJ9rQjfHMb1BUdvaNayomm7aWLAnD+X7jm6o8sNT5An1cwEAob7obWDM6s # X93wphwJNBJAstH9Ozs6LwISOX6sKS7CKm9N3Kp8hOUue0ZHAtZdFl6o5u12wy+z # zieGEI50fKnN77FfNKFOWKlS6OJwlArcbFegB5K89LcE5iNSmaM3VMB2ADV1FEcj # GSHw4lTg1Wx+WMAMdl/7nbvfFxJ9uu5tNiT54B0s+lZO/HztwXYQUczdsFon3pjs # Nrsk9ZlalBi5SHkIu+F6g7tWiEv3rtVApmJRnLkUr2Xq2a4nbslUCt4jKs5UX4V1 # nSX8OM++AXoyVGO+iTj7z+pl6XE9Gw/Td6WKKKsxggMaMIIDFgIBATBoMFQxCzAJ # BgNVBAYTAkdCMRgwFgYDVQQKEw9TZWN0aWdvIExpbWl0ZWQxKzApBgNVBAMTIlNl # Y3RpZ28gUHVibGljIENvZGUgU2lnbmluZyBDQSBSMzYCEAec4OTRFH+FzTlzz3Yt # N+swDQYJYIZIAWUDBAIBBQCggYQwGAYKKwYBBAGCNwIBDDEKMAigAoAAoQKAADAZ # BgkqhkiG9w0BCQMxDAYKKwYBBAGCNwIBBDAcBgorBgEEAYI3AgELMQ4wDAYKKwYB # BAGCNwIBFTAvBgkqhkiG9w0BCQQxIgQgOZ3aQlhc+hRRD4u3nbMUeQUvC8KV/eL0 # sCd5dZZbz5owDQYJKoZIhvcNAQEBBQAEggIAKChHwjy4mJoNZt82bRvDPwC9oIrL # kUJjp6BWHwRJWBt/tnHwvNSdf/eflEuySfOggYm3Y0ySOQ5+y8cQQ3YbDPwMO8K4 # 2URlauNukJChb/nKMsocAG4Rcze2ffwuUW8fwR6RQyQYutaua8gJFsIa+a/Teqfb # 1UQ5zabzqioPB3usKvghq/iHguxE+u0vW3hZYt7SBmFRY5bRtoJs+17SUrl5X3Cb # K/HRlbixxqec087rx5RpivFJclirbugemj79EZPJ/dC8lQvOaZKKZ+B7usiJ2ltk # 21aKOPZ342tgkTJMdbcJwTaTVGI7poJSXvLuvswBdOaI7lbGrSwhjD6CFtNMQfe8 # j2ZNUJ3m1QUoq+J+sB+ZzGUpnE3/uoL3aRHdk0ibw2wtHlzrenfrhi8AIlkxPIYU # HMJI204jmdGYzTJMOvcIyNrmxQTAw2MNIl4X8N+NDDZcSgzTUAAQeHT3BIhabNkJ # kp63vUGPk3jjrQFARuAfwK0K+zvr1w8XJkEvIl0fD35qMXFpkpE8kClyQ5ebwb8Z # 6pHlE51l8p8B5s9N+7OcjYrL1M8SGKEIHBVmmbsuWIM89HFh4zU9d+dIL5slyrcX # sBuF1QdWxhKBQ83+xbgw5vYFNsowwRtWrcbxdoia3rT+aw0X+nJUzAsdrNRtA/sj # mx/5ccUq2k59EwY= # SIG # End signature block |