NsfRecordCommands.ps1
|
#requires -Version 5.1 function Script:ConvertTo-NSFJsonValue { Param([Parameter(Mandatory = $true)][AllowNull()] $InputObject) if ($null -eq $InputObject) { return $null } $base = $InputObject if ($InputObject -is [System.Management.Automation.PSObject] -and $null -ne $InputObject.PSObject) { $bo = $InputObject.PSObject.BaseObject if ($null -ne $bo) { $base = $bo } } if ($base -is [string] -or $base -is [System.ValueType]) { return $base } if ($base -is [System.Collections.IDictionary]) { $ht = New-Object 'System.Collections.Hashtable' foreach ($key in $base.Keys) { $ht[$key] = ConvertTo-NSFJsonValue -InputObject $base[$key] } return $ht } if ($base -is [System.Collections.IEnumerable]) { $list = New-Object 'System.Collections.Generic.List[object]' foreach ($item in $base) { $list.Add((ConvertTo-NSFJsonValue -InputObject $item)) | Out-Null } return $list.ToArray() } if ($null -ne $InputObject.PSObject -and $InputObject.PSObject.Properties.Count -gt 0) { $ht = New-Object 'System.Collections.Hashtable' foreach ($prop in $InputObject.PSObject.Properties) { $ht[$prop.Name] = ConvertTo-NSFJsonValue -InputObject $prop.Value } return $ht } return $base } function Script:Resolve-KeeperNSFFieldValue { Param( [Parameter(Mandatory = $true)][AllowEmptyString()][string] $RawValue ) if ([string]::IsNullOrEmpty($RawValue)) { return $RawValue } if ($RawValue -clike '$JSON:*') { $jsonStr = $RawValue.Substring(6) if ([string]::IsNullOrEmpty($jsonStr)) { Write-Warning "JSON value cannot be empty. Format: `$JSON:<json_object>" return $RawValue } try { $parsed = $jsonStr | ConvertFrom-Json -ErrorAction Stop } catch { Write-Warning "Invalid JSON value: $($_.Exception.Message)" return $RawValue } return (ConvertTo-NSFJsonValue -InputObject $parsed) } return $RawValue } function Script:Parse-KeeperNSFFieldSpecs { Param( [Parameter(Mandatory = $true)] [AllowEmptyCollection()] [string[]] $FieldSpecs ) $fieldDict = New-Object 'System.Collections.Generic.Dictionary[string,object]' $skipped = New-Object 'System.Collections.Generic.List[string]' foreach ($f in $FieldSpecs) { if ([string]::IsNullOrWhiteSpace($f)) { continue } $key = $null $val = $null $eqIdx = $f.IndexOf('=') if ($eqIdx -gt 0) { $key = $f.Substring(0, $eqIdx).Trim() $val = $f.Substring($eqIdx + 1).Trim() } else { $colonIdx = $f.IndexOf(':') if ($colonIdx -gt 0) { $key = $f.Substring(0, $colonIdx).Trim() $val = $f.Substring($colonIdx + 1).Trim() if ($val.Length -ge 2 -and $val[0] -eq '"' -and $val[$val.Length - 1] -eq '"') { $val = $val.Substring(1, $val.Length - 2) } Write-Host "Warning: Field '$f' uses ':' as delimiter; prefer key=value (e.g. ${key}=${val})." -ForegroundColor Yellow } } if ([string]::IsNullOrEmpty($key)) { $skipped.Add($f) | Out-Null Write-Host "Warning: Skipping invalid field '$f'. Expected format: key=value" -ForegroundColor Yellow continue } $resolved = Resolve-KeeperNSFFieldValue -RawValue $val if ($resolved -is [System.Management.Automation.PSObject] -and $null -ne $resolved.PSObject.BaseObject) { $resolved = $resolved.PSObject.BaseObject } $fieldDict[$key] = $resolved } return [PSCustomObject]@{ Dictionary = $fieldDict Skipped = $skipped ParsedCount = $fieldDict.Count } } function Add-KeeperNSFRecord { <# .Synopsis Creates a new Keeper NSF record. .Description Creates a new record in Keeper NSF using the v3 API. Supports setting title, type, notes, folder, and field values. .Parameter Title Title for the new record. .Parameter RecordType Record type (e.g. login, general). Defaults to 'login'. .Parameter FolderUid Optional folder UID to place the record in. .Parameter Notes Optional notes for the record. .Parameter Fields Optional field values as key=value pairs (e.g. login=admin password=secret url=https://example.com). Complex (object-typed) fields can be supplied with the $JSON:<json> indirection token, e.g. for a `databaseCredentials` record's `host` field: 'host=$JSON:{"hostName":"1.2.3.4","port":"1234"}' NOTE: single-quote the spec so PowerShell does not parse $JSON: as a drive-qualified variable. .Parameter GeneratePassword When present, generates a random password via CryptoUtils.GeneratePassword and stores it on the 'password' field, overriding any explicit password=... value supplied in -Fields. #> [CmdletBinding()] Param ( [Parameter(Position = 0, Mandatory = $true)] [string] $Title, [Parameter()] [string] $RecordType = 'login', [Parameter()] [string] $FolderUid, [Parameter()] [string] $Notes, [Parameter()] [switch] $GeneratePassword, [Parameter(ValueFromRemainingArguments = $true)] [string[]] $Fields ) try { [KeeperSecurity.Vault.VaultOnline]$vault = getVault } catch { Write-Host "Error getting vault: $($_.Exception.Message)" -ForegroundColor Red return } $fieldDict = $null if (($Fields -and $Fields.Count -gt 0) -or $GeneratePassword.IsPresent) { $fieldDict = New-Object 'System.Collections.Generic.Dictionary[string,object]' if ($Fields) { $parsed = Parse-KeeperNSFFieldSpecs -FieldSpecs $Fields foreach ($key in $parsed.Dictionary.Keys) { $fieldDict[$key] = $parsed.Dictionary[$key] } } if ($GeneratePassword.IsPresent) { $fieldDict['password'] = [KeeperSecurity.Utils.CryptoUtils]::GeneratePassword($null) } } try { $recordUid = $vault.CreateKeeperNSFRecord($Title, $RecordType, $FolderUid, $Notes, $fieldDict).GetAwaiter().GetResult() Write-Host "Record '$Title' created successfully (UID: $recordUid)." -ForegroundColor Green return $recordUid } catch { Write-Host "Error creating record: $($_.Exception.Message)" -ForegroundColor Red } } New-Alias -Name nsf-record-add -Value Add-KeeperNSFRecord function Edit-KeeperNSFRecord { <# .Synopsis Updates an existing Keeper NSF record. .Description Updates the title, type, notes, and/or fields of a Keeper NSF record using the v3 API. Only specified parameters are changed; others are preserved. .Parameter RecordUid UID of the record to update. .Parameter Title New title for the record. .Parameter RecordType New record type. .Parameter Notes New notes for the record. .Parameter Fields Field values to add or update as key=value pairs (e.g. login=newuser password=newpass). Complex (object-typed) fields can be supplied with the $JSON:<json> indirection token, e.g. updating a `databaseCredentials` record's `host` field: nsf-record-update <UID> -RecordType databaseCredentials 'host=$JSON:{"hostName":"1.2.3.4","port":"1234"}' NOTE: single-quote the spec so PowerShell does not parse $JSON: as a drive-qualified variable. .Parameter GeneratePassword When present, generates a random password via CryptoUtils.GeneratePassword and stores it on the 'password' field, overriding any explicit password=... value supplied in -Fields. #> [CmdletBinding()] Param ( [Parameter(Position = 0, Mandatory = $true)] [string] $RecordUid, [Parameter()] [string] $Title, [Parameter()] [string] $RecordType, [Parameter()] [string] $Notes, [Parameter()] [switch] $GeneratePassword, [Parameter(ValueFromRemainingArguments = $true)] [string[]] $Fields ) $hasTitle = $PSBoundParameters.ContainsKey('Title') $hasType = $PSBoundParameters.ContainsKey('RecordType') $hasNotes = $PSBoundParameters.ContainsKey('Notes') $hasFields = $Fields -and $Fields.Count -gt 0 if (-not $hasTitle -and -not $hasType -and -not $hasNotes -and -not $hasFields -and -not $GeneratePassword.IsPresent) { Write-Host "Error: At least one of -Title, -RecordType, -Notes, -GeneratePassword, or field values must be specified." -ForegroundColor Red return } try { [KeeperSecurity.Vault.VaultOnline]$vault = getVault } catch { Write-Host "Error getting vault: $($_.Exception.Message)" -ForegroundColor Red return } $titleParam = if ($hasTitle) { $Title } else { [NullString]::Value } $typeParam = if ($hasType) { $RecordType } else { [NullString]::Value } $notesParam = if ($hasNotes) { $Notes } else { [NullString]::Value } $fieldDict = $null if ($hasFields -or $GeneratePassword.IsPresent) { $fieldDict = New-Object 'System.Collections.Generic.Dictionary[string,object]' if ($hasFields) { $parsed = Parse-KeeperNSFFieldSpecs -FieldSpecs $Fields foreach ($key in $parsed.Dictionary.Keys) { $fieldDict[$key] = $parsed.Dictionary[$key] } if ($parsed.ParsedCount -eq 0 -and -not $GeneratePassword.IsPresent) { Write-Host "Error: No valid field values were parsed. Use key=value (e.g. login=a12)." -ForegroundColor Red return } } if ($GeneratePassword.IsPresent) { $fieldDict['password'] = [KeeperSecurity.Utils.CryptoUtils]::GeneratePassword($null) } } try { [void]$vault.UpdateKeeperNSFRecord($RecordUid, $titleParam, $typeParam, $notesParam, $fieldDict).GetAwaiter().GetResult() Write-Host "Record '$RecordUid' updated successfully." -ForegroundColor Green } catch { Write-Host "Error updating record: $($_.Exception.Message)" -ForegroundColor Red } } New-Alias -Name nsf-record-update -Value Edit-KeeperNSFRecord function Set-KeeperNSFRecordAccess { <# .Synopsis Grant or revoke user access to a Keeper NSF record. .Description Shares or unshares a Keeper NSF record with a user using the v3 API. When granting, encrypts and sends the record key to the recipient. .Parameter RecordUid UID of the record to share/unshare. .Parameter Action Action to perform: 'grant' (default) or 'revoke'. .Parameter Email One or more user email addresses to grant/revoke access. .Parameter Role Access role for grant action: viewer (default), share-manager, content-manager, content-share-manager, full-manager. .Parameter ExpireIn Optional. Share expiration period from now (e.g. 30d, 6mo, 1y, 24h, 30mi), integer minutes, or a TimeSpan. Same as Grant-KeeperRecordAccess. .Parameter ExpireAt Optional. Absolute share expiration as ISO datetime (e.g. 2027-01-01T00:00:00Z). #> [CmdletBinding()] [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseShouldProcessForStateChangingFunctions", "")] Param ( [Parameter(Position = 0, Mandatory = $true)] [string] $RecordUid, [Parameter()] [ValidateSet('grant', 'revoke')] [string] $Action = 'grant', [Parameter(Mandatory = $true)] [string[]] $Email, [Parameter()] [ValidateSet('viewer', 'share-manager', 'content-manager', 'content-share-manager', 'full-manager')] [string] $Role = 'viewer', [Alias('expire-in')] [Parameter()] [System.Object] $ExpireIn, [Alias('expire-at')] [Parameter()] [string] $ExpireAt ) try { [KeeperSecurity.Vault.VaultOnline]$vault = getVault } catch { Write-Host "Error getting vault: $($_.Exception.Message)" -ForegroundColor Red return } [KeeperSecurity.Vault.KeeperNSFRecord]$tmpRecord = $null if (-not $vault.TryGetKeeperNSFRecord($RecordUid, [ref]$tmpRecord)) { Write-Host "Error: NSF record '$RecordUid' not found." -ForegroundColor Red return } $shareOptions = $null if ($Action -eq 'grant' -and ($ExpireIn -or $ExpireAt)) { try { $expirationDto = Get-ExpirationDate -ExpireIn $ExpireIn -ExpireAt $ExpireAt } catch { Write-Host "Error: $($_.Exception.Message)" -ForegroundColor Red return } $shareOptions = New-Object KeeperSecurity.Vault.SharedFolderRecordOptions $shareOptions.Expiration = $expirationDto } foreach ($user in $Email) { try { if ($Action -eq 'grant') { [void]$vault.ShareKeeperNSFRecord($RecordUid, $user, $Role, $shareOptions).GetAwaiter().GetResult() $expireMsg = if ($shareOptions -and $shareOptions.Expiration) { " (expires $($shareOptions.Expiration.LocalDateTime.ToString('g')))" } else { '' } Write-Host "Granted '$Role' access to '$user' on record '$RecordUid'$expireMsg." -ForegroundColor Green } else { [void]$vault.UnshareKeeperNSFRecord($RecordUid, $user).GetAwaiter().GetResult() Write-Host "Revoked access for '$user' from record '$RecordUid'." -ForegroundColor Green } } catch { Write-Host "Error ${Action}ing access for '$user': $($_.Exception.Message)" -ForegroundColor Red } } } New-Alias -Name nsf-share-record -Value Set-KeeperNSFRecordAccess function Set-KeeperNSFRecordPermission { <# .Synopsis Bulk grant or revoke record-level sharing permissions for records in a Keeper NSF folder. .Description Modifies sharing permissions on all records within a Keeper NSF folder using the v3 API. Fetches current access permissions, computes required changes, displays a plan, and executes the changes after confirmation. .Parameter FolderUid Folder UID or name containing the records. If omitted, operates on root-level records. .Parameter Action Action to perform: 'grant' or 'revoke'. .Parameter Role Access role: viewer, share-manager, content-manager, content-share-manager, full-manager. Required for grant action. For revoke, if specified, only revokes users with that role. .Parameter Recursive If specified, includes records in subfolders. .Parameter Force Skip confirmation prompt. .Parameter DryRun Show what would change without making modifications. #> [CmdletBinding()] [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseShouldProcessForStateChangingFunctions", "")] Param ( [Parameter(Position = 0)] [string] $FolderUid, [Parameter(Mandatory = $true)] [ValidateSet('grant', 'revoke')] [string] $Action, [Parameter()] [ValidateSet('viewer', 'share-manager', 'content-manager', 'content-share-manager', 'full-manager')] [string] $Role, [Parameter()] [switch] $Recursive, [Parameter()] [switch] $Force, [Parameter()] [switch] $DryRun ) if ($Action -eq 'grant' -and -not $Role) { Write-Host "Error: -Role is required for grant action." -ForegroundColor Red return } try { [KeeperSecurity.Vault.VaultOnline]$vault = getVault } catch { Write-Host "Error getting vault: $($_.Exception.Message)" -ForegroundColor Red return } if ($FolderUid) { [KeeperSecurity.Vault.FolderNode]$tmpFolder = $null if (-not $vault.TryGetKeeperNSFFolder($FolderUid, [ref]$tmpFolder)) { foreach ($f in $vault.KeeperNSFFolderNodes) { if ($f.Name -and $f.Name -ieq $FolderUid) { $FolderUid = $f.FolderUid; break } } } } $folderDisplay = if ($FolderUid) { $FolderUid } else { 'root' } $roleLabel = if ($Role) { "'$Role'" } else { 'all' } $scopeLabel = if ($Recursive.IsPresent) { 'recursively' } else { 'only' } Write-Host "" Write-Host "Request to $($Action.ToUpper()) $roleLabel permission(s) in '$folderDisplay' folder $scopeLabel" -ForegroundColor Cyan try { $permResult = $vault.UpdateKeeperNSFRecordPermissions($FolderUid, $Action, $Role, $Recursive.IsPresent, $true).GetAwaiter().GetResult() } catch { Write-Host "Error: $($_.Exception.Message)" -ForegroundColor Red return } $hasChanges = ($permResult.Grants.Count -gt 0) -or ($permResult.Revokes.Count -gt 0) if ($permResult.Skipped.Count -gt 0) { Write-Host "" Write-Host " SKIPPED ($($permResult.Skipped.Count)):" -ForegroundColor Yellow foreach ($s in $permResult.Skipped) { $email = if ($s.Email) { $s.Email } else { '-' } $curRole = if ($s.CurrentRole) { $s.CurrentRole } else { '-' } Write-Host " $($s.RecordUid) $email [$curRole] $($s.Message)" } } if ($permResult.Grants.Count -gt 0) { Write-Host "" Write-Host " PLANNED GRANTS ($($permResult.Grants.Count)):" -ForegroundColor Green foreach ($g in $permResult.Grants) { $inherited = if ($g.ChangeType -eq 'create') { ' (inherited override)' } else { '' } Write-Host " $($g.RecordUid) $($g.Email) $($g.CurrentRole) -> $($g.NewRole)$inherited" } } if ($permResult.Revokes.Count -gt 0) { Write-Host "" Write-Host " PLANNED REVOKES ($($permResult.Revokes.Count)):" -ForegroundColor Red foreach ($r in $permResult.Revokes) { Write-Host " $($r.RecordUid) $($r.Email) [$($r.CurrentRole)]" } } if (-not $hasChanges -and $permResult.Skipped.Count -eq 0) { Write-Host "No permission changes are needed." -ForegroundColor DarkYellow return } if ($DryRun.IsPresent) { Write-Host "" Write-Host "[Dry-run mode - no changes were made]" -ForegroundColor Yellow $planTotal = $permResult.Grants.Count + $permResult.Revokes.Count Write-Host "Summary: $planTotal planned, $($permResult.Skipped.Count) skipped" -ForegroundColor Cyan return } if (-not $hasChanges) { Write-Host "" Write-Host "Summary: 0 changes, $($permResult.Skipped.Count) skipped" -ForegroundColor Cyan return } if (-not $Force) { $confirmation = Read-Host "Are you sure you want to apply the above changes? (yes/No)" if ($confirmation -notmatch '^(y|yes)$') { Write-Host "Update operation cancelled" return } } try { $permResult = $vault.UpdateKeeperNSFRecordPermissions($FolderUid, $Action, $Role, $Recursive.IsPresent, $false).GetAwaiter().GetResult() } catch { Write-Host "Error executing changes: $($_.Exception.Message)" -ForegroundColor Red return } if ($permResult.Grants.Count -gt 0) { Write-Host "" Write-Host " GRANT ($($permResult.Grants.Count)):" -ForegroundColor Green foreach ($g in $permResult.Grants) { $statusIcon = if ($g.Success) { '[OK]' } else { '[FAIL]' } $statusColor = if ($g.Success) { 'Green' } else { 'Red' } $inherited = if ($g.ChangeType -eq 'create') { ' (inherited override)' } else { '' } Write-Host " $statusIcon $($g.RecordUid) $($g.Email) $($g.CurrentRole) -> $($g.NewRole)$inherited" -ForegroundColor $statusColor if (-not $g.Success -and $g.Message) { Write-Host " Error: $($g.Message)" -ForegroundColor Red } } } if ($permResult.Revokes.Count -gt 0) { Write-Host "" Write-Host " REVOKE ($($permResult.Revokes.Count)):" -ForegroundColor Red foreach ($r in $permResult.Revokes) { $statusIcon = if ($r.Success) { '[OK]' } else { '[FAIL]' } $statusColor = if ($r.Success) { 'Green' } else { 'Red' } Write-Host " $statusIcon $($r.RecordUid) $($r.Email) [$($r.CurrentRole)]" -ForegroundColor $statusColor if (-not $r.Success -and $r.Message) { Write-Host " Error: $($r.Message)" -ForegroundColor Red } } } $successCount = ($permResult.Grants | Where-Object { $_.Success }).Count + ($permResult.Revokes | Where-Object { $_.Success }).Count $failCount = ($permResult.Grants | Where-Object { -not $_.Success }).Count + ($permResult.Revokes | Where-Object { -not $_.Success }).Count Write-Host "" Write-Host "Summary: $successCount succeeded, $failCount failed, $($permResult.Skipped.Count) skipped" -ForegroundColor Cyan } New-Alias -Name nsf-record-permission -Value Set-KeeperNSFRecordPermission function Get-KeeperNSFShortcut { <# .Synopsis Lists Keeper NSF records that appear in more than one folder (shortcuts). .Description Scans all Keeper NSF folder-record links and reports records that exist in two or more folders. Optionally filters by a specific record UID/title or folder UID/name. .Parameter Target Optional record UID, record title, folder UID, or folder name to filter results. .Parameter Format Output format: table (default), csv, or json. .Parameter Output Path to output file. Ignored for table format. #> [CmdletBinding()] Param ( [Parameter(Position = 0)] [string] $Target, [Parameter()] [ValidateSet('table', 'csv', 'json')] [string] $Format = 'table', [Parameter()] [string] $Output ) try { [KeeperSecurity.Vault.VaultOnline]$vault = getVault } catch { Write-Host "Error getting vault: $($_.Exception.Message)" -ForegroundColor Red return } $recordUid = $null $folderUid = $null if ($Target) { [KeeperSecurity.Vault.KeeperNSFRecord]$tmpRecord = $null if ($vault.TryGetKeeperNSFRecord($Target, [ref]$tmpRecord)) { $recordUid = $Target } else { [KeeperSecurity.Vault.FolderNode]$tmpFolder = $null if ($vault.TryGetKeeperNSFFolder($Target, [ref]$tmpFolder)) { $folderUid = $Target } else { $foundByTitle = $false foreach ($r in $vault.KeeperNSFRecordEntries) { if ($r.Title -and $r.Title -ieq $Target) { $recordUid = $r.RecordUid $foundByTitle = $true break } } if (-not $foundByTitle) { foreach ($f in $vault.KeeperNSFFolderNodes) { if ($f.Name -and $f.Name -ieq $Target) { $folderUid = $f.FolderUid $foundByTitle = $true break } } } if (-not $foundByTitle) { Write-Host "Error: Target '$Target' not found as record UID, title, folder UID, or folder name." -ForegroundColor Red return } } } } try { $entries = $vault.GetKeeperNSFShortcuts($recordUid, $folderUid) } catch { Write-Host "Error: $($_.Exception.Message)" -ForegroundColor Red return } if ($entries.Count -eq 0) { Write-Host "No shortcut records found." -ForegroundColor DarkYellow return } if ($Format -eq 'json') { $jsonItems = @() foreach ($e in $entries) { $folders = @() foreach ($f in $e.Folders) { $folders += [ordered]@{ folder_uid = $f.FolderUid; name = $f.Name } } $jsonItems += [ordered]@{ record_uid = $e.RecordUid record_title = $e.Title folders = $folders } } $jsonText = $jsonItems | ConvertTo-Json -Depth 4 if ($Output) { $jsonText | Out-File -FilePath $Output -Encoding utf8 Write-Host "JSON output written to '$Output' ($($entries.Count) shortcuts)." -ForegroundColor Green } else { $jsonText } return } if ($Format -eq 'csv') { $csvData = @() foreach ($e in $entries) { $folderNames = ($e.Folders | ForEach-Object { $_.Name }) -join '; ' $folderUids = ($e.Folders | ForEach-Object { $_.FolderUid }) -join '; ' $csvData += [PSCustomObject]@{ RecordUid = $e.RecordUid Title = $e.Title FolderCount = $e.Folders.Count FolderUids = $folderUids FolderNames = $folderNames } } if ($Output) { $csvData | Export-Csv -Path $Output -NoTypeInformation -Encoding utf8 Write-Host "CSV output written to '$Output' ($($entries.Count) shortcuts)." -ForegroundColor Green } else { $csvData | ConvertTo-Csv -NoTypeInformation } return } Write-Host "" Write-Host " Shortcut Records ($($entries.Count)):" -ForegroundColor Cyan Write-Host "" foreach ($e in $entries) { Write-Host " $($e.RecordUid) $($e.Title) [$($e.Folders.Count) folders]" -ForegroundColor White foreach ($f in $e.Folders) { Write-Host " - $($f.Name) ($($f.FolderUid))" } } Write-Host "" } New-Alias -Name nsf-shortcut-list -Value Get-KeeperNSFShortcut function Set-KeeperNSFShortcutKeep { <# .Synopsis Keep a Keeper NSF record in one folder and remove it from all others. .Description For a record that appears in multiple Keeper NSF folders (a shortcut), keeps it in the specified folder and unlinks it from all other folders. .Parameter RecordUid Record UID or title of the record. .Parameter FolderUid Folder UID or folder name to keep the record in. .Parameter Force Skip confirmation prompt. #> [CmdletBinding()] [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseShouldProcessForStateChangingFunctions", "")] Param ( [Parameter(Position = 0, Mandatory = $true)] [string] $RecordUid, [Parameter(Position = 1, Mandatory = $true)] [string] $FolderUid, [Parameter()] [switch] $Force ) try { [KeeperSecurity.Vault.VaultOnline]$vault = getVault } catch { Write-Host "Error getting vault: $($_.Exception.Message)" -ForegroundColor Red return } [KeeperSecurity.Vault.KeeperNSFRecord]$tmpRecord = $null if (-not $vault.TryGetKeeperNSFRecord($RecordUid, [ref]$tmpRecord)) { $found = $false foreach ($r in $vault.KeeperNSFRecordEntries) { if ($r.Title -and $r.Title -ieq $RecordUid) { $RecordUid = $r.RecordUid $found = $true break } } if (-not $found) { Write-Host "Error: Record '$RecordUid' not found." -ForegroundColor Red return } } [KeeperSecurity.Vault.FolderNode]$tmpFolder = $null if (-not $vault.TryGetKeeperNSFFolder($FolderUid, [ref]$tmpFolder)) { $found = $false foreach ($f in $vault.KeeperNSFFolderNodes) { if ($f.Name -and $f.Name -ieq $FolderUid) { $FolderUid = $f.FolderUid $found = $true break } } if (-not $found) { Write-Host "Error: Folder '$FolderUid' not found." -ForegroundColor Red return } } $shortcuts = $vault.GetKeeperNSFShortcuts($RecordUid, $null) if ($shortcuts.Count -eq 0) { Write-Host "Record '$RecordUid' does not appear in multiple folders." -ForegroundColor DarkYellow return } $entry = $shortcuts[0] $keepFolder = $entry.Folders | Where-Object { $_.FolderUid -eq $FolderUid } if (-not $keepFolder) { Write-Host "Error: Record '$RecordUid' is not in folder '$FolderUid'." -ForegroundColor Red return } $removeFolders = $entry.Folders | Where-Object { $_.FolderUid -ne $FolderUid } if (-not $Force) { Write-Host "" Write-Host " Will remove record '$($entry.Title)' ($RecordUid) from:" -ForegroundColor Yellow foreach ($rf in $removeFolders) { Write-Host " - $($rf.Name) ($($rf.FolderUid))" } Write-Host " Keeping in: $($keepFolder.Name) ($($keepFolder.FolderUid))" -ForegroundColor Green Write-Host "" $confirmation = Read-Host "Are you sure you want to keep the record only in '$($keepFolder.Name)' and remove it from the other folder(s) above? (yes/No)" if ($confirmation -notmatch '^(y|yes)$') { Write-Host "Shortcut-keep operation cancelled" return } } try { $result = $vault.KeepKeeperNSFRecordInFolder($RecordUid, $FolderUid).GetAwaiter().GetResult() } catch { Write-Host "Error: $($_.Exception.Message)" -ForegroundColor Red return } $successCount = ($result.Removals | Where-Object { $_.Success }).Count $failCount = ($result.Removals | Where-Object { -not $_.Success }).Count foreach ($removal in $result.Removals) { if ($removal.Success) { Write-Host " [OK] Removed from '$($removal.FolderName)' ($($removal.FolderUid))" -ForegroundColor Green } else { Write-Host " [FAIL] '$($removal.FolderName)' ($($removal.FolderUid)): $($removal.Message)" -ForegroundColor Red } } Write-Host "" Write-Host "Record kept in '$($result.KeptFolderName)'. $successCount removed, $failCount failed." -ForegroundColor Cyan } New-Alias -Name nsf-shortcut-keep -Value Set-KeeperNSFShortcutKeep function Remove-KeeperNSFRecord { <# .Synopsis Removes one or more Keeper NSF records (Keeper NSF v3 API). .Parameter Record One or more record UIDs or titles. .Parameter Folder Folder UID or name that provides context (required for unlink). .Parameter Operation Removal operation: owner-trash, folder-trash, or unlink. .Parameter Force Skip confirmation after preview. .Parameter DryRun Preview only; do not remove records. #> [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseShouldProcessForStateChangingFunctions", "")] [CmdletBinding(SupportsShouldProcess, DefaultParameterSetName = 'Default')] Param( [Parameter(Position = 0, Mandatory = $true, ValueFromPipeline = $true)] [string[]] $Record, [string] $Folder, [Alias('o')] [ValidateSet('owner-trash', 'folder-trash', 'unlink')] [string] $Operation = 'owner-trash', [Alias('f')] [switch] $Force, [switch] $DryRun ) begin { [KeeperSecurity.Vault.VaultOnline]$vault = getVault $removals = New-Object 'System.Collections.Generic.List[KeeperSecurity.Vault.KeeperNSFRecordRemoval]' $folderHint = $null if ($Folder) { [KeeperSecurity.Vault.FolderNode]$folderNode = $null if (-not $vault.TryResolveKeeperNSFFolder($Folder, [ref]$folderNode)) { Write-Error -Message "Keeper NSF folder `"$Folder`" was not found. Run Sync-Keeper or nsf-list first." return } $folderHint = $Folder } elseif ($Operation -ne 'owner-trash' -and $Script:Context.CurrentFolder) { $folderHint = $Script:Context.CurrentFolder } $op = switch ($Operation) { 'owner-trash' { [KeeperSecurity.Vault.KeeperNSFRecordRemoveOperation]::OwnerTrash } 'folder-trash' { [KeeperSecurity.Vault.KeeperNSFRecordRemoveOperation]::FolderTrash } 'unlink' { [KeeperSecurity.Vault.KeeperNSFRecordRemoveOperation]::Unlink } } if ($op -eq [KeeperSecurity.Vault.KeeperNSFRecordRemoveOperation]::Unlink -and [string]::IsNullOrWhiteSpace($folderHint)) { Write-Error -Message "Folder context is required for unlink. Use -Folder or cd into a Keeper NSF folder." return } } process { foreach ($name in $Record) { [KeeperSecurity.Vault.KeeperNSFRecord]$kdRecord = $null if (-not $vault.TryResolveKeeperNSFRecord($name, [ref]$kdRecord)) { Write-Error -Message "Keeper NSF record `"$name`" was not found. Run Sync-Keeper or nsf-list first." continue } [string]$folderUid = $null if (-not $vault.TryResolveKeeperNSFRecordRemovalFolder($kdRecord.RecordUid, $folderHint, $op, [ref]$folderUid)) { if ($Folder) { Write-Error -Message "Keeper NSF folder `"$Folder`" was not found. Run Sync-Keeper or nsf-list first." } else { Write-Error -Message "No folder context for record `"$name`". Use -Folder or -Operation owner-trash." } continue } $removal = New-Object KeeperSecurity.Vault.KeeperNSFRecordRemoval $removal.RecordUid = $kdRecord.RecordUid $removal.FolderUid = $folderUid $removal.Operation = $op $removals.Add($removal) } } end { if ($removals.Count -eq 0) { return } Write-Host "" Write-Host "=== Keeper NSF Remove Preview ===" -ForegroundColor Cyan $previewResult = $vault.RemoveKeeperNSFRecords($removals, $true).GetAwaiter().GetResult() Write-KeeperNSFRemoveImpact -Response $previewResult.PreviewResponse $previewErrors = @($previewResult.PreviewResponse.Results | Where-Object { $_.Error -and -not [string]::IsNullOrWhiteSpace($_.Error.Message) }) if ($previewErrors.Count -gt 0) { Write-Host "" Write-Host "One or more records could not be previewed. Aborting." -ForegroundColor Yellow return } try { [KeeperSecurity.Vault.VaultOnline]::ValidateRemoveResponse($previewResult.PreviewResponse, $false) } catch { Write-Error -Message $_.Exception.Message return } if ($DryRun) { Write-Host "" Write-Host "Dry run: no records were removed." -ForegroundColor DarkYellow return } if (-not $Force) { $prompt = if ($Operation -eq 'owner-trash') { "Are you sure you want to move the record(s) above to your trash? (yes/No)" } elseif ($Operation -eq 'folder-trash') { "Are you sure you want to move the record(s) above to folder trash? (yes/No)" } else { "Are you sure you want to unlink the record(s) above from the folder? (yes/No)" } $confirmation = Read-Host $prompt if ($confirmation -notmatch '^(y|yes)$') { Write-Host "Remove operation cancelled" return } } if ($previewResult.PreviewResponse.ConfirmationToken.IsEmpty) { Write-Error -Message "Preview did not return a confirmation token." return } Write-Host "" Write-Host "Removing records..." -ForegroundColor Cyan $confirmResult = $vault.RemoveKeeperNSFRecords($removals, $false).GetAwaiter().GetResult() if (-not $confirmResult.Confirmed) { Write-Error -Message "Record removal was not confirmed by the server." return } $vault.SyncDown($false).GetAwaiter().GetResult() | Out-Null Write-Host "" Write-Host "Keeper NSF record removal completed." -ForegroundColor Green } } New-Alias -Name nsf-rm -Value Remove-KeeperNSFRecord function Link-KeeperNSFRecord { <# .Synopsis Links a Keeper NSF record into a Keeper NSF folder (Keeper NSF v3 API). .Parameter Record Record UID or title. .Parameter Folder Destination folder UID, name, or "/" for Keeper NSF root. #> [CmdletBinding(SupportsShouldProcess, DefaultParameterSetName = 'Default')] Param( [Parameter(Position = 0, Mandatory = $true)] [string] $Record, [Parameter(Position = 1, Mandatory = $true)] [string] $Folder ) [KeeperSecurity.Vault.VaultOnline]$vault = getVault [KeeperSecurity.Vault.KeeperNSFRecord]$kdRecord = $null if (-not $vault.TryResolveKeeperNSFRecord($Record, [ref]$kdRecord)) { Write-Error -Message "Keeper NSF record `"$Record`" was not found. Run Sync-Keeper or nsf-list first." return } [KeeperSecurity.Vault.FolderNode]$folderNode = $null if (-not $vault.TryResolveKeeperNSFFolder($Folder, [ref]$folderNode)) { Write-Error -Message "Keeper NSF folder `"$Folder`" was not found. Run Sync-Keeper or nsf-list first." return } $folderLabel = if ([string]::IsNullOrEmpty($folderNode.FolderUid)) { 'root' } else { "$($folderNode.Name) ($($folderNode.FolderUid))" } $target = "$($kdRecord.RecordUid) -> $folderLabel" if (-not $PSCmdlet.ShouldProcess($target, "Link Keeper NSF record into folder")) { return } try { $result = $vault.LinkKeeperNSFRecordToFolder($Record, $Folder).GetAwaiter().GetResult() [KeeperSecurity.Vault.VaultOnline]::ValidateFolderRecordUpdateResult($result) } catch { Write-Error -Message $_.Exception.Message return } $vault.SyncDown($false).GetAwaiter().GetResult() | Out-Null Write-Host "Keeper NSF record linked into folder." -ForegroundColor Green } New-Alias -Name nsf-ln -Value Link-KeeperNSFRecord function Transfer-KeeperNSFRecordOwnership { <# .Synopsis Transfers ownership of one or more Keeper NSF records to another user (Keeper NSF v3 API). .Description Positional arguments: one or more record UIDs or titles, then the new owner's email address. After a successful transfer you will no longer have access to the record(s). .Example nsf-transfer-record rvwIBG_ban2VTH64OsnzLn alice@example.com .Example nsf-transfer-record rec1 rec2 rec3 alice@example.com #> [CmdletBinding(SupportsShouldProcess, DefaultParameterSetName = 'Default')] Param( [Parameter(ValueFromRemainingArguments = $true, Mandatory = $true)] [string[]] $ArgumentList, [Alias('f')] [switch] $Force ) if ($ArgumentList.Count -lt 2) { Write-Error -Message "Usage: nsf-transfer-record RECORD [RECORD...] NEW_OWNER_EMAIL" return } $newOwnerEmail = $ArgumentList[-1] $recordArgs = @($ArgumentList[0..($ArgumentList.Count - 2)]) if ($recordArgs.Count -eq 0 -or [string]::IsNullOrWhiteSpace($newOwnerEmail)) { Write-Error -Message "Record UID(s) and new owner email are required." return } if ($newOwnerEmail -notmatch '@') { Write-Error -Message "New owner must be an email address: `"$newOwnerEmail`"" return } [KeeperSecurity.Vault.VaultOnline]$vault = getVault $resolvedRecords = New-Object 'System.Collections.Generic.List[string]' foreach ($name in $recordArgs) { [KeeperSecurity.Vault.KeeperNSFRecord]$kdRecord = $null if (-not $vault.TryResolveKeeperNSFRecord($name, [ref]$kdRecord)) { Write-Error -Message "Keeper NSF record `"$name`" was not found. Run Sync-Keeper or nsf-list first." return } $resolvedRecords.Add($kdRecord.RecordUid) } if (-not $Force) { Write-Host "" Write-Host "*** WARNING ***" -ForegroundColor Yellow Write-Host "After ownership is transferred you will lose owner rights on the record(s)." Write-Host "You may still see the record(s) if you retain access via a shared folder or admin role; otherwise they will disappear after sync." Write-Host "Make sure the new owner is correct before continuing." Write-Host "" $confirmation = Read-Host "Are you sure you want to transfer ownership to '$newOwnerEmail'? This action cannot be undone. (yes/No)" if ($confirmation -notmatch '^(y|yes)$') { Write-Host "Transfer operation cancelled" return } } try { $results = $vault.TransferKeeperNSFRecordOwnership($resolvedRecords, $newOwnerEmail).GetAwaiter().GetResult() [KeeperSecurity.Vault.VaultOnline]::ValidateKeeperNSFTransferResults($results) foreach ($result in $results) { Write-Host "Record '$($result.RecordUid)' ownership transferred to $($result.Username)." -ForegroundColor Green Write-Host "You no longer own this record. Run Sync-Keeper to refresh; it will remain visible only if you retain access via a shared folder or admin role." -ForegroundColor Yellow } } catch { Write-Error -Message $_.Exception.Message return } $vault.SyncDown($false).GetAwaiter().GetResult() | Out-Null } New-Alias -Name nsf-transfer-record -Value Transfer-KeeperNSFRecordOwnership # SIG # Begin signature block # MIInvgYJKoZIhvcNAQcCoIInrzCCJ6sCAQExDzANBglghkgBZQMEAgEFADB5Bgor # BgEEAYI3AgEEoGswaTA0BgorBgEEAYI3AgEeMCYCAwEAAAQQH8w7YFlLCE63JNLG # KX7zUQIBAAIBAAIBAAIBAAIBADAxMA0GCWCGSAFlAwQCAQUABCDMzv3inyfLwyrj # 9PiMyIf4AHcEg9qq+naIrUvyY686zKCCITswggWNMIIEdaADAgECAhAOmxiO+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 # twGpn1eqXijiuZQwggawMIIEmKADAgECAhAIrUCyYNKcTJ9ezam9k67ZMA0GCSqG # SIb3DQEBDAUAMGIxCzAJBgNVBAYTAlVTMRUwEwYDVQQKEwxEaWdpQ2VydCBJbmMx # GTAXBgNVBAsTEHd3dy5kaWdpY2VydC5jb20xITAfBgNVBAMTGERpZ2lDZXJ0IFRy # dXN0ZWQgUm9vdCBHNDAeFw0yMTA0MjkwMDAwMDBaFw0zNjA0MjgyMzU5NTlaMGkx # CzAJBgNVBAYTAlVTMRcwFQYDVQQKEw5EaWdpQ2VydCwgSW5jLjFBMD8GA1UEAxM4 # RGlnaUNlcnQgVHJ1c3RlZCBHNCBDb2RlIFNpZ25pbmcgUlNBNDA5NiBTSEEzODQg # MjAyMSBDQTEwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQDVtC9C0Cit # eLdd1TlZG7GIQvUzjOs9gZdwxbvEhSYwn6SOaNhc9es0JAfhS0/TeEP0F9ce2vnS # 1WcaUk8OoVf8iJnBkcyBAz5NcCRks43iCH00fUyAVxJrQ5qZ8sU7H/Lvy0daE6ZM # swEgJfMQ04uy+wjwiuCdCcBlp/qYgEk1hz1RGeiQIXhFLqGfLOEYwhrMxe6TSXBC # Mo/7xuoc82VokaJNTIIRSFJo3hC9FFdd6BgTZcV/sk+FLEikVoQ11vkunKoAFdE3 # /hoGlMJ8yOobMubKwvSnowMOdKWvObarYBLj6Na59zHh3K3kGKDYwSNHR7OhD26j # q22YBoMbt2pnLdK9RBqSEIGPsDsJ18ebMlrC/2pgVItJwZPt4bRc4G/rJvmM1bL5 # OBDm6s6R9b7T+2+TYTRcvJNFKIM2KmYoX7BzzosmJQayg9Rc9hUZTO1i4F4z8ujo # 7AqnsAMrkbI2eb73rQgedaZlzLvjSFDzd5Ea/ttQokbIYViY9XwCFjyDKK05huzU # tw1T0PhH5nUwjewwk3YUpltLXXRhTT8SkXbev1jLchApQfDVxW0mdmgRQRNYmtwm # KwH0iU1Z23jPgUo+QEdfyYFQc4UQIyFZYIpkVMHMIRroOBl8ZhzNeDhFMJlP/2NP # TLuqDQhTQXxYPUez+rbsjDIJAsxsPAxWEQIDAQABo4IBWTCCAVUwEgYDVR0TAQH/ # BAgwBgEB/wIBADAdBgNVHQ4EFgQUaDfg67Y7+F8Rhvv+YXsIiGX0TkIwHwYDVR0j # BBgwFoAU7NfjgtJxXWRM3y5nP+e6mK4cD08wDgYDVR0PAQH/BAQDAgGGMBMGA1Ud # JQQMMAoGCCsGAQUFBwMDMHcGCCsGAQUFBwEBBGswaTAkBggrBgEFBQcwAYYYaHR0 # cDovL29jc3AuZGlnaWNlcnQuY29tMEEGCCsGAQUFBzAChjVodHRwOi8vY2FjZXJ0 # cy5kaWdpY2VydC5jb20vRGlnaUNlcnRUcnVzdGVkUm9vdEc0LmNydDBDBgNVHR8E # PDA6MDigNqA0hjJodHRwOi8vY3JsMy5kaWdpY2VydC5jb20vRGlnaUNlcnRUcnVz # dGVkUm9vdEc0LmNybDAcBgNVHSAEFTATMAcGBWeBDAEDMAgGBmeBDAEEATANBgkq # hkiG9w0BAQwFAAOCAgEAOiNEPY0Idu6PvDqZ01bgAhql+Eg08yy25nRm95RysQDK # r2wwJxMSnpBEn0v9nqN8JtU3vDpdSG2V1T9J9Ce7FoFFUP2cvbaF4HZ+N3HLIvda # qpDP9ZNq4+sg0dVQeYiaiorBtr2hSBh+3NiAGhEZGM1hmYFW9snjdufE5BtfQ/g+ # lP92OT2e1JnPSt0o618moZVYSNUa/tcnP/2Q0XaG3RywYFzzDaju4ImhvTnhOE7a # brs2nfvlIVNaw8rpavGiPttDuDPITzgUkpn13c5UbdldAhQfQDN8A+KVssIhdXNS # y0bYxDQcoqVLjc1vdjcshT8azibpGL6QB7BDf5WIIIJw8MzK7/0pNVwfiThV9zeK # iwmhywvpMRr/LhlcOXHhvpynCgbWJme3kuZOX956rEnPLqR0kq3bPKSchh/jwVYb # KyP/j7XqiHtwa+aguv06P0WmxOgWkVKLQcBIhEuWTatEQOON8BUozu3xGFYHKi8Q # xAwIZDwzj64ojDzLj4gLDb879M4ee47vtevLt/B3E+bnKD+sEq6lLyJsQfmCXBVm # zGwOysWGw/YmMwwHS6DTBwJqakAwSEs0qFEgu60bhQjiWQ1tygVQK+pKHJ6l/aCn # HwZ05/LWUpD9r4VIIflXO7ScA+2GRfS0YW6/aOImYIbqyK+p/pQd52MbOoZWeE4w # gga0MIIEnKADAgECAhANx6xXBf8hmS5AQyIMOkmGMA0GCSqGSIb3DQEBCwUAMGIx # CzAJBgNVBAYTAlVTMRUwEwYDVQQKEwxEaWdpQ2VydCBJbmMxGTAXBgNVBAsTEHd3 # dy5kaWdpY2VydC5jb20xITAfBgNVBAMTGERpZ2lDZXJ0IFRydXN0ZWQgUm9vdCBH # NDAeFw0yNTA1MDcwMDAwMDBaFw0zODAxMTQyMzU5NTlaMGkxCzAJBgNVBAYTAlVT # MRcwFQYDVQQKEw5EaWdpQ2VydCwgSW5jLjFBMD8GA1UEAxM4RGlnaUNlcnQgVHJ1 # c3RlZCBHNCBUaW1lU3RhbXBpbmcgUlNBNDA5NiBTSEEyNTYgMjAyNSBDQTEwggIi # MA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQC0eDHTCphBcr48RsAcrHXbo0Zo # dLRRF51NrY0NlLWZloMsVO1DahGPNRcybEKq+RuwOnPhof6pvF4uGjwjqNjfEvUi # 6wuim5bap+0lgloM2zX4kftn5B1IpYzTqpyFQ/4Bt0mAxAHeHYNnQxqXmRinvuNg # xVBdJkf77S2uPoCj7GH8BLuxBG5AvftBdsOECS1UkxBvMgEdgkFiDNYiOTx4OtiF # cMSkqTtF2hfQz3zQSku2Ws3IfDReb6e3mmdglTcaarps0wjUjsZvkgFkriK9tUKJ # m/s80FiocSk1VYLZlDwFt+cVFBURJg6zMUjZa/zbCclF83bRVFLeGkuAhHiGPMvS # GmhgaTzVyhYn4p0+8y9oHRaQT/aofEnS5xLrfxnGpTXiUOeSLsJygoLPp66bkDX1 # ZlAeSpQl92QOMeRxykvq6gbylsXQskBBBnGy3tW/AMOMCZIVNSaz7BX8VtYGqLt9 # MmeOreGPRdtBx3yGOP+rx3rKWDEJlIqLXvJWnY0v5ydPpOjL6s36czwzsucuoKs7 # Yk/ehb//Wx+5kMqIMRvUBDx6z1ev+7psNOdgJMoiwOrUG2ZdSoQbU2rMkpLiQ6bG # RinZbI4OLu9BMIFm1UUl9VnePs6BaaeEWvjJSjNm2qA+sdFUeEY0qVjPKOWug/G6 # X5uAiynM7Bu2ayBjUwIDAQABo4IBXTCCAVkwEgYDVR0TAQH/BAgwBgEB/wIBADAd # BgNVHQ4EFgQU729TSunkBnx6yuKQVvYv1Ensy04wHwYDVR0jBBgwFoAU7NfjgtJx # XWRM3y5nP+e6mK4cD08wDgYDVR0PAQH/BAQDAgGGMBMGA1UdJQQMMAoGCCsGAQUF # BwMIMHcGCCsGAQUFBwEBBGswaTAkBggrBgEFBQcwAYYYaHR0cDovL29jc3AuZGln # aWNlcnQuY29tMEEGCCsGAQUFBzAChjVodHRwOi8vY2FjZXJ0cy5kaWdpY2VydC5j # b20vRGlnaUNlcnRUcnVzdGVkUm9vdEc0LmNydDBDBgNVHR8EPDA6MDigNqA0hjJo # dHRwOi8vY3JsMy5kaWdpY2VydC5jb20vRGlnaUNlcnRUcnVzdGVkUm9vdEc0LmNy # bDAgBgNVHSAEGTAXMAgGBmeBDAEEAjALBglghkgBhv1sBwEwDQYJKoZIhvcNAQEL # BQADggIBABfO+xaAHP4HPRF2cTC9vgvItTSmf83Qh8WIGjB/T8ObXAZz8OjuhUxj # aaFdleMM0lBryPTQM2qEJPe36zwbSI/mS83afsl3YTj+IQhQE7jU/kXjjytJgnn0 # hvrV6hqWGd3rLAUt6vJy9lMDPjTLxLgXf9r5nWMQwr8Myb9rEVKChHyfpzee5kH0 # F8HABBgr0UdqirZ7bowe9Vj2AIMD8liyrukZ2iA/wdG2th9y1IsA0QF8dTXqvcnT # mpfeQh35k5zOCPmSNq1UH410ANVko43+Cdmu4y81hjajV/gxdEkMx1NKU4uHQcKf # ZxAvBAKqMVuqte69M9J6A47OvgRaPs+2ykgcGV00TYr2Lr3ty9qIijanrUR3anzE # wlvzZiiyfTPjLbnFRsjsYg39OlV8cipDoq7+qNNjqFzeGxcytL5TTLL4ZaoBdqbh # OhZ3ZRDUphPvSRmMThi0vw9vODRzW6AxnJll38F0cuJG7uEBYTptMSbhdhGQDpOX # gpIUsWTjd6xpR6oaQf/DJbg3s6KCLPAlZ66RzIg9sC+NJpud/v4+7RWsWCiKi9EO # LLHfMR2ZyJ/+xhCx9yHbxtl5TPau1j/1MIDpMPx0LckTetiSuEtQvLsNz3Qbp7wG # WqbIiOWCnb5WqxL3/BAPvIXKUjPSxyZsq8WhbaM2tszWkPZPubdcMIIG7TCCBNWg # AwIBAgIQCoDvGEuN8QWC0cR2p5V0aDANBgkqhkiG9w0BAQsFADBpMQswCQYDVQQG # EwJVUzEXMBUGA1UEChMORGlnaUNlcnQsIEluYy4xQTA/BgNVBAMTOERpZ2lDZXJ0 # IFRydXN0ZWQgRzQgVGltZVN0YW1waW5nIFJTQTQwOTYgU0hBMjU2IDIwMjUgQ0Ex # MB4XDTI1MDYwNDAwMDAwMFoXDTM2MDkwMzIzNTk1OVowYzELMAkGA1UEBhMCVVMx # FzAVBgNVBAoTDkRpZ2lDZXJ0LCBJbmMuMTswOQYDVQQDEzJEaWdpQ2VydCBTSEEy # NTYgUlNBNDA5NiBUaW1lc3RhbXAgUmVzcG9uZGVyIDIwMjUgMTCCAiIwDQYJKoZI # hvcNAQEBBQADggIPADCCAgoCggIBANBGrC0Sxp7Q6q5gVrMrV7pvUf+GcAoB38o3 # zBlCMGMyqJnfFNZx+wvA69HFTBdwbHwBSOeLpvPnZ8ZN+vo8dE2/pPvOx/Vj8Tch # TySA2R4QKpVD7dvNZh6wW2R6kSu9RJt/4QhguSssp3qome7MrxVyfQO9sMx6ZAWj # FDYOzDi8SOhPUWlLnh00Cll8pjrUcCV3K3E0zz09ldQ//nBZZREr4h/GI6Dxb2Uo # yrN0ijtUDVHRXdmncOOMA3CoB/iUSROUINDT98oksouTMYFOnHoRh6+86Ltc5zjP # KHW5KqCvpSduSwhwUmotuQhcg9tw2YD3w6ySSSu+3qU8DD+nigNJFmt6LAHvH3KS # uNLoZLc1Hf2JNMVL4Q1OpbybpMe46YceNA0LfNsnqcnpJeItK/DhKbPxTTuGoX7w # JNdoRORVbPR1VVnDuSeHVZlc4seAO+6d2sC26/PQPdP51ho1zBp+xUIZkpSFA8vW # doUoHLWnqWU3dCCyFG1roSrgHjSHlq8xymLnjCbSLZ49kPmk8iyyizNDIXj//cOg # rY7rlRyTlaCCfw7aSUROwnu7zER6EaJ+AliL7ojTdS5PWPsWeupWs7NpChUk555K # 096V1hE0yZIXe+giAwW00aHzrDchIc2bQhpp0IoKRR7YufAkprxMiXAJQ1XCmnCf # gPf8+3mnAgMBAAGjggGVMIIBkTAMBgNVHRMBAf8EAjAAMB0GA1UdDgQWBBTkO/zy # Me39/dfzkXFjGVBDz2GM6DAfBgNVHSMEGDAWgBTvb1NK6eQGfHrK4pBW9i/USezL # TjAOBgNVHQ8BAf8EBAMCB4AwFgYDVR0lAQH/BAwwCgYIKwYBBQUHAwgwgZUGCCsG # AQUFBwEBBIGIMIGFMCQGCCsGAQUFBzABhhhodHRwOi8vb2NzcC5kaWdpY2VydC5j # b20wXQYIKwYBBQUHMAKGUWh0dHA6Ly9jYWNlcnRzLmRpZ2ljZXJ0LmNvbS9EaWdp # Q2VydFRydXN0ZWRHNFRpbWVTdGFtcGluZ1JTQTQwOTZTSEEyNTYyMDI1Q0ExLmNy # dDBfBgNVHR8EWDBWMFSgUqBQhk5odHRwOi8vY3JsMy5kaWdpY2VydC5jb20vRGln # aUNlcnRUcnVzdGVkRzRUaW1lU3RhbXBpbmdSU0E0MDk2U0hBMjU2MjAyNUNBMS5j # cmwwIAYDVR0gBBkwFzAIBgZngQwBBAIwCwYJYIZIAYb9bAcBMA0GCSqGSIb3DQEB # CwUAA4ICAQBlKq3xHCcEua5gQezRCESeY0ByIfjk9iJP2zWLpQq1b4URGnwWBdEZ # D9gBq9fNaNmFj6Eh8/YmRDfxT7C0k8FUFqNh+tshgb4O6Lgjg8K8elC4+oWCqnU/ # ML9lFfim8/9yJmZSe2F8AQ/UdKFOtj7YMTmqPO9mzskgiC3QYIUP2S3HQvHG1FDu # +WUqW4daIqToXFE/JQ/EABgfZXLWU0ziTN6R3ygQBHMUBaB5bdrPbF6MRYs03h4o # bEMnxYOX8VBRKe1uNnzQVTeLni2nHkX/QqvXnNb+YkDFkxUGtMTaiLR9wjxUxu2h # ECZpqyU1d0IbX6Wq8/gVutDojBIFeRlqAcuEVT0cKsb+zJNEsuEB7O7/cuvTQasn # M9AWcIQfVjnzrvwiCZ85EE8LUkqRhoS3Y50OHgaY7T/lwd6UArb+BOVAkg2oOvol # /DJgddJ35XTxfUlQ+8Hggt8l2Yv7roancJIFcbojBcxlRcGG0LIhp6GvReQGgMgY # xQbV1S3CrWqZzBt1R9xJgKf47CdxVRd/ndUlQ05oxYy2zRWVFjF7mcr4C34Mj3oc # CVccAvlKV9jEnstrniLvUxxVZE/rptb7IRE2lskKPIJgbaP5t2nGj/ULLi49xTcB # ZU8atufk+EMF/cWuiC7POGT75qaL6vdCvHlshtjdNXOCIUjsarfNZzCCB0kwggUx # oAMCAQICEAHdzU+FVN9jCMv0HhHagNUwDQYJKoZIhvcNAQELBQAwaTELMAkGA1UE # BhMCVVMxFzAVBgNVBAoTDkRpZ2lDZXJ0LCBJbmMuMUEwPwYDVQQDEzhEaWdpQ2Vy # dCBUcnVzdGVkIEc0IENvZGUgU2lnbmluZyBSU0E0MDk2IFNIQTM4NCAyMDIxIENB # MTAeFw0yNjA2MDUwMDAwMDBaFw0yNzA2MDQyMzU5NTlaMIHRMRMwEQYLKwYBBAGC # NzwCAQMTAlVTMRkwFwYLKwYBBAGCNzwCAQITCERlbGF3YXJlMR0wGwYDVQQPDBRQ # cml2YXRlIE9yZ2FuaXphdGlvbjEQMA4GA1UEBRMHMzQwNzk4NTELMAkGA1UEBhMC # VVMxETAPBgNVBAgTCElsbGlub2lzMRAwDgYDVQQHEwdDaGljYWdvMR0wGwYDVQQK # ExRLZWVwZXIgU2VjdXJpdHkgSW5jLjEdMBsGA1UEAxMUS2VlcGVyIFNlY3VyaXR5 # IEluYy4wggGiMA0GCSqGSIb3DQEBAQUAA4IBjwAwggGKAoIBgQCb4DRTV0sNQsa1 # 0YRh+bliabmLOVYr6S0+BSVvRJAN3SHP6x52i1Dkpki5xVDIH06ZnnsToVrgvTv+ # QxGwsn9SAPHEZ/PIJRFxbMR4ShDaptYyL4f0u4k/3HwRzIleWE4mTUonYH8BdgLw # /F53B7wa7VTDHtxXltYTibEOwJxYCOi4Zr2FYQhjw14/CHcqS3FSMs6YYU2T56+g # w819hQM3K0YlwTNOFoIm1v7/ZZZiJGH8uGDsvy1makh1Xyyo/wN8EbQ1nbslmePT # roPm9w7WqiP/yiq+CZHiuTk9JK5bEgkWG3ns+v25cI251WidJx3SU7IZnX0OTd6/ # ZdKhprD5Gcfy5GBbJdcYw2WycQRW0PT5BEt55xRE0heufkpDaTUN6RdOuJdXbkl0 # hV91IZIuhueEMCk3h5mDTlU5gImxqj0R/TbAxjSSGTKCeuYFkQIRqytSabdrZZ48 # kW5hOIZMVDY1f4kpPJa8UeEvDZXT3vrtj36aSJrwez2uh4FMNlkCAwEAAaOCAgIw # ggH+MB8GA1UdIwQYMBaAFGg34Ou2O/hfEYb7/mF7CIhl9E5CMB0GA1UdDgQWBBT1 # SmCYU/7Yrz1fX66Ur5nSzlSYOzA9BgNVHSAENjA0MDIGBWeBDAEDMCkwJwYIKwYB # BQUHAgEWG2h0dHA6Ly93d3cuZGlnaWNlcnQuY29tL0NQUzAOBgNVHQ8BAf8EBAMC # B4AwEwYDVR0lBAwwCgYIKwYBBQUHAwMwgbUGA1UdHwSBrTCBqjBToFGgT4ZNaHR0 # cDovL2NybDMuZGlnaWNlcnQuY29tL0RpZ2lDZXJ0VHJ1c3RlZEc0Q29kZVNpZ25p # bmdSU0E0MDk2U0hBMzg0MjAyMUNBMS5jcmwwU6BRoE+GTWh0dHA6Ly9jcmw0LmRp # Z2ljZXJ0LmNvbS9EaWdpQ2VydFRydXN0ZWRHNENvZGVTaWduaW5nUlNBNDA5NlNI # QTM4NDIwMjFDQTEuY3JsMIGUBggrBgEFBQcBAQSBhzCBhDAkBggrBgEFBQcwAYYY # aHR0cDovL29jc3AuZGlnaWNlcnQuY29tMFwGCCsGAQUFBzAChlBodHRwOi8vY2Fj # ZXJ0cy5kaWdpY2VydC5jb20vRGlnaUNlcnRUcnVzdGVkRzRDb2RlU2lnbmluZ1JT # QTQwOTZTSEEzODQyMDIxQ0ExLmNydDAJBgNVHRMEAjAAMA0GCSqGSIb3DQEBCwUA # A4ICAQBcavcUHNFEg872HDRq2+hRlnvaghCXv7X/6h9HSzjAQP3rt95BZty3ASqi # 2MYyGQLGdDl4DToe/WhajtEOBOYa83agW6tBvrfcKRrDrwJOMPTbwNYvn+GuiL4T # CKzXaytWiJJbrc5odc7Ecat2ZvJylpPmNainr4Q0LzzH23Gea/Mm/hIJTN4IGgrH # hrXiTIIW/ZUzrY6g8b3RZB4BA497n43wNdSqP+C3ntFw6NiGB4Z25SW4YntIxYPv # Kf37OVhF0xqxLC1sK/XxgK0EGQ6iaj8Ncpr2C5vSNZqfW2MndxOA1W67pgDpg83k # UWG+/YJeGhqOTF82/0kIzQXeI/lIqbnL/IJAJqSm/ROSpsGUKVbzk03cpTD55ZQX # WjM0fLirypBqY05T8gnh1L0fSwxr/SwJZ8OddivgyK1YOMn02nnsEG5kxBt9cMX4 # JCYABhypmAVDRvyYifEVdoFWv2gAXXW+PPRvlNa6E4aMCZrVcoKHiyeMAXOi1IC9 # mHvC2+foTSMFueq3AdnYfeKnZnAiKXKRhXcdHbQYcR2A7AIzIcqahPYr4FNEgb/E # /y/kypAkf0rMHlYl1kNqLs2Nv1UnMEHYT5YmDVLO63+1Trcw4zTZ70zuqIqeID/d # nbOlgtyG6DSRCL7f0E7kP18f4RoX5i1PkfeO4VJHsAuCeNG1qjGCBdkwggXVAgEB # MH0waTELMAkGA1UEBhMCVVMxFzAVBgNVBAoTDkRpZ2lDZXJ0LCBJbmMuMUEwPwYD # VQQDEzhEaWdpQ2VydCBUcnVzdGVkIEc0IENvZGUgU2lnbmluZyBSU0E0MDk2IFNI # QTM4NCAyMDIxIENBMQIQAd3NT4VU32MIy/QeEdqA1TANBglghkgBZQMEAgEFAKCB # hDAYBgorBgEEAYI3AgEMMQowCKACgAChAoAAMBkGCSqGSIb3DQEJAzEMBgorBgEE # AYI3AgEEMBwGCisGAQQBgjcCAQsxDjAMBgorBgEEAYI3AgEVMC8GCSqGSIb3DQEJ # BDEiBCDVAqaTz8AGxgGkU06GvI3D3Jw8WTLFR4VW9FdvFbqgqDANBgkqhkiG9w0B # AQEFAASCAYATjZ9F51DxERNlNWrxncPaXf0flf1tf1BaiF1SDKE/lxi4TRfMuCJ+ # 0NEngZoXZ25iWEJjDsf9ST3nTJYf7ypciwu3p8B8ILhSUvyZYEP24nO3RFice/Ue # lM866ynLq1AAcjep7gc8LWV0LgUz0KVm2CznunVmAcsj2TuSm32MHzis/5eBQ8Pu # oMp0p31k3bj1oOL04fHKOkQYtl747vmlb9IWIK9070gjSujlQa4uDmbDCBGO0KLt # /h8xovaQoO5tALwZkB2QEh3izyw9FJYqZ2ccPN3d5uYixkLSKOtX55ZF9IfEgQwL # U2PZUrOIlKFZpaKX223jlDFevS2OT01J+2rqIlSOdJi3+aeyaN5kBJNtH8Ic+4om # 9vRqkvyXs4ECxbzM9jXOP4vj9pGpjqoDvSwySMNSSh7mKBJQg126F/8NfN/7UVAe # 2lVWZNJCSfq3FECN3tDOmOA8Ak3pgmks4fB+U8vR7/dD5iTjpbXMfwvNKqoBYRsn # TePbH7wz2MChggMmMIIDIgYJKoZIhvcNAQkGMYIDEzCCAw8CAQEwfTBpMQswCQYD # VQQGEwJVUzEXMBUGA1UEChMORGlnaUNlcnQsIEluYy4xQTA/BgNVBAMTOERpZ2lD # ZXJ0IFRydXN0ZWQgRzQgVGltZVN0YW1waW5nIFJTQTQwOTYgU0hBMjU2IDIwMjUg # Q0ExAhAKgO8YS43xBYLRxHanlXRoMA0GCWCGSAFlAwQCAQUAoGkwGAYJKoZIhvcN # AQkDMQsGCSqGSIb3DQEHATAcBgkqhkiG9w0BCQUxDxcNMjYwNjEzMDA1NjMxWjAv # BgkqhkiG9w0BCQQxIgQgJodudyDiPi6npeKjWtv6//glNgT3/+uUavkSjOkU1QQw # DQYJKoZIhvcNAQEBBQAEggIAgkXul1H3ba1mofrsSEqW6og2VL4kv4GeNpka4Mft # wQ0sCD0SHLVKkg7+rZGz/3XzQj2I7QUXoWpJS+hga3D0JQh/PRdFP0qA6cTwVgeD # iGhb8gU5g9vNH0w1/FPODPI8ID/DKF1kb5auldHz9MSMZ/s/E3ttVlquM2vPJKkK # haS35f76uXNajmTjHeIo66DpRBYCFUV3ooWjV0ikja720kiVUNwmqL59nYqBE5KJ # 140tQIMiz+5JclMHcLGHX4NM1yVjdlpQ+5KxE3tktx9/49S+mTiVFpaYmd64Nn6r # svYdainZvE38wJ0iKEVakjmDRsIZzelOAHRbLoFffh2ABZzctQoUQbsF/Tqy7e9b # DtO1PkrwV2r5FMr+MMWKXiYol6cIKSyKhGWnyuD2nj4xl5aLBJ/1Cr0gObGmIvv0 # oNWesKaCfjn359gzWd8ArZarkf5UWwAnNcbSJJHBLr//rlHzwlKKX3Tgy3/NU8DI # 6OByYDY+/4W2/DLIMnxm+QdIEp/r+iHinq3J5zPwjlk+f3iXgAkKtY8+LL4MwEoZ # dW5G7rrF1XqvMp/i4y1L+6seTqOTPfiKC/jsmhT1FcC2wwnWq/E1UlWPIqyLvAYg # piOQLVtaZ6DSHUFxj9d6NkdLImdrv205IxW+AO7rshHjujevFParDIf6zoBZnFbd # /RY= # SIG # End signature block |