RecordCommands.ps1
#requires -Version 5.1 function Get-KeeperRecord { <# .Synopsis Get Keeper Records .Parameter Uid Record UID .Parameter Filter Return matching records only #> [CmdletBinding()] [OutputType([KeeperSecurity.Vault.KeeperRecord[]])] Param ( [string] $Uid, [string] $Filter ) [KeeperSecurity.Vault.VaultOnline]$vault = getVault if ($Uid) { [KeeperSecurity.Vault.KeeperRecord] $record = $null if ($vault.TryGetKeeperRecord($uid, [ref]$record)) { $record } } else { foreach ($record in $vault.KeeperRecords) { if ($Filter) { $match = $($record.Uid, $record.TypeName, $record.Title, $record.Notes) | Select-String $Filter | Select-Object -First 1 if (-not $match) { continue } } $record } } } New-Alias -Name kr -Value Get-KeeperRecord function Copy-KeeperToClipboard { <# .Synopsis Copy record password to clipboard or output .Parameter Record Record UID or any object containing property Uid .Parameter Field Record field to copy to clipboard. Record password is default. .Parameter Output Password output destination. Clipboard is default. Use "Stdout" for scripting #> [CmdletBinding()] Param ( [Parameter(Mandatory = $true, ValueFromPipeline = $true)] $Record, [string] [ValidateSet('Login' , 'Password', 'URL')] $Field = 'Password', [string] [ValidateSet('Clipboard' , 'Stdout')] $Output = 'Clipboard' ) Process { if ($Record -is [Array]) { if ($Record.Count -ne 1) { Write-Error -Message 'Only one record is expected' return } $Record = $Record[0] } [KeeperSecurity.Vault.VaultOnline]$vault = getVault $uid = $null if ($Record -is [String]) { $uid = $Record } elseif ($null -ne $Record.Uid) { $uid = $Record.Uid } $found = $false if ($uid) { [KeeperSecurity.Vault.KeeperRecord] $rec = $null if (-not $vault.TryGetKeeperRecord($uid, [ref]$rec)) { $entries = Get-KeeperChildItem -Filter $uid -ObjectType Record if ($entries.Uid) { $vault.TryGetRecord($entries[0].Uid, [ref]$rec) | Out-Null } } if ($rec) { $found = $true $value = '' if ($rec -is [KeeperSecurity.Vault.PasswordRecord]) { switch ($Field) { 'Login' { $value = $rec.Login } 'Password' { $value = $rec.Password } 'URL' { $value = $rec.Link } } } elseif ($rec -is [KeeperSecurity.Vault.TypedRecord]) { $fieldType = '' switch ($Field) { 'Login' { $fieldType = 'login' } 'Password' { $fieldType = 'password' } 'URL' { $fieldType = 'url' } } if ($fieldType) { $recordField = $rec.Fields | Where-Object FieldName -eq $fieldType | Select-Object -First 1 if (-not $recordField) { $recordField = $rec.Custom | Where-Object FieldName -eq $fieldType | Select-Object -First 1 } if ($recordField) { $value = $recordField.ObjectValue } } } if ($value) { if ($Output -eq 'Stdout') { $value } else { if ([System.Threading.Thread]::CurrentThread.GetApartmentState() -eq [System.Threading.ApartmentState]::MTA) { powershell -sta "Set-Clipboard -Value '$value'" } else { Set-Clipboard -Value $value } Write-Output "Copied to clipboard: $Field for $($rec.Title)" } if ($Field -eq 'Password') { $vault.AuditLogRecordCopyPassword($rec.Uid) } } else { Write-Output "Record $($rec.Title) has no $Field" } } } if (-not $found) { Write-Error -Message "Cannot find a Keeper record: $Record" } } } New-Alias -Name kcc -Value Copy-KeeperToClipboard function Get-KeeperPasswordVisible { <# .Synopsis Show/hide secret fields #> if ($Script:PasswordVisible) { $true } else { $false } } function Set-KeeperPasswordVisible { [CmdletBinding()] [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseShouldProcessForStateChangingFunctions", "")] Param ([switch] $Visible) $Script:PasswordVisible = $Visible.IsPresent } function Show-TwoFactorCode { [CmdletBinding()] Param ( [Parameter(Mandatory = $true, ValueFromPipeline = $true)] $Records ) Begin { [KeeperSecurity.Vault.VaultOnline]$vault = getVault $totps = @() } Process { foreach ($r in $Records) { $uid = $null if ($r -is [String]) { $uid = $r } elseif ($null -ne $r.Uid) { $uid = $r.Uid } if ($uid) { [KeeperSecurity.Vault.KeeperRecord] $rec = $null if ($vault.TryGetKeeperRecord($uid, [ref]$rec)) { if ($rec -is [KeeperSecurity.Vault.PasswordRecord]) { if ($rec.ExtraFields) { foreach ($ef in $rec.ExtraFields) { if ($ef.FieldType -eq 'totp') { $totps += [PSCustomObject]@{ RecordUid = $rec.Uid Title = $rec.Title TotpData = $ef.Custom['data'] } } } } } elseif ($rec -is [KeeperSecurity.Vault.TypedRecord]) { $recordTypeField = New-Object KeeperSecurity.Vault.RecordTypeField 'oneTimeCode', $null [KeeperSecurity.Vault.ITypedField]$recordField = $null if ([KeeperSecurity.Vault.VaultDataExtensions]::FindTypedField($rec, $recordTypeField, [ref]$recordField)) { $data = $recordField.TypedValue if ($data) { $totps += [PSCustomObject]@{ RecordUid = $rec.Uid Title = $rec.Title TotpData = $data } } } } } } } } End { $output = @() foreach ($totp in $totps) { [Tuple[string, int, int]]$code = [KeeperSecurity.Utils.CryptoUtils]::GetTotpCode($totp.TotpData) if ($code) { $output += [PSCustomObject]@{ PSTypeName = 'TOTP.Codes' RecordTitle = $totp.Title TOTPCode = $code.Item1 Elapsed = $code.Item2 Left = $code.Item3 - $code.Item2 } } } $output | Format-Table } } New-Alias -Name 2fa -Value Show-TwoFactorCode $Keeper_RecordTypeNameCompleter = { param($commandName, $parameterName, $wordToComplete, $commandAst, $fakeBoundParameters) $result = @() [KeeperSecurity.Vault.VaultOnline]$vault = $Script:Context.Vault if ($vault) { $toComplete = $wordToComplete + '*' foreach ($rt in $vault.RecordTypes) { if ($rt.Name -like $toComplete) { $result += $rt.Name } } } if ($result.Count -gt 0) { return $result } else { return $null } } function Add-KeeperRecord { <# .Synopsis Creates or Modifies a Keeper record in the current folder. .Parameter Uid Record UID. If provided the existing record to be updated. Otherwise record is added. .Parameter RecordType Record Type (if account supports record types). .Parameter Title Record Title. Mandatory field for added record. .Parameter Notes Record Notes. .Parameter GeneratePassword Generate random password. .Parameter Fields A list of record Fields. See DESCRIPTION .DESCRIPTION Record field format [NAME=VALUE] or [-name $value] if field starts with `-` then the following parameter contains field value otherwise NAME=VALUE pattern is assumed Predefined fields are login Login Name password Password url Web Address Any other name is added to Custom Fields Typed records only: A field has [TYPE.LABEL] format. A TYPE or LABEL can be omitted. Field Type Description Value Type Examples =========== ================== ========== ===================================== date Unix epoch time. integer 1668639533000 | 03/23/2022 host host name / port object @{hostName=''; port=''} 192.168.1.2:4321 address Address object @{street1=""; street2=""; city=""; state=""; zip=""; country=""} 123 Main St, SmallTown, CA 12345, USA phone Phone object @{region=""; number=""; ext=""; type=""} Mobile: US (555)555-1234 name Person name object @{first=""; middle=""; last=""} Doe, John Jr. | Jane Doe paymentCard Payment Card object @{cardNumber=""; cardExpirationDate=""; cardSecurityCode=""} 4111111111111111 04/2026 123 bankAccount Bank Account object @{accountType=""; routingNumber=""; accountNumber=""} Checking: 123456789 987654321 keyPair Key Pair object @{publicKey=""; privateKey=""} oneTimeCode TOTP URL string otpauth://totp/Example?secret=JBSWY3DPEHPK3PXP note Masked multiline text string multiline Multiline text string secret Masked text string login Login string email Email string 'name@company.com' password Password string url URL string https://google.com/ text Free form text string This field type generally has a label .EXAMPLE PS> $password = Read-Host -AsSecureString -Prompt "Enter Password" PS> Add-KeeperRecord -Title "New Record" login=username -password $password .EXAMPLE PS> $h = @{hostName='google.com'; port='123'} PS> Add-KeeperRecord -Uid ... -"host.Google Host" $h .EXAMPLE PS> Add-KeeperRecord -Uid ... "host.Google Host=google.com:123" .EXAMPLE PS> $rsa = [System.Security.Cryptography.RSA]::Create(2048) PS> $privateKey = [Convert]::ToBase64String($rsa.ExportPkcs8PrivateKey()) PS> $publicKey = [Convert]::ToBase64String($rsa.ExportRSAPublicKey()) PS> $keyPair = @{privateKey=$privateKey; publicKey=$publicKey} PS> Add-KeeperRecord -Uid ... -keyPair $keyPair #> [CmdletBinding(DefaultParameterSetName = 'add')] Param ( [Parameter()] [switch] $GeneratePassword, [Parameter(ParameterSetName = 'add')] [string] $RecordType, [Parameter(ParameterSetName = 'add')] [string] $Folder, [Parameter(ParameterSetName = 'edit', Mandatory = $True)] [string] $Uid, [Parameter()] [string] $Title, [Parameter()] [string] $Notes, [Parameter(ValueFromRemainingArguments = $true)] $Extra ) Begin { [KeeperSecurity.Vault.VaultOnline]$vault = getVault [KeeperSecurity.Vault.KeeperRecord]$record = $null $fields = @{} $fieldName = $null foreach ($var in $Extra) { if ($var -match '^-') { $fieldName = $var.Substring(1) if ($var -match ':$') { $fieldName = $fieldName.Substring(0, $fieldName.Length - 1) } } elseif ($null -ne $fieldName) { $fields[$fieldName] = $var $fieldName = $null } else { if ($var -match '^([^=]+)=(.*)?') { $n = $Matches[1].Trim() $v = $Matches[2].Trim() if ($n -and $v) { $fields[$n] = $v } } } } } Process { if ($Uid) { if (-not $vault.TryGetKeeperRecord($Uid, [ref]$record)) { $objs = Get-KeeperChildItem -ObjectType Record | Where-Object Name -eq $Uid if ($objs.Length -gt 1) { $vault.TryGetKeeperRecord($objs[0].Uid, [ref]$record) } } if (-not $record) { Write-Error -Message "Record `"$Uid`" not found" -ErrorAction Stop return } } else { if (!$Title) { Write-Error -Message "-Title parameter is required" -ErrorAction Stop } if (-not $RecordType -or $RecordType -eq 'legacy') { $record = New-Object KeeperSecurity.Vault.PasswordRecord } else { $record = New-Object KeeperSecurity.Vault.TypedRecord $RecordType [KeeperSecurity.Utils.RecordTypesUtils]::AdjustTypedRecord($vault, $record) } } if ($Title) { $record.Title = $Title } if ($Notes -is [string]) { if ($Notes.Length -gt 0 -and $Notes[0] -eq '+') { $Notes = $record.Notes + "`n" + $Notes.Substring(1) } elseif ($Notes -eq '-') { $Notes = '' } $record.Notes = $Notes } if ($GeneratePassword.IsPresent) { $fields['password'] = [Keepersecurity.Utils.CryptoUtils]::GenerateUid() } foreach ($fieldName in $fields.Keys) { $fieldValue = $fields[$fieldName] $fieldLabel = '' if ($fieldName -match '^([^.]+)(\..+)?$') { if ($Matches[1] -and $Matches[2]) { $fieldName = $Matches[1].Trim() $fieldLabel = $Matches[2].Trim().Substring(1) } } if ($fieldName -match '^\$') { $fieldName = $fieldName.Substring(1).Trim() } if ($fieldValue -is [securestring]) { $fieldValue = (New-Object PSCredential 'a', $fieldValue).GetNetworkCredential().Password } if ($record -is [KeeperSecurity.Vault.PasswordRecord]) { switch ($fieldName) { 'login' { $record.Login = $fieldValue } 'password' { $record.Password = $fieldValue } 'url' { $record.Link = $fieldValue } Default { if ($fieldLabel) { if ($fieldName -eq 'text') { $fieldName = $fieldLabel } else { $fieldName = "${fieldName}:${fieldLabel}" } } if ($fieldValue) { $record.SetCustomField($fieldName, $fieldValue) | Out-Null } else { $record.DeleteCustomField($fieldName) | Out-Null } } } } elseif ($record -is [KeeperSecurity.Vault.TypedRecord]) { if (-not $fieldLabel) { [KeeperSecurity.Vault.RecordField]$recordField = $null if (-not [KeeperSecurity.Vault.RecordTypesConstants]::TryGetRecordField($fieldName, [ref]$recordField)) { $fieldLabel = $fieldName $fieldName = 'text' } } $recordTypeField = New-Object KeeperSecurity.Vault.RecordTypeField $fieldName, $fieldLabel [KeeperSecurity.Vault.ITypedField]$typedField = $null if ([KeeperSecurity.Vault.VaultDataExtensions]::FindTypedField($record, $recordTypeField, [ref]$typedField)) { } else { if ($fieldValue) { $typedField = [KeeperSecurity.Vault.VaultDataExtensions]::CreateTypedField($fieldName, $fieldLabel) if ($typedField) { $record.Custom.Add($typedField) } } } if ($typedField) { if ($fieldValue) { $typedField.ObjectValue = $fieldValue } else { $typedField.DeleteValueAt(0) } } } } } End { if ($record.Uid) { $task = $vault.UpdateRecord($record) } else { $folderUid = $Script:Context.CurrentFolder if ($Folder) { $folderNode = resolveFolderNode $vault $Folder $folderUid = $folderNode.FolderUid } $task = $vault.CreateRecord($record, $folderUid) } $task.GetAwaiter().GetResult() } } New-Alias -Name kadd -Value Add-KeeperRecord Register-ArgumentCompleter -CommandName Add-KeeperRecord -ParameterName Folder -ScriptBlock $Keeper_FolderPathRecordCompleter Register-ArgumentCompleter -CommandName Add-KeeperRecord -ParameterName RecordType -ScriptBlock $Keeper_RecordTypeNameCompleter function Remove-KeeperRecord { <# .Synopsis Removes Keeper record. .Parameter Name Folder name or Folder UID #> [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseShouldProcessForStateChangingFunctions", "")] [CmdletBinding(DefaultParameterSetName = 'Default')] Param ( [Parameter(Position = 0, Mandatory = $true)][string] $Name ) [KeeperSecurity.Vault.VaultOnline]$vault = getVault $folderUid = $null $recordUid = $null [KeeperSecurity.Vault.KeeperRecord] $record = $null if ($vault.TryGetKeeperRecord($Name, [ref]$record)) { $recordUid = $record.Uid if (-not $vault.RootFolder.Records.Contains($recordUid)) { foreach ($f in $vault.Folders) { if ($f.Records.Contains($recordUid)) { $folderUid = $f.FolderUid break } } } } if (-not $recordUid) { $objs = Get-KeeperChildItem -ObjectType Record | Where-Object Name -eq $Name if (-not $objs) { Write-Error -Message "Record `"$Name`" does not exist" return } if ($objs.Length -gt 1) { Write-Error -Message "There are more than one records with name `"$Name`". Use Record UID do delete the correct one." return } $recordUid = $objs[0].Uid $folderUid = $Script:Context.CurrentFolder } $recordPath = New-Object KeeperSecurity.Vault.RecordPath $recordPath.RecordUid = $recordUid $recordPath.FolderUid = $folderUid $task = $vault.DeleteRecords(@($recordPath)) $task.GetAwaiter().GetResult() | Out-Null } New-Alias -Name kdel -Value Remove-KeeperRecord function Move-RecordToFolder { <# .Synopsis Moves records to Folder. .Parameter Record Record UID, Path or any object containing property Uid. .Parameter Folder Folder Name, Path, or UID #> [CmdletBinding()] Param ( [Parameter(Mandatory = $true, ValueFromPipeline = $true)]$Records, [Parameter(Position = 0, Mandatory = $true)][string]$Folder, [Parameter()][switch]$Link ) Begin { [KeeperSecurity.Vault.VaultOnline]$vault = getVault $folderNode = resolveFolderNode $vault $Folder $sourceRecords = @() } Process { foreach ($r in $Records) { if ($null -ne $r.Uid) { $r = $r.Uid } [KeeperSecurity.Vault.FolderNode]$folder = $null [KeeperSecurity.Vault.KeeperRecord]$record = $null if ($vault.TryGetKeeperRecord($r, [ref]$record)) { if ($record -is [KeeperSecurity.Vault.PasswordRecord] -or $record -is [KeeperSecurity.Vault.TypedRecord]) { if ($folderNode.FolderUid -and $vault.RootFolder.Records.Contains($record.Uid)) { $folder = $vault.RootFolder } else { foreach ($fol in $vault.Folders) { if ($fol.FolderUid -eq $folderNode.FolderUid) { continue } if ($fol.Records.Contains($record.Uid)) { $folder = $fol break } } } } else { Write-Error "`$r`" record type is not supported." -ErrorAction Stop } } else { [KeeperSecurity.Vault.FolderNode]$fol = $null if (-not $vault.TryGetFolder($Script:Context.CurrentFolder, [ref]$fol)) { $fol = $vault.RootFolder } $comps = splitKeeperPath $r $folder, $rest = parseKeeperPath $comps $vault $fol if (-not $rest) { Write-Error "`"$r`" should be a record" -ErrorAction Stop } [KeeperSecurity.Vault.KeeperRecord]$rec = $null foreach ($recordUid in $folder.Records) { if ($vault.TryGetKeeperRecord($recordUid, [ref]$rec)) { if ($rec.Title -eq $rest) { if ($rec -is [KeeperSecurity.Vault.PasswordRecord] -or $rec -is [KeeperSecurity.Vault.TypedRecord]) { $record = $rec break } } } } } if (-not $record -or -not $folder) { Write-Error "Record `"$r`" cannot be found" -ErrorAction Stop } $rp = New-Object KeeperSecurity.Vault.RecordPath $rp.RecordUid = $record.Uid $rp.FolderUid = $folder.FolderUid $sourceRecords += $rp } } End { if (-not $sourceRecords) { Write-Error "There are no records to move" -ErrorAction Stop } $vault.MoveRecords($sourceRecords, $folderNode.FolderUid, $Link.IsPresent).GetAwaiter().GetResult() | Out-Null $vault.ScheduleSyncDown([System.TimeSpan]::FromSeconds(0)).GetAwaiter().GetResult() | Out-Null } } New-Alias -Name kmv -Value Move-RecordToFolder Register-ArgumentCompleter -CommandName Move-RecordToFolder -ParameterName Folder -ScriptBlock $Keeper_FolderPathRecordCompleter function Get-KeeperRecordType { <# .Synopsis Get Record/Field Type Information .Parameter ShowFields Show Field Types .Parameter Name Record Type Name #> [CmdletBinding()] Param ( [switch] $ShowFields, [Parameter(Position = 0, Mandatory = $false)][string] $Name ) [KeeperSecurity.Vault.VaultOnline]$vault = getVault if ($ShowFields.IsPresent) { [KeeperSecurity.Vault.RecordTypesConstants]::RecordFields | Where-Object { -not $Name -or $_.Name -eq $Name } | Sort-Object Name } else { $vault.RecordTypes | Where-Object { -not $Name -or $_.Name -eq $Name } | Sort-Object Name } } New-Alias -Name krti -Value Get-KeeperRecordType function resolveFolderNode { Param ([KeeperSecurity.Vault.VaultOnline]$vault, $path) [KeeperSecurity.Vault.FolderNode]$folder = $null if (-not $vault.TryGetFolder($path, [ref]$folder)) { if (-not $vault.TryGetFolder($Script:Context.CurrentFolder, [ref]$folder)) { $folder = $vault.RootFolder } $comps = splitKeeperPath $path $folder, $rest = parseKeeperPath $comps $vault $folder if ($rest) { Write-Error "Folder $path not found" -ErrorAction Stop } } $folder } function New-KeeperRecordType { <# .SYNOPSIS Add a new custom Keeper Record Type. .DESCRIPTION Adds a custom record type to the Vault. Record type definition can be passed as a JSON string or a file reference prefixed with '@'. .PARAMETER Data Required. Record type definition as a JSON string or file reference (prefix with '@' for file path). .EXAMPLE New-KeeperRecordType -Data '@("C:\record_type.json")' .EXAMPLE New-KeeperRecordType -Data '{\"$id\":\"myCustomType_dotnet_test\",\"description\":\"My custom record\",\"fields\":[{\"$ref\":\"login\"},{\"$ref\":\"password\"}]}' #> [CmdletBinding()] Param ( [Parameter(Mandatory = $true)][string] $Data ) [KeeperSecurity.Vault.VaultOnline]$vault = getVault if ($Data.StartsWith("@")) { $path = $Data.TrimStart("@").Trim('"') try { $fullPath = [System.IO.Path]::GetFullPath($path) } catch { Write-Error "Invalid file path: $path" -ErrorAction Stop } if (-not (Test-Path $fullPath)) { Write-Error "File not found: $fullPath" -ErrorAction Stop } $Data = Get-Content $fullPath -Raw } try { $recordTypeId = $vault.AddRecordType($Data).GetAwaiter().GetResult() Write-Host "Created Record Type ID: $recordTypeId" } catch { Write-Error "Error adding record type: $($_.Exception.Message)" -ErrorAction Stop } } function Edit-KeeperRecordType { <# .SYNOPSIS Update an existing custom Keeper Record Type. .DESCRIPTION Updates a custom record type in the Vault. The updated record type definition is passed as a JSON string or file reference prefixed with '@'. The record type ID to update must be provided separately. .PARAMETER RecordTypeId Required. The UID of the record type to update. .PARAMETER Data Required. Record type definition as a JSON string or file reference (prefix with '@' for file path). .EXAMPLE Edit-KeeperRecordType -RecordTypeId '22500' -Data '@("C:\record_type_update.json")' .EXAMPLE Edit-KeeperRecordType -RecordTypeId '22500' -Data '{\"$id\":\"myCustomType_dotnet_test\",\"description\":\"My custom record\",\"fields\":[{\"$ref\":\"login\"},{\"$ref\":\"password\"}]}' #> [CmdletBinding()] Param ( [Parameter(Mandatory = $true)][string] $RecordTypeId, [Parameter(Mandatory = $true)][string] $Data ) [KeeperSecurity.Vault.VaultOnline]$vault = getVault if ($Data.StartsWith("@")) { $path = $Data.TrimStart("@").Trim('"') try { $fullPath = [System.IO.Path]::GetFullPath($path) } catch { Write-Error "Invalid file path: $path" -ErrorAction Stop } if (-not (Test-Path $fullPath)) { Write-Error "File not found: $fullPath" -ErrorAction Stop } $Data = Get-Content $fullPath -Raw } try { $result = $vault.UpdateRecordTypeAsync($RecordTypeId, $Data).GetAwaiter().GetResult() Write-Host "Updated Record Type ID: $result" } catch { Write-Error "Error updating record type: $($_.Exception.Message)" -ErrorAction Stop } } function Remove-KeeperRecordType { <# .SYNOPSIS Delete a custom Keeper Record Type by its ID. .DESCRIPTION Removes a custom record type from the Vault. Only the Record Type ID is required. .PARAMETER RecordTypeId Required. The UID of the record type to delete. .EXAMPLE Remove-KeeperRecordType -RecordTypeId <recordTypeId> #> [CmdletBinding(SupportsShouldProcess = $true, ConfirmImpact = "High")] Param ( [Parameter(Mandatory = $true)][string] $RecordTypeId ) [KeeperSecurity.Vault.VaultOnline]$vault = getVault try { if ($PSCmdlet.ShouldProcess("RecordTypeId '$RecordTypeId'", "Delete record type")) { $result = $vault.DeleteRecordTypeAsync($RecordTypeId).GetAwaiter().GetResult() Write-Host "Deleted Record Type ID: $result" } } catch { Write-Error "Error deleting record type: $($_.Exception.Message)" -ErrorAction Stop } } function Test-RecordTypeFile { param ( [Parameter(Mandatory = $true)] [string]$FilePath ) if (-not (Test-Path $FilePath)) { throw "Record type file not found: $FilePath" } $json = Get-Content $FilePath -Raw | ConvertFrom-Json if (-not $json.record_types) { throw "Missing 'record_types' array in the file." } return $json.record_types } function Get-ExistingRecordTypes { [KeeperSecurity.Vault.VaultOnline]$vault = getVault $types = $vault.RecordTypes $map = @{} foreach ($type in $types) { if ($type.Name) { $map[$type.Name] = $type } } return $map } function ConvertTo-CustomRecordTypeObject { param ( [Parameter(Mandatory = $true)] $InputRecordType ) $fields = @() foreach ($f in $InputRecordType.fields) { $field = @{ '$ref' = $f.'$type' } if ($f.label) { $field["label"] = $f.label } if ($f.Required -eq $true) { $field["required"] = $true } $fields += ,$field } return @{ '$id' = $InputRecordType.record_type_name description = $InputRecordType.description categories = $InputRecordType.categories fields = $fields } } function Import-KeeperRecordTypes { <# .SYNOPSIS Imports custom record types into Keeper from a JSON file. .DESCRIPTION This command reads a JSON file containing custom record type definitions and uploads new record types to the Keeper vault. Existing record types (based on name) are skipped. It reports the number of successfully uploaded, skipped, and failed imports. .PARAMETER FilePath The full path to the JSON file containing record type definitions. The file must contain a `record_types` array at the root. .EXAMPLE Import-KeeperRecordTypes -FilePath "C:\configs\custom_record_types.json" Loads and uploads custom record types from the specified file. .EXAMPLE Import-KeeperRecordTypes -FilePath "./data/types.json" Works with relative paths as well. .OUTPUTS [System.Collections.Generic.List[string]] Returns a list of successfully uploaded record type IDs. .NOTES Requires the Add-KeeperRecordType and Get-KeeperRecordTypes functions to be available in the current session. #> [CmdletBinding()] param ( [Parameter(Mandatory = $true, Position = 0, HelpMessage = "Path to the JSON file containing record types.")] [ValidateNotNullOrEmpty()] [string]$FilePath ) $uploadedRecordTypeIds = @() $existingRecordTypeIds = @() $failedRecordTypeIds = @() $uploadCount = 0 try { $newRecordTypes = Test-RecordTypeFile -FilePath $FilePath $existingTypes = Get-ExistingRecordTypes foreach ($recordType in $newRecordTypes) { if ($existingTypes.ContainsKey($recordType.record_type_name)) { $existingRecordTypeIds += $recordType.record_type_name continue } try { $parsed = ConvertTo-CustomRecordTypeObject -InputRecordType $recordType $json = $parsed | ConvertTo-Json -Depth 10 -Compress $recordTypeId = New-KeeperRecordType -Data $json $uploadedRecordTypeIds += $recordTypeId $uploadCount++ } catch { Write-Warning "Failed to upload record type '$($recordType.record_type_name)': $_" $failedRecordTypeIds += $recordType.record_type_name continue } } Write-Host "Record types loaded: $uploadCount" Write-Host "Existing Record Types (skipped): $($existingRecordTypeIds -join ', ')" Write-Host "Failed Record Types: $($failedRecordTypeIds -join ', ')" } catch { throw "Import failed: $_" } return $uploadedRecordTypeIds } # SIG # Begin signature block # MIIngQYJKoZIhvcNAQcCoIIncjCCJ24CAQExDzANBglghkgBZQMEAgEFADB5Bgor # BgEEAYI3AgEEoGswaTA0BgorBgEEAYI3AgEeMCYCAwEAAAQQH8w7YFlLCE63JNLG # KX7zUQIBAAIBAAIBAAIBAAIBADAxMA0GCWCGSAFlAwQCAQUABCAJrik7/TnXxzu2 # zT6LsLcRFFyzYuoG6jVpo71EiGmyvqCCIQQwggWNMIIEdaADAgECAhAOmxiO+dAt # 5+/bUOIIQBhaMA0GCSqGSIb3DQEBDAUAMGUxCzAJBgNVBAYTAlVTMRUwEwYDVQQK # EwxEaWdpQ2VydCBJbmMxGTAXBgNVBAsTEHd3dy5kaWdpY2VydC5jb20xJDAiBgNV # BAMTG0RpZ2lDZXJ0IEFzc3VyZWQgSUQgUm9vdCBDQTAeFw0yMjA4MDEwMDAwMDBa # Fw0zMTExMDkyMzU5NTlaMGIxCzAJBgNVBAYTAlVTMRUwEwYDVQQKEwxEaWdpQ2Vy # dCBJbmMxGTAXBgNVBAsTEHd3dy5kaWdpY2VydC5jb20xITAfBgNVBAMTGERpZ2lD # ZXJ0IFRydXN0ZWQgUm9vdCBHNDCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoC # ggIBAL/mkHNo3rvkXUo8MCIwaTPswqclLskhPfKK2FnC4SmnPVirdprNrnsbhA3E # MB/zG6Q4FutWxpdtHauyefLKEdLkX9YFPFIPUh/GnhWlfr6fqVcWWVVyr2iTcMKy # unWZanMylNEQRBAu34LzB4TmdDttceItDBvuINXJIB1jKS3O7F5OyJP4IWGbNOsF # xl7sWxq868nPzaw0QF+xembud8hIqGZXV59UWI4MK7dPpzDZVu7Ke13jrclPXuU1 # 5zHL2pNe3I6PgNq2kZhAkHnDeMe2scS1ahg4AxCN2NQ3pC4FfYj1gj4QkXCrVYJB # MtfbBHMqbpEBfCFM1LyuGwN1XXhm2ToxRJozQL8I11pJpMLmqaBn3aQnvKFPObUR # WBf3JFxGj2T3wWmIdph2PVldQnaHiZdpekjw4KISG2aadMreSx7nDmOu5tTvkpI6 # nj3cAORFJYm2mkQZK37AlLTSYW3rM9nF30sEAMx9HJXDj/chsrIRt7t/8tWMcCxB # YKqxYxhElRp2Yn72gLD76GSmM9GJB+G9t+ZDpBi4pncB4Q+UDCEdslQpJYls5Q5S # UUd0viastkF13nqsX40/ybzTQRESW+UQUOsxxcpyFiIJ33xMdT9j7CFfxCBRa2+x # q4aLT8LWRV+dIPyhHsXAj6KxfgommfXkaS+YHS312amyHeUbAgMBAAGjggE6MIIB # NjAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQWBBTs1+OC0nFdZEzfLmc/57qYrhwP # TzAfBgNVHSMEGDAWgBRF66Kv9JLLgjEtUYunpyGd823IDzAOBgNVHQ8BAf8EBAMC # AYYweQYIKwYBBQUHAQEEbTBrMCQGCCsGAQUFBzABhhhodHRwOi8vb2NzcC5kaWdp # Y2VydC5jb20wQwYIKwYBBQUHMAKGN2h0dHA6Ly9jYWNlcnRzLmRpZ2ljZXJ0LmNv # bS9EaWdpQ2VydEFzc3VyZWRJRFJvb3RDQS5jcnQwRQYDVR0fBD4wPDA6oDigNoY0 # aHR0cDovL2NybDMuZGlnaWNlcnQuY29tL0RpZ2lDZXJ0QXNzdXJlZElEUm9vdENB # LmNybDARBgNVHSAECjAIMAYGBFUdIAAwDQYJKoZIhvcNAQEMBQADggEBAHCgv0Nc # Vec4X6CjdBs9thbX979XB72arKGHLOyFXqkauyL4hxppVCLtpIh3bb0aFPQTSnov # Lbc47/T/gLn4offyct4kvFIDyE7QKt76LVbP+fT3rDB6mouyXtTP0UNEm0Mh65Zy # oUi0mcudT6cGAxN3J0TU53/oWajwvy8LpunyNDzs9wPHh6jSTEAZNUZqaVSwuKFW # juyk1T3osdz9HNj0d1pcVIxv76FQPfx2CWiEn2/K2yCNNWAcAgPLILCsWKAOQGPF # mCLBsln1VWvPJ6tsds5vIy30fnFqI2si/xK4VC0nftg62fC2h5b9W9FcrBjDTZ9z # twGpn1eqXijiuZQwggauMIIElqADAgECAhAHNje3JFR82Ees/ShmKl5bMA0GCSqG # SIb3DQEBCwUAMGIxCzAJBgNVBAYTAlVTMRUwEwYDVQQKEwxEaWdpQ2VydCBJbmMx # GTAXBgNVBAsTEHd3dy5kaWdpY2VydC5jb20xITAfBgNVBAMTGERpZ2lDZXJ0IFRy # dXN0ZWQgUm9vdCBHNDAeFw0yMjAzMjMwMDAwMDBaFw0zNzAzMjIyMzU5NTlaMGMx # CzAJBgNVBAYTAlVTMRcwFQYDVQQKEw5EaWdpQ2VydCwgSW5jLjE7MDkGA1UEAxMy # RGlnaUNlcnQgVHJ1c3RlZCBHNCBSU0E0MDk2IFNIQTI1NiBUaW1lU3RhbXBpbmcg # Q0EwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQDGhjUGSbPBPXJJUVXH # JQPE8pE3qZdRodbSg9GeTKJtoLDMg/la9hGhRBVCX6SI82j6ffOciQt/nR+eDzMf # UBMLJnOWbfhXqAJ9/UO0hNoR8XOxs+4rgISKIhjf69o9xBd/qxkrPkLcZ47qUT3w # 1lbU5ygt69OxtXXnHwZljZQp09nsad/ZkIdGAHvbREGJ3HxqV3rwN3mfXazL6IRk # tFLydkf3YYMZ3V+0VAshaG43IbtArF+y3kp9zvU5EmfvDqVjbOSmxR3NNg1c1eYb # qMFkdECnwHLFuk4fsbVYTXn+149zk6wsOeKlSNbwsDETqVcplicu9Yemj052FVUm # cJgmf6AaRyBD40NjgHt1biclkJg6OBGz9vae5jtb7IHeIhTZgirHkr+g3uM+onP6 # 5x9abJTyUpURK1h0QCirc0PO30qhHGs4xSnzyqqWc0Jon7ZGs506o9UD4L/wojzK # QtwYSH8UNM/STKvvmz3+DrhkKvp1KCRB7UK/BZxmSVJQ9FHzNklNiyDSLFc1eSuo # 80VgvCONWPfcYd6T/jnA+bIwpUzX6ZhKWD7TA4j+s4/TXkt2ElGTyYwMO1uKIqjB # Jgj5FBASA31fI7tk42PgpuE+9sJ0sj8eCXbsq11GdeJgo1gJASgADoRU7s7pXche # MBK9Rp6103a50g5rmQzSM7TNsQIDAQABo4IBXTCCAVkwEgYDVR0TAQH/BAgwBgEB # /wIBADAdBgNVHQ4EFgQUuhbZbU2FL3MpdpovdYxqII+eyG8wHwYDVR0jBBgwFoAU # 7NfjgtJxXWRM3y5nP+e6mK4cD08wDgYDVR0PAQH/BAQDAgGGMBMGA1UdJQQMMAoG # CCsGAQUFBwMIMHcGCCsGAQUFBwEBBGswaTAkBggrBgEFBQcwAYYYaHR0cDovL29j # c3AuZGlnaWNlcnQuY29tMEEGCCsGAQUFBzAChjVodHRwOi8vY2FjZXJ0cy5kaWdp # Y2VydC5jb20vRGlnaUNlcnRUcnVzdGVkUm9vdEc0LmNydDBDBgNVHR8EPDA6MDig # NqA0hjJodHRwOi8vY3JsMy5kaWdpY2VydC5jb20vRGlnaUNlcnRUcnVzdGVkUm9v # dEc0LmNybDAgBgNVHSAEGTAXMAgGBmeBDAEEAjALBglghkgBhv1sBwEwDQYJKoZI # hvcNAQELBQADggIBAH1ZjsCTtm+YqUQiAX5m1tghQuGwGC4QTRPPMFPOvxj7x1Bd # 4ksp+3CKDaopafxpwc8dB+k+YMjYC+VcW9dth/qEICU0MWfNthKWb8RQTGIdDAiC # qBa9qVbPFXONASIlzpVpP0d3+3J0FNf/q0+KLHqrhc1DX+1gtqpPkWaeLJ7giqzl # /Yy8ZCaHbJK9nXzQcAp876i8dU+6WvepELJd6f8oVInw1YpxdmXazPByoyP6wCeC # RK6ZJxurJB4mwbfeKuv2nrF5mYGjVoarCkXJ38SNoOeY+/umnXKvxMfBwWpx2cYT # gAnEtp/Nh4cku0+jSbl3ZpHxcpzpSwJSpzd+k1OsOx0ISQ+UzTl63f8lY5knLD0/ # a6fxZsNBzU+2QJshIUDQtxMkzdwdeDrknq3lNHGS1yZr5Dhzq6YBT70/O3itTK37 # xJV77QpfMzmHQXh6OOmc4d0j/R0o08f56PGYX/sr2H7yRp11LB4nLCbbbxV7HhmL # NriT1ObyF5lZynDwN7+YAN8gFk8n+2BnFqFmut1VwDophrCYoCvtlUG3OtUVmDG0 # YgkPCr2B2RP+v6TR81fZvAT6gt4y3wSJ8ADNXcL50CN/AAvkdgIm2fBldkKmKYcJ # RyvmfxqkhQ/8mJb2VVQrH4D6wPIOK+XW+6kvRBVK5xMOHds3OBqhK/bt1nz8MIIG # sDCCBJigAwIBAgIQCK1AsmDSnEyfXs2pvZOu2TANBgkqhkiG9w0BAQwFADBiMQsw # CQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3d3cu # ZGlnaWNlcnQuY29tMSEwHwYDVQQDExhEaWdpQ2VydCBUcnVzdGVkIFJvb3QgRzQw # HhcNMjEwNDI5MDAwMDAwWhcNMzYwNDI4MjM1OTU5WjBpMQswCQYDVQQGEwJVUzEX # MBUGA1UEChMORGlnaUNlcnQsIEluYy4xQTA/BgNVBAMTOERpZ2lDZXJ0IFRydXN0 # ZWQgRzQgQ29kZSBTaWduaW5nIFJTQTQwOTYgU0hBMzg0IDIwMjEgQ0ExMIICIjAN # BgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEA1bQvQtAorXi3XdU5WRuxiEL1M4zr # PYGXcMW7xIUmMJ+kjmjYXPXrNCQH4UtP03hD9BfXHtr50tVnGlJPDqFX/IiZwZHM # gQM+TXAkZLON4gh9NH1MgFcSa0OamfLFOx/y78tHWhOmTLMBICXzENOLsvsI8Irg # nQnAZaf6mIBJNYc9URnokCF4RS6hnyzhGMIazMXuk0lwQjKP+8bqHPNlaJGiTUyC # EUhSaN4QvRRXXegYE2XFf7JPhSxIpFaENdb5LpyqABXRN/4aBpTCfMjqGzLmysL0 # p6MDDnSlrzm2q2AS4+jWufcx4dyt5Big2MEjR0ezoQ9uo6ttmAaDG7dqZy3SvUQa # khCBj7A7CdfHmzJawv9qYFSLScGT7eG0XOBv6yb5jNWy+TgQ5urOkfW+0/tvk2E0 # XLyTRSiDNipmKF+wc86LJiUGsoPUXPYVGUztYuBeM/Lo6OwKp7ADK5GyNnm+960I # HnWmZcy740hQ83eRGv7bUKJGyGFYmPV8AhY8gyitOYbs1LcNU9D4R+Z1MI3sMJN2 # FKZbS110YU0/EpF23r9Yy3IQKUHw1cVtJnZoEUETWJrcJisB9IlNWdt4z4FKPkBH # X8mBUHOFECMhWWCKZFTBzCEa6DgZfGYczXg4RTCZT/9jT0y7qg0IU0F8WD1Hs/q2 # 7IwyCQLMbDwMVhECAwEAAaOCAVkwggFVMBIGA1UdEwEB/wQIMAYBAf8CAQAwHQYD # VR0OBBYEFGg34Ou2O/hfEYb7/mF7CIhl9E5CMB8GA1UdIwQYMBaAFOzX44LScV1k # TN8uZz/nupiuHA9PMA4GA1UdDwEB/wQEAwIBhjATBgNVHSUEDDAKBggrBgEFBQcD # AzB3BggrBgEFBQcBAQRrMGkwJAYIKwYBBQUHMAGGGGh0dHA6Ly9vY3NwLmRpZ2lj # ZXJ0LmNvbTBBBggrBgEFBQcwAoY1aHR0cDovL2NhY2VydHMuZGlnaWNlcnQuY29t # L0RpZ2lDZXJ0VHJ1c3RlZFJvb3RHNC5jcnQwQwYDVR0fBDwwOjA4oDagNIYyaHR0 # cDovL2NybDMuZGlnaWNlcnQuY29tL0RpZ2lDZXJ0VHJ1c3RlZFJvb3RHNC5jcmww # HAYDVR0gBBUwEzAHBgVngQwBAzAIBgZngQwBBAEwDQYJKoZIhvcNAQEMBQADggIB # ADojRD2NCHbuj7w6mdNW4AIapfhINPMstuZ0ZveUcrEAyq9sMCcTEp6QRJ9L/Z6j # fCbVN7w6XUhtldU/SfQnuxaBRVD9nL22heB2fjdxyyL3WqqQz/WTauPrINHVUHmI # moqKwba9oUgYftzYgBoRGRjNYZmBVvbJ43bnxOQbX0P4PpT/djk9ntSZz0rdKOtf # JqGVWEjVGv7XJz/9kNF2ht0csGBc8w2o7uCJob054ThO2m67Np375SFTWsPK6Wrx # oj7bQ7gzyE84FJKZ9d3OVG3ZXQIUH0AzfAPilbLCIXVzUstG2MQ0HKKlS43Nb3Y3 # LIU/Gs4m6Ri+kAewQ3+ViCCCcPDMyu/9KTVcH4k4Vfc3iosJocsL6TEa/y4ZXDlx # 4b6cpwoG1iZnt5LmTl/eeqxJzy6kdJKt2zyknIYf48FWGysj/4+16oh7cGvmoLr9 # Oj9FpsToFpFSi0HASIRLlk2rREDjjfAVKM7t8RhWByovEMQMCGQ8M4+uKIw8y4+I # Cw2/O/TOHnuO77Xry7fwdxPm5yg/rBKupS8ibEH5glwVZsxsDsrFhsP2JjMMB0ug # 0wcCampAMEhLNKhRILutG4UI4lkNbcoFUCvqShyepf2gpx8GdOfy1lKQ/a+FSCH5 # Vzu0nAPthkX0tGFuv2jiJmCG6sivqf6UHedjGzqGVnhOMIIGvDCCBKSgAwIBAgIQ # C65mvFq6f5WHxvnpBOMzBDANBgkqhkiG9w0BAQsFADBjMQswCQYDVQQGEwJVUzEX # MBUGA1UEChMORGlnaUNlcnQsIEluYy4xOzA5BgNVBAMTMkRpZ2lDZXJ0IFRydXN0 # ZWQgRzQgUlNBNDA5NiBTSEEyNTYgVGltZVN0YW1waW5nIENBMB4XDTI0MDkyNjAw # MDAwMFoXDTM1MTEyNTIzNTk1OVowQjELMAkGA1UEBhMCVVMxETAPBgNVBAoTCERp # Z2lDZXJ0MSAwHgYDVQQDExdEaWdpQ2VydCBUaW1lc3RhbXAgMjAyNDCCAiIwDQYJ # KoZIhvcNAQEBBQADggIPADCCAgoCggIBAL5qc5/2lSGrljC6W23mWaO16P2RHxjE # iDtqmeOlwf0KMCBDEr4IxHRGd7+L660x5XltSVhhK64zi9CeC9B6lUdXM0s71EOc # Re8+CEJp+3R2O8oo76EO7o5tLuslxdr9Qq82aKcpA9O//X6QE+AcaU/byaCagLD/ # GLoUb35SfWHh43rOH3bpLEx7pZ7avVnpUVmPvkxT8c2a2yC0WMp8hMu60tZR0Cha # V76Nhnj37DEYTX9ReNZ8hIOYe4jl7/r419CvEYVIrH6sN00yx49boUuumF9i2T8U # uKGn9966fR5X6kgXj3o5WHhHVO+NBikDO0mlUh902wS/Eeh8F/UFaRp1z5SnROHw # SJ+QQRZ1fisD8UTVDSupWJNstVkiqLq+ISTdEjJKGjVfIcsgA4l9cbk8Smlzddh4 # EfvFrpVNnes4c16Jidj5XiPVdsn5n10jxmGpxoMc6iPkoaDhi6JjHd5ibfdp5uzI # Xp4P0wXkgNs+CO/CacBqU0R4k+8h6gYldp4FCMgrXdKWfM4N0u25OEAuEa3Jyidx # W48jwBqIJqImd93NRxvd1aepSeNeREXAu2xUDEW8aqzFQDYmr9ZONuc2MhTMizch # NULpUEoA6Vva7b1XCB+1rxvbKmLqfY/M/SdV6mwWTyeVy5Z/JkvMFpnQy5wR14GJ # cv6dQ4aEKOX5AgMBAAGjggGLMIIBhzAOBgNVHQ8BAf8EBAMCB4AwDAYDVR0TAQH/ # BAIwADAWBgNVHSUBAf8EDDAKBggrBgEFBQcDCDAgBgNVHSAEGTAXMAgGBmeBDAEE # AjALBglghkgBhv1sBwEwHwYDVR0jBBgwFoAUuhbZbU2FL3MpdpovdYxqII+eyG8w # HQYDVR0OBBYEFJ9XLAN3DigVkGalY17uT5IfdqBbMFoGA1UdHwRTMFEwT6BNoEuG # SWh0dHA6Ly9jcmwzLmRpZ2ljZXJ0LmNvbS9EaWdpQ2VydFRydXN0ZWRHNFJTQTQw # OTZTSEEyNTZUaW1lU3RhbXBpbmdDQS5jcmwwgZAGCCsGAQUFBwEBBIGDMIGAMCQG # CCsGAQUFBzABhhhodHRwOi8vb2NzcC5kaWdpY2VydC5jb20wWAYIKwYBBQUHMAKG # TGh0dHA6Ly9jYWNlcnRzLmRpZ2ljZXJ0LmNvbS9EaWdpQ2VydFRydXN0ZWRHNFJT # QTQwOTZTSEEyNTZUaW1lU3RhbXBpbmdDQS5jcnQwDQYJKoZIhvcNAQELBQADggIB # AD2tHh92mVvjOIQSR9lDkfYR25tOCB3RKE/P09x7gUsmXqt40ouRl3lj+8QioVYq # 3igpwrPvBmZdrlWBb0HvqT00nFSXgmUrDKNSQqGTdpjHsPy+LaalTW0qVjvUBhcH # zBMutB6HzeledbDCzFzUy34VarPnvIWrqVogK0qM8gJhh/+qDEAIdO/KkYesLyTV # OoJ4eTq7gj9UFAL1UruJKlTnCVaM2UeUUW/8z3fvjxhN6hdT98Vr2FYlCS7Mbb4H # v5swO+aAXxWUm3WpByXtgVQxiBlTVYzqfLDbe9PpBKDBfk+rabTFDZXoUke7zPgt # d7/fvWTlCs30VAGEsshJmLbJ6ZbQ/xll/HjO9JbNVekBv2Tgem+mLptR7yIrpaid # RJXrI+UzB6vAlk/8a1u7cIqV0yef4uaZFORNekUgQHTqddmsPCEIYQP7xGxZBIhd # mm4bhYsVA6G2WgNFYagLDBzpmk9104WQzYuVNsxyoVLObhx3RugaEGru+SojW4dH # PoWrUhftNpFC5H7QEY7MhKRyrBe7ucykW7eaCuWBsBb4HOKRFVDcrZgdwaSIqMDi # CLg4D+TPVgKx2EgEdeoHNHT9l3ZDBD+XgbF+23/zBjeCtxz+dL/9NWR6P2eZRi7z # cEO1xwcdcqJsyz/JceENc2Sg8h3KeFUCS7tpFk7CrDqkMIIHSTCCBTGgAwIBAgIQ # BaOjGrg1T58olh09AgdhuDANBgkqhkiG9w0BAQsFADBpMQswCQYDVQQGEwJVUzEX # MBUGA1UEChMORGlnaUNlcnQsIEluYy4xQTA/BgNVBAMTOERpZ2lDZXJ0IFRydXN0 # ZWQgRzQgQ29kZSBTaWduaW5nIFJTQTQwOTYgU0hBMzg0IDIwMjEgQ0ExMB4XDTI0 # MTIzMTAwMDAwMFoXDTI1MTIzMDIzNTk1OVowgdExEzARBgsrBgEEAYI3PAIBAxMC # VVMxGTAXBgsrBgEEAYI3PAIBAhMIRGVsYXdhcmUxHTAbBgNVBA8MFFByaXZhdGUg # T3JnYW5pemF0aW9uMRAwDgYDVQQFEwczNDA3OTg1MQswCQYDVQQGEwJVUzERMA8G # A1UECBMISWxsaW5vaXMxEDAOBgNVBAcTB0NoaWNhZ28xHTAbBgNVBAoTFEtlZXBl # ciBTZWN1cml0eSBJbmMuMR0wGwYDVQQDExRLZWVwZXIgU2VjdXJpdHkgSW5jLjCC # AaIwDQYJKoZIhvcNAQEBBQADggGPADCCAYoCggGBAM7/rBevApUP+XJjlSxdyASA # AnLFQ1r4NFXPo/S0RaTv1OCahApEeSN6oy+0OwbLNlwaQeooOanMcZhh64/+fF8S # zCMHDc/Pv8aBsd1B2XIw/VT+Nawfj0NxAX1zpKPp/tPqavm6smRDMOAeOo7qLxzI # u68bS2EnqvST1367tMpxhggrVl3GYKPhdCPeNDRskwheCSxI2czR8oe7mguo2nVa # ZR5VEq4xYkMZwTuT7RN8ER4r5crOSbJFyabp79SgYP7NyKmDcYZ6XJ26AfZsEDZr # e4VhzaqO0rl8i5HBmVmDKwU0PaIoAUdyeultIaS5oe0FjcTjGtrkBl+B7TCtvN1J # RE9Tmy3spnqLyvlRhrVJdDKCGovQKKJk87BAjIoiNSmEXs0H0PbB1ZYOA6m4ce7/ # BOmUafliYWBqrWHmHixqi/ha5ZKxKlYxGlikD4p1WlMmDEBhg3RPodW1Z5eGq92Z # exMGOWsfOQp3YhTDdMOA7tjWP2XzAaebGxCeOENEpQIDAQABo4ICAjCCAf4wHwYD # VR0jBBgwFoAUaDfg67Y7+F8Rhvv+YXsIiGX0TkIwHQYDVR0OBBYEFOcovsKg6xAz # zjzRmmWQRpa7p47MMD0GA1UdIAQ2MDQwMgYFZ4EMAQMwKTAnBggrBgEFBQcCARYb # aHR0cDovL3d3dy5kaWdpY2VydC5jb20vQ1BTMA4GA1UdDwEB/wQEAwIHgDATBgNV # HSUEDDAKBggrBgEFBQcDAzCBtQYDVR0fBIGtMIGqMFOgUaBPhk1odHRwOi8vY3Js # My5kaWdpY2VydC5jb20vRGlnaUNlcnRUcnVzdGVkRzRDb2RlU2lnbmluZ1JTQTQw # OTZTSEEzODQyMDIxQ0ExLmNybDBToFGgT4ZNaHR0cDovL2NybDQuZGlnaWNlcnQu # Y29tL0RpZ2lDZXJ0VHJ1c3RlZEc0Q29kZVNpZ25pbmdSU0E0MDk2U0hBMzg0MjAy # MUNBMS5jcmwwgZQGCCsGAQUFBwEBBIGHMIGEMCQGCCsGAQUFBzABhhhodHRwOi8v # b2NzcC5kaWdpY2VydC5jb20wXAYIKwYBBQUHMAKGUGh0dHA6Ly9jYWNlcnRzLmRp # Z2ljZXJ0LmNvbS9EaWdpQ2VydFRydXN0ZWRHNENvZGVTaWduaW5nUlNBNDA5NlNI # QTM4NDIwMjFDQTEuY3J0MAkGA1UdEwQCMAAwDQYJKoZIhvcNAQELBQADggIBALIq # AoEjkKZluMiOffwU+V+wiKkmDblKIZymyszEZot+niB6g7tRXrWkQo6gn8OG2qG6 # IO8L+o0VvwW0+V08p6gVqb0jeR9kCm7kDZk2RmzevhZDrRbZj0Q7Kb3pIeD9KEuc # RfEF0UGqgp0q7jerFXPzKtQk5kJpP65sSRV7bghIMWtq5sdHn/iGUMj+8Fd9AExq # 4kR+dyTw/6p1ZFiY7pIv4YAjjDrjkyUMSogt6ej9YGwTC8yVXJsjarLq2F+svwn8 # NlU+T03U/ZjXc/ZxDc5g3iqrl5Gm9QCaLhG2aLIrGRXN59Pcokp7JFNa6nkkWSSg # h4w01tz+xRSyiqKWAXNs2lHTD2F9ceGlz9Uw/RvPhPcl6bILqJcR6RUkzZtrKHNK # j85PBm/Kmurx0co5xRxXsXsF3tmp2r+Tt11veA9je+pyzuqE/kRQPn5hF8fIRuea # h7JVMaaHBTMbRaDcVFioGmCGHUx270yhLapA0eYXpZJv0n62QIMoX9NPcW2EcwhL # WGAV1IW+TIo/xcprAXBtXCO/mhscgInbMzesdg0uWsboiy4HfeTEzCe9ld54biUK # TJQu4wqbzkN5SGewOKTd/+c4k5w6yzuUWsk3YZpjWqsgpTlA3zU591uvMFsq0FYd # A3Py8YsVabLwTxz9d7kpBAHTPRYwDcsKNLGMPc+6MYIF0zCCBc8CAQEwfTBpMQsw # CQYDVQQGEwJVUzEXMBUGA1UEChMORGlnaUNlcnQsIEluYy4xQTA/BgNVBAMTOERp # Z2lDZXJ0IFRydXN0ZWQgRzQgQ29kZSBTaWduaW5nIFJTQTQwOTYgU0hBMzg0IDIw # MjEgQ0ExAhAFo6MauDVPnyiWHT0CB2G4MA0GCWCGSAFlAwQCAQUAoIGEMBgGCisG # AQQBgjcCAQwxCjAIoAKAAKECgAAwGQYJKoZIhvcNAQkDMQwGCisGAQQBgjcCAQQw # HAYKKwYBBAGCNwIBCzEOMAwGCisGAQQBgjcCARUwLwYJKoZIhvcNAQkEMSIEIFbi # +nEMP9F6IKgJn+tWasTkvYHrSkopNsLVUUBlQk+dMA0GCSqGSIb3DQEBAQUABIIB # gFkdImIcHdCynJM//BzzM7Hz8UCb0BAWVXQr/CTNNHPDpBmM0hRp41FbYBeJ4Crc # mp0KF1JkhvZKeGfuptP8PP0vBAjMlwfVnS22aj/2kMLYIl5xfghNVgW/B4hkeRv8 # bzSefJk+wdHXoZ8VJ4rh4X6J9L0/6DpHk7jZg9xKiY0N/TISvdN3/rFFGcAzTe8s # 76CQpFYOxvW0uzTNsQmNYLIkVCntvSdlGdnr5m1K6BkErfreMutSOgfBOhPyIsbB # QAnPxER7f7ssdl1v3PSoPv/Osnt/LcLVcfHQnrfpwCoIwbB+zVpnyma+CoI3Dgm9 # tTYI/2u6Ch0OoxI4Kh70RGkL7kxOuglJtdx+GDupQstcr3sXHEE+QnQA5XOCa0vA # eevKUO9aFXFmMp9MT/wPboVzbU1vfQEg28zy2zaqM3sUFCZaKMI2bdlGw7vp9c0R # X76C+EW1QLtCOj18krI9JcN1VsXKZkrHM3sA5YQDBmi3iMZ/O7najsD1rKMzK9Qv # Q6GCAyAwggMcBgkqhkiG9w0BCQYxggMNMIIDCQIBATB3MGMxCzAJBgNVBAYTAlVT # MRcwFQYDVQQKEw5EaWdpQ2VydCwgSW5jLjE7MDkGA1UEAxMyRGlnaUNlcnQgVHJ1 # c3RlZCBHNCBSU0E0MDk2IFNIQTI1NiBUaW1lU3RhbXBpbmcgQ0ECEAuuZrxaun+V # h8b56QTjMwQwDQYJYIZIAWUDBAIBBQCgaTAYBgkqhkiG9w0BCQMxCwYJKoZIhvcN # AQcBMBwGCSqGSIb3DQEJBTEPFw0yNTA2MzAxNzI2MTlaMC8GCSqGSIb3DQEJBDEi # BCDuWXfdkOAksehGkEyMfqjSfd8G88jSIo/8E0AQpj/ZQTANBgkqhkiG9w0BAQEF # AASCAgCAV2uyc6nKneHI2YmXuDYmUNzjrjNT9pHC4rGNvOFQ7+1XO8aHAP/6d9jM # qlm4n1Sne/5ssKam0dlnWIcQdMruFES8zdtEC+a0m3FT6oMFhfs1CS3dCKAfPL4m # c85TGDbzkNo54eJT3hK3Mybo7i6z4ZHA1VStaiC4dI6uovCz1JSaEuBSZ4aDma45 # cilq/KOD6NOizvvvndin35U7EwEgb37LJKwPRi42M3LurkHu/Fbv50Rhwwbs/L+r # JcOB7bQ1yEN5bhG/bUokMlHf/nz5j61r3giYgqNlFlIlcxjjAzgP9botkgiPqr8F # cVabJuQ9lG4y4czRFSL7uvm15onFszZLBD9bAZudnLCipwfFqOgBsPburHVUJaUP # FlzOGDppYM3fCWRxMZxUzuzFMasdDBwfRpzgXLoA37682RvB4U8XsNG5AM9YcLzJ # TM2+RbwALhByvzccNB0/dcqDTk0wzz9rBM2IBWZ4CZG26h6rm3RK9nR/4uU4/ibN # 1RXJxMh0hHvcF2nzG7m0Ysc1aAGdYkbX6nYC64Pso9bKFCjqr3iBXe1YKbX2jZfg # W70d0987nb2dNho2yTmfn1NOa0JmQwcNty2zidSzY/S8Nm7yRHSC3eqcqflilRF9 # 7M3jOGPOI1aHXbqCTsGmB7ndd/qcx6C71Y59HoxL4wesza1aDA== # SIG # End signature block |