public/Get-IvantiWCRegistry.ps1

<#
.SYNOPSIS
    Reads and parses Ivanti Workspace Control registry sets from a Building Block XML file or a
    standalone .reg file.
.DESCRIPTION
    Accepts either:
    - An Ivanti Workspace Control Building Block XML file: extracts every <registry type="registry">
      node, decodes the hex-encoded registryfile, and parses it. Name and description are taken
      from the <name> and <description> XML elements.
    - A standalone .reg file: parses it directly. Name and description are read from the
      ;<PFNAME> and ;<PFDESC> comment tags if present.
 
    Each value entry is returned as a structured object with its registry hive, key, value name,
    value data, value type, and an optional per-value description captured from the ;<PF>...</PF>
    comment tag that may follow the value line.
 
    Supports all standard .reg value types:
    - REG_SZ (quoted string or hex(1):)
    - REG_EXPAND_SZ (hex(2):)
    - REG_BINARY (hex: / hex(3):)
    - REG_DWORD (dword: / hex(4):)
    - REG_MULTI_SZ (hex(7):)
    - REG_QWORD (qword: / hex(b):)
 
.PARAMETER Path
    Path to an Ivanti Workspace Control Building Block XML file or a standalone .reg file.
 
.PARAMETER ExportFor
    Target export format. 'AppVentiX' adds the AppVentiXParams property to each output object.
    Defaults to 'WEM'.
 
.EXAMPLE
    # From a Building Block XML
    Get-IvantiWCRegistry -Path 'C:\temp\LAB-BB.xml'
 
.EXAMPLE
    # From a standalone .reg file, exported for AppVentiX
    Get-IvantiWCRegistry -Path 'C:\temp\registry.reg' -ExportFor AppVentiX
 
.NOTES
    Function : Get-IvantiWCRegistry
    Author : John Billekens
    Copyright : (c) John Billekens Consultancy & AppVentiX
    Version : 2026.0309.1200
#>

