ReportCommands/ShareReport.ps1

#requires -Version 5.1

function Script:Get-ShareReportFolderPath {
    param(
        [Parameter(Mandatory = $true)]
        [KeeperSecurity.Vault.VaultOnline] $Vault,

        [Parameter()]
        [string] $FolderUid
    )

    if ([string]::IsNullOrEmpty($FolderUid)) {
        return ''
    }

    $parts = [System.Collections.Generic.List[string]]::new()
    $visited = [System.Collections.Generic.HashSet[string]]::new()
    $current = $FolderUid

    while (-not [string]::IsNullOrEmpty($current) -and $visited.Add($current)) {
        [KeeperSecurity.Vault.FolderNode] $folder = $null
        if (-not $Vault.TryGetFolder($current, [ref]$folder)) { break }
        $parts.Add($folder.Name)
        $current = $folder.ParentUid
    }

    $parts.Reverse()
    return ($parts -join '\')
}

function Script:Get-ShareReportRecordShares {
    param(
        [Parameter(Mandatory = $true)]
        [KeeperSecurity.Vault.VaultOnline] $Vault,

        [Parameter(Mandatory = $true)]
        [string[]] $RecordUids
    )

    $batchSize = 100
    $allShares = [System.Collections.Generic.List[KeeperSecurity.Vault.RecordSharePermissions]]::new()

    for ($i = 0; $i -lt $RecordUids.Count; $i += $batchSize) {
        $end = [Math]::Min($i + $batchSize, $RecordUids.Count)
        $batch = [System.Collections.Generic.List[string]]::new()
        for ($j = $i; $j -lt $end; $j++) {
            $batch.Add($RecordUids[$j])
        }
        try {
            $shares = $Vault.GetSharesForRecords([System.Collections.Generic.IEnumerable[string]]$batch).GetAwaiter().GetResult()
            foreach ($s in $shares) {
                $allShares.Add($s)
            }
        }
        catch {
            Write-Warning "Failed to retrieve record shares: $($_.Exception.Message)"
        }
    }

    return $allShares
}

function Script:Get-ShareReportTeamMembers {
    param(
        [Parameter(Mandatory = $true)]
        [KeeperSecurity.Vault.VaultOnline] $Vault
    )

    $teamMembers = @{}

    try {
        $enterprise = getEnterprise
    }
    catch {
        Write-Warning "Enterprise data is not available for team expansion. Details: $($_.Exception.Message)"
        return $teamMembers
    }

    if ($null -eq $enterprise -or $null -eq $enterprise.enterpriseData) {
        Write-Warning 'Enterprise data is not available for team expansion.'
        return $teamMembers
    }

    $enterpriseData = $enterprise.enterpriseData
    foreach ($team in $Vault.Teams) {
        $teamUid = $team.TeamUid
        $members = [System.Collections.Generic.List[string]]::new()
        foreach ($userId in $enterpriseData.GetUsersForTeam($teamUid)) {
            $user = $null
            if ($enterpriseData.TryGetUserById($userId, [ref]$user)) {
                $members.Add($user.Email)
            }
        }
        $teamMembers[$teamUid] = $members
    }

    return $teamMembers
}

function Script:Get-ShareReportAuditEvents {
    param(
        [Parameter(Mandatory = $true)]
        [KeeperSecurity.Authentication.IAuthentication] $Auth,

        [Parameter(Mandatory = $true)]
        [string] $RecordUid,

        [Parameter(Mandatory = $true)]
        [ref] $AramEnabled
    )

    $rq = New-Object KeeperSecurity.Enterprise.AuditLogCommands.GetAuditEventReportsCommand
    $rq.ReportType = 'raw'
    $rq.Limit = 1000
    $rq.Order = 'descending'

    $filter = New-Object KeeperSecurity.Enterprise.AuditLogCommands.ReportFilter
    $filter.EventTypes = @('share', 'record_share_outside_user', 'remove_share',
        'folder_add_team', 'folder_remove_team', 'folder_add_record', 'folder_remove_record')
    $filter.RecordUid = @($RecordUid)
    $rq.Filter = $filter

    try {
        $response = $Auth.ExecuteAuthCommand(
            $rq,
            [KeeperSecurity.Enterprise.AuditLogCommands.GetAuditEventReportsResponse],
            $true
        ).GetAwaiter().GetResult()
        $rs = [KeeperSecurity.Enterprise.AuditLogCommands.GetAuditEventReportsResponse]$response
    }
    catch [KeeperSecurity.Authentication.KeeperApiException] {
        $apiEx = $_.Exception
        if ($apiEx.Code -eq 'not_an_enterprise_user' -or $apiEx.Code -eq 'access_denied') {
            $AramEnabled.Value = $false
        }
        return @()
    }
    catch {
        return @()
    }

    if (-not $rs.Events -or $rs.Events.Count -eq 0) {
        return @()
    }

    $latestRecordEvents = [ordered]@{}
    $latestFolderEvents = [ordered]@{}

    foreach ($evt in $rs.Events) {
        $eventType = if ($evt.ContainsKey('audit_event_type')) { $evt['audit_event_type'].ToString() } else { '' }
        $recUid = if ($evt.ContainsKey('record_uid')) { $evt['record_uid'].ToString() } else { '' }
        $created = Get-ShareReportCreatedUnixSeconds -AuditEvent $evt

        if ($evt.ContainsKey('to_username')) {
            $toUser = $evt['to_username'].ToString()
            $key = "${recUid}-${toUser}"
            if ([string]::IsNullOrEmpty($recUid) -or [string]::IsNullOrEmpty($toUser)) { continue }

            $existing = $latestRecordEvents[$key]
            if ($null -eq $existing -or $created -gt $existing.Created) {
                $latestRecordEvents[$key] = [PSCustomObject]@{
                    Created = $created
                    Event   = $evt
                }
            }
        }
        elseif ($evt.ContainsKey('shared_folder_uid')) {
            $sfUid = $evt['shared_folder_uid'].ToString()
            $key = "${recUid}-${sfUid}"
            if ([string]::IsNullOrEmpty($recUid) -or [string]::IsNullOrEmpty($sfUid)) { continue }

            $existing = $latestFolderEvents[$key]
            if ($null -eq $existing -or $created -gt $existing.Created) {
                $latestFolderEvents[$key] = [PSCustomObject]@{
                    Created = $created
                    Event   = $evt
                }
            }
        }
    }

    $activeEvents = [System.Collections.Generic.List[object]]::new()
    foreach ($entry in $latestRecordEvents.Values) {
        $eventType = if ($entry.Event.ContainsKey('audit_event_type')) { $entry.Event['audit_event_type'].ToString() } else { '' }
        if ($eventType -in 'share', 'record_share_outside_user') {
            $activeEvents.Add($entry.Event) | Out-Null
        }
    }
    foreach ($entry in $latestFolderEvents.Values) {
        $eventType = if ($entry.Event.ContainsKey('audit_event_type')) { $entry.Event['audit_event_type'].ToString() } else { '' }
        if ($eventType -eq 'folder_add_record') {
            $activeEvents.Add($entry.Event) | Out-Null
        }
    }

    return $activeEvents
}

function Script:Get-ShareReportCreatedUnixSeconds {
    param($AuditEvent)

    if ($null -eq $AuditEvent -or -not $AuditEvent.ContainsKey('created') -or $null -eq $AuditEvent['created']) { return 0L }

    $raw = $AuditEvent['created'].ToString()
    $epoch = 0L
    if (-not [long]::TryParse($raw, [ref]$epoch)) { return 0L }

    $maxReasonableSeconds = [DateTimeOffset]::UtcNow.ToUnixTimeSeconds() + (10 * 365 * 24 * 60 * 60)
    if ($epoch -le $maxReasonableSeconds) {
        return $epoch
    }

    if ($epoch -le ($maxReasonableSeconds * 100)) {
        return [long]($epoch / 100)
    }

    return [long]($epoch / 1000)
}

function Script:Get-ShareDateForUser {
    param($ActivityList, [string] $Username)

    if (-not $ActivityList -or $ActivityList.Count -eq 0) { return '' }

    $matchesUserName = $ActivityList | Where-Object {
        $_.ContainsKey('to_username') -and $_['to_username'].ToString() -eq $Username
    }

    if (-not $matchesUserName -or $matchesUserName.Count -eq 0) { return '' }

    $activity = $null
    $createdSec = 0L
    foreach ($m in $matchesUserName) {
        $sec = Get-ShareReportCreatedUnixSeconds -AuditEvent $m
        if ($sec -gt $createdSec) {
            $createdSec = $sec
            $activity = $m
        }
    }

    if ($createdSec -le 0) { return '' }
    if ($createdSec -lt 946684800) { return '' }
    $dt = [DateTimeOffset]::FromUnixTimeSeconds($createdSec).ToLocalTime().ToString('yyyy-MM-dd HH:mm:ss zzz')
    $eventType = if ($activity.ContainsKey('audit_event_type')) { $activity['audit_event_type'].ToString() } else { '' }

    if ($eventType -eq 'record_share_outside_user') {
        return "(externally shared on $dt)"
    }
    return "(shared on $dt)"
}

function Script:Get-ShareDateForFolder {
    param($ActivityList, [string] $SharedFolderUid)

    if (-not $ActivityList -or $ActivityList.Count -eq 0) { return '' }

    $matchesFolder = $ActivityList | Where-Object {
        $_.ContainsKey('audit_event_type') -and $_['audit_event_type'].ToString() -match 'folder' -and
        $_.ContainsKey('shared_folder_uid') -and $_['shared_folder_uid'].ToString() -eq $SharedFolderUid
    }

    if (-not $matchesFolder -or $matchesFolder.Count -eq 0) { return '' }

    $createdSec = 0L
    foreach ($m in $matchesFolder) {
        $sec = Get-ShareReportCreatedUnixSeconds -AuditEvent $m
        if ($sec -gt $createdSec) {
            $createdSec = $sec
        }
    }
    if ($createdSec -le 0) { return '' }
    if ($createdSec -lt 946684800) { return '' }
    $dt = [DateTimeOffset]::FromUnixTimeSeconds($createdSec).ToLocalTime().ToString('yyyy-MM-dd HH:mm:ss zzz')
    return "(shared on $dt)"
}

function Script:Get-ShareReportRecordDate {
    param($ActivityList)

    if (-not $ActivityList -or $ActivityList.Count -eq 0) { return '' }

    $createdSec = 0L
    foreach ($activity in $ActivityList) {
        if (-not $activity.ContainsKey('audit_event_type')) { continue }

        $eventType = $activity['audit_event_type'].ToString()
        if ($eventType -notin @('share', 'record_share_outside_user', 'folder_add_record')) { continue }

        $sec = Get-ShareReportCreatedUnixSeconds -AuditEvent $activity
        if ($sec -le 0) { continue }

        if ($createdSec -eq 0 -or $sec -lt $createdSec) {
            $createdSec = $sec
        }
    }

    if ($createdSec -le 0) { return '' }
    return [DateTimeOffset]::FromUnixTimeSeconds($createdSec).ToUniversalTime().ToString('yyyy-MM-dd HH:mm:ss UTC')
}

function Script:Get-PermissionsText {
    param([bool] $CanEdit, [bool] $CanShare)

    if (-not $CanEdit -and -not $CanShare) {
        return 'Read Only'
    }

    $parts = @()
    if ($CanShare) { $parts += 'Share' }
    if ($CanEdit) { $parts += 'Edit' }
    return 'Can ' + ($parts -join ' & ')
}

function Script:Get-SharedFolderPermissionsText {
    param([bool] $ManageRecords, [bool] $ManageUsers)

    if (-not $ManageRecords -and -not $ManageUsers) {
        return 'No User Permissions'
    }

    $parts = @()
    if ($ManageUsers) { $parts += 'Manage Users' }
    if ($ManageRecords) { $parts += 'Manage Records' }
    return 'Can ' + ($parts -join ' & ')
}

function Script:Write-ShareReportOutput {
    param(
        [Parameter(Mandatory = $true)]
        [AllowEmptyCollection()]
        [object[]] $Rows,

        [Parameter()]
        [string] $Title,

        [Parameter(Mandatory = $true)]
        [ValidateSet('table', 'json', 'csv')]
        [string] $Format,

        [Parameter()]
        [string] $Output
    )

    if (-not $Rows -or $Rows.Count -eq 0) {
        Write-Warning 'No data to display.'
        return
    }

    switch ($Format) {
        'json' {
            $jsonText = $Rows | ConvertTo-Json -Depth 5
            if ($Output) {
                Set-Content -Path $Output -Value $jsonText -Encoding utf8
                Write-Host "Output written to $Output"
                return
            }
            return $jsonText
        }
        'csv' {
            $csvText = $Rows | ConvertTo-Csv -NoTypeInformation
            if ($Output) {
                Set-Content -Path $Output -Value $csvText -Encoding utf8
                Write-Host "Output written to $Output"
                return
            }
            return $csvText
        }
        default {
            if ($Output) {
                $tableText = @($Rows | Format-Table -Property * -AutoSize | Out-String -Width 8192)
                $content = @()
                if ($Title) { $content += $Title; $content += '' }
                $content += $tableText
                Set-Content -Path $Output -Value $content -Encoding utf8
                Write-Host "Output written to $Output"
                return
            }

            if ($Title) {
                Write-Host ''
                Write-Host $Title
            }
            $Rows | Format-Table -Property * -AutoSize | Out-String -Width 8192
        }
    }
}

function Script:Build-RecordToFolderIndex {
    param(
        [Parameter(Mandatory = $true)]
        [KeeperSecurity.Vault.VaultOnline] $Vault
    )

    $index = @{}
    foreach ($fn in $Vault.Folders) {
        foreach ($recUid in $fn.Records) {
            if (-not $index.ContainsKey($recUid)) {
                $index[$recUid] = [System.Collections.Generic.List[string]]::new()
            }
            $index[$recUid].Add($fn.FolderUid)
        }
    }
    return $index
}

function Script:Add-ShareReportSetValue {
    param(
        [Parameter(Mandatory = $true)]
        [hashtable] $Map,
        [Parameter(Mandatory = $true)]
        [string] $Key,
        [Parameter(Mandatory = $true)]
        [string] $Value
    )

    if ([string]::IsNullOrEmpty($Key) -or [string]::IsNullOrEmpty($Value)) {
        return
    }

    if (-not $Map.ContainsKey($Key)) {
        $Map[$Key] = [System.Collections.Generic.HashSet[string]]::new()
    }

    [void]$Map[$Key].Add($Value)
}

function Script:Build-ShareReportSharedFolderCache {
    param(
        [Parameter(Mandatory = $true)]
        [KeeperSecurity.Vault.VaultOnline] $Vault,
        [Parameter(Mandatory = $true)]
        [AllowEmptyCollection()]
        [object[]] $AllShares
    )

    $sfMembershipCache = @{}
    foreach ($shareInfo in $AllShares) {
        if (-not $shareInfo.SharedFolderPermissions) { continue }

        foreach ($sfp in $shareInfo.SharedFolderPermissions) {
            if ($sfMembershipCache.ContainsKey($sfp.SharedFolderUid)) { continue }

            [KeeperSecurity.Vault.SharedFolder] $sf = $null
            if ($Vault.TryGetSharedFolder($sfp.SharedFolderUid, [ref]$sf)) {
                $sfMembershipCache[$sfp.SharedFolderUid] = $sf
            }
        }
    }

    return $sfMembershipCache
}

function Script:Build-ShareReportRecordOwnerMap {
    param(
        [Parameter(Mandatory = $true)]
        [string[]] $RecordUids,
        [Parameter(Mandatory = $true)]
        [hashtable] $SharesMap,
        [Parameter(Mandatory = $true)]
        [KeeperSecurity.Vault.VaultOnline] $Vault,
        [Parameter(Mandatory = $true)]
        [string] $CurrentUser
    )

    $recordOwnerMap = @{}
    foreach ($uid in $RecordUids) {
        $shareInfo = $SharesMap[$uid]
        $ownerName = ''

        if ($shareInfo -and $shareInfo.UserPermissions) {
            foreach ($up in $shareInfo.UserPermissions) {
                if ($up.Owner) {
                    $ownerName = $up.Username
                    break
                }
            }
        }

        [KeeperSecurity.Vault.KeeperRecord] $rec = $null
        if (-not $ownerName -and $Vault.TryGetKeeperRecord($uid, [ref]$rec) -and $rec.Owner) {
            $ownerName = $CurrentUser
        }

        $recordOwnerMap[$uid] = $ownerName
    }

    return $recordOwnerMap
}

function Script:Get-ShareReportFolderPathText {
    param(
        [Parameter(Mandatory = $true)]
        [KeeperSecurity.Vault.VaultOnline] $Vault,
        [Parameter(Mandatory = $true)]
        [hashtable] $RecordToFolderIndex,
        [Parameter(Mandatory = $true)]
        [string] $RecordUid,
        [Parameter()]
        [System.Collections.Generic.HashSet[string]] $FilterFolderUids
    )

    $folderPaths = [System.Collections.Generic.List[string]]::new()
    $folderUids = $RecordToFolderIndex[$RecordUid]
    if (-not $folderUids) {
        return ''
    }

    foreach ($fUid in $folderUids) {
        if ($FilterFolderUids -and -not $FilterFolderUids.Contains($fUid)) {
            continue
        }

        $folderPath = Get-ShareReportFolderPath -Vault $Vault -FolderUid $fUid
        if ($folderPath) {
            $folderPaths.Add($folderPath)
        }
    }

    return ($folderPaths -join "`n")
}

function Script:Add-ShareReportSharedRecordRow {
    param(
        [Parameter(Mandatory = $true)]
        [AllowEmptyCollection()]
        [System.Collections.Generic.List[PSCustomObject]] $Table,
        [Parameter(Mandatory = $true)]
        [string] $RecordUid,
        [Parameter(Mandatory = $true)]
        [string] $Title,
        [Parameter(Mandatory = $true)]
        [string] $ShareType,
        [Parameter(Mandatory = $true)]
        [string] $SharedTo,
        [Parameter(Mandatory = $true)]
        [string] $Permissions,
        [Parameter(Mandatory = $true)]
        [string] $FolderPath,
        [Parameter(Mandatory = $true)]
        [bool] $IncludeOwner,
        [Parameter()]
        [string] $Owner
    )

    $row = [ordered]@{
        'Record UID'  = $RecordUid
        'Title'       = $Title
        'Share Type'  = $ShareType
        'Shared To'   = $SharedTo
        'Permissions' = $Permissions
        'Folder Path' = $FolderPath
    }

    if ($IncludeOwner) {
        $row.Insert(0, 'Owner', $Owner)
    }

    $Table.Add([PSCustomObject]$row)
}

function Script:Write-ShareReportFolders {
    param(
        [KeeperSecurity.Vault.VaultOnline] $Vault,
        [hashtable] $TeamMembers,
        [bool] $ShowTeamUsers,
        [string] $Format,
        [string] $Output
    )

    $table = [System.Collections.Generic.List[PSCustomObject]]::new()
    foreach ($sf in $Vault.SharedFolders) {
        $folderPath = Get-ShareReportFolderPath -Vault $Vault -FolderUid $sf.Uid
        if (-not $folderPath) { $folderPath = $sf.Name }

        foreach ($perm in $sf.UsersPermissions) {
            $permText = Get-SharedFolderPermissionsText -ManageRecords $perm.ManageRecords -ManageUsers $perm.ManageUsers
            $isTeam = $perm.UserType -eq [KeeperSecurity.Vault.UserType]::Team

            if ($isTeam -and $ShowTeamUsers) {
                $table.Add([PSCustomObject][ordered]@{
                    'Folder UID'  = $sf.Uid
                    'Folder Name' = $sf.Name
                    'Shared To'   = "(Team) $($perm.Name)"
                    'Permissions' = $permText
                    'Folder Path' = $folderPath
                })

                $members = $TeamMembers[$perm.Uid]
                if ($members) {
                    foreach ($member in $members) {
                        $table.Add([PSCustomObject][ordered]@{
                            'Folder UID'  = ''
                            'Folder Name' = ''
                            'Shared To'   = " $([char]0x2514) $member"
                            'Permissions' = ''
                            'Folder Path' = ''
                        })
                    }
                }
            }
            else {
                $displayName = if ($isTeam) { "(Team) $($perm.Name)" } else { $perm.Name }
                $table.Add([PSCustomObject][ordered]@{
                    'Folder UID'  = $sf.Uid
                    'Folder Name' = $sf.Name
                    'Shared To'   = $displayName
                    'Permissions' = $permText
                    'Folder Path' = $folderPath
                })
            }
        }
    }

    Write-ShareReportOutput -Rows $table -Title 'Shared Folders' -Format $Format -Output $Output
}

function Script:Write-ShareReportRecordDetail {
    param(
        [string[]] $RecordUids,
        [hashtable] $SharesMap,
        [hashtable] $RecordTitleMap,
        [hashtable] $SfMembershipCache
    )

    foreach ($uid in $RecordUids) {
        $shareInfo = $SharesMap[$uid]
        $title = $RecordTitleMap[$uid]

        Write-Host ''
        Write-Host ('{0,20} {1}' -f 'Record UID:', $uid)
        Write-Host ('{0,20} {1}' -f 'Title:', $title)

        $i = 0
        if ($shareInfo -and $shareInfo.UserPermissions) {
            foreach ($up in $shareInfo.UserPermissions) {
                if ($up.Owner) { continue }
                $label = if ($i -eq 0) { 'Shared with:' } else { '' }
                $permText = Get-PermissionsText -CanEdit $up.CanEdit -CanShare $up.CanShare
                $target = $up.Username
                if ($up.AwaitingApproval) { $target += ' (pending)' }
                Write-Host ('{0,20} {1} => {2}' -f $label, $target, $permText)
                $i++
            }
        }

        if ($shareInfo -and $shareInfo.SharedFolderPermissions) {
            foreach ($sfp in $shareInfo.SharedFolderPermissions) {
                $sf = $SfMembershipCache[$sfp.SharedFolderUid]
                $sfName = if ($sf) { $sf.Name } else { $sfp.SharedFolderUid }
                $label = if ($i -eq 0) { 'Shared with:' } else { '' }
                Write-Host ('{0,20} via Shared Folder: {1}' -f $label, $sfName)
                $i++
            }
        }
        Write-Host ''
    }
}

function Script:Write-ShareReportUserSharedFolders {
    param(
        [hashtable] $SfShares,
        [hashtable] $SfMembershipCache,
        [KeeperSecurity.Vault.VaultOnline] $Vault,
        [System.Collections.Generic.HashSet[string]] $UserFilter,
        [string] $Format,
        [string] $Output
    )

    $table = [System.Collections.Generic.List[PSCustomObject]]::new()
    foreach ($user in $SfShares.Keys) {
        if (-not $UserFilter.Contains($user)) { continue }
        foreach ($sfUid in $SfShares[$user]) {
            $sf = $SfMembershipCache[$sfUid]
            if (-not $sf) {
                [KeeperSecurity.Vault.SharedFolder] $sfObj = $null
                if ($Vault.TryGetSharedFolder($sfUid, [ref]$sfObj)) { $sf = $sfObj }
            }
            $sfName = if ($sf) { $sf.Name } else { '' }
            $table.Add([PSCustomObject][ordered]@{
                'Username'          = $user
                'Shared Folder UID' = $sfUid
                'Name'              = $sfName
            })
        }
    }

    Write-ShareReportOutput -Rows $table -Title 'Shared Folders by User' -Format $Format -Output $Output
}

function Script:Build-ShareReportFolderShares {
    param(
        [Parameter(Mandatory = $true)]
        [KeeperSecurity.Vault.VaultOnline] $Vault,
        [Parameter()]
        [hashtable] $TeamMembers,
        [Parameter()]
        [bool] $ShowTeamUsers
    )

    $sfShares = @{}
    foreach ($sf in $Vault.SharedFolders) {
        foreach ($perm in $sf.UsersPermissions) {
            $target = $perm.Name
            Add-ShareReportSetValue -Map $sfShares -Key $target -Value $sf.Uid

            $isTeam = $perm.UserType -eq [KeeperSecurity.Vault.UserType]::Team
            if ($isTeam -and $ShowTeamUsers) {
                $members = $TeamMembers[$perm.Uid]
                if ($members) {
                    foreach ($member in $members) {
                        Add-ShareReportSetValue -Map $sfShares -Key $member -Value $sf.Uid
                    }
                }
            }
        }
    }
    return $sfShares
}

function Script:Write-ShareReportUserRecords {
    param(
        [hashtable] $RecordShares,
        [hashtable] $RecordOwnerMap,
        [hashtable] $RecordTitleMap,
        [System.Collections.Generic.HashSet[string]] $UserFilter,
        [string] $Format,
        [string] $Output
    )

    $table = [System.Collections.Generic.List[PSCustomObject]]::new()
    foreach ($user in $RecordShares.Keys) {
        if (-not $UserFilter.Contains($user)) { continue }
        foreach ($uid in $RecordShares[$user]) {
            $table.Add([PSCustomObject][ordered]@{
                'Username'     = $user
                'Record Owner' = $RecordOwnerMap[$uid]
                'Record UID'   = $uid
                'Record Title' = $RecordTitleMap[$uid]
            })
        }
    }

    Write-ShareReportOutput -Rows $table -Title 'Shared Records by User' -Format $Format -Output $Output
}

function Script:Write-ShareReportOwner {
    param(
        [System.Collections.Generic.List[string]] $RecordUids,
        [hashtable] $SharesMap,
        [hashtable] $RecordOwnerMap,
        [hashtable] $RecordTitleMap,
        [hashtable] $SfMembershipCache,
        [hashtable] $RecordToFolderIndex,
        [KeeperSecurity.Vault.VaultOnline] $Vault,
        [hashtable] $TeamMembers,
        [bool] $ShowTeamUsers,
        [bool] $IncludeShareDate,
        [bool] $DetailedView,
        $UserFilter,
        [string] $Format,
        [string] $Output
    )

    $aramEnabled = $true
    $table = [System.Collections.Generic.List[PSCustomObject]]::new()

    foreach ($uid in $RecordUids) {
        $shareInfo = $SharesMap[$uid]
        if (-not $shareInfo) { continue }

        $recOwner = $RecordOwnerMap[$uid]
        $title = $RecordTitleMap[$uid]

        $folderPathStr = Get-ShareReportFolderPathText -Vault $Vault -RecordToFolderIndex $RecordToFolderIndex -RecordUid $uid

        $shareEvents = @()
        if ($IncludeShareDate -and $aramEnabled) {
            $shareEvents = Get-ShareReportAuditEvents -Auth $Vault.Auth -RecordUid $uid -AramEnabled ([ref]$aramEnabled)
        }

        $recordShareDate = ''
        if ($IncludeShareDate -and $shareEvents.Count -gt 0) {
            $recordShareDate = Get-ShareReportRecordDate -ActivityList $shareEvents
        }

        $shareTargets = [System.Collections.Generic.List[string]]::new()
        $permissionsList = [System.Collections.Generic.List[object]]::new()

        if ($shareInfo.UserPermissions) {
            foreach ($up in $shareInfo.UserPermissions) {
                if (-not $up.Owner) { $permissionsList.Add($up) }
            }
        }

        if ($UserFilter) {
            $filtered = [System.Collections.Generic.List[object]]::new()
            foreach ($p in $permissionsList) {
                if ($UserFilter.Contains($p.Username)) { $filtered.Add($p) }
            }
            $permissionsList = $filtered
        }

        if (-not $DetailedView) {
            $shareWith = $permissionsList.Count
            if ($shareInfo.SharedFolderPermissions) {
                foreach ($sfp in $shareInfo.SharedFolderPermissions) {
                    $sf = $SfMembershipCache[$sfp.SharedFolderUid]
                    if ($sf) { $shareWith += $sf.UsersPermissions.Count }
                }
            }
            $shareInfoText = $shareWith.ToString()
        }
        else {
            if ($ShowTeamUsers -and $recOwner) {
                $shareTargets.Add("$recOwner => Owner")
            }

            foreach ($up in $permissionsList) {
                $permText = Get-PermissionsText -CanEdit $up.CanEdit -CanShare $up.CanShare
                $dateSuffix = ''

                if ($IncludeShareDate) {
                    $dateStr = ''
                    if ($shareEvents.Count -gt 0) {
                        $dateStr = Get-ShareDateForUser -ActivityList $shareEvents -Username $up.Username
                    }
                    if ($dateStr) {
                        $dateSuffix = $dateStr
                    }
                    else {
                        $dateSuffix = '(share date unavailable)'
                    }
                }
                $shareTargets.Add("$($up.Username) => $permText$(if ($dateSuffix) { " $dateSuffix" } else { '' })")

                if ($up.Expiration) {
                    $expDt = $up.Expiration.Value.LocalDateTime
                    $shareTargets.Add("`t(expires on $expDt)")
                }
            }

            if ($shareInfo.SharedFolderPermissions) {
                foreach ($sfp in $shareInfo.SharedFolderPermissions) {
                    $sf = $SfMembershipCache[$sfp.SharedFolderUid]
                    if (-not $sf) { continue }
                    foreach ($sfUser in $sf.UsersPermissions) {
                        if ($sfUser.Name -eq $recOwner) { continue }
                        if ($UserFilter -and -not $UserFilter.Contains($sfUser.Name)) { continue }

                        $isTeam = $sfUser.UserType -eq [KeeperSecurity.Vault.UserType]::Team
                        $displayName = if ($isTeam -and $ShowTeamUsers) { "(Team) $($sfUser.Name)" } else { $sfUser.Name }
                        $permText = Get-PermissionsText -CanEdit $sfUser.ManageRecords -CanShare $sfUser.ManageUsers

                        $dateSuffix = ''

                        if ($IncludeShareDate) {
                            $dateStr = ''
                            if ($shareEvents.Count -gt 0) {
                                if (-not $isTeam) {
                                    $dateStr = Get-ShareDateForUser -ActivityList $shareEvents -Username $sfUser.Name
                                }
                                else {
                                    $dateStr = Get-ShareDateForFolder -ActivityList $shareEvents -SharedFolderUid $sfp.SharedFolderUid
                                }
                            }
                            if ($dateStr) {
                                $dateSuffix = $dateStr
                            }
                            else {
                                $dateSuffix = '(share date unavailable)'
                            }
                        }
                        $shareTargets.Add("$displayName => $permText$(if ($dateSuffix) { " $dateSuffix" } else { '' })")

                        if ($isTeam -and $ShowTeamUsers) {
                            $members = $TeamMembers[$sfUser.Uid]
                            if ($members) {
                                foreach ($member in $members) {
                                    $shareTargets.Add("(Team User) $member => $permText")
                                }
                            }
                        }
                    }
                }
            }

            $shareInfoText = $shareTargets -join "`n"
        }

        $row = [ordered]@{
            'Record Owner' = $recOwner
            'Record UID'   = $uid
            'Record Title' = $title
            'Shared With'  = $shareInfoText
            'Folder Path'  = $folderPathStr
        }
        if ($IncludeShareDate) {
            $row['Share Date'] = $recordShareDate
        }
        $table.Add([PSCustomObject]$row)
    }

    Write-ShareReportOutput -Rows $table -Title 'Record Share Report (Owner)' -Format $Format -Output $Output
}

function Script:Write-ShareReportSummary {
    param(
        [hashtable] $RecordShares,
        [hashtable] $SfShares,
        [string] $CurrentUser,
        $UserFilter,
        [string] $Format,
        [string] $Output
    )

    $RecordShares.Remove($CurrentUser)
    $SfShares.Remove($CurrentUser)

    $allTargets = [System.Collections.Generic.HashSet[string]]::new([System.StringComparer]::OrdinalIgnoreCase)
    foreach ($key in $RecordShares.Keys) { [void]$allTargets.Add($key) }
    foreach ($key in $SfShares.Keys) { [void]$allTargets.Add($key) }

    if ($UserFilter) {
        $allTargets = [System.Collections.Generic.HashSet[string]]::new(
            ($allTargets | Where-Object { $UserFilter.Contains($_) }),
            [System.StringComparer]::OrdinalIgnoreCase
        )
    }

    $table = [System.Collections.Generic.List[PSCustomObject]]::new()
    foreach ($target in ($allTargets | Sort-Object)) {
        $recCount = if ($RecordShares.ContainsKey($target)) { $RecordShares[$target].Count } else { $null }
        $sfCount = if ($SfShares.ContainsKey($target)) { $SfShares[$target].Count } else { $null }
        $table.Add([PSCustomObject][ordered]@{
            'Shared To'      = $target
            'Records'        = $recCount
            'Shared Folders' = $sfCount
        })
    }

    Write-ShareReportOutput -Rows $table -Title 'Share Report Summary' -Format $Format -Output $Output
}

function Get-KeeperShareReport {
    <#
    .SYNOPSIS
    Show a report of shared records and shared folders.

    .DESCRIPTION
    Generates a report of records and folders shared both with and by the current user.
    Supports multiple modes: summary, per-record detail, per-user filter, shared folders listing,
    and owner report with optional share-date information.
    Use -Verbose or -ShowTeamUsers to show detailed share permissions per target.

    .PARAMETER Format
    Output format: table, json, or csv. Default is table.

    .PARAMETER Output
    Path to write the report to a file.

    .PARAMETER Record
    Record name(s) or UID(s) to show share information for.

    .PARAMETER Email
    User email(s) or team name(s) to filter the report by.

    .PARAMETER Owner
    Show record ownership information in the report.

    .PARAMETER ShareDate
    Include the date when each record was shared. Requires enterprise admin with report permissions.
    Only applies to the owner report (-Owner).

    .PARAMETER SharedFolders
    Display shared folder detail instead of records. Used with -Email.

    .PARAMETER Folders
    Limit the report to shared folders only (excludes shared records).

    .PARAMETER ShowTeamUsers
    Expand team shares to show individual team members. Requires enterprise admin.

    .EXAMPLE
    Get-KeeperShareReport
    Display a summary of all shares grouped by share target.

    .EXAMPLE
    Get-KeeperShareReport -Record "5R7Ued8#JctulYbBLwM$"
    Display share info for a specific record.

    .EXAMPLE
    Get-KeeperShareReport -Format csv -Output share_report.csv
    Export share report as CSV.

    .EXAMPLE
    Get-KeeperShareReport -Email "john.doe@gmail.com" -Owner -ShareDate -Verbose
    Show records shared with a user including owner and share dates.

    .EXAMPLE
    Get-KeeperShareReport -Folders
    Display a list of shared folders with their access permissions.

    .EXAMPLE
    Get-KeeperShareReport -Folders -ShowTeamUsers
    Display shared folders with team memberships expanded to individual users.
    #>


    [CmdletBinding()]
    Param (
        [Parameter()]
        [ValidateSet('table', 'json', 'csv')]
        [string] $Format = 'table',

        [Parameter()]
        [string] $Output,

        [Parameter()]
        [string[]] $Record,

        [Parameter()]
        [string[]] $Email,

        [Parameter()]
        [switch] $Owner,

        [Parameter()]
        [switch] $ShareDate,

        [Parameter()]
        [switch] $SharedFolders,

        [Parameter()]
        [switch] $Folders,

        [Parameter()]
        [switch] $ShowTeamUsers
    )

    try {
        [KeeperSecurity.Vault.VaultOnline]$vault = getVault
    }
    catch {
        Write-Error "Unable to load Keeper vault context. Run Connect-Keeper and Sync-Keeper first. Details: $($_.Exception.Message)" -ErrorAction Stop
    }

    $currentUser = $vault.Auth.Username
    $verbose = $PSCmdlet.MyInvocation.BoundParameters.ContainsKey('Verbose') -or $ShowTeamUsers.IsPresent

    $teamMembers = @{}
    if ($ShowTeamUsers.IsPresent) {
        $teamMembers = Get-ShareReportTeamMembers -Vault $vault
    }

    if ($Folders.IsPresent) {
        Write-ShareReportFolders -Vault $vault -TeamMembers $teamMembers -ShowTeamUsers $ShowTeamUsers.IsPresent `
            -Format $Format -Output $Output
        return
    }

    $sharedRecordUids = [System.Collections.Generic.List[string]]::new()
    $recordTitleMap = @{}

    if ($Record) {
        foreach ($r in $Record) {
            [KeeperSecurity.Vault.KeeperRecord] $rec = $null
            if ($vault.TryGetKeeperRecord($r, [ref]$rec)) {
                $sharedRecordUids.Add($rec.Uid)
                $recordTitleMap[$rec.Uid] = $rec.Title
            }
            else {
                $found = $false
                foreach ($kr in $vault.KeeperRecords) {
                    if ($kr.Title -eq $r) {
                        $sharedRecordUids.Add($kr.Uid)
                        $recordTitleMap[$kr.Uid] = $kr.Title
                        $found = $true
                        break
                    }
                }
                if (-not $found) {
                    Write-Warning "Cannot find a Keeper record: $r"
                }
            }
        }
    }
    else {
        foreach ($kr in $vault.KeeperRecords) {
            if ($kr.Shared) {
                $sharedRecordUids.Add($kr.Uid)
                $recordTitleMap[$kr.Uid] = $kr.Title
            }
        }
    }

    if ($sharedRecordUids.Count -eq 0 -and -not ($Email -and $SharedFolders.IsPresent)) {
        Write-Warning 'No shared records found.'
        return
    }

    $allShares = Get-ShareReportRecordShares -Vault $vault -RecordUids $sharedRecordUids.ToArray()
    $sharesMap = @{}
    foreach ($s in $allShares) {
        $sharesMap[$s.RecordUid] = $s
    }

    $sfMembershipCache = Build-ShareReportSharedFolderCache -Vault $vault -AllShares $allShares
    $recordOwnerMap = Build-ShareReportRecordOwnerMap -RecordUids $sharedRecordUids.ToArray() `
        -SharesMap $sharesMap -Vault $vault -CurrentUser $currentUser

    if ($Record) {
        Write-ShareReportRecordDetail -RecordUids $sharedRecordUids.ToArray() -SharesMap $sharesMap `
            -RecordTitleMap $recordTitleMap -SfMembershipCache $sfMembershipCache
        return
    }

    $userFilter = $null
    if ($Email) {
        $userFilter = [System.Collections.Generic.HashSet[string]]::new([System.StringComparer]::OrdinalIgnoreCase)
        foreach ($e in $Email) { [void]$userFilter.Add($e) }
    }

    if ($Owner.IsPresent) {
        $recordToFolderIndex = Build-RecordToFolderIndex -Vault $vault

        Write-ShareReportOwner -RecordUids $sharedRecordUids -SharesMap $sharesMap `
            -RecordOwnerMap $recordOwnerMap -RecordTitleMap $recordTitleMap `
            -SfMembershipCache $sfMembershipCache -RecordToFolderIndex $recordToFolderIndex `
            -Vault $vault -TeamMembers $teamMembers -ShowTeamUsers $ShowTeamUsers.IsPresent `
            -IncludeShareDate $ShareDate.IsPresent -DetailedView $verbose `
            -UserFilter $userFilter -Format $Format -Output $Output
        return
    }

    $recordShares = @{}

    foreach ($uid in $sharedRecordUids) {
        $shareInfo = $sharesMap[$uid]
        if (-not $shareInfo) { continue }

        if ($shareInfo.UserPermissions) {
            foreach ($up in $shareInfo.UserPermissions) {
                if ($up.Owner) { continue }
                Add-ShareReportSetValue -Map $recordShares -Key $up.Username -Value $uid
            }
        }

        if ($shareInfo.SharedFolderPermissions) {
            foreach ($sfp in $shareInfo.SharedFolderPermissions) {
                $sf = $sfMembershipCache[$sfp.SharedFolderUid]
                if (-not $sf) { continue }

                foreach ($perm in $sf.UsersPermissions) {
                    $isTeam = $perm.UserType -eq [KeeperSecurity.Vault.UserType]::Team

                    Add-ShareReportSetValue -Map $recordShares -Key $perm.Name -Value $uid

                    if ($isTeam -and $ShowTeamUsers.IsPresent) {
                        $members = $teamMembers[$perm.Uid]
                        if ($members) {
                            foreach ($member in $members) {
                                Add-ShareReportSetValue -Map $recordShares -Key $member -Value $uid
                            }
                        }
                    }
                }
            }
        }
    }

    $sfShares = Build-ShareReportFolderShares -Vault $vault -TeamMembers $teamMembers -ShowTeamUsers $ShowTeamUsers.IsPresent

    if ($Email -and $SharedFolders.IsPresent) {
        Write-ShareReportUserSharedFolders -SfShares $sfShares -SfMembershipCache $sfMembershipCache `
            -Vault $vault -UserFilter $userFilter -Format $Format -Output $Output
        return
    }

    if ($Email) {
        Write-ShareReportUserRecords -RecordShares $recordShares -RecordOwnerMap $recordOwnerMap `
            -RecordTitleMap $recordTitleMap -UserFilter $userFilter -Format $Format -Output $Output
        return
    }

        Write-ShareReportSummary -RecordShares $recordShares -SfShares $sfShares `
        -CurrentUser $currentUser -UserFilter $userFilter -Format $Format -Output $Output
}


function Get-KeeperSharedRecordsReport {
    <#
    .SYNOPSIS
    Report shared records for the logged-in user.

    .DESCRIPTION
    Generates a report of all shared records showing the share type, recipient, and permissions per row.
    the share type (Direct Share, Share Folder, Share Team Folder), permissions, and folder path.
    By default only owned shared records are included. Use -AllRecords to include non-owned records.
    Mirrors the Python Commander 'ksrr' command.

    Alias: ksrr

    .PARAMETER ShowTeamUsers
    Expand team shares to show individual team members. Requires enterprise admin.

    .PARAMETER AllRecords
    Include all shared records in the vault, not just records owned by the current user.

    .PARAMETER Folder
    Optional folder path(s) or UID(s) to scope the report to records within those folders.

    .PARAMETER Format
    Output format: table (default), json, or csv.

    .PARAMETER Output
    Path to write the report to a file.

    .EXAMPLE
    Get-KeeperSharedRecordsReport
    Report all owned shared records in table format.

    .EXAMPLE
    Get-KeeperSharedRecordsReport -AllRecords
    Report all shared records in the vault (including non-owned).

    .EXAMPLE
    Get-KeeperSharedRecordsReport -ShowTeamUsers
    Report with team shares expanded to individual team members.

    .EXAMPLE
    Get-KeeperSharedRecordsReport -Format csv -Output "shared_records.csv"
    Export the shared records report to a CSV file.

    .EXAMPLE
    Get-KeeperSharedRecordsReport -Folder "Shared\Projects"
    Report shared records within a specific folder.
    #>


    [CmdletBinding()]
    Param (
        [Parameter()]
        [Alias('tu')]
        [switch] $ShowTeamUsers,

        [Parameter()]
        [switch] $AllRecords,

        [Parameter()]
        [string[]] $Folder,

        [Parameter()]
        [ValidateSet('table', 'json', 'csv')]
        [string] $Format = 'table',

        [Parameter()]
        [string] $Output
    )

    try {
        [KeeperSecurity.Vault.VaultOnline]$vault = getVault
    }
    catch {
        Write-Error "Unable to load Keeper vault context. Run Connect-Keeper and Sync-Keeper first. Details: $($_.Exception.Message)" -ErrorAction Stop
    }

    $currentUser = $vault.Auth.Username

    $allowedVersions = if ($AllRecords.IsPresent) { @(0, 1, 2, 3, 5, 6) } else { @(2, 3) }

    $teamMembers = @{}
    if ($ShowTeamUsers.IsPresent) {
        $teamMembers = Get-ShareReportTeamMembers -Vault $vault
    }

    $records = @{}
    $filterFolderUids = $null

    if ($Folder -and $Folder.Count -gt 0) {
        $filterFolderUids = [System.Collections.Generic.HashSet[string]]::new()
        foreach ($folderName in $Folder) {
            $folderFound = $false
            foreach ($fn in $vault.Folders) {
                $folderPath = Get-ShareReportFolderPath -Vault $vault -FolderUid $fn.FolderUid
                if ($fn.FolderUid -eq $folderName -or $fn.Name -eq $folderName -or $folderPath -eq $folderName) {
                    [void]$filterFolderUids.Add($fn.FolderUid)
                    $folderFound = $true
                    foreach ($recUid in $fn.Records) {
                        [KeeperSecurity.Vault.KeeperRecord] $rec = $null
                        if (-not $vault.TryGetKeeperRecord($recUid, [ref]$rec)) { continue }
                        if (-not $rec.Shared) { continue }
                        if ($rec.Version -notin $allowedVersions) { continue }
                        $records[$rec.Uid] = $rec
                    }
                }
            }
            if (-not $folderFound) {
                Write-Warning "Folder '$folderName' could not be found."
            }
        }
    }
    else {
        foreach ($kr in $vault.KeeperRecords) {
            if (-not $kr.Shared) { continue }
            if ($kr.Version -notin $allowedVersions) { continue }
            $records[$kr.Uid] = $kr
        }
    }

    if (-not $AllRecords.IsPresent) {
        $ownedOnly = @{}
        foreach ($entry in $records.GetEnumerator()) {
            if ($entry.Value.Owner) {
                $ownedOnly[$entry.Key] = $entry.Value
            }
        }
        $records = $ownedOnly
    }

    if ($records.Count -eq 0) {
        Write-Warning 'No shared records found.'
        return
    }

    $recordUids = [string[]]@($records.Keys)
    $allShares = Get-ShareReportRecordShares -Vault $vault -RecordUids $recordUids

    $sharesMap = @{}
    foreach ($s in $allShares) {
        $sharesMap[$s.RecordUid] = $s
    }

    $sfMembershipCache = Build-ShareReportSharedFolderCache -Vault $vault -AllShares $allShares

    $recordToFolderIndex = Build-RecordToFolderIndex -Vault $vault

    $table = [System.Collections.Generic.List[PSCustomObject]]::new()

    foreach ($recordUid in $recordUids) {
        $rec = $records[$recordUid]
        $shareInfo = $sharesMap[$recordUid]
        if (-not $shareInfo) { continue }

        $recTitle = $rec.Title
        if ($Format -eq 'table' -and $recTitle.Length -gt 40) {
            $recTitle = $recTitle.Substring(0, 38) + '...'
        }

        $owner = $null
        if ($shareInfo.UserPermissions) {
            foreach ($up in $shareInfo.UserPermissions) {
                if ($up.Owner) { $owner = $up.Username; break }
            }
        }
        if (-not $owner -and $rec.Owner) {
            $owner = $currentUser
        }

        $folderPathStr = Get-ShareReportFolderPathText -Vault $vault -RecordToFolderIndex $recordToFolderIndex `
            -RecordUid $recordUid -FilterFolderUids $filterFolderUids

        if ($shareInfo.UserPermissions) {
            foreach ($up in $shareInfo.UserPermissions) {
                if ($up.Owner) { continue }
                if (-not $AllRecords.IsPresent -and $up.Username -eq $currentUser) { continue }
                $permText = Get-PermissionsText -CanEdit $up.CanEdit -CanShare $up.CanShare
                Add-ShareReportSharedRecordRow -Table $table -RecordUid $recordUid -Title $recTitle `
                    -ShareType 'Direct Share' -SharedTo $up.Username -Permissions $permText `
                    -FolderPath $folderPathStr -IncludeOwner $AllRecords.IsPresent -Owner $owner
            }
        }

        if ($shareInfo.SharedFolderPermissions) {
            foreach ($sfp in $shareInfo.SharedFolderPermissions) {
                $sf = $sfMembershipCache[$sfp.SharedFolderUid]
                if (-not $sf) {
                    $permText = Get-PermissionsText -CanEdit $sfp.CanEdit -CanShare $sfp.CanShare
                    Add-ShareReportSharedRecordRow -Table $table -RecordUid $recordUid -Title $recTitle `
                        -ShareType 'Share Folder' -SharedTo '***' -Permissions $permText `
                        -FolderPath $sfp.SharedFolderUid -IncludeOwner $AllRecords.IsPresent -Owner $owner
                    continue
                }

                $sfFolderPath = Get-ShareReportFolderPath -Vault $vault -FolderUid $sf.Uid
                if (-not $sfFolderPath) { $sfFolderPath = $sf.Name }

                foreach ($sfUser in $sf.UsersPermissions) {
                    $isTeam = $sfUser.UserType -eq [KeeperSecurity.Vault.UserType]::Team
                    $permText = Get-PermissionsText -CanEdit $sfp.CanEdit -CanShare $sfp.CanShare

                    if ($isTeam) {
                        if ($ShowTeamUsers.IsPresent -and $teamMembers.ContainsKey($sfUser.Uid)) {
                            foreach ($member in $teamMembers[$sfUser.Uid]) {
                                Add-ShareReportSharedRecordRow -Table $table -RecordUid $recordUid -Title $recTitle `
                                    -ShareType 'Share Team Folder' -SharedTo "($($sfUser.Name)) $member" `
                                    -Permissions $permText -FolderPath $sfFolderPath `
                                    -IncludeOwner $AllRecords.IsPresent -Owner $owner
                            }
                        }
                        else {
                            Add-ShareReportSharedRecordRow -Table $table -RecordUid $recordUid -Title $recTitle `
                                -ShareType 'Share Team Folder' -SharedTo $sfUser.Name -Permissions $permText `
                                -FolderPath $sfFolderPath -IncludeOwner $AllRecords.IsPresent -Owner $owner
                        }
                    }
                    else {
                        if (-not $AllRecords.IsPresent -and $sfUser.Name -eq $currentUser) { continue }
                        Add-ShareReportSharedRecordRow -Table $table -RecordUid $recordUid -Title $recTitle `
                            -ShareType 'Share Folder' -SharedTo $sfUser.Name -Permissions $permText `
                            -FolderPath $sfFolderPath -IncludeOwner $AllRecords.IsPresent -Owner $owner
                    }
                }
            }
        }
    }

    Write-ShareReportOutput -Rows $table -Title 'Shared Records Report' -Format $Format -Output $Output
}

New-Alias -Name ksrr -Value Get-KeeperSharedRecordsReport
# SIG # Begin signature block
# MIInvgYJKoZIhvcNAQcCoIInrzCCJ6sCAQExDzANBglghkgBZQMEAgEFADB5Bgor
# BgEEAYI3AgEEoGswaTA0BgorBgEEAYI3AgEeMCYCAwEAAAQQH8w7YFlLCE63JNLG
# KX7zUQIBAAIBAAIBAAIBAAIBADAxMA0GCWCGSAFlAwQCAQUABCACf7iKj8qevFIS
# YVUpgz38L5p+a8f1Jw4HLCozuGhoeqCCITswggWNMIIEdaADAgECAhAOmxiO+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
# BDEiBCC8z7gQ7OzCglg0Wd2fQ3j3ARfdA+9YjmIMz4+anca/JzANBgkqhkiG9w0B
# AQEFAASCAYAV6dyhwacU9cRLRpSQAU/dlQFGBgVlwDF3c1rRwPaqYXM7OU+v1iR7
# DN7ExwpgeSaKebzjUs9Ah59UwPUivk3n8VRwfHtLLgYZS4nnaKBk9NdQu6qeA5MP
# 8VB9inB3/uzs3XWq2fX495WFpmcKiYIluh9JPRN7WlFyNYWXaVEgOrb1OEP0oJxg
# BA8IjJtS0gCQUn2XWfDPsRuXUclhn3u+qnZCFV0eko6gnVMRAfsFqgmHTs3iT49j
# Z9BzqVvwjgRqmjdMYYjYB/tiET2j+PMotIRyTaeNRKsqilp6kv9Un7OHKQaw9lDC
# mqzBqG+bbDeza89wxCfzrS5ggruArSBJAThVvh2UfS+G86En1OpK4G7dQjYLS1eO
# bI3OHE+OMeT3uZQUu61y2F3FVJNi5md+WHtpPxRl4DtbxgrYYqDq3hBiHDg+Toj6
# gR5wD9k/yvXvJC7F8IhPhOG6Kjkg8KwSJbuV1o3KQLusL/sKVXQW4Zl9HNw7TGsz
# pTSyc/f7pC6hggMmMIIDIgYJKoZIhvcNAQkGMYIDEzCCAw8CAQEwfTBpMQswCQYD
# VQQGEwJVUzEXMBUGA1UEChMORGlnaUNlcnQsIEluYy4xQTA/BgNVBAMTOERpZ2lD
# ZXJ0IFRydXN0ZWQgRzQgVGltZVN0YW1waW5nIFJTQTQwOTYgU0hBMjU2IDIwMjUg
# Q0ExAhAKgO8YS43xBYLRxHanlXRoMA0GCWCGSAFlAwQCAQUAoGkwGAYJKoZIhvcN
# AQkDMQsGCSqGSIb3DQEHATAcBgkqhkiG9w0BCQUxDxcNMjYwNjI0MTgwNDQxWjAv
# BgkqhkiG9w0BCQQxIgQgPDhslGajfzgOmaZ1NeM8OspgMQhvN6vqPBZjhyhkRZUw
# DQYJKoZIhvcNAQEBBQAEggIAXHfPXgDc6atLa0i5bsNOBd+MyMd37wXFMX8rrBNf
# CKoB7PwzSajBPUxJ6a+SfsRPi/nlpyy+6QAiUE+8+KBJN2DHwWvDglIUT60ftDc0
# QoLkhVl2UI29cho26Z8/Y8YN4quOrGpnUUiGHt2n/5rxXi/6pxJbSHQbj1WRtKkS
# LcSlCLjqaSGseXH+o5VTNmMHDoZFhGxPRE7KKVrjgOZp4IknnEWeREcmZhinjSMk
# xMnMOeiMDa3oCI+rfa8t2BC1jHvT4O0TkrjzSylJHs2crVTlgwaEYe5bEQ1ZTf96
# Smwp6x/rneYJW2FqnZqIrcl4nNwCBHsJbpLvUxtXyXptvqPs0qBBlmZAeiBXKQTA
# HzgCetVzAgfe6QbH++pfIqicgWCEqspSOku164dwrMYz8s7WrOKkGxNAkepcn6II
# c9J0LaND5Z0HOeG6rar5IJZWzqCTlH2HTRkFcy56nL8+IqYNHwzQRYdodZHOSsag
# b3V3swjE6BJOYFwS1Qs8IniBCHiyscuzluzPaSpANMYEt/vy/R1f8m80SHaIrkFY
# UzRDVkRjpxYiMxZefkF6fujjdyxquJd54AQjkA3x0exmvvlWdQy3QNiDlmFJxh8z
# u+Oj3yNnrj57L5C5r9e36yJazdo/zBvlmzxeOzp+1Xp9Yv2xXbyDPQRl+ve8fD8j
# sqM=
# SIG # End signature block