Strapper.psm1

#Region '.\Classes\_setup.ps1' -1

$StrapperSession = [pscustomobject]@{
    LogPath = $null
    ErrorPath = $null
    WorkingPath = $null
    ScriptTitle = $null
    IsLoaded = $true
    IsElevated = $false
    LogsToDB = $true
    LogTable = $null
    DBPath = "$PSScriptRoot/Strapper.db"
    Platform = [System.Environment]::OSVersion.Platform
}

if ($MyInvocation.PSCommandPath) {
    $scriptObject = Get-Item -Path $MyInvocation.PSCommandPath
    $StrapperSession.WorkingPath = $($scriptObject.DirectoryName)
    $StrapperSession.LogPath = Join-Path $StrapperSession.WorkingPath "$($scriptObject.BaseName)-log.txt"
    $StrapperSession.ErrorPath = Join-Path $StrapperSession.WorkingPath "$($scriptObject.BaseName)-error.txt"
    $StrapperSession.ScriptTitle = $scriptObject.BaseName
    $StrapperSession.LogTable = "$($scriptObject.BaseName)_log"
} else {
    $StrapperSession.WorkingPath = (Get-Location).Path
    $currentDate = (Get-Date).ToString('yyyyMMdd')
    $StrapperSession.LogPath = Join-Path $StrapperSession.WorkingPath "$currentDate-log.txt"
    $StrapperSession.ErrorPath = Join-Path $StrapperSession.WorkingPath "$currentDate-error.txt"
    $StrapperSession.ScriptTitle = $currentDate
    $StrapperSession.LogTable = "$($currentDate)_log"
}

if ($StrapperSession.Platform -eq 'Win32NT') {
    $StrapperSession.IsElevated = (
        New-Object `
            -TypeName Security.Principal.WindowsPrincipal `
            -ArgumentList ([Security.Principal.WindowsIdentity]::GetCurrent())
    ).IsInRole([Security.Principal.WindowsBuiltinRole]::Administrator)
} else {
    $StrapperSession.IsElevated = $(id -u) -eq 0
}

if(!(Test-Path -LiteralPath $StrapperSession.DBPath)) {
    [System.Data.SQLite.SQLiteConnection]::CreateFile($StrapperSession.DBPath)
}

if($IsLinux -or $IsMacOS) {
    chmod 776 $StrapperSession.DBPath
} else {
    $dbPathAcl = Get-Acl -Path $StrapperSession.DBPath
    $worldGroupName = (New-Object System.Security.Principal.SecurityIdentifier('S-1-1-0')).Translate([System.Security.Principal.NTAccount]).Value
    $fsar = [System.Security.AccessControl.FileSystemAccessRule]::new($worldGroupName, "FullControl", "Allow")
    $dbPathAcl.SetAccessRule($fsar)
    Set-Acl -Path $StrapperSession.DBPath -AclObject $dbPathAcl
}
Export-ModuleMember -Variable StrapperSession
#EndRegion '.\Classes\_setup.ps1' 54
#Region '.\Classes\StrapperLog.ps1' -1

enum StrapperLogLevel {
    Verbose = 0
    Debug = 1
    Information = 2
    Warning = 3
    Error = 4
    Fatal = 5
}

<#
.SYNOPSIS
    A class representing a log entry from the Strapper database.
.LINK
    https://github.com/ProVal-Tech/Strapper/blob/main/docs/StrapperLog.md
#>

class StrapperLog {
    [int]$Id
    [StrapperLogLevel]$Level
    [string]$Message
    [datetime]$Timestamp
}

#EndRegion '.\Classes\StrapperLog.ps1' 23
#Region '.\Public\Copy-RegistryItem.ps1' -1

function Copy-RegistryItem {
    <#
    .SYNOPSIS
        Copies a registry property or key to the target destination.
    .PARAMETER Path
        The path to the key to copy.
    .PARAMETER Destination
        The path the the key to copy to.
    .PARAMETER Name
        The name of the property to copy.
    .PARAMETER Recurse
        Recursively copy all subkeys from the target key path.
    .PARAMETER Force
        Create the destination key if it does not exist.
    .EXAMPLE
        Copy-RegistryItem -Path HKLM:\SOFTWARE\Canon -Destination HKLM:\SOFTWARE\_automation\RegistryBackup -Force -Recurse
        Copy all keys, subkeys, and properties from HKLM:\SOFTWARE\Canon to HKLM:\SOFTWARE\_automation\RegistryBackup
    .EXAMPLE
        Copy-RegistryItem -Path HKLM:\SOFTWARE\Adobe -Name PDFFormat -Destination HKLM:\SOFTWARE\_automation\RegistryBackup\Adobe -Force
        Copy the PDFFormat property from HKLM:\SOFTWARE\Adobe to HKLM:\SOFTWARE\_automation\RegistryBackup\Adobe
    #>

    [CmdletBinding()]
    [OutputType([Microsoft.Win32.RegistryKey])]
    param (
        [Parameter(ParameterSetName = 'Property')]
        [Parameter(ParameterSetName = 'Key')]
        [Parameter(Mandatory)][string]$Path,
        [Parameter(ParameterSetName = 'Property')]
        [Parameter(ParameterSetName = 'Key')]
        [Parameter(Mandatory)][string]$Destination,
        [Parameter(ParameterSetName = 'Property')]
        [string]$Name,
        [Parameter(ParameterSetName = 'Key')]
        [switch]$Recurse,
        [Parameter(ParameterSetName = 'Property')]
        [Parameter(ParameterSetName = 'Key')]
        [switch]$Force
    )

    if($StrapperSession.Platform -ne 'Win32NT') {
        Write-Error 'This function is only supported on Windows-based platforms.' -ErrorAction Stop
    }

    if ((Get-Item -Path ($Path -split '\\')[0]).GetType() -ne [Microsoft.Win32.RegistryKey]) {
        Write-Log -Level Error -Text 'The supplied path does not correlate to a registry key.'
        return $null
    } elseif ((Get-Item -Path ($Destination -split '\\')[0]).GetType() -ne [Microsoft.Win32.RegistryKey]) {
        Write-Log -Level Error -Text 'The supplied destination does not correlate to a registry key.'
        return $null
    } elseif (!(Test-Path -Path $Path)) {
        Write-Log -Level Error -Text "Path '$Path' does not exist."
        return $null
    } elseif (!(Test-Path -Path $Destination) -and $Force) {
        Write-Log -Level Error -Text "'$Destination' does not exist. Creating."
        New-Item -Path $Destination -Force | Out-Null
    } elseif (!(Test-Path -Path $Destination)) {
        Write-Log -Level Error -Text "Destination '$Destination' does not exist."
        return $null
    }

    if ($Name) {
        if (Copy-ItemProperty -Path $Path -Destination $Destination -Name $Name -PassThru) {
            return Get-Item -Path $Destination
        } else {
            Write-Log -Level Error -Message "An error occurred when writing the registry property: $($error[0].Exception.Message)"
        }
    } else {
        return Copy-Item -Path $Path -Destination $Destination -Recurse:$Recurse -PassThru
    }
}

#EndRegion '.\Public\Copy-RegistryItem.ps1' 72
#Region '.\Public\Get-RegistryHivePath.ps1' -1

function Get-RegistryHivePath {
    <#
    .SYNOPSIS
        Gets a list of registry hives from the local computer.
    .NOTES
        Bootstrap use only.
    .EXAMPLE
        Get-RegistryHivePath
        Returns the full list of registry hives.
    .PARAMETER ExcludeDefault
        Exclude the Default template hive from the return.
    #>

    [CmdletBinding()]
    [OutputType([PSCustomObject])]
    param (
        [Parameter(Mandatory = $false)][switch]$ExcludeDefault
    )
    if($StrapperSession.Platform -ne 'Win32NT') {
        Write-Error 'This function is only supported on Windows-based platforms.' -ErrorAction Stop
    }
    # Regex pattern for SIDs
    $patternSID = '((S-1-5-21)|(S-1-12-1))-\d+-\d+\-\d+\-\d+$'

    # Get Username, SID, and location of ntuser.dat for all users
    $profileList = @(
        Get-ItemProperty 'HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion\ProfileList\*' | Where-Object { $_.PSChildName -match $PatternSID } |
            Select-Object @{name = 'SID'; expression = { $_.PSChildName } },
            @{name = 'UserHive'; expression = { "$($_.ProfileImagePath)\ntuser.dat" } },
            @{name = 'Username'; expression = { (New-Object System.Security.Principal.SecurityIdentifier($_.PSChildName)).Translate([System.Security.Principal.NTAccount]).Value } }
    )

    # If the default user was not excluded, add it to the list of profiles to process.
    if (!$ExcludeDefault) {
        $profileList += [PSCustomObject]@{
            SID = 'DefaultUserTemplate'
            UserHive = "$env:SystemDrive\Users\Default\ntuser.dat"
            Username = 'DefaultUserTemplate'
        }
    }
    return $profileList
}

#EndRegion '.\Public\Get-RegistryHivePath.ps1' 43
#Region '.\Public\Get-StrapperWorkingPath.ps1' -1

function Get-StrapperWorkingPath {
    return $StrapperSession.WorkingPath
}

#EndRegion '.\Public\Get-StrapperWorkingPath.ps1' 5
#Region '.\Public\Get-UserRegistryKeyProperty.ps1' -1

function Get-UserRegistryKeyProperty {
    <#
    .SYNOPSIS
        Gets a list of existing user registry properties.
    .EXAMPLE
        Get-UserRegistryKeyProperty -Path "SOFTWARE\_automation\Prompter" -Name "Timestamp"
        Gets the Prompter Timestamp property from each available user's registry hive.
    .PARAMETER Path
        The relative registry path to the target property.
        Ex: To retrieve the property information for each user's Level property under the path HKEY_CURRENT_USER\SOFTWARE\7-Zip\Compression: pass "SOFTWARE\7-Zip\Compression"
    .PARAMETER Name
        The name of the property to target.
        Ex: To retrieve the property information for each user's Level property under the path HKEY_CURRENT_USER\SOFTWARE\7-Zip\Compression: pass "Level"
    #>

    [CmdletBinding()]
    [OutputType([PSCustomObject])]
    param (
        [Parameter(Mandatory = $true)][string]$Path,
        [Parameter(Mandatory = $false)][string]$Name = '(Default)'
    )

    if($StrapperSession.Platform -ne 'Win32NT') {
        Write-Error 'This function is only supported on Windows-based platforms.' -ErrorAction Stop
    }
    
    # Regex pattern for SIDs
    $patternSID = '((S-1-5-21)|(S-1-12-1))-\d+-\d+\-\d+\-\d+$'

    # Get Username, SID, and location of ntuser.dat for all users
    $profileList = Get-RegistryHivePath

    # Get all user SIDs found in HKEY_USERS (ntuser.dat files that are loaded)
    $loadedHives = Get-ChildItem Registry::HKEY_USERS | Where-Object { $_.PSChildname -match $PatternSID } | Select-Object @{name = 'SID'; expression = { $_.PSChildName } }

    # Get all user hives that are not currently logged in
    if ($LoadedHives) {
        $UnloadedHives = Compare-Object $ProfileList.SID $LoadedHives.SID | Select-Object @{name = 'SID'; expression = { $_.InputObject } }, UserHive, Username
    } else {
        $UnloadedHives = $ProfileList
    }

    $returnEntries = @(
        foreach ($profile in $ProfileList) {
            # Load user ntuser.dat if it's not already loaded
            if ($profile.SID -in $UnloadedHives.SID) {
                reg load HKU\$($profile.SID) $($profile.UserHive) | Out-Null
            }

            # Get the absolute path to the key for the currently iterated user.
            $propertyPath = "Registry::HKEY_USERS\$($profile.SID)\$Path"

            # Get the target registry entry
            $returnEntry = $null
            $returnEntry = Get-ItemProperty -Path $propertyPath -Name $Name -ErrorAction SilentlyContinue | Select-Object -ExpandProperty $Name

            # If the get was successful, then pass back a custom object that describes the registry entry.
            if ($null -ne $returnEntry) {
                [PSCustomObject]@{
                    Username = $profile.Username
                    SID = $profile.SID
                    Path = $propertyPath
                    Hive = $profile.UserHive
                    Name = $Name
                    Value = $returnEntry
                }
            }

            # Collect garbage and close ntuser.dat if the hive was initially unloaded
            if ($profile.SID -in $UnloadedHives.SID) {
                [gc]::Collect()
                reg unload HKU\$($profile.SID) | Out-Null
            }
        }
    )
    return $returnEntries
}

#EndRegion '.\Public\Get-UserRegistryKeyProperty.ps1' 78
#Region '.\Public\Get-WebFile.ps1' -1