function Get-IvantiWCRegistry {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $true)]
        [ValidateScript({ Test-Path $_ -PathType Leaf })]
        [string]$Path,

        [ValidateSet('AppVentiX', 'WEM')]
        [string]$ExportFor = 'WEM'
    )

    #region Internal helpers

    function Convert-HexToString {
        param([string]$HexString)
        if ([string]::IsNullOrEmpty($HexString)) { return '' }
        $bytes = for ($i = 0; $i -lt $HexString.Length; $i += 2) {
            [System.Convert]::ToByte($HexString.Substring($i, 2), 16)
        }
        return [System.Text.Encoding]::GetEncoding(1252).GetString($bytes)
    }

    function Convert-RegistryHexToValue {
        param([string]$hexString, [string]$type)
        $hexData = $hexString -replace '^hex\(\w+\):', '' -replace '[,\s]', ''
        if ([string]::IsNullOrEmpty($hexData)) { return $null }
        try {
            $bytes = for ($i = 0; $i -lt $hexData.Length; $i += 2) {
                [System.Convert]::ToByte($hexData.Substring($i, 2), 16)
            }
            switch ($type) {
                'REG_SZ' { return [System.Text.Encoding]::Unicode.GetString($bytes).TrimEnd([char]0) }
                'REG_EXPAND_SZ' { return [System.Text.Encoding]::Unicode.GetString($bytes).TrimEnd([char]0) }
                'REG_BINARY' { return [System.BitConverter]::ToString($bytes) }
                'REG_DWORD' { if ($bytes.Count -eq 4) { return [System.BitConverter]::ToUInt32($bytes, 0) } return $null }
                'REG_MULTI_SZ' {
                    $fullString = [System.Text.Encoding]::Unicode.GetString($bytes)
                    return ($fullString -split '\0' | Where-Object { $_ })
                }
                'REG_QWORD' { if ($bytes.Count -eq 8) { return [System.BitConverter]::ToUInt64($bytes, 0) } return $null }
                default { return [System.BitConverter]::ToString($bytes) }
            }
        } catch {
            Write-Warning "Could not decode hex value for type $type. Error: $_"
            return $hexString
        }
    }

    function ConvertTo-RegistryEntries {
        param(
            [string]$Name,
            [string]$Description,
            [string]$RegContent,
            [string]$SourceFile,
            [bool]$Enabled = $true,
            [bool]$RunOnce = $false,
            [string]$ExportFor = 'WEM',
            [PSCustomObject[]]$Assignments = @()
        )

        $lines = $RegContent.Split([string[]]@("`r`n", "`n", "`r"), [StringSplitOptions]::None)

        # If name/description not provided, fall back to PFNAME/PFDESC tags in the content
        $resolvedName = $Name
        $resolvedDescription = $Description
        if ([string]::IsNullOrEmpty($resolvedName) -or [string]::IsNullOrEmpty($resolvedDescription)) {
            foreach ($line in $lines) {
                if ([string]::IsNullOrEmpty($resolvedName) -and $line -match ';<PFNAME>(.+)</PFNAME>') {
                    $resolvedName = $Matches[1].Trim()
                }
                if ([string]::IsNullOrEmpty($resolvedDescription) -and $line -match ';<PFDESC>(.+)</PFDESC>') {
                    $resolvedDescription = $Matches[1].Trim()
                }
                if (-not [string]::IsNullOrEmpty($resolvedName) -and -not [string]::IsNullOrEmpty($resolvedDescription)) {
                    break
                }
            }
        }

        Write-Verbose "Registry set: '$resolvedName' | Description: '$resolvedDescription' | Enabled: '$Enabled' | RunOnce: '$RunOnce'"

        $entries = [System.Collections.Generic.List[PSCustomObject]]::new()
        $currentHive = ''
        $currentKey = ''

        for ($i = 0; $i -lt $lines.Length; $i++) {
            $line = $lines[$i]

            # Section header: [HKEY_...] or ![HKEY_...] (delete entire key)
            if ($line -match '^(!?)\[((HKEY_[^\\]+)(\\(.+))?)\]$') {
                $isKeyDeletion = $Matches[1] -eq '!'
                $sectionHive = $Matches[3]
                $sectionKey = if ($Matches[5]) { $Matches[5] } else { '' }

                if ($isKeyDeletion) {
                    # Emit a key-deletion entry; do not update current section context
                    Write-Verbose " Delete key: $sectionHive\$sectionKey"
                    $entries.Add([PSCustomObject]([ordered]@{
                                RegistryHive      = $sectionHive
                                RegistryKey       = $sectionKey
                                RegistryKeyPath   = if ([string]::IsNullOrEmpty($sectionKey)) { $sectionHive } else { "$sectionHive\$sectionKey" }
                                RegistryValueName = $null
                                Value             = $null
                                ValueType         = $null
                                Description       = ''
                                Delete            = $true
                            }))
                } else {
                    $currentHive = $sectionHive
                    $currentKey = $sectionKey
                    Write-Verbose " Section: $currentHive\$currentKey"
                }
                continue
            }

            # Skip comments, blank lines, and the editor header
            if ([string]::IsNullOrWhiteSpace($line) -or
                $line.StartsWith(';') -or
                $line.StartsWith('Windows Registry Editor')) {
                continue
            }

            # Skip structural placeholder default values (@="") on keys with no meaningful path
            # These are Ivanti reg file artifacts — empty-string default values on intermediate keys
            if ($line -match '^@=""$' -and [string]::IsNullOrEmpty($currentKey)) {
                continue
            }

            # Deletion marker on value: !"ValueName"=...
            $isDeletion = $false
            $parsedLine = $line
            if ($parsedLine.TrimStart().StartsWith('!"')) {
                $isDeletion = $true
                $parsedLine = $parsedLine.TrimStart().Substring(1)
            }

            $valueName = $null
            $rawValue = $null

            if ($parsedLine -match '^"([^"]*)"=(.+)') {
                $valueName = $Matches[1]
                $rawValue = $Matches[2].Trim()
            } elseif ($parsedLine -match '^@=(.+)') {
                $valueName = ''
                $rawValue = $Matches[1].Trim()
            } else {
                continue
            }

            # Skip empty-string default values on intermediate keys (structural artifacts)
            if ($valueName -eq '' -and $rawValue -eq '""') {
                continue
            }

            # Peek at next line for optional ;<PF>...</PF> per-value description
            $valueDescription = ''
            if (($i + 1) -lt $lines.Length -and $lines[$i + 1] -match ';<PF>(.+)</PF>') {
                $valueDescription = $Matches[1].Trim()
            }

            # Decode value data and determine type
            $registryValue = $null
            $registryValueType = 'Unknown'

            if ($rawValue.StartsWith('dword:')) {
                $registryValueType = 'REG_DWORD'
                try { $registryValue = [uint64]("0x" + $rawValue.Substring(6)) }
                catch { $registryValue = $rawValue; $registryValueType = 'Unknown' }
            } elseif ($rawValue.StartsWith('qword:')) {
                $registryValueType = 'REG_QWORD'
                try { $registryValue = [uint64]("0x" + $rawValue.Substring(6)) }
                catch { $registryValue = $rawValue; $registryValueType = 'Unknown' }
            } elseif ($rawValue.StartsWith('"') -and $rawValue.EndsWith('"')) {
                $registryValueType = 'REG_SZ'
                $registryValue = $rawValue.Substring(1, $rawValue.Length - 2)
            } elseif ($rawValue.StartsWith('hex(1):')) {
                $registryValueType = 'REG_SZ'
                $registryValue = Convert-RegistryHexToValue -hexString $rawValue -type 'REG_SZ'
            } elseif ($rawValue.StartsWith('hex(2):')) {
                $registryValueType = 'REG_EXPAND_SZ'
                $registryValue = Convert-RegistryHexToValue -hexString $rawValue -type 'REG_EXPAND_SZ'
            } elseif ($rawValue.StartsWith('hex(3):') -or $rawValue.StartsWith('hex:')) {
                $registryValueType = 'REG_BINARY'
                $registryValue = Convert-RegistryHexToValue -hexString $rawValue -type 'REG_BINARY'
            } elseif ($rawValue.StartsWith('hex(4):')) {
                $registryValueType = 'REG_DWORD'
                $registryValue = Convert-RegistryHexToValue -hexString $rawValue -type 'REG_DWORD'
            } elseif ($rawValue.StartsWith('hex(7):')) {
                $registryValueType = 'REG_MULTI_SZ'
                $registryValue = Convert-RegistryHexToValue -hexString $rawValue -type 'REG_MULTI_SZ'
            } elseif ($rawValue.StartsWith('hex(b):')) {
                $registryValueType = 'REG_QWORD'
                $registryValue = Convert-RegistryHexToValue -hexString $rawValue -type 'REG_QWORD'
            } else {
                $registryValueType = 'REG_SZ'
                $registryValue = $rawValue
            }

            $entries.Add(
                [PSCustomObject](
                    [PSCustomObject]@{
                        RegistryHive      = $currentHive
                        RegistryKey       = $currentKey
                        RegistryKeyPath   = if ([string]::IsNullOrEmpty($currentKey)) { $currentHive } else { "$currentHive\$currentKey" }
                        RegistryValueName = $valueName
                        Value             = $registryValue
                        ValueType         = $registryValueType
                        Description       = $valueDescription
                        Delete            = $isDeletion
                    }
                )
            )
        }

        Write-Verbose " Parsed $($entries.Count) registry value(s)"

        $output = [ordered]@{
            Name        = $resolvedName
            Description = $resolvedDescription
            Enabled     = $Enabled
            RunOnce     = $RunOnce
            Entries     = $entries.ToArray()
            SourceFile  = $SourceFile
        }

        if ($ExportFor -eq 'AppVentiX') {
            $registryEntries = @(
                foreach ($entry in $entries) {
                    $deleteKey = $false
                    if ($entry.Delete -eq $true -and [string]::IsNullOrEmpty($entry.RegistryValueName)) {
                        $deleteKey = $true
                    }
                    $valueType = $entry.ValueType
                    if ([string]::IsNullOrEmpty(($valueType))) {
                        $valueType = 'REG_SZ'
                    }

                    [PSCustomObject]@{
                        RootKey   = $entry.RegistryHive
                        KeyPath   = $entry.RegistryKey
                        ValueName = $entry.RegistryValueName
                        ValueData = $entry.Value
                        ValueType = $valueType
                        Action    = if ($entry.Delete) { 'Remove' } else { 'Set' }
                        DeleteKey = $deleteKey
                    }

                }
            )
            $output.AppVentiXAssignments = @($Assignments | Select-Object Sid, Name, Type, DomainFQDN)
            $output.AppVentiXParams = (
                [ordered]@{
                    FriendlyName    = $resolvedName
                    Description     = $resolvedDescription
                    RegistryEntries = @($registryEntries)
                }
            )
        } elseif ($ExportFor -eq 'WEM') {
            Write-Warning "WEM export format is not implemented yet for registry sets!"
        }

        [PSCustomObject]$output
    }

    #endregion

    $absolutePath = (Resolve-Path -LiteralPath $Path).Path
    $extension = [System.IO.Path]::GetExtension($absolutePath).ToLower()

    if ($extension -eq '.xml') {
        # Building Block XML — extract all <registry type="registry"> nodes
        Write-Verbose "Loading Building Block XML: $absolutePath"
        try {
            $xmlContent = New-Object System.Xml.XmlDocument
            $xmlContent.Load($absolutePath)
        } catch {
            $PSCmdlet.ThrowTerminatingError(
                [System.Management.Automation.ErrorRecord]::new(
                    [System.Xml.XmlException]::new("Failed to parse XML file '$absolutePath': $($_.Exception.Message)"),
                    'XmlParseError',
                    [System.Management.Automation.ErrorCategory]::InvalidData,
                    $absolutePath
                )
            )
        }

        $registryNodes = @($xmlContent.GetElementsByTagName('registry') | Where-Object { $_.type -eq 'registry' })
        Write-Verbose "Found $($registryNodes.Count) registry set node(s)"

        if ($registryNodes.Count -eq 0) {
            Write-Warning "No <registry type='registry'> nodes found in '$absolutePath'"
            return
        }

        foreach ($node in $registryNodes) {
            $nodeName = $node.name
            $nodeDescription = $node.description
            $decodedContent = (Convert-HexToString -HexString $node.registryfile).TrimStart([char]0xFEFF)
            if ($node.enabled -ieq "yes") {
                $enabled = $true
            } else {
                $enabled = $false
            }
            if ($node.runonce -ieq "yes") {
                $runOnce = $true
            } else {
                $runOnce = $false
            }
            Write-Verbose "Processing registry set: '$nodeName'"
            $Assignments = @(ConvertFrom-IvantiAccessControl -AccessControl $node.accesscontrol -IWCComponentName $nodeName -IWCComponent "Policy")

            ConvertTo-RegistryEntries -Name $nodeName -Description $nodeDescription -RunOnce $runOnce -RegContent $decodedContent -SourceFile $absolutePath -ExportFor $ExportFor -Assignments $Assignments
        }

    } else {
        # Standalone .reg file
        Write-Verbose "Loading .reg file: $absolutePath"
        try {
            $rawContent = Get-Content -LiteralPath $absolutePath -Raw -ErrorAction Stop
        } catch {
            $PSCmdlet.ThrowTerminatingError(
                [System.Management.Automation.ErrorRecord]::new(
                    [System.IO.IOException]::new("Failed to read file '$absolutePath': $($_.Exception.Message)"),
                    'RegFileReadError',
                    [System.Management.Automation.ErrorCategory]::ReadError,
                    $absolutePath
                )
            )
        }
        ConvertTo-RegistryEntries -Name '' -Description '' -RegContent $rawContent -SourceFile $absolutePath -ExportFor $ExportFor
    }
}

