Nexthink-Omnissa-Connector.psm1
<#PSScriptInfo
.VERSION 0.1.0 .GUID 6e063c9e-4db5-4d3b-9f1d-omnissa202406 .AUTHOR integration.madrid@nexthink.com .COMPANYNAME Nexthink .PROJECTURI https://github.com/nexthink/vdi-experience.connector-omnissa-horizon-server #> <# .DESCRIPTION This script gathers machine and desktop pool information from Omnissa Horizon REST API and logs the results. #> # # Constants definition New-Variable -Name 'SCRIPT_VERSION' -Value "0.1.0" -Option ReadOnly -Scope Script -Force New-Variable -Name 'START_TIME' -Value (Get-Date) -Option ReadOnly -Scope Script -Force # LOGGING New-Variable -Name 'LOG_FORMAT' -Value "[%{timestamp:+yyyy-MM-dd HH:mm:ss.fffffzzz}][%{level:-7}][%{lineno:3}] %{message}" -Option ReadOnly -Scope Script -Force New-Variable -Name 'LOG_RETENTION_DAYS' -Value 7 -Option ReadOnly -Scope Script -Force New-Variable -Name 'LOG_LEVEL' -Value 'INFO' -Option ReadOnly -Scope Script -Force # CONFIG New-Variable -Name 'OMNISSA_ENV_CONFIG_NOT_FOUND' -Value 'Configuration for Omnissa environment not found' -Option ReadOnly -Scope Script -Force New-Variable -Name 'MISSING_FIELDS' -Value 'Missing required fields' -Option ReadOnly -Scope Script -Force # ENRICHMENT API New-Variable -Name 'ENRICHMENT_API_HOST' -Value 'api.eu-west-3.dev.nexthink.cloud' -Option ReadOnly -Scope Script -Force New-Variable -Name 'TARGET_CREDENTIALS_NAME' -Value 'nxt-ctx-credentials' -Option ReadOnly -Scope Script -Force New-Variable -Name 'JWT_URL' -Value 'https://{0}/api/v1/token' -Option ReadOnly -Scope Script -Force New-Variable -Name 'ENRICHMENT_URL' -Value 'https://{0}/api/v1/enrichment/data/fields' -Option ReadOnly -Scope Script -Force New-Variable -Name 'REQUEST_BATCH_SIZE' -Value 1000 -Option ReadOnly -Scope Script -Force # EXIT CODES New-Variable -Name 'EXIT_CODE_OK' -Value 0 -Option ReadOnly -Scope Script -Force New-Variable -Name 'EXIT_CODE_ERROR' -Value 1 -Option ReadOnly -Scope Script -Force # TRACE ID New-Variable -Name 'TRACE_ID_HEADER' -Value 'x-enrichment-trace-id' -Option ReadOnly -Scope Script -Force New-Variable -Name 'TRACE_ID_VALUE' -Value ([Guid]::NewGuid()) -Option ReadOnly -Scope Script -Force # TIMESTAMP New-Variable -Name 'TIMESTAMP' -Value '1970-01-01T00:00:00Z' -Option ReadOnly -Scope Script -Force New-Variable -Name 'EXECUTION_START_DATE' -Value (Get-Date) -Option ReadOnly -Scope Script -Force # VIRTUALIZATION CONSTANTS # horizon_on_prem = 5 New-Variable -Name 'DESKTOP_BROKER' -Value 5 -Option ReadOnly -Scope Script -Force # CACHE New-Variable -Name 'BASE_IMAGES_CACHE' -Value (@{}) -Option ReadOnly -Scope Script -Force New-Variable -Name 'BASE_SNAPSHOTS_CACHE' -Value (@{}) -Option ReadOnly -Scope Script -Force # # Main function # function Invoke-Main { param( [Parameter(Mandatory = $true)] [string]$OmnissaEnvironment, [Parameter(Mandatory = $true)] [string]$ScriptRootPath ) $Version = "0.1.0" $exitCode = $EXIT_CODE_OK try { Initialize-EnrichmentProcess -OmnissaEnvironment $OmnissaEnvironment -ScriptRootPath $ScriptRootPath Write-CustomLog -Message "Starting Omnissa connector $Version for environment $OmnissaEnvironment" -Severity 'INFO' Start-EnrichmentProcess -OmnissaEnvironment $OmnissaEnvironment } catch { Write-CustomLog -Message "The execution stopped unexpectedly. Details: $($_.Exception.Message)" -Severity 'ERROR' $exitCode = $EXIT_CODE_ERROR } Write-CustomLog -Message "Stopping Omnissa connector $Version with exit code $exitCode`n" -Severity 'INFO' Wait-Logging return $exitCode } function Initialize-EnrichmentProcess { param( [Parameter(Mandatory = $true)] [string]$OmnissaEnvironment, [Parameter(Mandatory = $true)] [string]$ScriptRootPath ) # Initialize path-dependent variables $logsFolder = Join-Path -Path (Join-Path -Path $ScriptRootPath -ChildPath "Logs") -ChildPath $OmnissaEnvironment $configFileFolder = Join-Path -Path (Join-Path -Path $ScriptRootPath -ChildPath "Config") -ChildPath "config.json" New-Variable -Name 'SCRIPT_FOLDER' -Value $ScriptRootPath -Option ReadOnly -Scope Script -Force New-Variable -Name 'LOGS_FOLDER' -Value $logsFolder -Option ReadOnly -Scope Script -Force New-Variable -Name 'LOGFILE_NAME' -Value (Join-Path -Path $logsFolder -ChildPath "OmnissaConnector-%{+yyyyMMdd}.log") -Option ReadOnly -Scope Script -Force New-Variable -Name 'ZIPFILE_NAME' -Value (Join-Path -Path $logsFolder -ChildPath "RotatedLogs.zip") -Option ReadOnly -Scope Script -Force New-Variable -Name 'CONFIG_FILE_NAME' -Value $configFileFolder -Option ReadOnly -Scope Script -Force Initialize-Folder -Path $logsFolder Initialize-Logger Get-ConfigData -OmnissaEnvironment $OmnissaEnvironment -ConfigFilePath $configFileFolder # Needed to update log level after reading it from the configuration Initialize-Logger New-Variable -Name 'TIMESTAMP' -Value $(Get-Date -Format o) -Option ReadOnly -Scope Script -Force } function Start-EnrichmentProcess { $pools = @(Get-DesktopPools) $machines = @(Get-Machines -Pools $pools) $rdsServers = @(Get-RdsServers -Pools $pools) $devices = $machines + $rdsServers $totalDevicesToSend = $devices.Count Write-CustomLog -Message "$totalDevicesToSend device(s) retrieved in total from Omnissa API." -Severity 'INFO' for ($devicesOffset = 0; $devicesOffset -lt $totalDevicesToSend; $devicesOffset += $REQUEST_BATCH_SIZE) { $deviceLimit = [Math]::Min($totalDevicesToSend - 1, $devicesOffset + $REQUEST_BATCH_SIZE - 1) $fullData = $devices[$devicesOffset..$deviceLimit] Send-EnrichmentRequest -FullData $fullData -OmnissaEnvironment $OmnissaEnvironment } } function Send-EnrichmentRequest ([Object[]]$FullData, [String]$OmnissaEnvironment) { # Time in milliseconds since the script started up to when the enrichment request is sent $executionDuration = [Int](((Get-Date) - $EXECUTION_START_DATE).TotalMilliseconds) $clientTelemetry = Get-ClientTelemetry -ExecutionDuration $executionDuration $jwt = Get-Jwt $enrichmentEndpoint = [String]::Format($ENRICHMENT_URL, $ENRICHMENT_API_HOST) $enrichmentBody = Get-JsonFromOmnissaData -OmnissaData $FullData -ClientTelemetry $clientTelemetry -OmnissaEnvironment $OmnissaEnvironment $response = Invoke-SendDataToEnrichmentAPI -EndpointURL $enrichmentEndpoint -JsonPayload $enrichmentBody -Jwt $jwt switch ( $response.statusCode ) { '200' { Write-CustomLog -Message "[$TRACE_ID_VALUE] Batch with $($FullData.Count) devices successfully processed by Enrichment API." -Severity 'INFO' } '207' { Write-CustomLog -Message "[$TRACE_ID_VALUE] Batch with $($FullData.Count) devices partially processed by Enrichment API." -Severity 'INFO' Write-CustomLog -Message "[$TRACE_ID_VALUE] Partial success response: $($response.content)" -Severity 'INFO' } default { $message = "[$TRACE_ID_VALUE] Error sending request to Enrichment API with status code: $($response.statusCode)" Write-CustomLog -Message $message -Severity 'ERROR' throw $message } } } function Get-Jwt () { $jwtUrl = [String]::Format($JWT_URL, $ENRICHMENT_API_HOST) if (-not (Test-ValidWebUrl($jwtUrl))) { throw "Invalid URL to retrieve the token: $jwtUrl" } try { $credentials = Get-ClientCredentials -Target $TARGET_CREDENTIALS_NAME $basicHeader = Get-StringAsBase64 -InputString "$($credentials.clientId):$($credentials.clientSecret)" $headers = New-Object "System.Collections.Generic.Dictionary[[String],[String]]" $headers.Add('Authorization', "Basic $basicHeader") $response = Invoke-WebRequest -Uri $jwtUrl -Method 'POST' -Headers $headers -UseBasicParsing $parsedResponse = ConvertFrom-Json $([String]::new($response.Content)) return $parsedResponse.access_token } catch [Net.WebException], [IO.IOException] { Write-CustomLog "Error sending request to get the JWT token. Details: [$($_.Exception.response.StatusCode.value__)] $($_.ErrorDetails)" -Severity "ERROR" throw "Unable to access token endpoint. Details: $($_.Exception.Message)" } catch { throw "An error occurred that could not be resolved. Details: $($_.Exception.Message)" } } function Test-ValidWebUrl($UrlToValidate) { $uri = $UrlToValidate -as [System.Uri] $null -ne $uri.AbsoluteURI -and $uri.Scheme -match '[http|https]' } function Get-ClientCredentials ([String]$Target) { $storedCredentials = Get-StoredCredential -Target $Target if ($storedCredentials -and $null -ne $storedCredentials.UserName -and $null -ne $storedCredentials.Password ) { $userName = $storedCredentials.UserName $securePassword = $storedCredentials.Password $BSTR = [System.Runtime.InteropServices.Marshal]::SecureStringToBSTR($securePassword) $unsecurePassword = [System.Runtime.InteropServices.Marshal]::PtrToStringAuto($BSTR) return @{ clientId = $userName; clientSecret = $unsecurePassword } } else { throw "Credentials not found or they are empty for Target: $Target" } } function Get-StringAsBase64 ([String]$InputString) { $Bytes = [System.Text.Encoding]::UTF8.GetBytes($InputString) $EncodedText = [Convert]::ToBase64String($Bytes) return $EncodedText } # # Omnissa Horizon functions # function Get-DesktopPools { Write-CustomLog -Message "Fetching desktop pools..." -Severity 'DEBUG' $headers = Get-RequestHeaders try { $desktopPoolsResponse = Invoke-RestMethod -Uri "$OMNISSA_HOST/rest/inventory/v7/desktop-pools" -Headers $headers -Method Get $desktopPools = $desktopPoolsResponse | Where-Object { $null -ne $_.id } $desktopPoolsCount = $desktopPools.Count Write-CustomLog -Message "$desktopPoolsCount desktop pool IDs found" -Severity 'DEBUG' return $desktopPools } catch { throw "Failed to get desktop pools. Details: $($_.Exception.Message)" } } function Get-Machines { param ( [Object[]]$Pools ) try { $machinesOutput = @() $headers = Get-RequestHeaders $machinesResponse = Invoke-RestMethod -Uri "$OMNISSA_HOST/rest/inventory/v5/machines" -Headers $headers -Method Get $machineCount = $machinesResponse.Count Write-CustomLog -Message "$machineCount machines found" -Severity 'DEBUG' if ($machineCount -eq 0) { # Early return if no machines were found return $machinesOutput } $poolLookup = Get-ObjectLookup -KeySelector { param($p) $p.id } -Objects $Pools foreach ($machine in $machinesResponse) { $pool = $poolLookup[$machine.desktop_pool_id] $vCenterId = $machine.managed_machine_data.virtual_center_id $baseVmId = $machine.managed_machine_data.base_vm_id $snapshotId = $machine.managed_machine_data.base_vm_snapshot_id $baseImage = Get-BaseImage -VCenterId $vCenterId -BaseVmId $baseVmId -SnapshotId $snapshotId $machinesOutput += [PSCustomObject]@{ MachineName = $machine.name DesktopPool = $pool.name PoolType = $pool.type Hostname = $machine.managed_machine_data.host_name UserAssignment = $pool.user_assignment DiskImage = $baseImage } } return $machinesOutput } catch { throw "Failed to get machines. Details: $($_.Exception.Message)" } } function Get-RdsServers { param ( [Object[]]$Pools ) try { $rdsServersOutput = @() $headers = Get-RequestHeaders $rdsServersResponse = Invoke-RestMethod -Uri "$OMNISSA_HOST/rest/inventory/v2/rds-servers" -Headers $headers -Method Get $rdsServersCount = $rdsServersResponse.Count Write-CustomLog -Message "$rdsServersCount RDS servers found" -Severity 'DEBUG' if ($rdsServersCount -eq 0) { # Early return if there are no RDS servers return $rdsServersOutput } $farmsResponse = @(Get-Farms) $farmLookup = Get-ObjectLookup -Objects $farmsResponse -KeySelector { param($p) $p.id } $poolLookup = Get-ObjectLookup -KeySelector { param($p) $p.farm_id } -Objects $Pools foreach ($rdsServer in $rdsServersResponse) { $pool = $poolLookup[$rdsServer.farm_id] $farm = $farmLookup[$rdsServer.farm_id] $vCenterId = $farm.automated_farm_settings.vcenter_id $baseVmId = $farm.automated_farm_settings.provisioning_settings.parent_vm_id $snapshotId = $farm.automated_farm_settings.provisioning_settings.base_snapshot_id $baseImage = Get-BaseImage -VCenterId $vCenterId -BaseVmId $baseVmId -SnapshotId $snapshotId $rdsServersOutput += [PSCustomObject]@{ MachineName = $rdsServer.name DesktopPool = $pool.name Hostname = $rdsServer.dns_name UserAssignment = $pool.user_assignment DiskImage = $baseImage } } return $rdsServersOutput } catch { throw "Failed to get RDS servers. Details: $($_.Exception.Message)" } } function Get-BaseImage { param ( [string]$VCenterId, [string]$BaseVmId, [string]$SnapshotId ) if ([string]::IsNullOrEmpty($VCenterId) -or [string]::IsNullOrEmpty($BaseVmId) -or [string]::IsNullOrEmpty($SnapshotId)) { Write-CustomLog -Message "Required parameters missing. Skipping base image retrieval." -Severity 'DEBUG' return $null } $baseImagesCacheKey = "$VCenterId" $baseVMLookup = Get-FromCacheOrInvoke -Cache $BASE_IMAGES_CACHE -Key $baseImagesCacheKey -Invoke { Get-BaseVmLookup -VCenterId $VCenterId } $baseVmPath = $baseVMLookup[$BaseVmId].path $baseSnapshotsCacheKey = "$VCenterId" + "_" + "$BaseVmId" $snapshotResponse = Get-FromCacheOrInvoke -Cache $BASE_SNAPSHOTS_CACHE -Key $baseSnapshotsCacheKey -Invoke { Get-BaseSnapshotLookup -VCenterId $VCenterId -BaseVmId $BaseVmId } $snaphotPath = $snapshotResponse[$SnapshotId].path return $baseVmPath + $snaphotPath } function Get-BaseVmLookup { param ( [Parameter(Mandatory = $true)][string]$VCenterId ) Write-CustomLog -Message "Fetching base VMs for VCenterId='$VCenterId'..." -Severity 'DEBUG' try { $headers = Get-RequestHeaders $baseVMsResponse = Invoke-RestMethod -Uri "$OMNISSA_HOST/rest/external/v3/base-vms?vcenter_id=$VCenterId" -Headers $headers -Method Get $baseVMsCount = $baseVMsResponse.Count Write-CustomLog -Message "$baseVMsCount base VMs found" -Severity 'DEBUG' return Get-ObjectLookup -Objects $baseVMsResponse -KeySelector { param($p) $p.id } } catch { throw "Failed to get base VMs. Details: $($_.toString())" } } function Get-BaseSnapshotLookup { param ( [Parameter(Mandatory = $true)][string]$VCenterId, [Parameter(Mandatory = $true)][string]$BaseVmId ) Write-CustomLog -Message "Fetching base snapshots for VCenterId='$VCenterId' and BaseVmId='$BaseVmId'..." -Severity 'DEBUG' try { $headers = Get-RequestHeaders $baseSnapshotsResponse = Invoke-RestMethod -Uri "$OMNISSA_HOST/rest/external/v2/base-snapshots?vcenter_id=$VCenterId&base_vm_id=$BaseVmId" -Headers $headers -Method Get $baseSnapshotsCount = $baseSnapshotsResponse.Count Write-CustomLog -Message "$baseSnapshotsCount base snapshots found" -Severity 'DEBUG' return Get-ObjectLookup -Objects $baseSnapshotsResponse -KeySelector { param($p) $p.id } } catch { throw "Failed to get base snapshots. Details: $($_.toString())" } } function Get-Farms { Write-CustomLog -Message "Fetching farms..." -Severity 'DEBUG' try { $headers = Get-RequestHeaders $farms = Invoke-RestMethod -Uri "$OMNISSA_HOST/rest/inventory/v7/farms" -Headers $headers -Method Get $farmsCount = $farms.Count Write-CustomLog -Message "$farmsCount farms found" -Severity 'DEBUG' return $farms } catch { throw "Failed to get farms. Details: $($_.toString())" } } function Get-RequestHeaders { $credentials = Get-ClientCredentials -Target $OMNISSA_CREDENTIALS_NAME $authBody = @{ domain = "NEXTHINK-OMNISSA-CONNECTOR" username = $credentials.clientId password = $credentials.clientSecret } | ConvertTo-Json try { $response = Invoke-WebRequest -Uri "$OMNISSA_HOST/rest/login" -Method POST -Body $authBody -ContentType "application/json" -UseBasicParsing $token = ($response.Content | ConvertFrom-Json).access_token if (-not $token) { throw "Authentication failed: No token returned." } } catch { throw "Authentication failed. Details: $($_.ToString())" } return @{ "Authorization" = "Bearer $token" "Accept" = "application/json" "Content-Type" = "application/json" } } # Function to determine virtualization type based on input string function Get-VirtualizationType { param( [Parameter(Mandatory = $true)] [string]$UserAssignment, [Parameter(Mandatory = $true)] [string]$PoolType ) # Default to UNSPECIFIED (0) if no match is found $result = 0 # Determine virtualization type based on UserAssignment and PoolType if ($UserAssignment -eq "DEDICATED") { # PERSONAL = 1 $result = 1 } elseif ($UserAssignment -eq "FLOATING") { # POOLED = 2 $result = 2 } elseif ($PoolType -eq "RDS") { # SHARED = 3 $result = 3 } return $result } # # Logging # function Write-CustomLog ([String]$Message, [String]$Severity = 'INFO') { Write-Log -Message $Message -Level $Severity } function Initialize-Folder ([String]$Path) { try { if (-not (Test-Path -Path $Path)) { [Void](New-Item -Path $Path -ItemType 'Directory' -Force -ErrorAction Stop) } } catch { throw "Error creating folder at $Path." } } function Initialize-Logger { Add-LoggingTarget -Name File -Configuration @{ Path = $LOGFILE_NAME Encoding = 'unicode' Level = $LOG_LEVEL Format = $LOG_FORMAT RotateAfterAmount = $LOG_RETENTION_DAYS RotateAmount = 1 CompressionPath = $ZIPFILE_NAME } Set-LoggingCallerScope 2 } # # Config functions # function Get-ConfigData { param( [Parameter(Mandatory = $true)] [string]$OmnissaEnvironment, [Parameter(Mandatory = $true)] [string]$ConfigFilePath ) $configData = Read-ConfigFile -ConfigFilePath $ConfigFilePath $omnissaEnvironmentConfigData = Get-OmnissaEnvironmentConfigData -ConfigData $configData -OmnissaEnvName $OmnissaEnvironment if ($null -eq $omnissaEnvironmentConfigData) { throw "$OMNISSA_ENV_CONFIG_NOT_FOUND" } if (-not (Test-ConfigData -ConfigData $configData -OmnissaEnvConfigData $omnissaEnvironmentConfigData)) { throw "$MISSING_FIELDS" } New-Variable -Name 'LOG_RETENTION_DAYS' -Value $configData.Logging.LogRetentionDays -Option ReadOnly -Scope Script -Force New-Variable -Name 'LOG_LEVEL' -Value $configData.Logging.LogLevel -Option ReadOnly -Scope Script -Force New-Variable -Name 'OMNISSA_HOST' -Value $omnissaEnvironmentConfigData.Host -Option ReadOnly -Scope Script -Force New-Variable -Name 'OMNISSA_CREDENTIALS_NAME' -Value $omnissaEnvironmentConfigData.WindowsCredentialEntry -Option ReadOnly -Scope Script -Force New-Variable -Name 'ENRICHMENT_API_HOST' -Value $configData.NexthinkAPI.HostFQDN -Option ReadOnly -Scope Script -Force New-Variable -Name 'TARGET_CREDENTIALS_NAME' -Value $configData.NexthinkAPI.WindowsCredentialEntry -Option ReadOnly -Scope Script -Force New-Variable -Name 'REQUEST_BATCH_SIZE' -Value $configData.NexthinkAPI.RequestBatchSize -Option ReadOnly -Scope Script -Force } function Read-ConfigFile { param( [Parameter(Mandatory = $true)] [string]$ConfigFilePath ) try { return (Get-Content "$ConfigFilePath" -Raw -ErrorAction Stop | ConvertFrom-Json -ErrorAction Stop) } catch { throw "Error loading config file. Details: $($_.toString())" } } function Get-OmnissaEnvironmentConfigData($ConfigData, $OmnissaEnvName) { return $ConfigData.OmnissaEnvironments | Where-Object { $_.Name -eq $OmnissaEnvName } } function Test-ConfigData($ConfigData, $OmnissaEnvConfigData) { return ($ConfigData.Logging.LogRetentionDays -and $ConfigData.Logging.LogLevel -and $ConfigData.NexthinkAPI.HostFQDN -and ` $ConfigData.NexthinkAPI.WindowsCredentialEntry -and $ConfigData.NexthinkAPI.RequestBatchSize -and ` $OmnissaEnvConfigData.Name -and $OmnissaEnvConfigData.Host -and ` $OmnissaEnvConfigData.WindowsCredentialEntry) } # # Utilities # function Get-ObjectLookup { param( [Object[]]$Objects, [Parameter(Mandatory = $true)] [ScriptBlock]$KeySelector ) $lookup = @{} foreach ($obj in $Objects) { $key = & $KeySelector $obj if ([string]::IsNullOrEmpty($key)) { continue } if (-not $lookup.ContainsKey($key)) { $lookup.Add($key, $obj) } } return $lookup } function Get-FromCacheOrInvoke { param ( [hashtable]$Cache, [string]$Key, [ScriptBlock]$Invoke ) if ($Cache.ContainsKey($Key)) { return $Cache[$Key] } $result = & $Invoke $Cache[$Key] = $result return $result } # # Enrichment API # function Get-JsonFromOmnissaData ([Object[]]$OmnissaData, [PSCustomObject]$ClientTelemetry, [String]$OmnissaEnvironment) { $jsonResult = '{"enrichments": [' foreach ($device in $OmnissaData) { try { if (Test-MachineIsValid($device)) { $virtualizationType = Get-VirtualizationType -UserAssignment $device.UserAssignment -PoolType $device.PoolType $currentRow = '{"identification":[{"name":"device/device/name","value":"' + $device.MachineName + '"}],' $currentRow = $currentRow + '"fields":[{"name":"device/device/virtualization/desktop_pool","value":"' + $device.DesktopPool + '"}' $currentRow = $currentRow + ',{"name":"device/device/virtualization/type","value": ' + $virtualizationType + '}' $currentRow = $currentRow + ',{"name":"device/device/virtualization/hostname","value":"' + $device.Hostname + '"}' $currentRow = $currentRow + ',{"name":"device/device/virtualization/environment_name","value":"' + $OmnissaEnvironment + '"}' $currentRow = $currentRow + ',{"name":"device/device/virtualization/desktop_broker","value": ' + $DESKTOP_BROKER + '}' if ($null -ne $device.DiskImage) { $currentRow = $currentRow + ',{"name":"device/device/virtualization/disk_image","value":"' + $device.DiskImage + '"}' } $currentRow = $currentRow + ',{"name":"device/device/virtualization/last_update","value":"' + $TIMESTAMP + '"}' $currentRow = $currentRow + ']},' $jsonResult = $jsonResult + $currentRow } else { Write-CustomLog -Message "Invalid device: '$($device | ConvertTo-Json -Compress)'" -Severity 'DEBUG' } } catch { Write-CustomLog -Message "Error processing device '$($device.MachineName)'. Details: $($_.Exception.Message)" -Severity 'ERROR' } } if ($jsonResult.EndsWith(',')) { $jsonResult = $jsonResult.Substring(0, $jsonResult.Length - 1) } $clientTelemetryJson = $clientTelemetry | ConvertTo-Json -Compress return $jsonResult + '], "domain":"omnissa", "clientTelemetry":' + $clientTelemetryJson + '}' } function Get-ClientTelemetry([Int]$ExecutionDuration) { return [PSCustomObject]@{ version = $SCRIPT_VERSION executionDurationInMs = $ExecutionDuration fullScan = "true" } } function Test-MachineIsValid([PSCustomObject]$Device) { if ($null -ne $Device.MachineName -and $null -ne $Device.DesktopPool -and $null -ne $Device.Hostname ) { return $true } return $false } function Invoke-SendDataToEnrichmentAPI ([String]$EndpointUrl, [String]$JsonPayload, [String]$Jwt) { try { $headers = New-Object "System.Collections.Generic.Dictionary[[String],[String]]" $headers.Add('Content-Type', 'application/json') $headers.Add('Authorization', "Bearer $Jwt") $headers.Add($TRACE_ID_HEADER, $TRACE_ID_VALUE) $response = Invoke-WebRequest -Uri $EndpointUrl -Method 'POST' -Headers $headers -Body $JsonPayload -UseBasicParsing $statusCode = $response.StatusCode $content = $response.Content } catch { Write-CustomLog -Message "[$TRACE_ID_VALUE] Error sending request to Enrichment API. Details: $($_.Exception.Message)" -Severity 'ERROR' Write-CustomLog -Message "[$TRACE_ID_VALUE] Error message: $_" -Severity 'ERROR' $statusCode = $_.Exception.response.StatusCode.value__ } return @{ statusCode = $statusCode; content = $content } } # # Module exports - Only export when running as a module # try { Export-ModuleMember -Function * } catch [System.InvalidOperationException] { # Expected when dot-sourced as script during testing } catch { # Silently ignore any other export errors } # SIG # Begin signature block # MIIpiAYJKoZIhvcNAQcCoIIpeTCCKXUCAQExDzANBglghkgBZQMEAgEFADB5Bgor # BgEEAYI3AgEEoGswaTA0BgorBgEEAYI3AgEeMCYCAwEAAAQQH8w7YFlLCE63JNLG # KX7zUQIBAAIBAAIBAAIBAAIBADAxMA0GCWCGSAFlAwQCAQUABCCyTCxiRbkVBG3z # +Ii3Z94J/zF4qoP6UTmRUsgoq7gxgaCCDnswggawMIIEmKADAgECAhAIrUCyYNKc # TJ9ezam9k67ZMA0GCSqGSIb3DQEBDAUAMGIxCzAJBgNVBAYTAlVTMRUwEwYDVQQK # EwxEaWdpQ2VydCBJbmMxGTAXBgNVBAsTEHd3dy5kaWdpY2VydC5jb20xITAfBgNV # BAMTGERpZ2lDZXJ0IFRydXN0ZWQgUm9vdCBHNDAeFw0yMTA0MjkwMDAwMDBaFw0z # NjA0MjgyMzU5NTlaMGkxCzAJBgNVBAYTAlVTMRcwFQYDVQQKEw5EaWdpQ2VydCwg # SW5jLjFBMD8GA1UEAxM4RGlnaUNlcnQgVHJ1c3RlZCBHNCBDb2RlIFNpZ25pbmcg # UlNBNDA5NiBTSEEzODQgMjAyMSBDQTEwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAw # ggIKAoICAQDVtC9C0CiteLdd1TlZG7GIQvUzjOs9gZdwxbvEhSYwn6SOaNhc9es0 # JAfhS0/TeEP0F9ce2vnS1WcaUk8OoVf8iJnBkcyBAz5NcCRks43iCH00fUyAVxJr # Q5qZ8sU7H/Lvy0daE6ZMswEgJfMQ04uy+wjwiuCdCcBlp/qYgEk1hz1RGeiQIXhF # LqGfLOEYwhrMxe6TSXBCMo/7xuoc82VokaJNTIIRSFJo3hC9FFdd6BgTZcV/sk+F # LEikVoQ11vkunKoAFdE3/hoGlMJ8yOobMubKwvSnowMOdKWvObarYBLj6Na59zHh # 3K3kGKDYwSNHR7OhD26jq22YBoMbt2pnLdK9RBqSEIGPsDsJ18ebMlrC/2pgVItJ # wZPt4bRc4G/rJvmM1bL5OBDm6s6R9b7T+2+TYTRcvJNFKIM2KmYoX7BzzosmJQay # g9Rc9hUZTO1i4F4z8ujo7AqnsAMrkbI2eb73rQgedaZlzLvjSFDzd5Ea/ttQokbI # YViY9XwCFjyDKK05huzUtw1T0PhH5nUwjewwk3YUpltLXXRhTT8SkXbev1jLchAp # QfDVxW0mdmgRQRNYmtwmKwH0iU1Z23jPgUo+QEdfyYFQc4UQIyFZYIpkVMHMIRro # OBl8ZhzNeDhFMJlP/2NPTLuqDQhTQXxYPUez+rbsjDIJAsxsPAxWEQIDAQABo4IB # WTCCAVUwEgYDVR0TAQH/BAgwBgEB/wIBADAdBgNVHQ4EFgQUaDfg67Y7+F8Rhvv+ # YXsIiGX0TkIwHwYDVR0jBBgwFoAU7NfjgtJxXWRM3y5nP+e6mK4cD08wDgYDVR0P # AQH/BAQDAgGGMBMGA1UdJQQMMAoGCCsGAQUFBwMDMHcGCCsGAQUFBwEBBGswaTAk # BggrBgEFBQcwAYYYaHR0cDovL29jc3AuZGlnaWNlcnQuY29tMEEGCCsGAQUFBzAC # hjVodHRwOi8vY2FjZXJ0cy5kaWdpY2VydC5jb20vRGlnaUNlcnRUcnVzdGVkUm9v # dEc0LmNydDBDBgNVHR8EPDA6MDigNqA0hjJodHRwOi8vY3JsMy5kaWdpY2VydC5j # b20vRGlnaUNlcnRUcnVzdGVkUm9vdEc0LmNybDAcBgNVHSAEFTATMAcGBWeBDAED # MAgGBmeBDAEEATANBgkqhkiG9w0BAQwFAAOCAgEAOiNEPY0Idu6PvDqZ01bgAhql # +Eg08yy25nRm95RysQDKr2wwJxMSnpBEn0v9nqN8JtU3vDpdSG2V1T9J9Ce7FoFF # UP2cvbaF4HZ+N3HLIvdaqpDP9ZNq4+sg0dVQeYiaiorBtr2hSBh+3NiAGhEZGM1h # mYFW9snjdufE5BtfQ/g+lP92OT2e1JnPSt0o618moZVYSNUa/tcnP/2Q0XaG3Ryw # YFzzDaju4ImhvTnhOE7abrs2nfvlIVNaw8rpavGiPttDuDPITzgUkpn13c5Ubdld # AhQfQDN8A+KVssIhdXNSy0bYxDQcoqVLjc1vdjcshT8azibpGL6QB7BDf5WIIIJw # 8MzK7/0pNVwfiThV9zeKiwmhywvpMRr/LhlcOXHhvpynCgbWJme3kuZOX956rEnP # LqR0kq3bPKSchh/jwVYbKyP/j7XqiHtwa+aguv06P0WmxOgWkVKLQcBIhEuWTatE # QOON8BUozu3xGFYHKi8QxAwIZDwzj64ojDzLj4gLDb879M4ee47vtevLt/B3E+bn # KD+sEq6lLyJsQfmCXBVmzGwOysWGw/YmMwwHS6DTBwJqakAwSEs0qFEgu60bhQji # WQ1tygVQK+pKHJ6l/aCnHwZ05/LWUpD9r4VIIflXO7ScA+2GRfS0YW6/aOImYIbq # yK+p/pQd52MbOoZWeE4wggfDMIIFq6ADAgECAhAHJlJYMMxBAtpVem3mdhUEMA0G # CSqGSIb3DQEBCwUAMGkxCzAJBgNVBAYTAlVTMRcwFQYDVQQKEw5EaWdpQ2VydCwg # SW5jLjFBMD8GA1UEAxM4RGlnaUNlcnQgVHJ1c3RlZCBHNCBDb2RlIFNpZ25pbmcg # UlNBNDA5NiBTSEEzODQgMjAyMSBDQTEwHhcNMjUwMzA2MDAwMDAwWhcNMjgwMzA3 # MjM1OTU5WjCByzETMBEGCysGAQQBgjc8AgEDEwJDSDEVMBMGCysGAQQBgjc8AgEC # EwRWYXVkMR0wGwYDVQQPDBRQcml2YXRlIE9yZ2FuaXphdGlvbjEYMBYGA1UEBRMP # Q0hFLTExMi4wMDAuNTc5MQswCQYDVQQGEwJDSDEPMA0GA1UEBxMGUHJpbGx5MRYw # FAYDVQQKEw1ORVhUaGluayBTLkEuMRYwFAYDVQQLEw1ORVhUaGluayBTLkEuMRYw # FAYDVQQDEw1ORVhUaGluayBTLkEuMIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIIC # CgKCAgEAyQBSj4N+dRT4Ft/77JxH1TCVXklR2+qgPiRzd1DHcjEu5mlpGbfFfGht # NiZ1yDKC05ieExBSO6qIfkw6Lcod5yDTd1ojwU13b2SroZUUG6tLcwHLVnnSDiBH # wi03dG7VmS9pqmgCLBRz7N3eF00AGY/cFJc8tT96HwXWSlVMAJ3DLkS6Db+1GLTi # /ne9f9uWyksWSR3fW/sskS4/X6jIR26jJjPSfA4PhrnjRTSUJiMostKTRlHIIZtH # T5m0Nf4adyU/4QCHHCSwx2cq+1z3H7C98yV7sZVb5hr0VUyVUYA+YP9xYQsKy1BQ # XXLBd8WxT1QT+/UlUl5reK/oo6mjfcOt5Sz3ZGj+24SYGZ2zgrTWEboe3rZHKoKB # 7fHqtKv4wrKXVkZt7J+kLuXg3fOusZ65V9NMyZx1gvL+t0RXd49dHGuBhg3ddVw7 # T4NZJ5M9XaWfwWUA04+aBouM4hqZLNKA9xpqv2JLFdg/UBof6JPETZ2cSP2tNKM1 # Ai+F00mm1Md196RC/+hltVGnmHqrw3rTScjiFT+HwHy0QWY5pdu275J9zCZcYZXz # qUEkFmJN5aU9Nz6TZlgK5sNy1TJensLcnFw4hMK/y0GLkjGoXht4VQUJFGP51Lyh # GSYo6EZJscVPx+kBg4c7I5P/4nEnFKguV3IjISiJbdDqNgbLHOsCAwEAAaOCAgIw # ggH+MB8GA1UdIwQYMBaAFGg34Ou2O/hfEYb7/mF7CIhl9E5CMB0GA1UdDgQWBBT0 # DqAKYCVBQLXZvFpqeHPu5oUVtjA9BgNVHSAENjA0MDIGBWeBDAEDMCkwJwYIKwYB # BQUHAgEWG2h0dHA6Ly93d3cuZGlnaWNlcnQuY29tL0NQUzAOBgNVHQ8BAf8EBAMC # B4AwEwYDVR0lBAwwCgYIKwYBBQUHAwMwgbUGA1UdHwSBrTCBqjBToFGgT4ZNaHR0 # cDovL2NybDMuZGlnaWNlcnQuY29tL0RpZ2lDZXJ0VHJ1c3RlZEc0Q29kZVNpZ25p # bmdSU0E0MDk2U0hBMzg0MjAyMUNBMS5jcmwwU6BRoE+GTWh0dHA6Ly9jcmw0LmRp # Z2ljZXJ0LmNvbS9EaWdpQ2VydFRydXN0ZWRHNENvZGVTaWduaW5nUlNBNDA5NlNI # QTM4NDIwMjFDQTEuY3JsMIGUBggrBgEFBQcBAQSBhzCBhDAkBggrBgEFBQcwAYYY # aHR0cDovL29jc3AuZGlnaWNlcnQuY29tMFwGCCsGAQUFBzAChlBodHRwOi8vY2Fj # ZXJ0cy5kaWdpY2VydC5jb20vRGlnaUNlcnRUcnVzdGVkRzRDb2RlU2lnbmluZ1JT # QTQwOTZTSEEzODQyMDIxQ0ExLmNydDAJBgNVHRMEAjAAMA0GCSqGSIb3DQEBCwUA # A4ICAQArEaG0O/idYThPmYwLNwjvWSTevsonyUhvSGjSnkshdGZ1ClZdCmeMisXc # jHystVNVtLZgy7q4d5a0m5lNKguI2p2/yPvWb9mGw9pateQ6v9AZGYrkL93FzvQK # b/cVp/pcfAmAtjAbhP4FaFQlEfPHyLJSswJMkZHa3lt+scmSjG0DwLbw3J4MK8MJ # sQubqt/Po0hMEm5IXfIAhTUMVrgtD2iVQOIv4YchJQePkOhfoa6T3QAbQGisioT1 # GXpaw+cMQuakS03Dj59KVzwZKgKWf2WttIcByN+iSI8oIBbzj7uFDbaH1BpocbYb # sEG1+4zXkX92e45aetxuKGMe3DdH5dP+f2Mf820NHc7awetz2h4ejcX2Iw4VXLiu # Gd2qz2+My8ayqXbpEdXDkRcnOOUPk00Z4SekIvmmdu/R75Ri8ApQbYcLq7OJWn4F # 3KHTlztKTdcaBeYK3AsYNcap50uKnceS+eW7CXmEGFj93z6PUAiMkKabj/LHmd47 # hi9qdvumQ1lPzjilxHUrfbI5ijSq58mbgyG/LU5+wGh00mxSyLQ6dEYE7OdwEW3x # 4KUjXWj4/9lmdCqAv25+qOR6HikM3dDKBgJQN+tk9soHD+MEyoyldDHBRjf8Gt6g # MDnJ0Bvk1tfxFBx/KE7xwib8KS594kK7Q89nFgH3Uo1reXKkejGCGmMwghpfAgEB # MH0waTELMAkGA1UEBhMCVVMxFzAVBgNVBAoTDkRpZ2lDZXJ0LCBJbmMuMUEwPwYD # VQQDEzhEaWdpQ2VydCBUcnVzdGVkIEc0IENvZGUgU2lnbmluZyBSU0E0MDk2IFNI # QTM4NCAyMDIxIENBMQIQByZSWDDMQQLaVXpt5nYVBDANBglghkgBZQMEAgEFAKB8 # MBAGCisGAQQBgjcCAQwxAjAAMBkGCSqGSIb3DQEJAzEMBgorBgEEAYI3AgEEMBwG # CisGAQQBgjcCAQsxDjAMBgorBgEEAYI3AgEVMC8GCSqGSIb3DQEJBDEiBCAgER1w # NjD1IyxW3mxgU3RzoCTV4ZGkhjqMnKm+SKICczANBgkqhkiG9w0BAQEFAASCAgCa # 3+SFjDhOGL+YRzrxeM3V1L+kleOTSE8yA1XGEage01Cofa9Hb2AiFKia0Q9uUldl # Q9vnfnxkKTr/7chx7VT4GJAXVnTyDc9I4/wRV1vslsplC1TN281bpsVsBOq/etBC # E+hifmqdfjmCNR0e6GWqZwmd8RM3jV4v6/6IJ3mCiJMN2HKjZ7EXfH7nzUpnyNQq # mAnznGAIcnux/4+7VKjCPqkH2jqwxXCWgzS8Gtmq6iWvsMAdMxlntLMMQjOgzAtS # wSHurzk5cDOBFdach0IxVpzGWglgwDNmQZiXMQ/Pzsp0sV/NxLrnD2VfW3lXTIxw # LLpkoGKfLgaabEsU0NTY9uYDRQpYhl6mmUtroYhr0/fRkhKlJQHXIeeoIM43J+Eb # mL8FhXdgXiuqGu7YILHrkpdWVBuwRey42eB1h+XA/ptRbctdsUq7MQBEmtutpfoZ # Qj19fgVogeLhVW5nGQ0qBogV0bbZEzQebUp1hM66h4Ow9rVRJEA1jhtUcwk+FI5+ # QuyPP66sYWchOUvuWsJKkvbF3XFwfutd2127OCJ+ZBhA3VvaA+42LycOwCvvy1dy # 1jG51rmL32Ly+5ZfznReEoLEaOxhMbVHqTNEAn7+GgdR5dXSCku4LeGlErq8gqLV # WLXi6pJ+K63P6WeEtfYnk8KRUIkPaaFkXzN+hU+eW6GCFzkwghc1BgorBgEEAYI3 # AwMBMYIXJTCCFyEGCSqGSIb3DQEHAqCCFxIwghcOAgEDMQ8wDQYJYIZIAWUDBAIB # BQAwdwYLKoZIhvcNAQkQAQSgaARmMGQCAQEGCWCGSAGG/WwHATAxMA0GCWCGSAFl # AwQCAQUABCAmGLJtanUhirspWdebpzbCPbAcueaxqYAuyCTelWknagIQOpPZj5Hs # n7cxXese22D5OBgPMjAyNTA3MDQwODE5MzBaoIITAzCCBrwwggSkoAMCAQICEAuu # Zrxaun+Vh8b56QTjMwQwDQYJKoZIhvcNAQELBQAwYzELMAkGA1UEBhMCVVMxFzAV # BgNVBAoTDkRpZ2lDZXJ0LCBJbmMuMTswOQYDVQQDEzJEaWdpQ2VydCBUcnVzdGVk # IEc0IFJTQTQwOTYgU0hBMjU2IFRpbWVTdGFtcGluZyBDQTAeFw0yNDA5MjYwMDAw # MDBaFw0zNTExMjUyMzU5NTlaMEIxCzAJBgNVBAYTAlVTMREwDwYDVQQKEwhEaWdp # Q2VydDEgMB4GA1UEAxMXRGlnaUNlcnQgVGltZXN0YW1wIDIwMjQwggIiMA0GCSqG # SIb3DQEBAQUAA4ICDwAwggIKAoICAQC+anOf9pUhq5Ywultt5lmjtej9kR8YxIg7 # apnjpcH9CjAgQxK+CMR0Rne/i+utMeV5bUlYYSuuM4vQngvQepVHVzNLO9RDnEXv # PghCaft0djvKKO+hDu6ObS7rJcXa/UKvNminKQPTv/1+kBPgHGlP28mgmoCw/xi6 # FG9+Un1h4eN6zh926SxMe6We2r1Z6VFZj75MU/HNmtsgtFjKfITLutLWUdAoWle+ # jYZ49+wxGE1/UXjWfISDmHuI5e/6+NfQrxGFSKx+rDdNMsePW6FLrphfYtk/FLih # p/feun0eV+pIF496OVh4R1TvjQYpAztJpVIfdNsEvxHofBf1BWkadc+Up0Th8Eif # kEEWdX4rA/FE1Q0rqViTbLVZIqi6viEk3RIySho1XyHLIAOJfXG5PEppc3XYeBH7 # xa6VTZ3rOHNeiYnY+V4j1XbJ+Z9dI8ZhqcaDHOoj5KGg4YuiYx3eYm33aebsyF6e # D9MF5IDbPgjvwmnAalNEeJPvIeoGJXaeBQjIK13SlnzODdLtuThALhGtyconcVuP # I8AaiCaiJnfdzUcb3dWnqUnjXkRFwLtsVAxFvGqsxUA2Jq/WTjbnNjIUzIs3ITVC # 6VBKAOlb2u29Vwgfta8b2ypi6n2PzP0nVepsFk8nlcuWfyZLzBaZ0MucEdeBiXL+ # nUOGhCjl+QIDAQABo4IBizCCAYcwDgYDVR0PAQH/BAQDAgeAMAwGA1UdEwEB/wQC # MAAwFgYDVR0lAQH/BAwwCgYIKwYBBQUHAwgwIAYDVR0gBBkwFzAIBgZngQwBBAIw # CwYJYIZIAYb9bAcBMB8GA1UdIwQYMBaAFLoW2W1NhS9zKXaaL3WMaiCPnshvMB0G # A1UdDgQWBBSfVywDdw4oFZBmpWNe7k+SH3agWzBaBgNVHR8EUzBRME+gTaBLhklo # dHRwOi8vY3JsMy5kaWdpY2VydC5jb20vRGlnaUNlcnRUcnVzdGVkRzRSU0E0MDk2 # U0hBMjU2VGltZVN0YW1waW5nQ0EuY3JsMIGQBggrBgEFBQcBAQSBgzCBgDAkBggr # BgEFBQcwAYYYaHR0cDovL29jc3AuZGlnaWNlcnQuY29tMFgGCCsGAQUFBzAChkxo # dHRwOi8vY2FjZXJ0cy5kaWdpY2VydC5jb20vRGlnaUNlcnRUcnVzdGVkRzRSU0E0 # MDk2U0hBMjU2VGltZVN0YW1waW5nQ0EuY3J0MA0GCSqGSIb3DQEBCwUAA4ICAQA9 # rR4fdplb4ziEEkfZQ5H2EdubTggd0ShPz9Pce4FLJl6reNKLkZd5Y/vEIqFWKt4o # KcKz7wZmXa5VgW9B76k9NJxUl4JlKwyjUkKhk3aYx7D8vi2mpU1tKlY71AYXB8wT # LrQeh83pXnWwwsxc1Mt+FWqz57yFq6laICtKjPICYYf/qgxACHTvypGHrC8k1TqC # eHk6u4I/VBQC9VK7iSpU5wlWjNlHlFFv/M93748YTeoXU/fFa9hWJQkuzG2+B7+b # MDvmgF8VlJt1qQcl7YFUMYgZU1WM6nyw23vT6QSgwX5Pq2m0xQ2V6FJHu8z4LXe/ # 371k5QrN9FQBhLLISZi2yemW0P8ZZfx4zvSWzVXpAb9k4Hpvpi6bUe8iK6WonUSV # 6yPlMwerwJZP/Gtbu3CKldMnn+LmmRTkTXpFIEB06nXZrDwhCGED+8RsWQSIXZpu # G4WLFQOhtloDRWGoCwwc6ZpPddOFkM2LlTbMcqFSzm4cd0boGhBq7vkqI1uHRz6F # q1IX7TaRQuR+0BGOzISkcqwXu7nMpFu3mgrlgbAW+BzikRVQ3K2YHcGkiKjA4gi4 # OA/kz1YCsdhIBHXqBzR0/Zd2QwQ/l4Gxftt/8wY3grcc/nS//TVkej9nmUYu83BD # tccHHXKibMs/yXHhDXNkoPIdynhVAku7aRZOwqw6pDCCBq4wggSWoAMCAQICEAc2 # N7ckVHzYR6z9KGYqXlswDQYJKoZIhvcNAQELBQAwYjELMAkGA1UEBhMCVVMxFTAT # BgNVBAoTDERpZ2lDZXJ0IEluYzEZMBcGA1UECxMQd3d3LmRpZ2ljZXJ0LmNvbTEh # MB8GA1UEAxMYRGlnaUNlcnQgVHJ1c3RlZCBSb290IEc0MB4XDTIyMDMyMzAwMDAw # MFoXDTM3MDMyMjIzNTk1OVowYzELMAkGA1UEBhMCVVMxFzAVBgNVBAoTDkRpZ2lD # ZXJ0LCBJbmMuMTswOQYDVQQDEzJEaWdpQ2VydCBUcnVzdGVkIEc0IFJTQTQwOTYg # U0hBMjU2IFRpbWVTdGFtcGluZyBDQTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCC # AgoCggIBAMaGNQZJs8E9cklRVcclA8TykTepl1Gh1tKD0Z5Mom2gsMyD+Vr2EaFE # FUJfpIjzaPp985yJC3+dH54PMx9QEwsmc5Zt+FeoAn39Q7SE2hHxc7Gz7iuAhIoi # GN/r2j3EF3+rGSs+QtxnjupRPfDWVtTnKC3r07G1decfBmWNlCnT2exp39mQh0YA # e9tEQYncfGpXevA3eZ9drMvohGS0UvJ2R/dhgxndX7RUCyFobjchu0CsX7LeSn3O # 9TkSZ+8OpWNs5KbFHc02DVzV5huowWR0QKfAcsW6Th+xtVhNef7Xj3OTrCw54qVI # 1vCwMROpVymWJy71h6aPTnYVVSZwmCZ/oBpHIEPjQ2OAe3VuJyWQmDo4EbP29p7m # O1vsgd4iFNmCKseSv6De4z6ic/rnH1pslPJSlRErWHRAKKtzQ87fSqEcazjFKfPK # qpZzQmiftkaznTqj1QPgv/CiPMpC3BhIfxQ0z9JMq++bPf4OuGQq+nUoJEHtQr8F # nGZJUlD0UfM2SU2LINIsVzV5K6jzRWC8I41Y99xh3pP+OcD5sjClTNfpmEpYPtMD # iP6zj9NeS3YSUZPJjAw7W4oiqMEmCPkUEBIDfV8ju2TjY+Cm4T72wnSyPx4Jduyr # XUZ14mCjWAkBKAAOhFTuzuldyF4wEr1GnrXTdrnSDmuZDNIztM2xAgMBAAGjggFd # MIIBWTASBgNVHRMBAf8ECDAGAQH/AgEAMB0GA1UdDgQWBBS6FtltTYUvcyl2mi91 # jGogj57IbzAfBgNVHSMEGDAWgBTs1+OC0nFdZEzfLmc/57qYrhwPTzAOBgNVHQ8B # Af8EBAMCAYYwEwYDVR0lBAwwCgYIKwYBBQUHAwgwdwYIKwYBBQUHAQEEazBpMCQG # CCsGAQUFBzABhhhodHRwOi8vb2NzcC5kaWdpY2VydC5jb20wQQYIKwYBBQUHMAKG # NWh0dHA6Ly9jYWNlcnRzLmRpZ2ljZXJ0LmNvbS9EaWdpQ2VydFRydXN0ZWRSb290 # RzQuY3J0MEMGA1UdHwQ8MDowOKA2oDSGMmh0dHA6Ly9jcmwzLmRpZ2ljZXJ0LmNv # bS9EaWdpQ2VydFRydXN0ZWRSb290RzQuY3JsMCAGA1UdIAQZMBcwCAYGZ4EMAQQC # MAsGCWCGSAGG/WwHATANBgkqhkiG9w0BAQsFAAOCAgEAfVmOwJO2b5ipRCIBfmbW # 2CFC4bAYLhBNE88wU86/GPvHUF3iSyn7cIoNqilp/GnBzx0H6T5gyNgL5Vxb122H # +oQgJTQxZ822EpZvxFBMYh0MCIKoFr2pVs8Vc40BIiXOlWk/R3f7cnQU1/+rT4os # equFzUNf7WC2qk+RZp4snuCKrOX9jLxkJodskr2dfNBwCnzvqLx1T7pa96kQsl3p # /yhUifDVinF2ZdrM8HKjI/rAJ4JErpknG6skHibBt94q6/aesXmZgaNWhqsKRcnf # xI2g55j7+6adcq/Ex8HBanHZxhOACcS2n82HhyS7T6NJuXdmkfFynOlLAlKnN36T # U6w7HQhJD5TNOXrd/yVjmScsPT9rp/Fmw0HNT7ZAmyEhQNC3EyTN3B14OuSereU0 # cZLXJmvkOHOrpgFPvT87eK1MrfvElXvtCl8zOYdBeHo46Zzh3SP9HSjTx/no8Zhf # +yvYfvJGnXUsHicsJttvFXseGYs2uJPU5vIXmVnKcPA3v5gA3yAWTyf7YGcWoWa6 # 3VXAOimGsJigK+2VQbc61RWYMbRiCQ8KvYHZE/6/pNHzV9m8BPqC3jLfBInwAM1d # wvnQI38AC+R2AibZ8GV2QqYphwlHK+Z/GqSFD/yYlvZVVCsfgPrA8g4r5db7qS9E # FUrnEw4d2zc4GqEr9u3WfPwwggWNMIIEdaADAgECAhAOmxiO+dAt5+/bUOIIQBha # MA0GCSqGSIb3DQEBDAUAMGUxCzAJBgNVBAYTAlVTMRUwEwYDVQQKEwxEaWdpQ2Vy # dCBJbmMxGTAXBgNVBAsTEHd3dy5kaWdpY2VydC5jb20xJDAiBgNVBAMTG0RpZ2lD # ZXJ0IEFzc3VyZWQgSUQgUm9vdCBDQTAeFw0yMjA4MDEwMDAwMDBaFw0zMTExMDky # MzU5NTlaMGIxCzAJBgNVBAYTAlVTMRUwEwYDVQQKEwxEaWdpQ2VydCBJbmMxGTAX # BgNVBAsTEHd3dy5kaWdpY2VydC5jb20xITAfBgNVBAMTGERpZ2lDZXJ0IFRydXN0 # ZWQgUm9vdCBHNDCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAL/mkHNo # 3rvkXUo8MCIwaTPswqclLskhPfKK2FnC4SmnPVirdprNrnsbhA3EMB/zG6Q4FutW # xpdtHauyefLKEdLkX9YFPFIPUh/GnhWlfr6fqVcWWVVyr2iTcMKyunWZanMylNEQ # RBAu34LzB4TmdDttceItDBvuINXJIB1jKS3O7F5OyJP4IWGbNOsFxl7sWxq868nP # zaw0QF+xembud8hIqGZXV59UWI4MK7dPpzDZVu7Ke13jrclPXuU15zHL2pNe3I6P # gNq2kZhAkHnDeMe2scS1ahg4AxCN2NQ3pC4FfYj1gj4QkXCrVYJBMtfbBHMqbpEB # fCFM1LyuGwN1XXhm2ToxRJozQL8I11pJpMLmqaBn3aQnvKFPObURWBf3JFxGj2T3 # wWmIdph2PVldQnaHiZdpekjw4KISG2aadMreSx7nDmOu5tTvkpI6nj3cAORFJYm2 # mkQZK37AlLTSYW3rM9nF30sEAMx9HJXDj/chsrIRt7t/8tWMcCxBYKqxYxhElRp2 # Yn72gLD76GSmM9GJB+G9t+ZDpBi4pncB4Q+UDCEdslQpJYls5Q5SUUd0viastkF1 # 3nqsX40/ybzTQRESW+UQUOsxxcpyFiIJ33xMdT9j7CFfxCBRa2+xq4aLT8LWRV+d # IPyhHsXAj6KxfgommfXkaS+YHS312amyHeUbAgMBAAGjggE6MIIBNjAPBgNVHRMB # Af8EBTADAQH/MB0GA1UdDgQWBBTs1+OC0nFdZEzfLmc/57qYrhwPTzAfBgNVHSME # GDAWgBRF66Kv9JLLgjEtUYunpyGd823IDzAOBgNVHQ8BAf8EBAMCAYYweQYIKwYB # BQUHAQEEbTBrMCQGCCsGAQUFBzABhhhodHRwOi8vb2NzcC5kaWdpY2VydC5jb20w # QwYIKwYBBQUHMAKGN2h0dHA6Ly9jYWNlcnRzLmRpZ2ljZXJ0LmNvbS9EaWdpQ2Vy # dEFzc3VyZWRJRFJvb3RDQS5jcnQwRQYDVR0fBD4wPDA6oDigNoY0aHR0cDovL2Ny # bDMuZGlnaWNlcnQuY29tL0RpZ2lDZXJ0QXNzdXJlZElEUm9vdENBLmNybDARBgNV # HSAECjAIMAYGBFUdIAAwDQYJKoZIhvcNAQEMBQADggEBAHCgv0NcVec4X6CjdBs9 # thbX979XB72arKGHLOyFXqkauyL4hxppVCLtpIh3bb0aFPQTSnovLbc47/T/gLn4 # offyct4kvFIDyE7QKt76LVbP+fT3rDB6mouyXtTP0UNEm0Mh65ZyoUi0mcudT6cG # AxN3J0TU53/oWajwvy8LpunyNDzs9wPHh6jSTEAZNUZqaVSwuKFWjuyk1T3osdz9 # HNj0d1pcVIxv76FQPfx2CWiEn2/K2yCNNWAcAgPLILCsWKAOQGPFmCLBsln1VWvP # J6tsds5vIy30fnFqI2si/xK4VC0nftg62fC2h5b9W9FcrBjDTZ9ztwGpn1eqXiji # uZQxggN2MIIDcgIBATB3MGMxCzAJBgNVBAYTAlVTMRcwFQYDVQQKEw5EaWdpQ2Vy # dCwgSW5jLjE7MDkGA1UEAxMyRGlnaUNlcnQgVHJ1c3RlZCBHNCBSU0E0MDk2IFNI # QTI1NiBUaW1lU3RhbXBpbmcgQ0ECEAuuZrxaun+Vh8b56QTjMwQwDQYJYIZIAWUD # BAIBBQCggdEwGgYJKoZIhvcNAQkDMQ0GCyqGSIb3DQEJEAEEMBwGCSqGSIb3DQEJ # BTEPFw0yNTA3MDQwODE5MzBaMCsGCyqGSIb3DQEJEAIMMRwwGjAYMBYEFNvThe5i # 29I+e+T2cUhQhyTVhltFMC8GCSqGSIb3DQEJBDEiBCBTi5/N1ErxSPMruB864Q2Z # IkL6AsHV1kNCrqMGI+AInTA3BgsqhkiG9w0BCRACLzEoMCYwJDAiBCB2dp+o8mMv # H0MLOiMwrtZWdf7Xc9sF1mW5BZOYQ4+a2zANBgkqhkiG9w0BAQEFAASCAgCyowV+ # O/jC87neul9O9zAehtz34Z2tX/+uiRdY2TOFjBSsZ5kyg/JqOTIR0f3lTqDru5Cj # Ag4FLcely3HXYUwPC7RhTwnigb2Msy8+VwaKUuXyPgVVn0/AA+HXqt0gnriiLKwR # eFrUwQK5IeW3xNla7OXTLkVso6P40BtVIW3uJlfbaddP28IVRthZSLcRBsKsvafr # GJ6i/nOGhKsPY8mlK0+hwNagooiOcZ/4vBbymj5SrgFennuATy8ExbpjgpfeoXI+ # 4mG6nsjweqx6u9JqY4Pf6qNNUZ86AxmNIKnASwQCQcYm1ubhjNZWhLgAuU535ifx # ihV3klKT7eVCH8J9AGN8s77bthCDEOv3XfStQARSCAA9KDVJlTsAsDE1FDfWTBTV # 0DcZocZlu2LXLFFliA/jkPgibq6jcZs0GfD60VV2goVo5PalzP8ziSuP8hvuyPD5 # RyS7jr5OhhBf/Ir+ntdQgqYo5zEa0Si9gFx1bL+/lkx9Uny+IyHVR0nRNn0cFSsj # 6GVSrE3/xbSyksYwTQDfGsQ7D7Vt85TCJHjOE/GxYI/HFr94vJvn1swMFURiwQZo # v+PkgqKmx0TWoD4Tawa6uBHUPFgnwaCevHVA30/wA1wXpeZaHR2PMDCap96S2oQ6 # ARhFAdV/y/TGM+9S10WnVhezN2OydTHUJTOkeA== # SIG # End signature block |