function Get-WebFile {
    <#
    .SYNOPSIS
        Download a file from the internet.
    .EXAMPLE
        Get-WebFile -Uri 'https://static.wikia.nocookie.net/vocaloid/images/5/57/Miku_v4_bundle_art.png' -Path 'C:\Temp\miku.png'
        Download the target PNG to 'C:\Temp\miku.png'.
    .EXAMPLE
        Get-WebFile -Uri 'https://static.wikia.nocookie.net/vocaloid/images/5/57/Miku_v4_bundle_art.png' -Path 'C:\Temp\miku.png' -Clobber
        Download the target PNG to 'C:\Temp\miku.png', overwriting it if it exists.
    .EXAMPLE
        $mikuPath = Get-WebFile -Uri 'https://static.wikia.nocookie.net/vocaloid/images/5/57/Miku_v4_bundle_art.png' -Path 'C:\Temp\miku.png' -Clobber -PassThru
        Download the target PNG to 'C:\Temp\miku.png', overwriting it if it exists, and returning the FileInfo object.
    .PARAMETER Uri
        The URI to download the target file from.
    .PARAMETER Path
        The local path to save the file to.
    .PARAMETER Clobber
        Allow overwriting of an existing file.
    .PARAMETER PassThru
        Return a FileInfo object representing the downloaded file upon success.
    #>

    [CmdletBinding()]
    [OutputType([System.Void], ParameterSetName="NoPassThru")]
    [OutputType([System.IO.FileInfo], ParameterSetName="PassThru")]
    param (
        [Parameter(Mandatory, ParameterSetName='NoPassThru')]
        [Parameter(Mandatory, ParameterSetName='PassThru')]
        [System.Uri]$Uri,

        [Parameter(Mandatory, ParameterSetName='NoPassThru')]
        [Parameter(Mandatory, ParameterSetName='PassThru')]
        [System.IO.FileInfo]$Path,

        [Parameter(ParameterSetName='NoPassThru')]
        [Parameter(ParameterSetName='PassThru')]
        [switch]$Clobber,

        [Parameter(Mandatory, ParameterSetName='PassThru')]
        [switch]$PassThru
    )
    Write-Debug -Message "URI: $Uri"
    Write-Debug -Message "Target file: $($Path.FullName)"
    if ($Path.Exists -and !$Clobber) {
        Write-Error -Message "The file '$($Path.FullName)' exists. To overwrite this file, pass the -Clobber switch." -ErrorAction Stop
    }
    Write-Debug -Message 'Starting file download.'
    (New-Object System.Net.WebClient).DownloadFile($Uri, $Path.FullName)

    Write-Debug -Message 'Refreshing FileInfo object.'
    $path.Refresh()

    Write-Debug -Message 'Validating that file was downloaded.'
    if ($path.Exists) {
        Write-Debug -Message "Successfully downloaded '$Uri' to '$($Path.FullName)'"
        Write-Information -MessageData "Successfully downloaded '$Uri' to '$($Path.FullName)'"
        Write-Debug -Message 'Checking if PassThru was set.'
        if ($PassThru) {
            Write-Debug -Message 'PassThru set. Returning object.'
            return $Path
        }
    } else {
        Write-Error -Message "An error occurred and '$Uri' was unable to be downloaded." -ErrorAction Stop
    }
}

#EndRegion '.\Public\Get-WebFile.ps1' 67
#Region '.\Public\Install-Chocolatey.ps1' -1

function Install-Chocolatey {
    <#
    .SYNOPSIS
        Installs or updates the Chocolatey package manager.
    .EXAMPLE
        PS C:\> Install-Chocolatey
    #>

    if($StrapperSession.Platform -ne 'Win32NT') {
        Write-Error 'Chocolatey is only supported on Windows-based platforms. Use your better package manager instead. ;)' -ErrorAction Stop
    }
    if ($env:path -split ';' -notcontains ";$($env:ALLUSERSPROFILE)\chocolatey\bin") {
        $env:Path = $env:Path + ";$($env:ALLUSERSPROFILE)\chocolatey\bin"
    }
    if (Test-Path -Path "$($env:ALLUSERSPROFILE)\chocolatey\bin") {
        Write-Log -Level Information -Text 'Chocolatey installation detected.'
        choco upgrade chocolatey -y | Out-Null
        choco feature enable -n=allowGlobalConfirmation -confirm | Out-Null
        choco feature disable -n=showNonElevatedWarnings -confirm | Out-Null
        return 0
    } else {
        [Net.ServicePointManager]::SecurityProtocol = [Enum]::ToObject([Net.SecurityProtocolType], 3072)
        Invoke-Expression ((New-Object System.Net.WebClient).DownloadString('https://chocolatey.org/install.ps1'))
        choco feature enable -n=allowGlobalConfirmation -confirm | Out-Null
        choco feature disable -n=showNonElevatedWarnings -confirm | Out-Null
    }

    if (!(Test-Path -Path "$($env:ALLUSERSPROFILE)\chocolatey\bin")) {
        Write-Log -Level Error -Text 'Chocolatey installation failed.'
        return 1
    }
    return 0
}

#EndRegion '.\Public\Install-Chocolatey.ps1' 34
#Region '.\Public\Install-GitHubModule.ps1' -1

function Install-GitHubModule {
    <#
    .SYNOPSIS
        Install a PowerShell module from a GitHub repository.
    .DESCRIPTION
        Install a PowerShell module from a GitHub repository via PowerShellGet v3.
 
        This script requires a separate Azure function that returns a GitHub Personal Access Token based on two Base64 encoded scripts passed to it.
    .PARAMETER Name
        The name of the Github module to install.
    .PARAMETER Username
        The username of the Github user to authenticate with.
    .PARAMETER GithubPackageUri
        The URI to the Github Nuget package repository.
    .PARAMETER AzureGithubPATUri
        The URI to the Azure function that will return the PAT.
    .PARAMETER AzureGithubPATFunctionKey
        The function key for the Azure function.
    .EXAMPLE
        Install-GitHubModule `
            -Name MyGithubModule `
            -Username GithubUser `
            -GitHubPackageUri 'https://nuget.pkg.github.com/GithubUser/index.json' `
            -AzureGithubPATUri 'https://pat-function-subdomain.azurewebsites.net/api/FunctionName' `
            -AzureGithubPATFunctionKey 'MyFunctionKey'
        Import-Module -Name MyGithubModule
    #>

    [CmdletBinding()]
    param (
        [Parameter(Mandatory)][string]$Name,
        [Parameter(Mandatory)][string]$Username,
        [Parameter(Mandatory)][string]$GithubPackageUri,
        [Parameter(Mandatory)][string]$AzureGithubPATUri,
        [Parameter(Mandatory)][string]$AzureGithubPATFunctionKey
    )
    Write-Debug -Message "--- Parameters ---"
    Write-Debug -Message "Name: $Name"
    Write-Debug -Message "GitHub Username: $Username"
    Write-Debug -Message "GitHub Package Uri: $GithubPackageUri"
    Write-Debug -Message "Azure Function Uri: $AzureGithubPATUri"
    Write-Debug -Message "Azure Function Key: $AzureGithubPATFunctionKey"
    
    # Install PowerShellGet v3+ if not already installed.
    Write-Debug -Message "Checking for PowerShellGet v3+"
    if (!(Get-Module -ListAvailable -Name PowerShellGet | Where-Object { $_.Version.Major -ge 3 })) {
        Write-Debug -Message "Installing PowerShellGet v3+"
        Install-Module -Name PowerShellGet -AllowPrerelease -Force
    }

    # Get 'Strapper.psm1' path and encode to Base64
    $moduleMemberPath = (Get-ChildItem (Get-Item (Get-Module -name Strapper).Path).Directory -Recurse -Filter "Strapper.psm1" -File).FullName
    Write-Debug -Message "Encoding '$moduleMemberPath' content as Base64 string."
    $base64EncodedModuleMember = [System.Convert]::ToBase64String([System.Text.Encoding]::Unicode.GetBytes((Get-Content -LiteralPath $moduleMemberPath -Raw)))
    Write-Debug -Message "Encoded $moduleMemberPath`: $base64EncodedModuleMember"

    # Encode the calling script to Base64
    Write-Debug -Message "Encoding $($MyInvocation.PSCommandPath) as Base64 string."
    $base64EncodedScript = [System.Convert]::ToBase64String([System.Text.Encoding]::Unicode.GetBytes((Get-Content -LiteralPath $($MyInvocation.PSCommandPath) -Raw)))
    Write-Debug -Message "Encoded $($MyInvocation.PSCommandPath)`: $base64EncodedScript"

    Write-Debug -Message "Registering '$GithubPackageUri' as temporary repo."
    Register-PSResourceRepository -Name TempGithub -Uri $GithubPackageUri -Trusted
    Write-Debug -Message "Acquiring GitHub PAT"
    $githubPAT = (
        Invoke-RestMethod `
            -Uri "$($AzureGithubPATUri)?code=$($AzureGithubPATFunctionKey)" `
            -Method Post `
            -Body $(
                @{
                    Script = $base64EncodedScript
                    ScriptExtension = [System.IO.FileInfo]::new($($MyInvocation.PSCommandPath)).Extension
                    ModuleMember = $base64EncodedModuleMember
                    ModuleMemberExtension = [System.IO.FileInfo]::new($moduleMemberPath).Extension
                } | ConvertTo-Json
            ) `
            -ContentType 'application/json'
    ) | ConvertTo-SecureString -AsPlainText -Force
    Write-Debug -Message "PAT Last 4: $(([System.Runtime.InteropServices.Marshal]::PtrToStringAuto([System.Runtime.InteropServices.Marshal]::SecureStringToBSTR($githubPAT)))[-4..-1])"
    Write-Debug -Message "Installing module '$Name'."

    Install-PSResource -Name $Name -Repository TempGithub -Credential (New-Object System.Management.Automation.PSCredential($Username, $githubPAT))
    Write-Debug -Message "Unregistering '$GithubPackageUri'."
    Unregister-PSResourceRepository -Name TempGithub
}

#EndRegion '.\Public\Install-GitHubModule.ps1' 86
#Region '.\Public\Invoke-Script.ps1' -1

function Invoke-Script {
    <#
    .SYNOPSIS
        Run a PowerShell script from a local or remote path.
    .EXAMPLE
        Get-WebFile -Uri 'C:\Users\User\Restart-MyComputer.ps1'
        Runs the PowerShell script 'C:\Users\User\Restart-MyComputer.ps1'.
    .EXAMPLE
        Get-WebFile -Uri 'https://file.contoso.com/scripts/Set-UserWallpaper.ps1' -Parameters @{
            User = 'Joe.Smith'
            Wallpaper = 'https://static.wikia.nocookie.net/vocaloid/images/5/57/Miku_v4_bundle_art.png'
        }
        Downloads and runs the PowerShell script 'Set-UserWallpaper.ps1', passing the given parameters to it.
    .PARAMETER Uri
        The local path or URL of the target PowerShell script.
    .PARAMETER Parameters
        A hashtable of parameters to pass to the target PowerShell script.
    .OUTPUTS
        This function will have varying output based on the called PowerShell script.
    #>

    #requires -Version 5
    [CmdletBinding()]
    param (
        [Parameter(Mandatory)][System.Uri]$Uri,
        [Parameter()][hashtable]$Parameters = @{}
    )
    $targetScriptPath = $uri.LocalPath
    if (!($Uri.IsFile)) {
        # Retrieve the base file name of the target file. This is required to account for redirection.
        $baseFileName = ([System.Net.WebRequest]::Create($Uri)).GetResponse().ResponseUri.Segments[-1]

        if($baseFileName -notmatch '\.ps1$') {
            Write-Log -Level Error -Text 'This function only supports invoking .ps1 files.'
            throw
        }
        # Download the file from the URI.
        if ([System.IO.FileInfo]$downloadedFile = Get-WebFile -Uri $Uri -Path "$env:TEMP\$baseFileName" -PassThru -Clobber) {
            $targetScriptPath = $downloadedFile.FullName
        } else {
            Write-Log -Level Error -Text "Failed to download file from '$Uri'"
            throw
        }
    } else {
        if($uri.Segments[-1] -notmatch '\.ps1$') {
            Write-Log -Level Error -Text 'This function only supports invoking .ps1 files.'
            throw
        }
    }
    . $targetScriptPath @Parameters
}

#EndRegion '.\Public\Invoke-Script.ps1' 52
#Region '.\Public\Publish-GitHubModule.ps1' -1

