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(ValueFromPipeline, Mandatory, Position = 0)][Alias('Message', 'Text')] [object]$InputObject, [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" } $Text = $InputObject.ToString() # 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' 111 # SIG # Begin signature block # MIIldAYJKoZIhvcNAQcCoIIlZTCCJWECAQExDzANBglghkgBZQMEAgEFADB5Bgor # BgEEAYI3AgEEoGswaTA0BgorBgEEAYI3AgEeMCYCAwEAAAQQH8w7YFlLCE63JNLG # KX7zUQIBAAIBAAIBAAIBAAIBADAxMA0GCWCGSAFlAwQCAQUABCCA4Y4T+hZXeFGl # iHjxRwD4cOXHqlpZuf2t//9vqC7bX6CCEvMwggXdMIIDxaADAgECAgh7LJvTFoAy # 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/7GRHo50wggaYMIIEgKADAgECAhAZraEDEXkpAS0+li99ndmf # MA0GCSqGSIb3DQEBCwUAMHgxCzAJBgNVBAYTAlVTMQ4wDAYDVQQIDAVUZXhhczEQ # MA4GA1UEBwwHSG91c3RvbjERMA8GA1UECgwIU1NMIENvcnAxNDAyBgNVBAMMK1NT # TC5jb20gQ29kZSBTaWduaW5nIEludGVybWVkaWF0ZSBDQSBSU0EgUjEwHhcNMjQw # ODEyMTg1NDI0WhcNMjUwOTA2MTg1NDI0WjB/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 # o4IBlTCCAZEwDAYDVR0TAQH/BAIwADAfBgNVHSMEGDAWgBRUwv4QlQCTzWr158DX # 2bJLuI8M4zB6BggrBgEFBQcBAQRuMGwwSAYIKwYBBQUHMAKGPGh0dHA6Ly9jZXJ0 # LnNzbC5jb20vU1NMY29tLVN1YkNBLUNvZGVTaWduaW5nLVJTQS00MDk2LVIxLmNl # cjAgBggrBgEFBQcwAYYUaHR0cDovL29jc3BzLnNzbC5jb20wUQYDVR0gBEowSDAI # BgZngQwBBAEwPAYMKwYBBAGCqTABAwMBMCwwKgYIKwYBBQUHAgEWHmh0dHBzOi8v # d3d3LnNzbC5jb20vcmVwb3NpdG9yeTATBgNVHSUEDDAKBggrBgEFBQcDAzBNBgNV # HR8ERjBEMEKgQKA+hjxodHRwOi8vY3Jscy5zc2wuY29tL1NTTGNvbS1TdWJDQS1D # b2RlU2lnbmluZy1SU0EtNDA5Ni1SMS5jcmwwHQYDVR0OBBYEFFD+6iHV3C018c3s # o1Yy1la03bfgMA4GA1UdDwEB/wQEAwIHgDANBgkqhkiG9w0BAQsFAAOCAgEACS71 # 1+k93Ta8cJheWDeZC08n9+K/SmnKCPnp9fKAXZGg0pSpDg3aSUJUoAcDURP4pww4 # 7l8bhg2S8mtQIEKjDsMq3zsafUkP/GIfvQDRsSyCj/DWMn8AZ+1bjq/8uK+nlb3n # NiRw1MpG8p3gPgCxmz0u++TYBCKGsgixZ02qJgyEqD8fME+fuVL9qNdbQmawixVf # SuWQoilK0/AVarnTa6X+cr6DGhOE4ry2kIwAVv5/q+PzgxHthqIxI+o7y2KzGcla # w1LKclFsf0CZe9Abk3BMSLY2DnIYhdCurGHJkemFKKR2SocVzSr/UvxWPIk9Zcs4 # mgxFRKrsCZwkRP6AkPYm1EOgbFATgwRCGGxl6bjZ0OxAbW39bpYeH33FGL6nV8yf # WNonuvA/BUAqwoGPsBLWhiXcEVL1SpxRXvIwOv34Y9kZ29f+uxhIrP8S3fWkTz7k # AS6vwXLL7woQsol5vtx1IcjEd9oeh6vlxOOzUFsSRmquH5cdwNeI6+7mz9h7oK7H # 2hijT2kQdOZ9twkEsvvraqGTyn0C2U9ZLoN+V7tUQDh4dBCw8l1k+ynopjnzZ2aG # VdSgq47PYPy3hhHfqyxczg+STd4GppaNh1MZb8/5s70ltLuqLhrzEikI3Q6erstb # 2Ct/iDSjl+H3XK1fA7fZLkH/bbvkxYugmgNp1kgxghHXMIIR0wIBATCBjDB4MQsw # CQYDVQQGEwJVUzEOMAwGA1UECAwFVGV4YXMxEDAOBgNVBAcMB0hvdXN0b24xETAP # BgNVBAoMCFNTTCBDb3JwMTQwMgYDVQQDDCtTU0wuY29tIENvZGUgU2lnbmluZyBJ # bnRlcm1lZGlhdGUgQ0EgUlNBIFIxAhAZraEDEXkpAS0+li99ndmfMA0GCWCGSAFl # AwQCAQUAoHwwEAYKKwYBBAGCNwIBDDECMAAwGQYJKoZIhvcNAQkDMQwGCisGAQQB # gjcCAQQwHAYKKwYBBAGCNwIBCzEOMAwGCisGAQQBgjcCARUwLwYJKoZIhvcNAQkE # MSIEINSVuVzYCNHKrHRv+G0Wk4QamlsrDixsJqqmmDQPNcm6MA0GCSqGSIb3DQEB # CwUABIIBgD7ldxmCQbOfy0/ynOxpiE9Nkq/W9DtDjwrHmi/RaAQltN7p7nO1p2VN # 8PWRegQhLH+TYtdQbbvIkIlqopRtXmoVhpxgZYXdsRtDbAJFZ7Eti59i3Giay8up # uu6jeguOd0SoS9Vfc4FxoLLNvMeNIbYP0Vazhd1zBoV7a5g0djWuMCq4HJFXVS0Z # 3P7sc7DPwn2R7WKZu4MAkmss7/F6ijQfSKGPq+X4xzBat9E5ZcUbor1TSymBqLv+ # 65NIuWvkH1laJIpi9VTxMntnrvBwlqoGlFjq3lB8E7q8we/hv9Y+tZcYYRwndprJ # mYHZmCXjh2CVXHpSK0yvJtQsPfsdQXmcTSNrjVAYpiKCz1gXL+1HNQsfYpMhrgGl # s680J6pdWFmgf3FntGDUCDcuV6RlFkjkbYKb2L5MZNOBPB5QOPwnvv62hEcUvqZn # 32lThsX+qfpX1cuGl2e1GrrcWn4g1RkiUZyOdVGK03v/+4DUbWsKlzfoe4pD7q3I # XgYwtaY7A6GCDx0wgg8ZBgorBgEEAYI3AwMBMYIPCTCCDwUGCSqGSIb3DQEHAqCC # DvYwgg7yAgEDMQ0wCwYJYIZIAWUDBAIBMH8GCyqGSIb3DQEJEAEEoHAEbjBsAgEB # BgwrBgEEAYKpMAEDBgEwMTANBglghkgBZQMEAgEFAAQgdNbLD3y42VxQDBvCBcVJ # EsRle2OhD5SFkGWzt03YHtgCCFHZFH+g4idTGA8yMDI0MTAyMjAwMjA1MlowAwIB # AQIGAZKxmRPfoIIMADCCBPwwggLkoAMCAQICEFparOgaNW60YoaNV33gPccwDQYJ # KoZIhvcNAQELBQAwczELMAkGA1UEBhMCVVMxDjAMBgNVBAgMBVRleGFzMRAwDgYD # VQQHDAdIb3VzdG9uMREwDwYDVQQKDAhTU0wgQ29ycDEvMC0GA1UEAwwmU1NMLmNv # bSBUaW1lc3RhbXBpbmcgSXNzdWluZyBSU0EgQ0EgUjEwHhcNMjQwMjE5MTYxODE5 # WhcNMzQwMjE2MTYxODE4WjBuMQswCQYDVQQGEwJVUzEOMAwGA1UECAwFVGV4YXMx # EDAOBgNVBAcMB0hvdXN0b24xETAPBgNVBAoMCFNTTCBDb3JwMSowKAYDVQQDDCFT # U0wuY29tIFRpbWVzdGFtcGluZyBVbml0IDIwMjQgRTEwWTATBgcqhkjOPQIBBggq # hkjOPQMBBwNCAASnYXL1MOl6xIMUlgVC49zonduUbdkyb0piy2i8t3JlQEwA74cj # K8g9mRC8GH1cAAVMIr8M2HdZpVgkV1LXBLB8o4IBWjCCAVYwHwYDVR0jBBgwFoAU # DJ0QJY6apxuZh0PPCH7hvYGQ9M8wUQYIKwYBBQUHAQEERTBDMEEGCCsGAQUFBzAC # hjVodHRwOi8vY2VydC5zc2wuY29tL1NTTC5jb20tdGltZVN0YW1waW5nLUktUlNB # LVIxLmNlcjBRBgNVHSAESjBIMDwGDCsGAQQBgqkwAQMGATAsMCoGCCsGAQUFBwIB # Fh5odHRwczovL3d3dy5zc2wuY29tL3JlcG9zaXRvcnkwCAYGZ4EMAQQCMBYGA1Ud # JQEB/wQMMAoGCCsGAQUFBwMIMEYGA1UdHwQ/MD0wO6A5oDeGNWh0dHA6Ly9jcmxz # LnNzbC5jb20vU1NMLmNvbS10aW1lU3RhbXBpbmctSS1SU0EtUjEuY3JsMB0GA1Ud # DgQWBBRQTySs77U+YxMjCZIm7Lo6luRdIjAOBgNVHQ8BAf8EBAMCB4AwDQYJKoZI # hvcNAQELBQADggIBAJigjwMAkbyrxGRBf0Ih4r+rbCB57lTuwViC6nH2fZSciMog # pqSzrSeVZ2eIb5vhj9rT7jqWXZn02Fncs4YTrA1QyxJW36yjC4jl5/bsFCaWuXzG # Xt2Y6Ifp//A3Z0sNTMWTTBobmceM3sqnovdX9ToRFP+29r5yQnPcgRTI2PvrVSqL # xY9Eyk9/0cviM3W29YBl080ENblRcu3Y8RsfzRtVT/2snuDocRxvRYmd0TPaMgIj # 2xII651QnPp1hiq9xU0AyovLzbsi5wlR5Ip4i/i8+x+HwYJNety5cYtdWJ7uQP6Y # aZtW/jNoHp76qNftq/IlSx6xEYBRjFBxHSq2fzhUQ5oBawk2OsZ2j0wOf7q7AqjC # t6t/+fbmWjrAWYWZGj/RLjltqdFPBpIKqdhjVIxaGgzVhaE/xHKBg4k4DfFZkBYJ # 9BWuP93Tm+paWBDwXI7Fg3alGsboErWPWlvwMAmpeJUjeKLZY26JPLt9ZWceTVWu # Iyujerqb5IMmeqLJm5iFq/Qy4YPGyPiolw5w1k9OeO4ErmS2FKvk1ejvw4SWR+S1 # VyWnktY442WaoStxBCCVWZdMWFeB+EpL8uoQNq1MhSt/sIUjUudkyZLIbMVQjj7b # 6gPXnD6mS8FgWiCAhuM1a/hgA+6o1sJWizHdmcpYDhyNzorf9KVRE6iR7rcmMIIG # /DCCBOSgAwIBAgIQbVIYcIfoI02FYADQgI+TVjANBgkqhkiG9w0BAQsFADB8MQsw # CQYDVQQGEwJVUzEOMAwGA1UECAwFVGV4YXMxEDAOBgNVBAcMB0hvdXN0b24xGDAW # BgNVBAoMD1NTTCBDb3Jwb3JhdGlvbjExMC8GA1UEAwwoU1NMLmNvbSBSb290IENl # cnRpZmljYXRpb24gQXV0aG9yaXR5IFJTQTAeFw0xOTExMTMxODUwMDVaFw0zNDEx # MTIxODUwMDVaMHMxCzAJBgNVBAYTAlVTMQ4wDAYDVQQIDAVUZXhhczEQMA4GA1UE # BwwHSG91c3RvbjERMA8GA1UECgwIU1NMIENvcnAxLzAtBgNVBAMMJlNTTC5jb20g # VGltZXN0YW1waW5nIElzc3VpbmcgUlNBIENBIFIxMIICIjANBgkqhkiG9w0BAQEF # AAOCAg8AMIICCgKCAgEArlEQE9L5PCCgIIXeyVAcZMnh/cXpNP8KfzFI6HJaxV6o # Yf3xh/dRXPu35tDBwhOwPsJjoqgY/Tg6yQGBqt65t94wpx0rAgTVgEGMqGri6vCI # 6rEtSZVy9vagzTDHcGfFDc0Eu71mTAyeNCUhjaYTBkyANqp9m6IRrYEXOKdd/eRE # sqVDmhryd7dBTS9wbipm+mHLTHEFBdrKqKDM3fPYdBOro3bwQ6OmcDZ1qMY+2Jn1 # o0l4N9wORrmPcpuEGTOThFYKPHm8/wfoMocgizTYYeDG/+MbwkwjFZjWKwb4hoHT # 2WK8pvGW/OE0Apkrl9CZSy2ulitWjuqpcCEm2/W1RofOunpCm5Qv10T9tIALtQo7 # 3GHIlIDU6xhYPH/ACYEDzgnNfwgnWiUmMISaUnYXijp0IBEoDZmGT4RTguiCmjAF # F5OVNbY03BQoBb7wK17SuGswFlDjtWN33ZXSAS+i45My1AmCTZBV6obAVXDzLgdJ # 1A1ryyXz4prLYyfJReEuhAsVp5VouzhJVcE57dRrUanmPcnb7xi57VPhXnCuw26h # w1Hd+ulK3jJEgbc3rwHPWqqGT541TI7xaldaWDo85k4lR2bQHPNGwHxXuSy3yczy # Og57TcqqG6cE3r0KR6jwzfaqjTvN695GsPAPY/h2YksNgF+XBnUD9JBtL4c34AcC # AwEAAaOCAYEwggF9MBIGA1UdEwEB/wQIMAYBAf8CAQAwHwYDVR0jBBgwFoAU3QQJ # B6L1en1SUxKSle44gCUNplkwgYMGCCsGAQUFBwEBBHcwdTBRBggrBgEFBQcwAoZF # aHR0cDovL3d3dy5zc2wuY29tL3JlcG9zaXRvcnkvU1NMY29tUm9vdENlcnRpZmlj # YXRpb25BdXRob3JpdHlSU0EuY3J0MCAGCCsGAQUFBzABhhRodHRwOi8vb2NzcHMu # c3NsLmNvbTA/BgNVHSAEODA2MDQGBFUdIAAwLDAqBggrBgEFBQcCARYeaHR0cHM6 # Ly93d3cuc3NsLmNvbS9yZXBvc2l0b3J5MBMGA1UdJQQMMAoGCCsGAQUFBwMIMDsG # A1UdHwQ0MDIwMKAuoCyGKmh0dHA6Ly9jcmxzLnNzbC5jb20vc3NsLmNvbS1yc2Et # Um9vdENBLmNybDAdBgNVHQ4EFgQUDJ0QJY6apxuZh0PPCH7hvYGQ9M8wDgYDVR0P # AQH/BAQDAgGGMA0GCSqGSIb3DQEBCwUAA4ICAQCSGXUNplpCzxkH2fL8lPrAm/AV # 6USWWi9xM91Q5RN7mZN3D8T7cm1Xy7qmnItFukgdtiUzLbQokDJyFTrF1pyLgGw/ # 2hU3FJEywSN8crPsBGo812lyWFgAg0uOwUYw7WJQ1teICycX/Fug0KB94xwxhsvJ # BiRTpQyhu/2Kyu1Bnx7QQBA1XupcmfhbQrK5O3Q/yIi//kN0OkhQEiS0NlyPPYoR # boHWC++wogzV6yNjBbKUBrMFxABqR7mkA0x1Kfy3Ud08qyLC5Z86C7JFBrMBfyhf # PpKVlIiiTQuKz1rTa8ZW12ERoHRHcfEjI1EwwpZXXK5J5RcW6h7FZq/cZE9kLRZh # vnRKtb+X7CCtLx2h61ozDJmifYvuKhiUg9LLWH0Or9D3XU+xKRsRnfOuwHWuhWch # 8G7kEmnTG9CtD9Dgtq+68KgVHtAWjKk2ui1s1iLYAYxnDm13jMZm0KpRM9mLQHBK # 5Gb4dFgAQwxOFPBslf99hXWgLyYE33vTIi9p0gYqGHv4OZh1ElgGsvyKdUUJkAr5 # hfbDX6pYScJI8v9VNYm1JEyFAV9x4MpskL6kE2Sy8rOqS9rQnVnIyPWLi8N9K4GZ # vPit/Oy+8nFL6q5kN2SZbox5d69YYFe+rN1sDD4CpNWwBBTI/q0V4pkgvhL99IV2 # XasjHZf4peSrHdL4RjGCAlcwggJTAgEBMIGHMHMxCzAJBgNVBAYTAlVTMQ4wDAYD # VQQIDAVUZXhhczEQMA4GA1UEBwwHSG91c3RvbjERMA8GA1UECgwIU1NMIENvcnAx # LzAtBgNVBAMMJlNTTC5jb20gVGltZXN0YW1waW5nIElzc3VpbmcgUlNBIENBIFIx # AhBaWqzoGjVutGKGjVd94D3HMAsGCWCGSAFlAwQCAaCCAWEwGgYJKoZIhvcNAQkD # MQ0GCyqGSIb3DQEJEAEEMBwGCSqGSIb3DQEJBTEPFw0yNDEwMjIwMDIwNTJaMCgG # CSqGSIb3DQEJNDEbMBkwCwYJYIZIAWUDBAIBoQoGCCqGSM49BAMCMC8GCSqGSIb3 # DQEJBDEiBCAYuOrrFmbC35SpV4zZzL5Ddq3yjGCUKpuB4LIMEHrELDCByQYLKoZI # hvcNAQkQAi8xgbkwgbYwgbMwgbAEIJ1xf43CN2Wqzl5KsOH1ddeaF9Qc7tj9r+8D # /T29iUfnMIGLMHekdTBzMQswCQYDVQQGEwJVUzEOMAwGA1UECAwFVGV4YXMxEDAO # BgNVBAcMB0hvdXN0b24xETAPBgNVBAoMCFNTTCBDb3JwMS8wLQYDVQQDDCZTU0wu # Y29tIFRpbWVzdGFtcGluZyBJc3N1aW5nIFJTQSBDQSBSMQIQWlqs6Bo1brRiho1X # feA9xzAKBggqhkjOPQQDAgRGMEQCIGgAagkOC+IHNY1/NzZUzGdi1uQZVuuA/NJU # TFZGHmkjAiBAAAF/y+ou93X3e7miLQdUjl6PQFl1tHpsQTRlQUqdFg== # SIG # End signature block |