# SIG # Begin signature block
# MIImdwYJKoZIhvcNAQcCoIImaDCCJmQCAQExDzANBglghkgBZQMEAgEFADB5Bgor
# BgEEAYI3AgEEoGswaTA0BgorBgEEAYI3AgEeMCYCAwEAAAQQH8w7YFlLCE63JNLG
# KX7zUQIBAAIBAAIBAAIBAAIBADAxMA0GCWCGSAFlAwQCAQUABCB4GjVcwK8Xe1J4
# N4GEaE9cBZ4zAjIXzYmLcMvb3LoCEqCCIAowggYUMIID/KADAgECAhB6I67aU2mW
# D5HIPlz0x+M/MA0GCSqGSIb3DQEBDAUAMFcxCzAJBgNVBAYTAkdCMRgwFgYDVQQK
# Ew9TZWN0aWdvIExpbWl0ZWQxLjAsBgNVBAMTJVNlY3RpZ28gUHVibGljIFRpbWUg
# U3RhbXBpbmcgUm9vdCBSNDYwHhcNMjEwMzIyMDAwMDAwWhcNMzYwMzIxMjM1OTU5
# WjBVMQswCQYDVQQGEwJHQjEYMBYGA1UEChMPU2VjdGlnbyBMaW1pdGVkMSwwKgYD
# VQQDEyNTZWN0aWdvIFB1YmxpYyBUaW1lIFN0YW1waW5nIENBIFIzNjCCAaIwDQYJ
# KoZIhvcNAQEBBQADggGPADCCAYoCggGBAM2Y2ENBq26CK+z2M34mNOSJjNPvIhKA
# VD7vJq+MDoGD46IiM+b83+3ecLvBhStSVjeYXIjfa3ajoW3cS3ElcJzkyZlBnwDE
# JuHlzpbN4kMH2qRBVrjrGJgSlzzUqcGQBaCxpectRGhhnOSwcjPMI3G0hedv2eNm
# GiUbD12OeORN0ADzdpsQ4dDi6M4YhoGE9cbY11XxM2AVZn0GiOUC9+XE0wI7CQKf
# OUfigLDn7i/WeyxZ43XLj5GVo7LDBExSLnh+va8WxTlA+uBvq1KO8RSHUQLgzb1g
# bL9Ihgzxmkdp2ZWNuLc+XyEmJNbD2OIIq/fWlwBp6KNL19zpHsODLIsgZ+WZ1AzC
# s1HEK6VWrxmnKyJJg2Lv23DlEdZlQSGdF+z+Gyn9/CRezKe7WNyxRf4e4bwUtrYE
# 2F5Q+05yDD68clwnweckKtxRaF0VzN/w76kOLIaFVhf5sMM/caEZLtOYqYadtn03
# 4ykSFaZuIBU9uCSrKRKTPJhWvXk4CllgrwIDAQABo4IBXDCCAVgwHwYDVR0jBBgw
# FoAU9ndq3T/9ARP/FqFsggIv0Ao9FCUwHQYDVR0OBBYEFF9Y7UwxeqJhQo1SgLqz
# YZcZojKbMA4GA1UdDwEB/wQEAwIBhjASBgNVHRMBAf8ECDAGAQH/AgEAMBMGA1Ud
# JQQMMAoGCCsGAQUFBwMIMBEGA1UdIAQKMAgwBgYEVR0gADBMBgNVHR8ERTBDMEGg
# P6A9hjtodHRwOi8vY3JsLnNlY3RpZ28uY29tL1NlY3RpZ29QdWJsaWNUaW1lU3Rh
# bXBpbmdSb290UjQ2LmNybDB8BggrBgEFBQcBAQRwMG4wRwYIKwYBBQUHMAKGO2h0
# dHA6Ly9jcnQuc2VjdGlnby5jb20vU2VjdGlnb1B1YmxpY1RpbWVTdGFtcGluZ1Jv
# b3RSNDYucDdjMCMGCCsGAQUFBzABhhdodHRwOi8vb2NzcC5zZWN0aWdvLmNvbTAN
# BgkqhkiG9w0BAQwFAAOCAgEAEtd7IK0ONVgMnoEdJVj9TC1ndK/HYiYh9lVUacah
# RoZ2W2hfiEOyQExnHk1jkvpIJzAMxmEc6ZvIyHI5UkPCbXKspioYMdbOnBWQUn73
# 3qMooBfIghpR/klUqNxx6/fDXqY0hSU1OSkkSivt51UlmJElUICZYBodzD3M/SFj
# eCP59anwxs6hwj1mfvzG+b1coYGnqsSz2wSKr+nDO+Db8qNcTbJZRAiSazr7KyUJ
# Go1c+MScGfG5QHV+bps8BX5Oyv9Ct36Y4Il6ajTqV2ifikkVtB3RNBUgwu/mSiSU
# ice/Jp/q8BMk/gN8+0rNIE+QqU63JoVMCMPY2752LmESsRVVoypJVt8/N3qQ1c6F
# ibbcRabo3azZkcIdWGVSAdoLgAIxEKBeNh9AQO1gQrnh1TA8ldXuJzPSuALOz1Uj
# b0PCyNVkWk7hkhVHfcvBfI8NtgWQupiaAeNHe0pWSGH2opXZYKYG4Lbukg7HpNi/
# KqJhue2Keak6qH9A8CeEOB7Eob0Zf+fU+CCQaL0cJqlmnx9HCDxF+3BLbUufrV64
# EbTI40zqegPZdA+sXCmbcZy6okx/SjwsusWRItFA3DE8MORZeFb6BmzBtqKJ7l93
# 9bbKBy2jvxcJI98Va95Q5JnlKor3m0E7xpMeYRriWklUPsetMSf2NvUQa/E5vVye
# fQIwggZFMIIELaADAgECAhAIMk+dt9qRb2Pk8qM8Xl1RMA0GCSqGSIb3DQEBCwUA
# MFYxCzAJBgNVBAYTAlBMMSEwHwYDVQQKExhBc3NlY28gRGF0YSBTeXN0ZW1zIFMu
# QS4xJDAiBgNVBAMTG0NlcnR1bSBDb2RlIFNpZ25pbmcgMjAyMSBDQTAeFw0yNDA0
# MDQxNDA0MjRaFw0yNzA0MDQxNDA0MjNaMGsxCzAJBgNVBAYTAk5MMRIwEAYDVQQH
# DAlTY2hpam5kZWwxIzAhBgNVBAoMGkpvaG4gQmlsbGVrZW5zIENvbnN1bHRhbmN5
# MSMwIQYDVQQDDBpKb2huIEJpbGxla2VucyBDb25zdWx0YW5jeTCCAaIwDQYJKoZI
# hvcNAQEBBQADggGPADCCAYoCggGBAMslntDbSQwHZXwFhmibivbnd0Qfn6sqe/6f
# os3pKzKxEsR907RkDMet2x6RRg3eJkiIr3TFPwqBooyXXgK3zxxpyhGOcuIqyM9J
# 28DVf4kUyZHsjGO/8HFjrr3K1hABNUszP0o7H3o6J31eqV1UmCXYhQlNoW9FOmRC
# 1amlquBmh7w4EKYEytqdmdOBavAD5Xq4vLPxNP6kyA+B2YTtk/xM27TghtbwFGKn
# u9Vwnm7dFcpLxans4ONt2OxDQOMA5NwgcUv/YTpjhq9qoz6ivG55NRJGNvUXsM3w
# 2o7dR6Xh4MuEGrTSrOWGg2A5EcLH1XqQtkF5cZnAPM8W/9HUp8ggornWnFVQ9/6M
# ga+ermy5wy5XrmQpN+x3u6tit7xlHk1Hc+4XY4a4ie3BPXG2PhJhmZAn4ebNSBwN
# Hh8z7WTT9X9OFERepGSytZVeEP7hgyptSLcuhpwWeR4QdBb7dV++4p3PsAUQVHFp
# wkSbrRTv4EiJ0Lcz9P1HPGFoHiFAQQIDAQABo4IBeDCCAXQwDAYDVR0TAQH/BAIw
# ADA9BgNVHR8ENjA0MDKgMKAuhixodHRwOi8vY2NzY2EyMDIxLmNybC5jZXJ0dW0u
# cGwvY2NzY2EyMDIxLmNybDBzBggrBgEFBQcBAQRnMGUwLAYIKwYBBQUHMAGGIGh0
# dHA6Ly9jY3NjYTIwMjEub2NzcC1jZXJ0dW0uY29tMDUGCCsGAQUFBzAChilodHRw
# Oi8vcmVwb3NpdG9yeS5jZXJ0dW0ucGwvY2NzY2EyMDIxLmNlcjAfBgNVHSMEGDAW
# gBTddF1MANt7n6B0yrFu9zzAMsBwzTAdBgNVHQ4EFgQUO6KtBpOBgmrlANVAnyiQ
# C6W6lJwwSwYDVR0gBEQwQjAIBgZngQwBBAEwNgYLKoRoAYb2dwIFAQQwJzAlBggr
# BgEFBQcCARYZaHR0cHM6Ly93d3cuY2VydHVtLnBsL0NQUzATBgNVHSUEDDAKBggr
# BgEFBQcDAzAOBgNVHQ8BAf8EBAMCB4AwDQYJKoZIhvcNAQELBQADggIBAEQsN8wg
# PMdWVkwHPPTN+jKpdns5AKVFjcn00psf2NGVVgWWNQBIQc9lEuTBWb54IK6Ga3hx
# QRZfnPNo5HGl73YLmFgdFQrFzZ1lnaMdIcyh8LTWv6+XNWfoyCM9wCp4zMIDPOs8
# LKSMQqA/wRgqiACWnOS4a6fyd5GUIAm4CuaptpFYr90l4Dn/wAdXOdY32UhgzmSu
# xpUbhD8gVJUaBNVmQaRqeU8y49MxiVrUKJXde1BCrtR9awXbqembc7Nqvmi60tYK
# lD27hlpKtj6eGPjkht0hHEsgzU0Fxw7ZJghYG2wXfpF2ziN893ak9Mi/1dmCNmor
# GOnybKYfT6ff6YTCDDNkod4egcMZdOSv+/Qv+HAeIgEvrxE9QsGlzTwbRtbm6gwY
# YcVBs/SsVUdBn/TSB35MMxRhHE5iC3aUTkDbceo/XP3uFhVL4g2JZHpFfCSu2TQr
# rzRn2sn07jfMvzeHArCOJgBW1gPqR3WrJ4hUxL06Rbg1gs9tU5HGGz9KNQMfQFQ7
# 0Wz7UIhezGcFcRfkIfSkMmQYYpsc7rfzj+z0ThfDVzzJr2dMOFsMlfj1T6l22GBq
# 9XQx0A4lcc5Fl9pRxbOuHHWFqIBD/BCEhwniOCySzqENd2N+oz8znKooSISStnkN
# aYXt6xblJF2dx9Dn89FK7d1IquNxOwt0tI5dMIIGYjCCBMqgAwIBAgIRAKQpO24e
# 3denNAiHrXpOtyQwDQYJKoZIhvcNAQEMBQAwVTELMAkGA1UEBhMCR0IxGDAWBgNV
# BAoTD1NlY3RpZ28gTGltaXRlZDEsMCoGA1UEAxMjU2VjdGlnbyBQdWJsaWMgVGlt
# ZSBTdGFtcGluZyBDQSBSMzYwHhcNMjUwMzI3MDAwMDAwWhcNMzYwMzIxMjM1OTU5
# WjByMQswCQYDVQQGEwJHQjEXMBUGA1UECBMOV2VzdCBZb3Jrc2hpcmUxGDAWBgNV
# BAoTD1NlY3RpZ28gTGltaXRlZDEwMC4GA1UEAxMnU2VjdGlnbyBQdWJsaWMgVGlt
# ZSBTdGFtcGluZyBTaWduZXIgUjM2MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIIC
# CgKCAgEA04SV9G6kU3jyPRBLeBIHPNyUgVNnYayfsGOyYEXrn3+SkDYTLs1crcw/
# ol2swE1TzB2aR/5JIjKNf75QBha2Ddj+4NEPKDxHEd4dEn7RTWMcTIfm492TW22I
# 8LfH+A7Ehz0/safc6BbsNBzjHTt7FngNfhfJoYOrkugSaT8F0IzUh6VUwoHdYDpi
# ln9dh0n0m545d5A5tJD92iFAIbKHQWGbCQNYplqpAFasHBn77OqW37P9BhOASdmj
# p3IijYiFdcA0WQIe60vzvrk0HG+iVcwVZjz+t5OcXGTcxqOAzk1frDNZ1aw8nFhG
# EvG0ktJQknnJZE3D40GofV7O8WzgaAnZmoUn4PCpvH36vD4XaAF2CjiPsJWiY/j2
# xLsJuqx3JtuI4akH0MmGzlBUylhXvdNVXcjAuIEcEQKtOBR9lU4wXQpISrbOT8ux
# +96GzBq8TdbhoFcmYaOBZKlwPP7pOp5Mzx/UMhyBA93PQhiCdPfIVOCINsUY4U23
# p4KJ3F1HqP3H6Slw3lHACnLilGETXRg5X/Fp8G8qlG5Y+M49ZEGUp2bneRLZoyHT
# yynHvFISpefhBCV0KdRZHPcuSL5OAGWnBjAlRtHvsMBrI3AAA0Tu1oGvPa/4yeei
# Ayu+9y3SLC98gDVbySnXnkujjhIh+oaatsk/oyf5R2vcxHahajMCAwEAAaOCAY4w
# ggGKMB8GA1UdIwQYMBaAFF9Y7UwxeqJhQo1SgLqzYZcZojKbMB0GA1UdDgQWBBSI
# YYyhKjdkgShgoZsx0Iz9LALOTzAOBgNVHQ8BAf8EBAMCBsAwDAYDVR0TAQH/BAIw
# ADAWBgNVHSUBAf8EDDAKBggrBgEFBQcDCDBKBgNVHSAEQzBBMDUGDCsGAQQBsjEB
# AgEDCDAlMCMGCCsGAQUFBwIBFhdodHRwczovL3NlY3RpZ28uY29tL0NQUzAIBgZn
# gQwBBAIwSgYDVR0fBEMwQTA/oD2gO4Y5aHR0cDovL2NybC5zZWN0aWdvLmNvbS9T
# ZWN0aWdvUHVibGljVGltZVN0YW1waW5nQ0FSMzYuY3JsMHoGCCsGAQUFBwEBBG4w
# bDBFBggrBgEFBQcwAoY5aHR0cDovL2NydC5zZWN0aWdvLmNvbS9TZWN0aWdvUHVi
# bGljVGltZVN0YW1waW5nQ0FSMzYuY3J0MCMGCCsGAQUFBzABhhdodHRwOi8vb2Nz
# cC5zZWN0aWdvLmNvbTANBgkqhkiG9w0BAQwFAAOCAYEAAoE+pIZyUSH5ZakuPVKK
# 4eWbzEsTRJOEjbIu6r7vmzXXLpJx4FyGmcqnFZoa1dzx3JrUCrdG5b//LfAxOGy9
# Ph9JtrYChJaVHrusDh9NgYwiGDOhyyJ2zRy3+kdqhwtUlLCdNjFjakTSE+hkC9F5
# ty1uxOoQ2ZkfI5WM4WXA3ZHcNHB4V42zi7Jk3ktEnkSdViVxM6rduXW0jmmiu71Z
# pBFZDh7Kdens+PQXPgMqvzodgQJEkxaION5XRCoBxAwWwiMm2thPDuZTzWp/gUFz
# i7izCmEt4pE3Kf0MOt3ccgwn4Kl2FIcQaV55nkjv1gODcHcD9+ZVjYZoyKTVWb4V
# qMQy/j8Q3aaYd/jOQ66Fhk3NWbg2tYl5jhQCuIsE55Vg4N0DUbEWvXJxtxQQaVR5
# xzhEI+BjJKzh3TQ026JxHhr2fuJ0mV68AluFr9qshgwS5SpN5FFtaSEnAwqZv3IS
# +mlG50rK7W3qXbWwi4hmpylUfygtYLEdLQukNEX1jiOKMIIGgjCCBGqgAwIBAgIQ
# NsKwvXwbOuejs902y8l1aDANBgkqhkiG9w0BAQwFADCBiDELMAkGA1UEBhMCVVMx
# EzARBgNVBAgTCk5ldyBKZXJzZXkxFDASBgNVBAcTC0plcnNleSBDaXR5MR4wHAYD
# VQQKExVUaGUgVVNFUlRSVVNUIE5ldHdvcmsxLjAsBgNVBAMTJVVTRVJUcnVzdCBS
# U0EgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkwHhcNMjEwMzIyMDAwMDAwWhcNMzgw
# MTE4MjM1OTU5WjBXMQswCQYDVQQGEwJHQjEYMBYGA1UEChMPU2VjdGlnbyBMaW1p
# dGVkMS4wLAYDVQQDEyVTZWN0aWdvIFB1YmxpYyBUaW1lIFN0YW1waW5nIFJvb3Qg
# UjQ2MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAiJ3YuUVnnR3d6Lkm
# gZpUVMB8SQWbzFoVD9mUEES0QUCBdxSZqdTkdizICFNeINCSJS+lV1ipnW5ihkQy
# C0cRLWXUJzodqpnMRs46npiJPHrfLBOifjfhpdXJ2aHHsPHggGsCi7uE0awqKggE
# /LkYw3sqaBia67h/3awoqNvGqiFRJ+OTWYmUCO2GAXsePHi+/JUNAax3kpqstbl3
# vcTdOGhtKShvZIvjwulRH87rbukNyHGWX5tNK/WABKf+Gnoi4cmisS7oSimgHUI0
# Wn/4elNd40BFdSZ1EwpuddZ+Wr7+Dfo0lcHflm/FDDrOJ3rWqauUP8hsokDoI7D/
# yUVI9DAE/WK3Jl3C4LKwIpn1mNzMyptRwsXKrop06m7NUNHdlTDEMovXAIDGAvYy
# nPt5lutv8lZeI5w3MOlCybAZDpK3Dy1MKo+6aEtE9vtiTMzz/o2dYfdP0KWZwZIX
# bYsTIlg1YIetCpi5s14qiXOpRsKqFKqav9R1R5vj3NgevsAsvxsAnI8Oa5s2oy25
# qhsoBIGo/zi6GpxFj+mOdh35Xn91y72J4RGOJEoqzEIbW3q0b2iPuWLA911cRxgY
# 5SJYubvjay3nSMbBPPFsyl6mY4/WYucmyS9lo3l7jk27MAe145GWxK4O3m3gEFEI
# kv7kRmefDR7Oe2T1HxAnICQvr9sCAwEAAaOCARYwggESMB8GA1UdIwQYMBaAFFN5
# v1qqK0rPVIDh2JvAnfKyA2bLMB0GA1UdDgQWBBT2d2rdP/0BE/8WoWyCAi/QCj0U
# JTAOBgNVHQ8BAf8EBAMCAYYwDwYDVR0TAQH/BAUwAwEB/zATBgNVHSUEDDAKBggr
# BgEFBQcDCDARBgNVHSAECjAIMAYGBFUdIAAwUAYDVR0fBEkwRzBFoEOgQYY/aHR0
# cDovL2NybC51c2VydHJ1c3QuY29tL1VTRVJUcnVzdFJTQUNlcnRpZmljYXRpb25B
# dXRob3JpdHkuY3JsMDUGCCsGAQUFBwEBBCkwJzAlBggrBgEFBQcwAYYZaHR0cDov
# L29jc3AudXNlcnRydXN0LmNvbTANBgkqhkiG9w0BAQwFAAOCAgEADr5lQe1oRLjl
# ocXUEYfktzsljOt+2sgXke3Y8UPEooU5y39rAARaAdAxUeiX1ktLJ3+lgxtoLQhn
# 5cFb3GF2SSZRX8ptQ6IvuD3wz/LNHKpQ5nX8hjsDLRhsyeIiJsms9yAWnvdYOdEM
# q1W61KE9JlBkB20XBee6JaXx4UBErc+YuoSb1SxVf7nkNtUjPfcxuFtrQdRMRi/f
# InV/AobE8Gw/8yBMQKKaHt5eia8ybT8Y/Ffa6HAJyz9gvEOcF1VWXG8OMeM7Vy7B
# s6mSIkYeYtddU1ux1dQLbEGur18ut97wgGwDiGinCwKPyFO7ApcmVJOtlw9FVJxw
# /mL1TbyBns4zOgkaXFnnfzg4qbSvnrwyj1NiurMp4pmAWjR+Pb/SIduPnmFzbSN/
# G8reZCL4fvGlvPFk4Uab/JVCSmj59+/mB2Gn6G/UYOy8k60mKcmaAZsEVkhOFuoj
# 4we8CYyaR9vd9PGZKSinaZIkvVjbH/3nlLb0a7SBIkiRzfPfS9T+JesylbHa1LtR
# V9U/7m0q7Ma2CQ/t392ioOssXW7oKLdOmMBl14suVFBmbzrt5V5cQPnwtd3UOTpS
# 9oCG+ZZheiIvPgkDmA8FzPsnfXW5qHELB43ET7HHFHeRPRYrMBKjkb8/IN7Po0d0
# hQoF4TeMM+zYAJzoKQnVKOLg8pZVPT8wgga5MIIEoaADAgECAhEAmaOACiZVO2Wr
# 3G6EprPqOTANBgkqhkiG9w0BAQwFADCBgDELMAkGA1UEBhMCUEwxIjAgBgNVBAoT
# GVVuaXpldG8gVGVjaG5vbG9naWVzIFMuQS4xJzAlBgNVBAsTHkNlcnR1bSBDZXJ0
# aWZpY2F0aW9uIEF1dGhvcml0eTEkMCIGA1UEAxMbQ2VydHVtIFRydXN0ZWQgTmV0
# d29yayBDQSAyMB4XDTIxMDUxOTA1MzIxOFoXDTM2MDUxODA1MzIxOFowVjELMAkG
# A1UEBhMCUEwxITAfBgNVBAoTGEFzc2VjbyBEYXRhIFN5c3RlbXMgUy5BLjEkMCIG
# A1UEAxMbQ2VydHVtIENvZGUgU2lnbmluZyAyMDIxIENBMIICIjANBgkqhkiG9w0B
# AQEFAAOCAg8AMIICCgKCAgEAnSPPBDAjO8FGLOczcz5jXXp1ur5cTbq96y34vuTm
# flN4mSAfgLKTvggv24/rWiVGzGxT9YEASVMw1Aj8ewTS4IndU8s7VS5+djSoMcbv
# IKck6+hI1shsylP4JyLvmxwLHtSworV9wmjhNd627h27a8RdrT1PH9ud0IF+njvM
# k2xqbNTIPsnWtw3E7DmDoUmDQiYi/ucJ42fcHqBkbbxYDB7SYOouu9Tj1yHIohzu
# C8KNqfcYf7Z4/iZgkBJ+UFNDcc6zokZ2uJIxWgPWXMEmhu1gMXgv8aGUsRdaCtVD
# 2bSlbfsq7BiqljjaCun+RJgTgFRCtsuAEw0pG9+FA+yQN9n/kZtMLK+Wo837Q4QO
# ZgYqVWQ4x6cM7/G0yswg1ElLlJj6NYKLw9EcBXE7TF3HybZtYvj9lDV2nT8mFSkc
# SkAExzd4prHwYjUXTeZIlVXqj+eaYqoMTpMrfh5MCAOIG5knN4Q/JHuurfTI5XDY
# O962WZayx7ACFf5ydJpoEowSP07YaBiQ8nXpDkNrUA9g7qf/rCkKbWpQ5boufUnq
# 1UiYPIAHlezf4muJqxqIns/kqld6JVX8cixbd6PzkDpwZo4SlADaCi2JSplKShBS
# ND36E/ENVv8urPS0yOnpG4tIoBGxVCARPCg1BnyMJ4rBJAcOSnAWd18Jx5n858JS
# qPECAwEAAaOCAVUwggFRMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFN10XUwA
# 23ufoHTKsW73PMAywHDNMB8GA1UdIwQYMBaAFLahVDkCw6A/joq8+tT4HKbROg79
# MA4GA1UdDwEB/wQEAwIBBjATBgNVHSUEDDAKBggrBgEFBQcDAzAwBgNVHR8EKTAn
# MCWgI6Ahhh9odHRwOi8vY3JsLmNlcnR1bS5wbC9jdG5jYTIuY3JsMGwGCCsGAQUF
# BwEBBGAwXjAoBggrBgEFBQcwAYYcaHR0cDovL3N1YmNhLm9jc3AtY2VydHVtLmNv
# bTAyBggrBgEFBQcwAoYmaHR0cDovL3JlcG9zaXRvcnkuY2VydHVtLnBsL2N0bmNh
# Mi5jZXIwOQYDVR0gBDIwMDAuBgRVHSAAMCYwJAYIKwYBBQUHAgEWGGh0dHA6Ly93
# d3cuY2VydHVtLnBsL0NQUzANBgkqhkiG9w0BAQwFAAOCAgEAdYhYD+WPUCiaU58Q
# 7EP89DttyZqGYn2XRDhJkL6P+/T0IPZyxfxiXumYlARMgwRzLRUStJl490L94C9L
# GF3vjzzH8Jq3iR74BRlkO18J3zIdmCKQa5LyZ48IfICJTZVJeChDUyuQy6rGDxLU
# UAsO0eqeLNhLVsgw6/zOfImNlARKn1FP7o0fTbj8ipNGxHBIutiRsWrhWM2f8pXd
# d3x2mbJCKKtl2s42g9KUJHEIiLni9ByoqIUul4GblLQigO0ugh7bWRLDm0CdY9rN
# LqyA3ahe8WlxVWkxyrQLjH8ItI17RdySaYayX3PhRSC4Am1/7mATwZWwSD+B7eMc
# ZNhpn8zJ+6MTyE6YoEBSRVrs0zFFIHUR08Wk0ikSf+lIe5Iv6RY3/bFAEloMU+vU
# BfSouCReZwSLo8WdrDlPXtR0gicDnytO7eZ5827NS2x7gCBibESYkOh1/w1tVxTp
# V2Na3PR7nxYVlPu1JPoRZCbH86gc96UTvuWiOruWmyOEMLOGGniR+x+zPF/2DaGg
# K2W1eEJfo2qyrBNPvF7wuAyQfiFXLwvWHamoYtPZo0LHuH8X3n9C+xN4YaNjt2yw
# zOr+tKyEVAotnyU9vyEVOaIYMk3IeBrmFnn0gbKeTTyYeEEUz/Qwt4HOUBCrW602
# NCmvO1nm+/80nLy5r0AZvCQxaQ4xggXDMIIFvwIBATBqMFYxCzAJBgNVBAYTAlBM
# MSEwHwYDVQQKExhBc3NlY28gRGF0YSBTeXN0ZW1zIFMuQS4xJDAiBgNVBAMTG0Nl
# cnR1bSBDb2RlIFNpZ25pbmcgMjAyMSBDQQIQCDJPnbfakW9j5PKjPF5dUTANBglg
# hkgBZQMEAgEFAKCBhDAYBgorBgEEAYI3AgEMMQowCKACgAChAoAAMBkGCSqGSIb3
# DQEJAzEMBgorBgEEAYI3AgEEMBwGCisGAQQBgjcCAQsxDjAMBgorBgEEAYI3AgEV
# MC8GCSqGSIb3DQEJBDEiBCBPkbpjIMiHhv3ekzplHkeXmVF3hC1rAdiOfhq6uowT
# izANBgkqhkiG9w0BAQEFAASCAYAZOh8S9vzjbCHWT0GHuZ0Ql5BXrEDkYLHFIvJj
# JAwzTdW3gSpKVceyhV36ThXTEV+S4XeVih6KY2xluEjgUA2/ZGYG3xnbg15mjiUm
# mTqCbyGNIo9Uc0Jk2+yFF+JNqOgK7lF3dbVn3+2G8eHVQxp/+14X51r26xp5eG5h
# QE+SXddbZd31JQlnv6mTCuOW3Fdb0ZBD17Ko4ayBB+aux0hwcHprUfgYzdSkG2dj
# D9JV8pmECmMQ/amEH0KOsfi0g2cmTKf6y7kTrC1j9n+7Q7ALvMsRJ0oZGoH6N/sD
# AQIIruu/wYdGfHaB95YoZXuxVJwe24BLMcgb7cYQ4lEU2vzz00H6UIsgSYOqb09d
# jEvuBb7qwFemrW9GLqYEkbwvTrjGl3t0kOq8p95+A51yCZ2cJ3dlabLLoYI0jC9P
# 43GM4LFe6MY25GtgVmy31zoSHCCri4VcFzMW+tv0hr1VlowO7vGUmb4No+Y9AyLQ
# eSEpG/8Vd3+XvMxAFckyIzfGQX+hggMjMIIDHwYJKoZIhvcNAQkGMYIDEDCCAwwC
# AQEwajBVMQswCQYDVQQGEwJHQjEYMBYGA1UEChMPU2VjdGlnbyBMaW1pdGVkMSww
# KgYDVQQDEyNTZWN0aWdvIFB1YmxpYyBUaW1lIFN0YW1waW5nIENBIFIzNgIRAKQp
# O24e3denNAiHrXpOtyQwDQYJYIZIAWUDBAICBQCgeTAYBgkqhkiG9w0BCQMxCwYJ
# KoZIhvcNAQcBMBwGCSqGSIb3DQEJBTEPFw0yNjA2MTYxODMyNDZaMD8GCSqGSIb3
# DQEJBDEyBDCIF6oz4pUHjAKvvtu5mJu63tv2HbvaCFNB9ltxFAbnDJBhUOdrXM6B
# xHwuvoYSTzcwDQYJKoZIhvcNAQEBBQAEggIAygTjy+FE52ij4kuZ6wVGkADcSqM0
# AbofgEn/GoNb1ci172sG3h2BzNCocA51Ww5aqjnDMtHadXh1IH8kFXPdPpYKLmd6
# 5yQbxeJSoVI83TX+Hyhmif/8HaxLwrdzELuAbmyI9xCWUFEoR5xl7rTeaSBu+JHZ
# QbvmAz11jHwzFyEufbsjKU0WIp6TZJ+3O0lZBkaIr+6JgIvEQxXJypyp0K58VXee
# vEv0bkKQms2g20LMGuSxGUfqRvX1pyibo8OXpKEw3F6eLo51UyQFtJAhUsYq1Exs
# bnMRbxgiKwz4K39f5BQnMVaMQCeXwq1aSLzBl19wjAoehU2H/Oiw4lA6gZFOcsOy
# hb4FSLWPeXhVBtWgBp2fKMouS0rvrlnxgZkbV7gguv3GAYxp8rfRTjF3yWqabwUZ
# JjY8kjuwigSLcpiPcwKlrExN5sg1+UzpEzWT9+s3+G60DYYfDYJYNfmmCbxHVy1J
# 7Kr1NOKAp3HaHL94YwQ5graXQ4JFmekU4DKEJ0kGcgYA8N/cbJ/BI6qIFoDvoM0p
# PAhtJygeX9bJm/Eo92d7V5cSJe2OEwobeAu9F/tXGPsl/HvJSVGNrSe/r68mtG4T
# qrqrobvOPov8TvfgWH5XSpTL5TWGnETJED9/IYnjyWIEAZyokMiLgKY9OjDGre2a
# C50siIGHefxd+m8=
# SIG # End signature block