function Publish-GitHubModule {
    <#
    .SYNOPSIS
        Publish a PowerShell module to a GitHub repository.
    .PARAMETER Path
        The path to the psd1 file for the module to publish.
    .PARAMETER Token
        The Github personal access token to use for publishing.
    .PARAMETER RepoUri
        The URI to the GitHub repo to publish to.
    .PARAMETER TempNugetPath
        The path to use to make a temporary NuGet repo.
    .EXAMPLE
        Publish-GitHubModule `
            -Path 'C:\users\user\Modules\MyModule\MyModule.psd1' `
            -Token 'ghp_abcdefg1234567' `
            -RepoUri 'https://github.com/user/MyModule'
    #>

    [CmdletBinding()]
    param (
        [Parameter(Mandatory)][string]$Path,
        [Parameter(Mandatory)][string]$Token,
        [Parameter(Mandatory)][string]$RepoUri,
        [Parameter()][string]$TempNugetPath = "$env:SystemDrive\temp\nuget\publish"
    )
    if (!(Get-Module -ListAvailable -Name PowerShellGet | Where-Object { $_.Version.Major -ge 3 })) {
        Install-Module -Name PowerShellGet -AllowPrerelease -Force
    }
    $targetModule = Get-Module $Path -ListAvailable
    if(!$targetModule) {
        Write-Error -Message "Failed to locate a module with the path '$targetModule'. Please pass a path to a .psd1 and try again."
        return
    }
    if(!(Test-Path -Path $TempNugetPath)) {
        New-Item -Path $TempNugetPath -ItemType Directory
    }
    Register-PSResourceRepository -Name TempNuget -Uri $TempNugetPath
    Publish-PSResource -Path $targetModule.ModuleBase -Repository TempNuget
    if(!((dotnet tool list --global) | Select-String "^gpr.*gpr.*$")) {
        dotnet tool install --global gpr
    }
    gpr push -k $Token "$TempNugetPath\$($targetModule.Name).$($targetModule.Version).nupkg" -r $RepoUri
    Unregister-PSResourceRepository -Name TempNuget
}

#EndRegion '.\Public\Publish-GitHubModule.ps1' 46
#Region '.\Public\Remove-UserRegistryKeyProperty.ps1' -1

function Remove-UserRegistryKeyProperty {
    <#
    .SYNOPSIS
        Removes a registry property value for existing user registry hives.
    .EXAMPLE
        Remove-UserRegistryKeyProperty -Path "SOFTWARE\_automation\Prompter" -Name "Timestamp"
        Removes registry property "Timestamp" under "SOFTWARE\_automation\Prompter" for each available user's registry hive.
    .PARAMETER Path
        The relative registry path to the target property.
    .PARAMETER Name
        The name of the property to target.
    #>

    [CmdletBinding()]
    [OutputType([System.Void])]
    param (
        [Parameter(Mandatory = $true)][string]$Path,
        [Parameter(Mandatory = $true)][string]$Name
    )

    if($StrapperSession.Platform -ne 'Win32NT') {
        Write-Error 'This function is only supported on Windows-based platforms.' -ErrorAction Stop
    }

    # Regex pattern for SIDs
    $patternSID = '((S-1-5-21)|(S-1-12-1))-\d+-\d+\-\d+\-\d+$'

    # Get Username, SID, and location of ntuser.dat for all users
    $profileList = Get-RegistryHivePath

    # Get all user SIDs found in HKEY_USERS (ntuser.dat files that are loaded)
    $loadedHives = Get-ChildItem Registry::HKEY_USERS | Where-Object { $_.PSChildname -match $PatternSID } | Select-Object @{name = 'SID'; expression = { $_.PSChildName } }

    # Get all user hives that are not currently logged in
    if ($LoadedHives) {
        $UnloadedHives = Compare-Object $ProfileList.SID $LoadedHives.SID | Select-Object @{name = 'SID'; expression = { $_.InputObject } }, UserHive, Username
    } else {
        $UnloadedHives = $ProfileList
    }

    # Iterate through each profile on the machine
    foreach ($profile in $ProfileList) {
        # Load User ntuser.dat if it's not already loaded
        if ($profile.SID -in $UnloadedHives.SID) {
            reg load HKU\$($profile.SID) $($profile.UserHive) | Out-Null
        }

        $propertyPath = "Registry::HKEY_USERS\$($profile.SID)\$Path"

        # If the entry does not exist then skip this user.
        if (!(Get-ItemProperty -Path $propertyPath -Name $Name -ErrorAction SilentlyContinue)) {
            Write-Log -Level Information -Text "The requested registry entry for user '$($profile.Username)' does not exist."
            continue
        }

        # Set the parameters to pass to Remove-ItemProperty
        $parameters = @{
            Path = $propertyPath
            Name = $Name
        }

        # Remove the target registry entry
        Remove-ItemProperty @parameters

        # Log the success or failure status of the removal.
        if ($?) {
            Write-Log -Level Information -Text "Removed the requested registry entry for user '$($profile.Username)'" -Type LOG
        } else {
            Write-Log -Level Error -Text "Failed to remove the requested registry entry for user '$($profile.Username)'"
        }

        # Collect garbage and close ntuser.dat if the hive was initially unloaded
        if ($profile.SID -in $UnloadedHives.SID) {
            [gc]::Collect()
            reg unload HKU\$($profile.SID) | Out-Null
        }
    }
}

#EndRegion '.\Public\Remove-UserRegistryKeyProperty.ps1' 79
#Region '.\Public\Set-RegistryKeyProperty.ps1' -1

function Set-RegistryKeyProperty {
    <#
    .SYNOPSIS
        Sets a Windows registry property value.
    .EXAMPLE
        Set-RegistryKeyProperty -Path "HKLM:\SOFTWARE\_automation\Test\1\2\3\4" -Name "MyValueName" -Value "1" -Type DWord
        Creates a DWord registry property with the name MyValueName and the value of 1. Will not create the key path if it does not exist.
    .EXAMPLE
        Set-RegistryKeyProperty -Path "HKLM:\SOFTWARE\_automation\Strings\New\Path" -Name "MyString" -Value "1234" -Force
        Creates a String registry property based on value type inference with the name MyString and the value of "1234". Creates the descending key path if it does not exist.
    .PARAMETER Path
        The registry path to the key to store the target property.
    .PARAMETER Name
        The name of the property to create/update.
    .PARAMETER Value
        The value to set for the property.
    .PARAMETER Type
        The type of value to set. If not passed, this will be inferred from the object type of the Value parameter.
    .PARAMETER Force
        Will create the registry key path to the property if it does not exist.
    #>

    [CmdletBinding()]
    param (
        [Parameter(Mandatory = $true)]
        [string]$Path,

        [Parameter(Mandatory = $false)]
        [string]$Name = '(Default)',

        [Parameter(Mandatory = $true)]
        [object]$Value,

        [Parameter(Mandatory = $false)]
        [ValidateSet('Unknown', 'String', 'ExpandString', 'Binary', 'DWord', 'MultiString', 'QWord', 'None')]
        [Microsoft.Win32.RegistryValueKind]$Type,

        [Parameter(Mandatory = $false)]
        [switch]$Force
    )
    
    if($StrapperSession.Platform -ne 'Win32NT') {
        Write-Error 'This function is only supported on Windows-based platforms.' -ErrorAction Stop
    }

    if ((Get-Item -Path ($Path -split '\\')[0]).GetType() -ne [Microsoft.Win32.RegistryKey]) {
        Write-Log -Level Error -Text 'The supplied path does not correlate to a registry key.'
        return $null
    }

    if (!(Test-Path -Path $Path) -and $Force) {
        Write-Log -Level Information -Text "'$Path' does not exist. Creating."
        New-Item -Path $Path -Force | Out-Null
    } elseif (!(Test-Path -Path $Path)) {
        Write-Log -Level Error -Text "'$Path' does not exist. Unable to create registry entry."
        return $null
    }

    $parameters = @{
        Path = $Path
        Name = $Name
        Value = $Value
        PassThru = $true
    }
    if ($Type) { $parameters.Add('Type', $Type) }
    return Set-ItemProperty @parameters
}

#EndRegion '.\Public\Set-RegistryKeyProperty.ps1' 68
#Region '.\Public\Set-StrapperEnvironment.ps1' -1

function Set-StrapperEnvironment {
    <#
    .SYNOPSIS
        Removes error and data files from the current working path and writes initialization information to the log.
    .EXAMPLE
        PS C:\> Set-StrapperEnvironment
    #>

    Remove-Item -Path $StrapperSession.ErrorPath -Force -ErrorAction SilentlyContinue
    Write-Log -Level Debug -Text $StrapperSession.ScriptTitle
    Write-Log -Level Debug -Text "System: $([Environment]::MachineName)"
    Write-Log -Level Debug -Text "User: $([Environment]::UserName)"
    Write-Log -Level Debug -Text "OS Bitness: $((32,64)[[Environment]::Is64BitOperatingSystem])"
    Write-Log -Level Debug -Text "PowerShell Bitness: $(if([Environment]::Is64BitProcess) {64} else {32})"
    Write-Log -Level Debug -Text "PowerShell Version: $(Get-Host | Select-Object -ExpandProperty Version | Select-Object -ExpandProperty Major)"
}

#EndRegion '.\Public\Set-StrapperEnvironment.ps1' 17
#Region '.\Public\Set-UserRegistryKeyProperty.ps1' -1

function Set-UserRegistryKeyProperty {
    <#
    .SYNOPSIS
        Creates or updates a registry property value for existing user registry hives.
    .EXAMPLE
        Set-UserRegistryKeyProperty -Path "SOFTWARE\_automation\Prompter" -Name "Timestamp" -Value 1
        Creates or updates a Dword registry property property for each available user's registry hive to a value of 1.
    .EXAMPLE
        Set-UserRegistryKeyProperty -Path "SOFTWARE\_automation\Strings\New\Path" -Name "MyString" -Value "1234" -Force
        Creates or updates a String registry property based on value type inference with the name MyString and the value of "1234". Creates the descending key path if it does not exist.
    .EXAMPLE
        Set-UserRegistryKeyProperty -Path "SOFTWARE\_automation\Strings\New\Path" -Username 'spike.spiegel' -Name "MyString" -Value "1234" -Force
        Creates or updates a String registry property for the local user 'spike.spiegel' based on value type inference with the name MyString and the value of "1234". Creates the descending key path if it does not exist.
    .EXAMPLE
        Set-UserRegistryKeyProperty -Path "SOFTWARE\_automation\Strings\New\Path" -Username 'BEBOP\faye.valentine' -Name "MyString" -Value "1234" -Force
        Creates or updates a String registry property for the domain user 'faye.valentine' based on value type inference with the name MyString and the value of "1234". Creates the descending key path if it does not exist.
    .PARAMETER Path
        The relative registry path to the target property.
    .PARAMETER Username
        The user to target for editing.
        Should be in the format: <Domain Short Name or Hostname>\Username. If the domain and hostname are omitted, local user targeting will be assumed.
    .PARAMETER Name
        The name of the property to target.
    .PARAMETER Value
        The value to set on the target property.
    .PARAMETER Type
        The type of value to set. If not passed, this will be inferred from the object type of the Value parameter.
    .PARAMETER ExcludeDefault
        Exclude the Default user template from having the registry keys set.
    .PARAMETER Force
        Will create the registry key path to the property if it does not exist.
    #>

    [CmdletBinding()]
    param (
        [Parameter(Mandatory = $true)]
        [string]$Path,

        [Parameter(Mandatory = $false)]
        [string]$Username,

        [Parameter(Mandatory = $false)]
        [string]$Name = '(Default)',

        [Parameter(Mandatory = $true)]
        [object]$Value,

        [Parameter(Mandatory = $false)]
        [ValidateSet('Unknown', 'String', 'ExpandString', 'Binary', 'DWord', 'MultiString', 'QWord', 'None')]
        [Microsoft.Win32.RegistryValueKind]$Type,

        [Parameter(Mandatory = $false)]
        [switch]$ExcludeDefault,

        [Parameter(Mandatory = $false)]
        [switch]$Force
    )

    if($StrapperSession.Platform -ne 'Win32NT') {
        Write-Error 'This function is only supported on Windows-based platforms.' -ErrorAction Stop
    }

    # Regex pattern for SIDs
    $patternSID = '((S-1-5-21)|(S-1-12-1))-\d+-\d+\-\d+\-\d+$'

    # Get Username, SID, and location of ntuser.dat for all users
    $profileList = Get-RegistryHivePath -ExcludeDefault:$ExcludeDefault

    # Get all user SIDs found in HKEY_USERS (ntuser.dat files that are loaded)
    $loadedHives = Get-ChildItem Registry::HKEY_USERS | Where-Object { $_.PSChildname -match $PatternSID } | Select-Object @{name = 'SID'; expression = { $_.PSChildName } }

    # Get all user hives that are not currently logged in
    if ($LoadedHives) {
        $UnloadedHives = Compare-Object $ProfileList.SID $LoadedHives.SID | Select-Object @{name = 'SID'; expression = { $_.InputObject } }, UserHive, Username
    } else {
        $UnloadedHives = $ProfileList
    }
    if ($Username) {
        if($Username -notmatch "\\") {
            $Username = "$env:COMPUTERNAME\$Username"
        }
        $profileList = $profileList | Where-Object { $_.Username -eq $Username }
    }
    # Iterate through each profile on the machine
    $returnEntries = @(
        foreach ($profile in $ProfileList) {
            if([string]::IsNullOrWhiteSpace($profile.Username)) {
                Write-Log -Level Warning -Text "$($profile.SID) does not have a username and is likely not a valid user. Skipping."
                continue
            }
            # Load User ntuser.dat if it's not already loaded
            if ($profile.SID -in $UnloadedHives.SID) {
                reg load HKU\$($profile.SID) $($profile.UserHive) | Out-Null
            }

            $propertyPath = "Registry::HKEY_USERS\$($profile.SID)\$Path"

            # Set the parameters to pass to Set-RegistryKeyProperty
            $parameters = @{
                Path = $propertyPath
                Name = $Name
                Value = $Value
                Force = $Force
            }
            if ($Type) { $parameters.Add('Type', $Type) }

            # Set the target registry entry
            $returnEntry = Set-RegistryKeyProperty @parameters | Select-Object -ExpandProperty $Name

            # If the set was successful, then pass back the return entry from Set-RegistryKeyProperty
            if ($returnEntry) {
                [PSCustomObject]@{
                    Username = $profile.Username
                    SID = $profile.SID
                    Path = $propertyPath
                    Hive = $profile.UserHive
                    Name = $Name
                    Value = $returnEntry
                }
            } else {
                Write-Log -Level Warning -Text "Failed to set the requested registry entry for user '$($profile.Username)'"
            }

            # Collect garbage and close ntuser.dat if the hive was initially unloaded
            if ($profile.SID -in $UnloadedHives.SID) {
                [gc]::Collect()
                reg unload HKU\$($profile.SID) | Out-Null
            }
        }
    )
    Write-Log -Level Information -Text "$($returnEntries.Count) user registry entries successfully updated."
    return $returnEntries
}

#EndRegion '.\Public\Set-UserRegistryKeyProperty.ps1' 134
#Region '.\Public\SQLite\Get-SQLiteTable.ps1' -1


function Get-SQLiteTable {
    <#
    .SYNOPSIS
        Get table information from a SQLite connection.
    .EXAMPLE
        Get-SQLiteTable -Connection $Connection
        Returns information about all tables from the provided SQLite connection.
    .EXAMPLE
        Get-SQLiteTable -TableName mydata -Connection $Connection
        Returns information about the mydata table from the provided SQLite connection.
    .PARAMETER Name
        The name of the table to retrieve.
    .PARAMETER Connection
        The SQLite connection to use.
    .OUTPUTS
        [pscustomobject] - The table with the specified name.
        [pscustomobject[]] - All tables from the target connection.
    #>

    [CmdletBinding(DefaultParameterSetName = 'All')]
    [OutputType([pscustomobject], ParameterSetName = 'Single')]
    [OutputType([pscustomobject[]], ParameterSetName = 'All')]
    param (
        [Parameter(ParameterSetName = 'Single')][string]$Name,
        [Parameter(Mandatory)][System.Data.SQLite.SQLiteConnection]$Connection
    )
    $schema = $Connection.GetSchema('Tables')
    $tablesToProcess = if (!$Name) {
        Write-Verbose -Message 'Returning all tables from schema.'
        $schema.Rows
    } else {
        Write-Verbose -Message "Attempting to locate table with name '$Name'."
        $lowerName = $name.ToLower()
        foreach ($table in $schema.Rows) {
            $tableLowerName = $table.TABLE_NAME.ToLower()
            Write-Verbose -Message "Comparing '$tableLowerName' to '$lowerName'"
            if ($lowerName.Equals($tableLowerName)) {
                @($table)
            }
        }
    }
    return $(foreach ($table in $tablesToProcess) {
            $columnRows = $connection.GetSchema('Columns', @($null, $null, $table.TABLE_NAME)).Rows
            Write-Verbose -Message "Processing $($columnRows.Count) columns for table '$($table.TABLE_NAME)'"
            $columns = $(
                foreach ($columnRow in $columnRows) {
                    [PSCustomObject]@{
                        TableCatalog = $columnRow.TABLE_CATALOG
                        TableSchema = $columnRow.TABLE_SCHEMA
                        TableName = $columnRow.TABLE_NAME
                        ColumnName = $columnRow.COLUMN_NAME
                        ColumnGuid = $columnRow.COLUMN_GUID
                        ColumnPropid = $columnRow.COLUMN_PROPID
                        OrdinalPosition = $columnRow.ORDINAL_POSITION
                        ColumnHasdefault = $columnRow.COLUMN_HASDEFAULT
                        ColumnDefault = $columnRow.COLUMN_DEFAULT
                        ColumnFlags = $columnRow.COLUMN_FLAGS
                        IsNullable = $columnRow.IS_NULLABLE
                        DataType = $columnRow.DATA_TYPE
                        TypeGuid = $columnRow.TYPE_GUID
                        CharacterMaximumLength = $columnRow.CHARACTER_MAXIMUM_LENGTH
                        CharacterOctetLength = $columnRow.CHARACTER_OCTET_LENGTH
                        NumericPrecision = $columnRow.NUMERIC_PRECISION
                        NumericScale = $columnRow.NUMERIC_SCALE
                        DatetimePrecision = $columnRow.DATETIME_PRECISION
                        CharacterSetCatalog = $columnRow.CHARACTER_SET_CATALOG
                        CharacterSetSchema = $columnRow.CHARACTER_SET_SCHEMA
                        CharacterSetName = $columnRow.CHARACTER_SET_NAME
                        CollationCatalog = $columnRow.COLLATION_CATALOG
                        CollationSchema = $columnRow.COLLATION_SCHEMA
                        CollationName = $columnRow.COLLATION_NAME
                        DomainCatalog = $columnRow.DOMAIN_CATALOG
                        DomainName = $columnRow.DOMAIN_NAME
                        Description = $columnRow.DESCRIPTION
                        PrimaryKey = $columnRow.PRIMARY_KEY
                        EdmType = $columnRow.EDM_TYPE
                        Autoincrement = $columnRow.AUTOINCREMENT
                        Unique = $columnRow.UNIQUE
                    }
                }
            )
            [PSCustomObject]@{
                TableCatalog = $table.TABLE_CATALOG
                TableSchema = $table.TABLE_SCHEMA
                TableName = $table.TABLE_NAME
                TableType = $table.TABLE_TYPE
                TableId = $table.TABLE_ID
                TableRootpage = $table.TABLE_ROOTPAGE  
                TableDefinition = $table.TABLE_DEFINITION
                Columns = $columns
            }
        }
    )
}

#EndRegion '.\Public\SQLite\Get-SQLiteTable.ps1' 96
#Region '.\Public\SQLite\Get-StoredObject.ps1' -1

function Get-StoredObject {
    <#
    .SYNOPSIS
        Get previously stored objects from a Strapper object table.
    .EXAMPLE
        Get-StoredObject -IncludeMetadata
        Gets the stored objects list from the default "<scriptname>_data" table, including the object metadata.
    .EXAMPLE
        Get-StoredObject -TableName disks
        Gets the stored objects list from the "<scriptname>_disks" table.
    .PARAMETER TableName
        The name of the table to retrieve objects from.
    .PARAMETER DataSource
        The target SQLite datasource to use. Defaults to Strapper's 'Strapper.db'.
    .PARAMETER IncludeMetadata
        Include a Metadata property on each object that describes additional information about table name, insertion time, and row ID.
    .OUTPUTS
        [System.Collections.Generic.List[pscustomobject]] - A list of previously stored objects.
    #>

    [CmdletBinding()]
    param(
        [Parameter()][ValidatePattern('^[a-zA-Z0-9\-_]+$')][string]$TableName,
        [Parameter()][string]$DataSource = $StrapperSession.DBPath,
        [Parameter()][switch]$IncludeMetadata
    )
    [System.Data.SQLite.SQLiteConnection]$sqliteConnection = New-SQLiteConnection -DataSource $DataSource -Open
    if (!$TableName) {
        $TableName = 'data'
    }
    [System.Data.SQLite.SQLiteConnection]$sqliteConnection = New-SQLiteConnection -DataSource $DataSource -Open
    if (!(Get-SQLiteTable -Name $TableName -Connection $sqliteConnection)) {
        Write-Error -Message "No log table with the name '$TableName' was found in the database '$DataSource'" -ErrorAction Stop
    }
    $sqliteCommand = $sqliteConnection.CreateCommand()
    $sqliteCommand.CommandText = "SELECT * FROM '$TableName'"
    Write-Verbose -Message "CommandText: $($sqliteCommand.CommandText)"
    $dataReader = $sqliteCommand.ExecuteReader()
    if (!($dataReader.HasRows)) {
        Write-Warning -Message "No entries found in '$TableName'."
        return
    }
    $objectList = [System.Collections.Generic.List[pscustomobject]]::new()
    try {
        while ($dataReader.Read()) {
            $returnObject = $dataReader.GetString(1) | ConvertFrom-Json
            if($IncludeMetadata) {
                Write-Verbose -Message "Adding metadata to the return object."
                $metadata = [PSCustomObject]@{
                    Id = $dataReader.GetInt32(0)
                    Timestamp = $dataReader.GetDateTime(2)
                    TableName = $dataReader.GetTableName(0)
                }
                Write-Verbose -Message "Id = $($metadata.Id)"
                Write-Verbose -Message "Timestamp = $($metadata.Timestamp)"
                Write-Verbose -Message "TableName = $($metadata.TableName)"
                $returnObject | Add-Member -MemberType NoteProperty -Name Metadata -Value $metadata
            }
            $objectList.Add($returnObject)
        }
        $objectList
    } catch {
        Write-Error -Message "An error occurred while attempting to query SQL: $($_.Exception)"
    } finally {
        $dataReader.Dispose()
        $sqliteConnection.Dispose()
    }
}

#EndRegion '.\Public\SQLite\Get-StoredObject.ps1' 69
#Region '.\Public\SQLite\Get-StrapperLog.ps1' -1

function Get-StrapperLog {
    <#
    .SYNOPSIS
        Get objects representing Strapper logs from a database.
    .EXAMPLE
        Get-StrapperLog
        Gets the Strapper logs from the "<scriptname>_logs" table with a minimum log level of 'Information'.
    .EXAMPLE
        Get-StrapperLog -MinimumLevel 'Error'
        Gets the Strapper logs from the "<scriptname>_logs" table with a minimum log level of 'Error'.
    .EXAMPLE
        Get-StrapperLog -MinimumLevel 'Fatal' -TableName 'MyCustomLogTable'
        Gets the Strapper logs from the "<scriptname>_MyCustomLogTable" table with a minimum log level of 'Fatal'.
    .PARAMETER MinimumLevel
        The minimum log level to gather from the table.
        Highest --- Fatal
                    Error
                    Warning
                    Information
                    Debug
         Lowest --- Verbose
    .PARAMETER TableName
        The name of the table to retrieve logs from.
    .PARAMETER DataSource
        The target SQLite datasource to use. Defaults to Strapper's 'Strapper.db'.
    .OUTPUTS
        [System.Collections.Generic.List[StrapperLog]] - A list of logs from the table.
    #>

    [CmdletBinding()]
    param (
        [Parameter()]
        [ValidateSet('Verbose', 'Debug', 'Information', 'Warning', 'Error', 'Fatal')]
        [string]$MinimumLevel = 'Information',
        [Parameter()][ValidatePattern('^[a-zA-Z0-9\-_]+$')][string]$TableName = $StrapperSession.LogTable,
        [Parameter()][string]$DataSource = $StrapperSession.DBPath
    )
    # Casting here instead of in the parameter because PowerShell modules don't support the export of classes/enums.
    [StrapperLogLevel]$MinimumLevel = [StrapperLogLevel]$MinimumLevel
    [System.Data.SQLite.SQLiteConnection]$sqliteConnection = New-SQLiteConnection -DataSource $DataSource -Open
    if (!(Get-SQLiteTable -Name $TableName -Connection $sqliteConnection)) {
        Write-Error -Message "No log table with the name '$TableName' was found in the database '$DataSource'" -ErrorAction Stop
    }
    $sqliteCommand = $sqliteConnection.CreateCommand()
    $sqliteCommand.CommandText = "SELECT * FROM '$TableName' WHERE Level >= $($MinimumLevel.value__)"
    Write-Verbose -Message "CommandText: $($sqliteCommand.CommandText)"
    $dataReader = $sqliteCommand.ExecuteReader()
    if (!($dataReader.HasRows)) {
        Write-Warning -Message "No entries found in '$TableName'."
        return
    }
    $logList = [System.Collections.Generic.List[StrapperLog]]::new()
    try {
        while ($dataReader.Read()) {
            Write-Verbose -Message "Id = $($dataReader.GetInt32(0))"
            Write-Verbose -Message "Level = $($dataReader.GetInt32(1))"
            Write-Verbose -Message "Message = $($dataReader.GetString(2))"
            Write-Verbose -Message "Timestamp = $($dataReader.GetDateTime(3))"
            $logList.Add(
                [StrapperLog]@{
                    Id = $dataReader.GetInt32(0)
                    Level = $dataReader.GetInt32(1)
                    Message = $dataReader.GetString(2)
                    Timestamp = $dataReader.GetDateTime(3)
                }
            )
        }
        $logList
    } catch {
        Write-Error -Message "An error occurred while attempting to query SQL: $($_.Exception)"
    } finally {
        $dataReader.Dispose()
        $sqliteConnection.Dispose()
    }
}

#EndRegion '.\Public\SQLite\Get-StrapperLog.ps1' 76
#Region '.\Public\SQLite\New-SQLiteConnection.ps1' -1

function New-SQLiteConnection {
    <#
    .SYNOPSIS
        Get a new a SQLite connection.
    .EXAMPLE
        New-SQLiteConnection
        Creates a new SQLite connection from the default Datasource in Strapper.
    .EXAMPLE
        New-SQLiteConnection -Datasource "C:\mySqlite.db" -Open
        Creates a new SQLite connection to the datasource "C:\mySqlite.db" and opens the connection before returning.
    .PARAMETER Datasource
        The datasource to use for the connection.
    .PARAMETER Open
        Use this switch to open the connection before returning it.
    .OUTPUTS
        [System.Data.SQLite.SQLiteConnection] - The resulting SQLite connection object.
    #>

    [CmdletBinding()]
    [OutputType([System.Data.SQLite.SQLiteConnection])]
    param(
        [Parameter()][string]$DataSource = $StrapperSession.DBPath,
        [Parameter()][switch]$Open
    )
    if ($Open) {
        return [System.Data.SQLite.SQLiteConnection]::new((New-SQLiteConnectionString -DataSource $DataSource)).OpenAndReturn()
    }
    return [System.Data.SQLite.SQLiteConnection]::new((New-SQLiteConnectionString -DataSource $DataSource))
}

#EndRegion '.\Public\SQLite\New-SQLiteConnection.ps1' 30
#Region '.\Public\SQLite\New-SQLiteConnectionString.ps1' -1

function New-SQLiteConnectionString {
    <#
    .SYNOPSIS
        Get a new a SQLite connection string.
    .EXAMPLE
        New-SQLiteConnectionString
        Creates a new SQLite connection string from the default Datasource in Strapper.
    .EXAMPLE
        New-SQLiteConnectionString -Datasource "C:\mySqlite.db"
        Creates a new SQLite connection string with the datasource "C:\mySqlite.db".
    .PARAMETER Datasource
        The datasource to use for the connection string.
    .OUTPUTS
        [string] - The resulting SQLite connection string.
    #>

    [CmdletBinding()]
    [OutputType([string])]
    param (
        [Parameter()][string]$DataSource = $StrapperSession.DBPath
    )
    $csBuilder = [System.Data.SQLite.SQLiteConnectionStringBuilder]::new()
    $csBuilder.DataSource = $DataSource
    return $csBuilder.ConnectionString
}

#EndRegion '.\Public\SQLite\New-SQLiteConnectionString.ps1' 26
#Region '.\Public\SQLite\New-SQLiteLogTable.ps1' -1

function New-SQLiteLogTable {
    <#
    .SYNOPSIS
        Creates a new SQLite table specifically designed for storing Strapper logs.
    .EXAMPLE
        New-SQLiteLogTable -Name 'myscript_logs' -Connection $Connection
        Creates a new Strapper log table named 'myscript_logs' if it does not exist.
    .EXAMPLE
        New-SQLiteLogTable -Name 'myscript_logs' -Connection $Connection -Clobber
        Creates a new Strapper log table named 'myscript_logs', overwriting any existing table.
    .EXAMPLE
        New-SQLiteLogTable -Name 'myscript_logs' -Connection $Connection -PassThru
        Creates a new Strapper log table named 'myscript_logs' if it does not exist and returns an object representing the created (or existing) table.
    .PARAMETER Name
        The name of the table to create.
    .PARAMETER Connection
        The connection to create the table with.
    .PARAMETER Clobber
        Recreate the table (removing all existing data) if it exists.
    .PARAMETER PassThru
        Return an object representing the created (or existing) table.
    .OUTPUTS
        [pscustomobject] - An object representing the created (or existing) table. Will only return if -PassThru is used.
    #>

    [CmdletBinding()]
    param(
        [Parameter(Mandatory)][ValidatePattern('^[a-zA-Z0-9\-_]+$')][string]$Name,
        [Parameter(Mandatory)][System.Data.SQLite.SQLiteConnection]$Connection,
        [Parameter()][switch]$Clobber,
        [Parameter()][switch]$PassThru
    )
    $targetTable = Get-SQLiteTable -Name $Name -Connection $Connection
    if ($targetTable -and !$Clobber) {
        Write-Verbose -Message "Target table '$Name' already exists. Pass -Clobber to overwrite this table."
    } else {
        Remove-SQLiteTable -Name $Name -Connection $Connection | Out-Null
        $createCommand = $Connection.CreateCommand()
        $createCommand.CommandText = @"
        CREATE TABLE "$Name" (
            "id" INTEGER NOT NULL UNIQUE,
            "level" INTEGER NOT NULL,
            "message" TEXT NOT NULL,
            "timestamp" DATETIME NOT NULL,
            PRIMARY KEY("id" AUTOINCREMENT)
        );
"@

        $rowsAffected = $createCommand.ExecuteNonQuery()
        Write-Verbose -Message "Affected row count: $rowsAffected"
        $targetTable = Get-SQLiteTable -Name $Name -Connection $Connection
        if (!$targetTable) {
            Write-Error -Exception ([System.Data.SQLite.SQLiteException]::new([System.Data.SQLite.SQLiteErrorCode]::IoErr, "Failed to create table '$Name'"))
            return
        }
    }
    if ($PassThru) {
        return $targetTable
    }
}

#EndRegion '.\Public\SQLite\New-SQLiteLogTable.ps1' 60
#Region '.\Public\SQLite\New-SQLiteObjectTable.ps1' -1

function New-SQLiteObjectTable {
    <#
    .SYNOPSIS
        Creates a new SQLite table specifically designed for storing JSON representations of objects.
    .EXAMPLE
        New-SQLiteObjectTable -Name 'myscript_data' -Connection $Connection
        Creates a new JSON object table named 'myscript_data' if it does not exist.
    .EXAMPLE
        New-SQLiteObjectTable -Name 'myscript_logs' -Connection $Connection -Clobber
        Creates a new JSON object table named 'myscript_data', overwriting any existing table.
    .EXAMPLE
        New-SQLiteObjectTable -Name 'myscript_logs' -Connection $Connection -PassThru
        Creates a new JSON object table named 'myscript_data' if it does not exist and returns an object representing the created (or existing) table.
    .PARAMETER Name
        The name of the table to create.
    .PARAMETER Connection
        The connection to create the table with.
    .PARAMETER Clobber
        Recreate the table (removing all existing data) if it exists.
    .PARAMETER PassThru
        Return an object representing the created (or existing) table.
    .OUTPUTS
        [pscustomobject] - An object representing the created (or existing) table. Will only return if -PassThru is used.
    #>

    [CmdletBinding()]
    param(
        [Parameter(Mandatory)][ValidatePattern('^[a-zA-Z0-9\-_]+$')][string]$Name,
        [Parameter(Mandatory)][System.Data.SQLite.SQLiteConnection]$Connection,
        [Parameter()][switch]$Clobber,
        [Parameter()][switch]$PassThru
    )
    $targetTable = Get-SQLiteTable -Name $Name -Connection $Connection
    if ($targetTable -and !$Clobber) {
        Write-Verbose -Message "Target table '$Name' already exists. Pass -Clobber to overwrite this table."
    } else {
        Remove-SQLiteTable -Name $Name -Connection $Connection | Out-Null
        $createCommand = $Connection.CreateCommand()
        $createCommand.CommandText = @"
        CREATE TABLE "$Name" (
            "id" INTEGER NOT NULL UNIQUE,
            "json" JSON NOT NULL,
            "timestamp" DATETIME NOT NULL,
            PRIMARY KEY("id" AUTOINCREMENT)
        );
"@

        $rowsAffected = $createCommand.ExecuteNonQuery()
        Write-Verbose -Message "Affected row count: $rowsAffected"
        $targetTable = Get-SQLiteTable -Name $Name -Connection $Connection
        if (!$targetTable) {
            Write-Error -Exception ([System.Data.SQLite.SQLiteException]::new([System.Data.SQLite.SQLiteErrorCode]::IoErr, "Failed to create table '$Name'"))
            return
        }
    }
    if ($PassThru) {
        return $targetTable
    }
}

#EndRegion '.\Public\SQLite\New-SQLiteObjectTable.ps1' 59
#Region '.\Public\SQLite\Remove-SQLiteTable.ps1' -1

function Remove-SQLiteTable {
    <#
    .SYNOPSIS
        Removes a SQLite table from a target connection.
    .EXAMPLE
        Remove-SQLiteTable -Name 'myscript_data' -Connection $Connection
        Drops the table named 'myscript_data' if it exists.
    .PARAMETER Name
        The name of the table to drop.
    .PARAMETER Connection
        The connection to drop the table from.
    .OUTPUTS
        [int] - Should always return -1 if the table was successfully dropped.
    #>

    [CmdletBinding()]
    [OutputType([int])]
    param (
        [Parameter(Mandatory)][ValidatePattern('^[a-zA-Z0-9\-_]+$')][string]$Name,
        [Parameter(Mandatory)][System.Data.SQLite.SQLiteConnection]$Connection
    )
    $Connection.CreateCommand()
    $dropCommand = $Connection.CreateCommand()
    $dropCommand.CommandText = "DROP TABLE IF EXISTS '$Name';"
    $rowsAffected = $dropCommand.ExecuteNonQuery()
    Write-Verbose -Message "Affected row count: $rowsAffected"
    return $rowsAffected
}

#EndRegion '.\Public\SQLite\Remove-SQLiteTable.ps1' 29
#Region '.\Public\SQLite\Write-SQLiteLog.ps1' -1

function Write-SQLiteLog {
    <#
    .SYNOPSIS
        Writes a log entry to a Strapper log table.
    .EXAMPLE
        Write-SQLiteLog -Message 'Logging a warning' -Level 'Warning'
        Logs a warning-level message to the default Strapper datasource and log table.
    .EXAMPLE
        Write-SQLiteLog -Message 'Logging a fatal error' -Level 'Fatal' -TableName 'myscript_error'
        Logs a fatal-level message to the default Strapper datasource under the 'myscript_error' table.
    .PARAMETER Message
        The message to write to the log table.
    .PARAMETER Level
        The log level of the message.
    .PARAMETER TableName
        The table to write the log message to. Must be a formatted Strapper log table.
    .PARAMETER DataSource
        The datasource to write the log message to. Defaults to the Strapper datasource.
    #>

    [CmdletBinding()]
    param (
        [Parameter(Mandatory)][string]$Message,    
        [Parameter(Mandatory)][StrapperLogLevel]$Level,
        [Parameter()][ValidatePattern('^[a-zA-Z0-9\-_]+$')][string]$TableName = $StrapperSession.LogTable,
        [Parameter()][string]$DataSource = $StrapperSession.DBPath
    )
    [System.Data.SQLite.SQLiteConnection]$sqliteConnection = New-SQLiteConnection -DataSource $DataSource -Open
    New-SQLiteLogTable -Name $TableName -Connection $sqliteConnection
    $sqliteCommand = $sqliteConnection.CreateCommand()
    $sqliteCommand.CommandText = "INSERT INTO '$TableName' (level, message, timestamp) VALUES (:level, :message, (SELECT datetime('now')))"
    $sqliteCommand.Parameters.AddWithValue(':level', $Level.value__) | Out-Null
    $sqliteCommand.Parameters.AddWithValue(':message', $Message) | Out-Null
    $rowsAffected = $sqliteCommand.ExecuteNonQuery()
    Write-Verbose -Message "Rows affected: $rowsAffected"
    $sqliteConnection.Dispose()
}

#EndRegion '.\Public\SQLite\Write-SQLiteLog.ps1' 38
#Region '.\Public\SQLite\Write-StoredObject.ps1' -1

function Write-StoredObject {
    <#
    .SYNOPSIS
        Write one or more objects to a Strapper object table.
    .EXAMPLE
        Get-Disk | Write-StoredObject
        Writes the output objects from Get-Disk to the default "<scriptname>_data" table.
    .EXAMPLE
        Get-Disk | Write-StoredObject -TableName disks
        Writes the output objects from Get-Disk to the "<scriptname>_disks" table.
    .PARAMETER TableName
        The name of the table to write objects to.
    .PARAMETER DataSource
        The target SQLite datasource to use. Defaults to Strapper's 'Strapper.db'.
    .PARAMETER InputObject
        The objects to write to the table.
    .PARAMETER Depth
        The depth that the JSON serializer will dive through an object's properties.
    .PARAMETER Clobber
        Recreate the table (removing all existing data) if it exists.
    #>

    [CmdletBinding()]
    param(
        [Parameter()][ValidatePattern('^[a-zA-Z0-9\-_]+$')][string]$TableName,
        [Parameter()][string]$DataSource = $StrapperSession.DBPath,
        [Parameter(Mandatory, ValueFromPipeline)][System.Object[]]$InputObject,
        [Parameter()][int]$Depth = 64,
        [Parameter()][switch]$Clobber
    )
    begin {
        [System.Data.SQLite.SQLiteConnection]$sqliteConnection = New-SQLiteConnection -DataSource $DataSource -Open
        if (!$TableName) {
            $TableName = 'data'
        }
        New-SQLiteObjectTable -Name $TableName -Connection $sqliteConnection -Clobber:$Clobber
        $sqliteCommand = $sqliteConnection.CreateCommand()
        $sqliteTransaction = $sqliteConnection.BeginTransaction()
        $sqliteCommand.Transaction = $sqliteTransaction
        $rowsAffected = 0
    }
    process {
        foreach ($obj in $InputObject) {
            $jsonObjectString = $obj | ConvertTo-Json -Depth $Depth -Compress
            $sqliteCommand.CommandText = "INSERT INTO '$TableName' (json, timestamp) VALUES (:json, (SELECT datetime('now')))"
            $sqliteCommand.Parameters.AddWithValue(':json', $jsonObjectString) | Out-Null
            $rowsAffected += $sqliteCommand.ExecuteNonQuery()
            $sqliteCommand.Parameters.Clear()
        }
    }
    end {
        $sqliteTransaction.Commit()
        Write-Verbose -Message "Rows affected: $rowsAffected"
        $sqliteTransaction.Dispose()
        $sqliteConnection.Dispose()
    }
}

#EndRegion '.\Public\SQLite\Write-StoredObject.ps1' 58
#Region '.\Public\Write-Log.ps1' -1

function Write-Log {
    <#
    .SYNOPSIS
        Writes a message to a log file, the console, or both.
    .EXAMPLE
        PS C:\> Write-Log -Level Error -Text "An error occurred."
        This will write an error to the console, the log file, and the error log file.
    .PARAMETER Text
        The message to pass to the log.
    .PARAMETER Level
        The log level assigned to the message.
        See https://github.com/ProVal-Tech/Strapper/blob/main/docs/Write-Log.md#log-levels for more information.
    .PARAMETER Exception
        An Exception object to add to an `Error` or `Fatal` log level type.
    .PARAMETER ErrorCategory
        An ErrorCategory to add to an `Error` or `Fatal` log level type.
    .LINK
        https://github.com/ProVal-Tech/Strapper/blob/main/docs/Write-Log.md
    #>

    [CmdletBinding(DefaultParameterSetName = 'Level')]
    param (
        [Parameter(Mandatory, Position = 0)][AllowEmptyString()][Alias('Message')]
        [string]$Text,
        [Parameter(Mandatory, DontShow, ParameterSetName = 'Type')]
        [string]$Type,
        [Parameter(ParameterSetName = 'Level')]
        [ValidateSet('Verbose', 'Debug', 'Information', 'Warning', 'Error', 'Fatal')]
        [string]$Level = 'Information',
        [Parameter()]
        [System.Exception]$Exception,
        [Parameter()]
        [System.Management.Automation.ErrorCategory]$ErrorCategory = [System.Management.Automation.ErrorCategory]::NotSpecified
    )
    if (!($StrapperSession.LogPath -and $StrapperSession.ErrorPath)) {
        $location = (Get-Location).Path
        $StrapperSession.LogPath = Join-Path -Path $location -ChildPath "$((Get-Date).ToString('yyyyMMdd'))-log.txt"
        $StrapperSession.ErrorPath = Join-Path -Path $location -ChildPath "$((Get-Date).ToString('yyyyMMdd'))-error.txt"
    }

    # Accounting for -Type to allow for backwards compatibility.
    if ($Type) {
        switch ($Type) {
            'LOG' { $Level = [StrapperLogLevel]::Information }
            'WARN' { $Level = [StrapperLogLevel]::Warning }
            'ERROR' { $Level = [StrapperLogLevel]::Error }
            'SUCCESS' { $Level = [StrapperLogLevel]::Information }
            'DATA' { $Level = [StrapperLogLevel]::Information }
            'INIT' { $Level = [StrapperLogLevel]::Debug }
            Default { $Level = [StrapperLogLevel]::Information }
        }
    } else {
        [StrapperLogLevel]$Level = $Level
    }
    
    switch ([StrapperLogLevel]$Level) {
        ([StrapperLogLevel]::Verbose) {
            $levelShortName = 'VER'
            Write-Verbose -Message $Text
            break
        }
        ([StrapperLogLevel]::Debug) {
            $levelShortName = 'DBG'
            Write-Debug -Message $Text
            break
        }
        ([StrapperLogLevel]::Information) {
            $levelShortName = 'INF'
            Write-Information -MessageData $Text
            break
        }
        ([StrapperLogLevel]::Warning) {
            $levelShortName = 'WRN'
            Write-Warning -Message $Text
            break
        }
        ([StrapperLogLevel]::Error) {
            $levelShortName = 'ERR'
            if ($Exception) {
                Write-Error -Message $Text -Exception $Exception -Category $ErrorCategory
                break
            }
            Write-Error -Message $Text -Category $ErrorCategory
            break
        }
        ([StrapperLogLevel]::Fatal) {
            $levelShortName = 'FTL'
            if ($Exception) {
                Write-Error -Message $Text -Category $ErrorCategory -Exception $Exception
                break
            }
            Write-Error -Message $Text -Category $ErrorCategory
            break
        }
        Default {
            $levelShortName = 'UNK'
            Write-Information -MessageData $Text
        }
    }
    $formattedLog = "$((Get-Date).ToString('yyyy-MM-dd HH:mm:ss.fff zzz')) [$levelShortName] $Text"
    Add-Content -Path $StrapperSession.logPath -Value $formattedLog
    if ([StrapperLogLevel]$Level -ge [StrapperLogLevel]::Error) {
        Add-Content -Path $StrapperSession.ErrorPath -Value $formattedLog
    }

    if($StrapperSession.LogsToDB) {
        Write-SQLiteLog -Message $Text -Level $Level
    }
}

#EndRegion '.\Public\Write-Log.ps1' 110

# SIG # Begin signature block
# MIIlhwYJKoZIhvcNAQcCoIIleDCCJXQCAQExDzANBglghkgBZQMEAgEFADB5Bgor
# BgEEAYI3AgEEoGswaTA0BgorBgEEAYI3AgEeMCYCAwEAAAQQH8w7YFlLCE63JNLG
# KX7zUQIBAAIBAAIBAAIBAAIBADAxMA0GCWCGSAFlAwQCAQUABCB0Vk/c6QzdtjgD
# 7qNeV6+xYFOMyGpE5i2Cc2tqM/M4gqCCEtEwggXdMIIDxaADAgECAgh7LJvTFoAy
# mTANBgkqhkiG9w0BAQsFADB8MQswCQYDVQQGEwJVUzEOMAwGA1UECAwFVGV4YXMx
# EDAOBgNVBAcMB0hvdXN0b24xGDAWBgNVBAoMD1NTTCBDb3Jwb3JhdGlvbjExMC8G
# A1UEAwwoU1NMLmNvbSBSb290IENlcnRpZmljYXRpb24gQXV0aG9yaXR5IFJTQTAe
# Fw0xNjAyMTIxNzM5MzlaFw00MTAyMTIxNzM5MzlaMHwxCzAJBgNVBAYTAlVTMQ4w
# DAYDVQQIDAVUZXhhczEQMA4GA1UEBwwHSG91c3RvbjEYMBYGA1UECgwPU1NMIENv
# cnBvcmF0aW9uMTEwLwYDVQQDDChTU0wuY29tIFJvb3QgQ2VydGlmaWNhdGlvbiBB
# dXRob3JpdHkgUlNBMIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEA+Q/d
# oyt9y9Aq/uxnhabnLhu6d+Hj9a+k7PpKXZHEV0drGHdrdvL9k+Q9D8IWngtmw1aU
# nheDhc5W7/IW/QBi9SIJVOhlF05BueBPRpeqG8i4bmJeabFf2yoCfvxsyvNB2O3Q
# 6Pw/YUjtsAMUHRAOSxngu07shmX/NvNeZwILnYZVYf16OO3+4hkAt2+hUGJ1dDyg
# +sglkrRueiLH+B6h47LdkTGrKx0E/6VKBDfphaQzK/3i1lU0fBmkSmjHsqjTt8qh
# k4jrwZe8jPkd2SKEJHTHBD1qqSmTzOu4W+H+XyWqNFjIwSNUnRuYEcM4nH49hmyl
# D0CGfAL0XAJPKMuucZ8POsgz/hElNer8usVgPdl8GNWyqdN1eANyIso6wx/vLOUu
# qfqeLLZRRv2vA9bqYGjqhRY2a4XpHsCz3cQk3IAqgUFtlD7I4MmBQQCeXr9/xQiY
# ohgsQkCz+W84J0tOgPQ9gUfgiHzqHM61dVxRLhwrfxpyKOcAtdF0xtfkn60Hk7ZT
# NTX8N+TD9l0WviFz3pIK+KBjaryWkmo++LxlVZve9Q2JJgT8JRqmJWnLwm3KfOJZ
# X5es6+8uyLzXG1k8K8zyGciTaydjGc/86Sb4ynGbf5P+NGeETpnr/LN4CTNwumam
# du0bc+sapQ3EIhMglFYKTixsTrH9z5wJuqIz7YcCAwEAAaNjMGEwHQYDVR0OBBYE
# FN0ECQei9Xp9UlMSkpXuOIAlDaZZMA8GA1UdEwEB/wQFMAMBAf8wHwYDVR0jBBgw
# FoAU3QQJB6L1en1SUxKSle44gCUNplkwDgYDVR0PAQH/BAQDAgGGMA0GCSqGSIb3
# DQEBCwUAA4ICAQAgGBGUKfsmnRweHnBh8ZVyk3EkrWiTWI4yrxuzcAP8JSt0hZA9
# eGr0uYullzu1GJG7Hqf5QFuR+VWZrx4R0Fwdp2bjsZQHDDI5puobsHnYHZxwROOK
# 3cT5lR+KOEM/AYWlR6c9RrK85SJo93uc2Cw+CiHILTOsv8WBmTF0wXVxxb6x8CNF
# 9J1r/BljnaO8BMYYCyW7U4kPs4BQ3kXuRH+rlHhkmNP2KN2H2HBldPsOuRPrpw9h
# qTKWzN677WNMGLupQPegVG4giHF1GOp6tDRy4CMnd1y2kOqGJUCr7zMPy5+CvqIg
# +/a1LRrmwoWxdA/7yGUCpFIBR91JIsG/2OtrrH7e7GMzFbcjCI/GD41BWt2OxbmP
# 5UU/eNu60htAsf5xTT/ggaK6XrTsFeCT3QgffuFVmQsh3pOeCvvmo0m9NjD+53ey
# oHWXtS2BiBdlIPfakACfyVLMMso1fPU9D9gr1/UmbMkGNJYW6nBZGjJ5eQu2iH8P
# Ukg9v2zYokQu0U63cljTiROV/kSr+NeLG26cvCygW9VqAK9fN+HV+hALmJyG5yaP
# zvDsbopXC4DjTrLAoGNhkLpVaDd0araS25+hhiK2ZScO7LafQmDkZ8K12kELxNOL
# YRu8+h+RK9dEB166KazZxenvU0ha64DxKFghzbAGVfsnP1OQcKkEHlcnuTCCBnIw
# ggRaoAMCAQICCGQzUdPHOJ8IMA0GCSqGSIb3DQEBCwUAMHwxCzAJBgNVBAYTAlVT
# MQ4wDAYDVQQIDAVUZXhhczEQMA4GA1UEBwwHSG91c3RvbjEYMBYGA1UECgwPU1NM
# IENvcnBvcmF0aW9uMTEwLwYDVQQDDChTU0wuY29tIFJvb3QgQ2VydGlmaWNhdGlv
# biBBdXRob3JpdHkgUlNBMB4XDTE2MDYyNDIwNDQzMFoXDTMxMDYyNDIwNDQzMFow
# eDELMAkGA1UEBhMCVVMxDjAMBgNVBAgMBVRleGFzMRAwDgYDVQQHDAdIb3VzdG9u
# MREwDwYDVQQKDAhTU0wgQ29ycDE0MDIGA1UEAwwrU1NMLmNvbSBDb2RlIFNpZ25p
# bmcgSW50ZXJtZWRpYXRlIENBIFJTQSBSMTCCAiIwDQYJKoZIhvcNAQEBBQADggIP
# ADCCAgoCggIBAJ+DE3OqsMZtIcvbi3qHdNBx3I6Xcprku4g0tN2AA8YvRaR0mr8e
# D1Dqnm1485/6USapPZ3RspRXPvs5iRuRK1bvZ8vmC+MOOYzGNfSMPd0l6QGsF0J9
# WBZA3PnVKEQdlWQwYTpk8pfXc0x9eyMCbfN161U9b6otxK++dKxd/mq2/OpceekP
# Q5y1UgUP7z6xsY/QSa2m40IZVD/zLw6hy3z+E/kjOdolHLg+AEo6bzIwN2Qex651
# B9hV0hjJDoq8o1zwfAqnhYHCDq+PmVzTYCW8g1ppHCUTzXL165yAm9wsZ8TdyQmY
# 1XPrxCGj5TKOPi9SmMZgN2SMsm9KVHIYzCeH+s11omMhTLU9ZP0rpptVryZMYLS5
# XP6rQ72t0BNmUB8L0omm/9eABvHDEQIzM2EX91Yfji87aOcV8XdWSimeA9rCKyZh
# MlugVuVJKY02p/XHUqJWAyAvOHiAvfYGrkE0y5RFvZvHiRgfC7r/qa5qQJkT3e9Q
# 3wG68gTW0DHfNDheV1vIOB5W1KxIpu3/+bjBO+3CJL5EYKd3zdU9mFm0Q+qqYH3N
# wuUv8ev11CDVlzRuXQRrBRHS05KMCSdE7U81MUZ+dBkFYuyJ4+ojcJjk0S/UihMY
# RpNl5Vhz00w9J3oiP8P4o1W3+eaHguxFHsVuOnyxTrmraPebY9WRQbypAgMBAAGj
# gfswgfgwDwYDVR0TAQH/BAUwAwEB/zAfBgNVHSMEGDAWgBTdBAkHovV6fVJTEpKV
# 7jiAJQ2mWTAwBggrBgEFBQcBAQQkMCIwIAYIKwYBBQUHMAGGFGh0dHA6Ly9vY3Nw
# cy5zc2wuY29tMBEGA1UdIAQKMAgwBgYEVR0gADATBgNVHSUEDDAKBggrBgEFBQcD
# AzA7BgNVHR8ENDAyMDCgLqAshipodHRwOi8vY3Jscy5zc2wuY29tL3NzbC5jb20t
# cnNhLVJvb3RDQS5jcmwwHQYDVR0OBBYEFFTC/hCVAJPNavXnwNfZsku4jwzjMA4G
# A1UdDwEB/wQEAwIBhjANBgkqhkiG9w0BAQsFAAOCAgEA9Q8mh3CvmaLK9dbJ8I1m
# PTmC04gj2IK/j1SEJ7bTgwfXnieJTYSOVNEg7mBD21dCPMewlfa+zOqjPY5PBsYr
# WYZ/63MbyuVAJuA9b8z2vXHGzX0OIEA51gXSr5QIv3/CUbcrtXuDIfBj2uWc4Wku
# dR1Oy2Ee9aUz3wKdFdntaZNXukZFLoC8Zb7nEj7eR/+QnBCt9laypNT61vwuvJch
# s3aD0pH6BlDRsYAogP7brQ9n7fh93NlwW3q6aLWzSmYXj+fw51fdaf68XuHVjJ8T
# u5WaFft5K4XVbT5nR24bB1z7VEUPFhEuEcOwvLVuHDNXlB7+QjRGjjFQTtszV5X6
# OOTmEturWC5Ft9kiyvRaR0ksKOhPjEI8ZGjp5kOsGZGpxxOCX/xxCje3nVB7PF33
# olKCNeS159MKb2v+jfmk19UdS+d9Ygj42desmUnbtYRBFC72LmCXU0ua/vGIenS6
# nnXp4NqnycwsO3tMCnjPlPc2YLaDPIpUy04NaCqUEXUmFOogN8zreRd2VXhxbeJJ
# ODM32+RsWccjYua8zi5US/1eAyrI3R5LcUTQdT4xYmWLKabtJOF6HYQ0f6QXfLSs
# fT81WMvDvxrdn1RWbUXlU/OIiisxo8o+UNEANOwnCMNnxlzoaL/PLhZluDxm/zuy
# lauajZ3MlPDteFB/7GRHo50wggZ2MIIEXqADAgECAhAhw65pJ8430IAeozVxNmcC
# MA0GCSqGSIb3DQEBCwUAMHgxCzAJBgNVBAYTAlVTMQ4wDAYDVQQIDAVUZXhhczEQ
# MA4GA1UEBwwHSG91c3RvbjERMA8GA1UECgwIU1NMIENvcnAxNDAyBgNVBAMMK1NT
# TC5jb20gQ29kZSBTaWduaW5nIEludGVybWVkaWF0ZSBDQSBSU0EgUjEwHhcNMjMw
# OTA3MTYyNDAzWhcNMjQwOTA2MTYyNDAzWjB/MQswCQYDVQQGEwJVUzEQMA4GA1UE
# CAwHRmxvcmlkYTEaMBgGA1UEBwwRQWx0YW1vbnRlIFNwcmluZ3MxIDAeBgNVBAoM
# F1Byb3ZhbCBUZWNobm9sb2dpZXMgSW5jMSAwHgYDVQQDDBdQcm92YWwgVGVjaG5v
# bG9naWVzIEluYzCCAaIwDQYJKoZIhvcNAQEBBQADggGPADCCAYoCggGBAKr0IQn+
# /jLR4pu0N3TPJaAu31BLTo5myZZxgEqw8daUfcUC3/K20pDCwTzjIEe3Rb/5xrs5
# NQhnlCrrVslrLU2vWlWIuDzrdahSapAH66AbHc9fwsHUCdpWRKglgDoaaAo4KDYS
# yR5BkRqlS4Zc/MbH7+T4hYWrmWGd6DiuQuROdyaTLG6mu+TB7clKMSl0aakOccYl
# 23+1RNPN9QIDv3Hv6V6C6mpqPJ/z7wSnHGH/ELiGcexIGDCoWon2H9/su6nbAn/R
# FR+4iwjGeIa9a7oDFs5e6Nk0ulR/PjMHVGhxMAm1dV2Fsd2lrP1pGA15k8GWi/h+
# V6u5C1toJtnFzy8E+q45U/6zyo2PQd4HlPzw9auzy9l6X4tMtMEQD55G8TR/+VYx
# 7ruJa9VCl477XcOY99oPyaWOYiliU7NbqtYcINHNun6xyDSC3pRidNOMHkovEXmn
# 3sAEYOgDLkNo7sljfXdWd/kawVXEOtZ7WqjdKcysZEdE6MrwGRtruufFdQIDAQAB
# o4IBczCCAW8wDAYDVR0TAQH/BAIwADAfBgNVHSMEGDAWgBRUwv4QlQCTzWr158DX
# 2bJLuI8M4zBYBggrBgEFBQcBAQRMMEowSAYIKwYBBQUHMAKGPGh0dHA6Ly9jZXJ0
# LnNzbC5jb20vU1NMY29tLVN1YkNBLUNvZGVTaWduaW5nLVJTQS00MDk2LVIxLmNl
# cjBRBgNVHSAESjBIMAgGBmeBDAEEATA8BgwrBgEEAYKpMAEDAwEwLDAqBggrBgEF
# BQcCARYeaHR0cHM6Ly93d3cuc3NsLmNvbS9yZXBvc2l0b3J5MBMGA1UdJQQMMAoG
# CCsGAQUFBwMDME0GA1UdHwRGMEQwQqBAoD6GPGh0dHA6Ly9jcmxzLnNzbC5jb20v
# U1NMY29tLVN1YkNBLUNvZGVTaWduaW5nLVJTQS00MDk2LVIxLmNybDAdBgNVHQ4E
# FgQUUP7qIdXcLTXxzeyjVjLWVrTdt+AwDgYDVR0PAQH/BAQDAgeAMA0GCSqGSIb3
# DQEBCwUAA4ICAQCW0L2RCITI/Hk1s2w0KVkTsMkgJ0GEEh19ZTZ3BcnYfaqNxe3u
# OeHCFGxHtB01MU7Ee8zObjiF/vt7wSlb3ln8eDT/VD/lD/ASP5QEAXf2SOXfkoTx
# G2N7gJAlqx5alnND71lb/NcLEF6/1ZAn+w4CSKsyfEHn7uMlP1HRew9dcksjXuFr
# czUnCh3kJp2qfsH7xN3JskuZyctZNHjNDur8XGEBVM3ddTPJDPyBuoV/VN90N559
# yNUn3G/mG8XPukLZBY7B/IO3AcAexhtotOgHgdodZaiW0lCuGdSAmHfMpZWE689G
# vAKNdlWUhLwt8agYN4zw0ObWClDJtwWqjTmv2mW1i4gxDyHZcrPNE70NIhzn536f
# zXMZVWc/5KTlxfB0RQPCSv5YQ/f3AsOeJSR7IMoikqE2GeCwhW0tVzENvXPIy1/G
# bjgQAiG+PKk93VDtgyZ3GFVuUV058olSqtUhJvbuDBSnrR7pdJMEOC4NRZ2rV1LC
# cz6IEzEsbq1hmdLffDm8ZVBwQXcolcf/ExwNMo2RAkI29t+VDrFsVGm+4rdo0cAF
# Yn/DwP1Ku1L4sKxdrrMJcpKi9ucd31sVTP5TbBGbiOhss+pMiY9eC2UIReeIKuZd
# aKh0JeSPvPFQRO9kFbZZXemsk9zq4UdRzAwQk80IL31WpXqwYB4S+dU0jjGCEgww
# ghIIAgEBMIGMMHgxCzAJBgNVBAYTAlVTMQ4wDAYDVQQIDAVUZXhhczEQMA4GA1UE
# BwwHSG91c3RvbjERMA8GA1UECgwIU1NMIENvcnAxNDAyBgNVBAMMK1NTTC5jb20g
# Q29kZSBTaWduaW5nIEludGVybWVkaWF0ZSBDQSBSU0EgUjECECHDrmknzjfQgB6j
# NXE2ZwIwDQYJYIZIAWUDBAIBBQCgga8wFAYKKwYBBAGCNwIBDDEGMAShAoAAMBkG
# CSqGSIb3DQEJAzEMBgorBgEEAYI3AgEEMBwGCisGAQQBgjcCAQsxDjAMBgorBgEE
# AYI3AgEVMC0GCSqGSIb3DQEJNDEgMB4wDQYJYIZIAWUDBAIBBQChDQYJKoZIhvcN
# AQELBQAwLwYJKoZIhvcNAQkEMSIEIEvLBcIyOtrhLahzKVbP6wsCsLH4CQZ5oYwO
# Lz/YJ62oMA0GCSqGSIb3DQEBCwUABIIBgJ8v5zz83joqSGMFdo6FPjUxcq4OZ4IV
# N7Nsyhe5xlYaXqFw5yFEUnny7fSH8T3QnuaxnoEbfiZgbJkbB6aEjJtV+PFXEMiU
# puQXUctllpfl14XP0fo0DbHfWoVbpi/+232UhMcP3rMG9Cuw/TwyQ7ur6RRQ7riU
# 7ilkKpDaFAYxdQX4CYYqrD9/HUp7AmnqQ4w5odwDbiJdtJ43IbPD/iKmsh0kKV5q
# cKPYFtrwbAfafKkEGDGiKLPEN7UJ4Il2sL5wL9rNNmy9LNnha6tviKpjsTCp26Oo
# +c6zo84XBPWcd1YV0x13sFIKsJF6neG8fS4SBR0Yb5JFcIlWFQ4fWhu0I0EjUik2
# sDKvmTvuFdDukZ/wPy8kTkbWxkBRISuxtx9HaSCj1z0PPkKv/jvyNnqlDLr+9LTc
# Ow12sQRffOF4XXq7ZY3ttTNJzXQ2yd/npiS05NmMRtWSk39+Efdh0ui8npPAWuC3
# C+70MAorhOHmG1NqvrYo+MBmEsBjzqhYj6GCDx4wgg8aBgorBgEEAYI3AwMBMYIP
# CjCCDwYGCSqGSIb3DQEHAqCCDvcwgg7zAgEDMQ0wCwYJYIZIAWUDBAIBMH8GCyqG
# SIb3DQEJEAEEoHAEbjBsAgEBBgwrBgEEAYKpMAEDBgEwMTANBglghkgBZQMEAgEF
# AAQg6Upf9ajnixAhXOpLHTsfuYAhKBAJWAAztR+SaUKgAbQCCGl9BXuBVG6mGA8y
# MDI0MDMxODE5NDQ0NlowAwIBAQIGAY5TGGP0oIIMADCCBPwwggLkoAMCAQICEFpa
# rOgaNW60YoaNV33gPccwDQYJKoZIhvcNAQELBQAwczELMAkGA1UEBhMCVVMxDjAM
# BgNVBAgMBVRleGFzMRAwDgYDVQQHDAdIb3VzdG9uMREwDwYDVQQKDAhTU0wgQ29y
# cDEvMC0GA1UEAwwmU1NMLmNvbSBUaW1lc3RhbXBpbmcgSXNzdWluZyBSU0EgQ0Eg
# UjEwHhcNMjQwMjE5MTYxODE5WhcNMzQwMjE2MTYxODE4WjBuMQswCQYDVQQGEwJV
# UzEOMAwGA1UECAwFVGV4YXMxEDAOBgNVBAcMB0hvdXN0b24xETAPBgNVBAoMCFNT
# TCBDb3JwMSowKAYDVQQDDCFTU0wuY29tIFRpbWVzdGFtcGluZyBVbml0IDIwMjQg
# RTEwWTATBgcqhkjOPQIBBggqhkjOPQMBBwNCAASnYXL1MOl6xIMUlgVC49zonduU
# bdkyb0piy2i8t3JlQEwA74cjK8g9mRC8GH1cAAVMIr8M2HdZpVgkV1LXBLB8o4IB
# WjCCAVYwHwYDVR0jBBgwFoAUDJ0QJY6apxuZh0PPCH7hvYGQ9M8wUQYIKwYBBQUH
# AQEERTBDMEEGCCsGAQUFBzAChjVodHRwOi8vY2VydC5zc2wuY29tL1NTTC5jb20t
# dGltZVN0YW1waW5nLUktUlNBLVIxLmNlcjBRBgNVHSAESjBIMDwGDCsGAQQBgqkw
# AQMGATAsMCoGCCsGAQUFBwIBFh5odHRwczovL3d3dy5zc2wuY29tL3JlcG9zaXRv
# cnkwCAYGZ4EMAQQCMBYGA1UdJQEB/wQMMAoGCCsGAQUFBwMIMEYGA1UdHwQ/MD0w
# O6A5oDeGNWh0dHA6Ly9jcmxzLnNzbC5jb20vU1NMLmNvbS10aW1lU3RhbXBpbmct
# SS1SU0EtUjEuY3JsMB0GA1UdDgQWBBRQTySs77U+YxMjCZIm7Lo6luRdIjAOBgNV
# HQ8BAf8EBAMCB4AwDQYJKoZIhvcNAQELBQADggIBAJigjwMAkbyrxGRBf0Ih4r+r
# bCB57lTuwViC6nH2fZSciMogpqSzrSeVZ2eIb5vhj9rT7jqWXZn02Fncs4YTrA1Q
# yxJW36yjC4jl5/bsFCaWuXzGXt2Y6Ifp//A3Z0sNTMWTTBobmceM3sqnovdX9ToR
# FP+29r5yQnPcgRTI2PvrVSqLxY9Eyk9/0cviM3W29YBl080ENblRcu3Y8RsfzRtV
# T/2snuDocRxvRYmd0TPaMgIj2xII651QnPp1hiq9xU0AyovLzbsi5wlR5Ip4i/i8
# +x+HwYJNety5cYtdWJ7uQP6YaZtW/jNoHp76qNftq/IlSx6xEYBRjFBxHSq2fzhU
# Q5oBawk2OsZ2j0wOf7q7AqjCt6t/+fbmWjrAWYWZGj/RLjltqdFPBpIKqdhjVIxa
# GgzVhaE/xHKBg4k4DfFZkBYJ9BWuP93Tm+paWBDwXI7Fg3alGsboErWPWlvwMAmp
# eJUjeKLZY26JPLt9ZWceTVWuIyujerqb5IMmeqLJm5iFq/Qy4YPGyPiolw5w1k9O
# eO4ErmS2FKvk1ejvw4SWR+S1VyWnktY442WaoStxBCCVWZdMWFeB+EpL8uoQNq1M
# hSt/sIUjUudkyZLIbMVQjj7b6gPXnD6mS8FgWiCAhuM1a/hgA+6o1sJWizHdmcpY
# DhyNzorf9KVRE6iR7rcmMIIG/DCCBOSgAwIBAgIQbVIYcIfoI02FYADQgI+TVjAN
# BgkqhkiG9w0BAQsFADB8MQswCQYDVQQGEwJVUzEOMAwGA1UECAwFVGV4YXMxEDAO
# BgNVBAcMB0hvdXN0b24xGDAWBgNVBAoMD1NTTCBDb3Jwb3JhdGlvbjExMC8GA1UE
# AwwoU1NMLmNvbSBSb290IENlcnRpZmljYXRpb24gQXV0aG9yaXR5IFJTQTAeFw0x
# OTExMTMxODUwMDVaFw0zNDExMTIxODUwMDVaMHMxCzAJBgNVBAYTAlVTMQ4wDAYD
# VQQIDAVUZXhhczEQMA4GA1UEBwwHSG91c3RvbjERMA8GA1UECgwIU1NMIENvcnAx
# LzAtBgNVBAMMJlNTTC5jb20gVGltZXN0YW1waW5nIElzc3VpbmcgUlNBIENBIFIx
# MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEArlEQE9L5PCCgIIXeyVAc
# ZMnh/cXpNP8KfzFI6HJaxV6oYf3xh/dRXPu35tDBwhOwPsJjoqgY/Tg6yQGBqt65
# t94wpx0rAgTVgEGMqGri6vCI6rEtSZVy9vagzTDHcGfFDc0Eu71mTAyeNCUhjaYT
# BkyANqp9m6IRrYEXOKdd/eREsqVDmhryd7dBTS9wbipm+mHLTHEFBdrKqKDM3fPY
# dBOro3bwQ6OmcDZ1qMY+2Jn1o0l4N9wORrmPcpuEGTOThFYKPHm8/wfoMocgizTY
# YeDG/+MbwkwjFZjWKwb4hoHT2WK8pvGW/OE0Apkrl9CZSy2ulitWjuqpcCEm2/W1
# RofOunpCm5Qv10T9tIALtQo73GHIlIDU6xhYPH/ACYEDzgnNfwgnWiUmMISaUnYX
# ijp0IBEoDZmGT4RTguiCmjAFF5OVNbY03BQoBb7wK17SuGswFlDjtWN33ZXSAS+i
# 45My1AmCTZBV6obAVXDzLgdJ1A1ryyXz4prLYyfJReEuhAsVp5VouzhJVcE57dRr
# UanmPcnb7xi57VPhXnCuw26hw1Hd+ulK3jJEgbc3rwHPWqqGT541TI7xaldaWDo8
# 5k4lR2bQHPNGwHxXuSy3yczyOg57TcqqG6cE3r0KR6jwzfaqjTvN695GsPAPY/h2
# YksNgF+XBnUD9JBtL4c34AcCAwEAAaOCAYEwggF9MBIGA1UdEwEB/wQIMAYBAf8C
# AQAwHwYDVR0jBBgwFoAU3QQJB6L1en1SUxKSle44gCUNplkwgYMGCCsGAQUFBwEB
# BHcwdTBRBggrBgEFBQcwAoZFaHR0cDovL3d3dy5zc2wuY29tL3JlcG9zaXRvcnkv
# U1NMY29tUm9vdENlcnRpZmljYXRpb25BdXRob3JpdHlSU0EuY3J0MCAGCCsGAQUF
# BzABhhRodHRwOi8vb2NzcHMuc3NsLmNvbTA/BgNVHSAEODA2MDQGBFUdIAAwLDAq
# BggrBgEFBQcCARYeaHR0cHM6Ly93d3cuc3NsLmNvbS9yZXBvc2l0b3J5MBMGA1Ud
# JQQMMAoGCCsGAQUFBwMIMDsGA1UdHwQ0MDIwMKAuoCyGKmh0dHA6Ly9jcmxzLnNz
# bC5jb20vc3NsLmNvbS1yc2EtUm9vdENBLmNybDAdBgNVHQ4EFgQUDJ0QJY6apxuZ
# h0PPCH7hvYGQ9M8wDgYDVR0PAQH/BAQDAgGGMA0GCSqGSIb3DQEBCwUAA4ICAQCS
# GXUNplpCzxkH2fL8lPrAm/AV6USWWi9xM91Q5RN7mZN3D8T7cm1Xy7qmnItFukgd
# tiUzLbQokDJyFTrF1pyLgGw/2hU3FJEywSN8crPsBGo812lyWFgAg0uOwUYw7WJQ
# 1teICycX/Fug0KB94xwxhsvJBiRTpQyhu/2Kyu1Bnx7QQBA1XupcmfhbQrK5O3Q/
# yIi//kN0OkhQEiS0NlyPPYoRboHWC++wogzV6yNjBbKUBrMFxABqR7mkA0x1Kfy3
# Ud08qyLC5Z86C7JFBrMBfyhfPpKVlIiiTQuKz1rTa8ZW12ERoHRHcfEjI1EwwpZX
# XK5J5RcW6h7FZq/cZE9kLRZhvnRKtb+X7CCtLx2h61ozDJmifYvuKhiUg9LLWH0O
# r9D3XU+xKRsRnfOuwHWuhWch8G7kEmnTG9CtD9Dgtq+68KgVHtAWjKk2ui1s1iLY
# AYxnDm13jMZm0KpRM9mLQHBK5Gb4dFgAQwxOFPBslf99hXWgLyYE33vTIi9p0gYq
# GHv4OZh1ElgGsvyKdUUJkAr5hfbDX6pYScJI8v9VNYm1JEyFAV9x4MpskL6kE2Sy
# 8rOqS9rQnVnIyPWLi8N9K4GZvPit/Oy+8nFL6q5kN2SZbox5d69YYFe+rN1sDD4C
# pNWwBBTI/q0V4pkgvhL99IV2XasjHZf4peSrHdL4RjGCAlgwggJUAgEBMIGHMHMx
# CzAJBgNVBAYTAlVTMQ4wDAYDVQQIDAVUZXhhczEQMA4GA1UEBwwHSG91c3RvbjER
# MA8GA1UECgwIU1NMIENvcnAxLzAtBgNVBAMMJlNTTC5jb20gVGltZXN0YW1waW5n
# IElzc3VpbmcgUlNBIENBIFIxAhBaWqzoGjVutGKGjVd94D3HMAsGCWCGSAFlAwQC
# AaCCAWEwGgYJKoZIhvcNAQkDMQ0GCyqGSIb3DQEJEAEEMBwGCSqGSIb3DQEJBTEP
# Fw0yNDAzMTgxOTQ0NDZaMCgGCSqGSIb3DQEJNDEbMBkwCwYJYIZIAWUDBAIBoQoG
# CCqGSM49BAMCMC8GCSqGSIb3DQEJBDEiBCDBLdiqoFe5wtKRLBy1Icah0aRW9C/k
# A65o8ugj971w1TCByQYLKoZIhvcNAQkQAi8xgbkwgbYwgbMwgbAEIJ1xf43CN2Wq
# zl5KsOH1ddeaF9Qc7tj9r+8D/T29iUfnMIGLMHekdTBzMQswCQYDVQQGEwJVUzEO
# MAwGA1UECAwFVGV4YXMxEDAOBgNVBAcMB0hvdXN0b24xETAPBgNVBAoMCFNTTCBD
# b3JwMS8wLQYDVQQDDCZTU0wuY29tIFRpbWVzdGFtcGluZyBJc3N1aW5nIFJTQSBD
# QSBSMQIQWlqs6Bo1brRiho1XfeA9xzAKBggqhkjOPQQDAgRHMEUCIBs0mlIcsqzw
# c42N1NUqMdGFxpySHHpXkrb+NjZ3Ufk1AiEAlUb2P80mn7lUo8xScRsm/jWj0kRq
# eq4VrqJhdruenCY=
# SIG # End signature block