EnterpriseInfo.ps1
|
$Script:UserStatusText = { param($s) switch ($s) { 'Active' { 'Active' } 'Inactive' { 'Invited' } 'Locked' { 'Locked' } 'Blocked' { 'Blocked' } 'Disabled' { 'Disabled' } default { $s } } } $Script:TransferStatusText = { param($s) switch ([int]$s) { 0 { 'Undefined' } 1 { 'Not required' } 2 { 'Pending transfer' } 3 { 'Partially accepted' } 4 { 'Transfer accepted' } default { $s } } } function Script:Get-EnterpriseNodeAndDescendantIds { param([object]$enterpriseData, [long]$rootId) if ($rootId -le 0) { return $null } $subnodes = @{} foreach ($n in $enterpriseData.Nodes) { $id = $n.Id if ($n.ParentNodeId -gt 0) { if (-not $subnodes[$n.ParentNodeId]) { $subnodes[$n.ParentNodeId] = [System.Collections.Generic.List[long]]::new() } $subnodes[$n.ParentNodeId].Add($id) | Out-Null } } $set = [System.Collections.Generic.HashSet[long]]::new() $queue = [System.Collections.Generic.Queue[long]]::new() $queue.Enqueue($rootId) | Out-Null while ($queue.Count -gt 0) { $nid = $queue.Dequeue() [void]$set.Add($nid) if ($subnodes[$nid]) { foreach ($c in $subnodes[$nid]) { $queue.Enqueue($c) | Out-Null } } } return ,$set } function Get-KeeperEnterpriseInfoTree { <# .SYNOPSIS Display a tree structure of the enterprise (nodes with users, roles, teams). .DESCRIPTION Outputs a tree view of the enterprise hierarchy (nodes with users, roles, and teams). Output format is always tree. .PARAMETER Node Limit output to this node and its descendants (node name or ID). .PARAMETER Detailed Include node IDs and list individual users/roles/teams by name. .PARAMETER Output If supplied, write output to this file path. .EXAMPLE Get-KeeperEnterpriseInfoTree Get-KeeperEnterpriseInfoTree -Node "Sales" -Detailed -Output tree.txt #> [CmdletBinding()] Param ( [Parameter()][string] $Node, [Parameter()][switch] $Detailed, [Parameter()][string] $Output ) $enterprise = getEnterprise $ed = $enterprise.enterpriseData $rd = $enterprise.roleData $subnodes = @{} foreach ($n in $ed.Nodes) { $id = $n.Id if (-not $subnodes.ContainsKey($id)) { $subnodes[$id] = [System.Collections.Generic.List[long]]::new() } if ($n.ParentNodeId -gt 0) { if (-not $subnodes.ContainsKey($n.ParentNodeId)) { $subnodes[$n.ParentNodeId] = [System.Collections.Generic.List[long]]::new() } $subnodes[$n.ParentNodeId].Add($id) | Out-Null } } $rootId = $ed.RootNode.Id if ($Node) { $resolved = resolveSingleNode $Node if (-not $resolved) { Write-Error "Node '$Node' not found"; return } $rootId = $resolved.Id } $usersByNode = @{} foreach ($u in $ed.Users) { $nid = $u.ParentNodeId if (-not $usersByNode.ContainsKey($nid)) { $usersByNode[$nid] = [System.Collections.Generic.List[object]]::new() } $usersByNode[$nid].Add($u) | Out-Null } $rolesByNode = @{} foreach ($r in $rd.Roles) { $nid = $r.ParentNodeId if (-not $rolesByNode.ContainsKey($nid)) { $rolesByNode[$nid] = [System.Collections.Generic.List[object]]::new() } $rolesByNode[$nid].Add($r) | Out-Null } $teamsByNode = @{} foreach ($t in $ed.Teams) { $nid = $t.ParentNodeId if (-not $teamsByNode.ContainsKey($nid)) { $teamsByNode[$nid] = [System.Collections.Generic.List[object]]::new() } $teamsByNode[$nid].Add($t) | Out-Null } $lines = [System.Collections.Generic.List[string]]::new() function writeTreeNode { param([long]$nodeId, [string]$prefix, [bool]$isLastSibling = $true) $n = $null if (-not $ed.TryGetNode($nodeId, [ref]$n)) { return } $name = $n.DisplayName if ([string]::IsNullOrEmpty($name)) { $name = $enterprise.loader.EnterpriseName } if ($Detailed) { $name += " ($nodeId)" } if ($n.RestrictVisibility) { $name += " |Isolated|" } if ($prefix -eq '') { $lines.Add($name) | Out-Null } else { $lines.Add("$prefix+-- $name") | Out-Null } $us = $usersByNode[$nodeId]; $ro = $rolesByNode[$nodeId]; $te = $teamsByNode[$nodeId] $childIds = if ($subnodes[$nodeId]) { @($subnodes[$nodeId]) } else { @() } $sortedChildIds = if ($childIds.Count -gt 0) { @($childIds | Sort-Object { $nn = $null; if ($ed.TryGetNode($_, [ref]$nn)) { $nn.DisplayName } else { '' } }) } else { @() } $contentItems = [System.Collections.Generic.List[object]]::new() foreach ($cid in $sortedChildIds) { $contentItems.Add([PSCustomObject]@{ NodeId = $cid }) | Out-Null } if ($us -and $us.Count -gt 0) { if ($Detailed) { foreach ($u in ($us | Sort-Object { $_.Email })) { $contentItems.Add($($u.Email) + " ($($u.Id))") | Out-Null } } else { $contentItems.Add("$($us.Count) user(s)") | Out-Null } } if ($ro -and $ro.Count -gt 0) { if ($Detailed) { $i = 0; foreach ($r in ($ro | Sort-Object { $_.DisplayName })) { if ($i -ge 50) { $contentItems.Add("$($ro.Count - 50) more role(s)"); break } $contentItems.Add("$($r.DisplayName) ($($r.Id))") | Out-Null; $i++ } } else { $contentItems.Add("$($ro.Count) role(s)") | Out-Null } } if ($te -and $te.Count -gt 0) { if ($Detailed) { $i = 0; foreach ($t in ($te | Sort-Object { $_.Name })) { if ($i -ge 50) { $contentItems.Add("$($te.Count - 50) more team(s)"); break } $contentItems.Add("$($t.Name) ($($t.Uid))") | Out-Null; $i++ } } else { $contentItems.Add("$($te.Count) team(s)") | Out-Null } } $total = $contentItems.Count for ($i = 0; $i -lt $total; $i++) { $isLast = ($i -eq $total - 1) $branch = if ($isLastSibling -and $isLast) { ' ' } else { ' | ' } $connector = if ($prefix -eq '') { ' ' } else { $prefix + $branch } $item = $contentItems[$i] if ($item -is [string]) { $lines.Add("$connector+-- $item") | Out-Null } else { writeTreeNode -nodeId $item.NodeId -prefix $connector -isLastSibling $isLast } } } writeTreeNode -nodeId $rootId -prefix "" -isLastSibling $true $out = $lines -join "`n" if ($Output) { Set-Content -Path $Output -Value $out -Encoding utf8 } else { $out } } function Get-KeeperEnterpriseInfoNode { <# .SYNOPSIS Display node information as a table. .DESCRIPTION Outputs nodes with parent path, user/team/role counts, and optionally user/team/role lists and provisioning. .PARAMETER Pattern Optional search pattern to filter nodes. .PARAMETER Columns Comma-separated columns: parent_node, user_count, users, team_count, teams, role_count, roles, provisioning. Default: parent_node, user_count, team_count, role_count. .PARAMETER Node Filter by node name or ID: only nodes that are this node or its descendants. .PARAMETER Format Output format: table (default), json, csv. .PARAMETER Output If supplied, write output to this file path. .PARAMETER Offset Number of rows to skip (for pagination). Default 0. .PARAMETER Limit Maximum number of rows to return (0 = no limit). Use with Offset for range/pagination. .EXAMPLE Get-KeeperEnterpriseInfoNode Get-KeeperEnterpriseInfoNode -Columns "parent_node,user_count,users" -Pattern "Sales" -Node "Sales" -Format json -Output nodes.json -Offset 0 -Limit 50 #> [CmdletBinding()] Param ( [Parameter(Position = 0)][string] $Pattern, [Parameter()][string] $Columns, [Parameter()][string] $Node, [Parameter()][ValidateSet('table', 'json', 'csv')][string] $Format = 'table', [Parameter()][string] $Output, [Parameter()][int] $Offset = 0, [Parameter()][int] $Limit = 0 ) $enterprise = getEnterprise $ed = $enterprise.enterpriseData $rd = $enterprise.roleData $userCount = @{}; $teamCount = @{}; $roleCount = @{} $userList = @{}; $teamList = @{}; $roleList = @{} foreach ($u in $ed.Users) { $prev = $userCount[$u.ParentNodeId]; if ($null -eq $prev) { $prev = 0 }; $userCount[$u.ParentNodeId] = $prev + 1 if (-not $userList[$u.ParentNodeId]) { $userList[$u.ParentNodeId] = [System.Collections.Generic.List[string]]::new() } $userList[$u.ParentNodeId].Add($u.Email) | Out-Null } foreach ($t in $ed.Teams) { $nid = if ($t.ParentNodeId -eq 0) { $ed.RootNode.Id } else { $t.ParentNodeId } $prev = $teamCount[$nid]; if ($null -eq $prev) { $prev = 0 }; $teamCount[$nid] = $prev + 1 if (-not $teamList[$nid]) { $teamList[$nid] = [System.Collections.Generic.List[string]]::new() } $teamList[$nid].Add($t.Name) | Out-Null } foreach ($r in $rd.Roles) { $prev = $roleCount[$r.ParentNodeId]; if ($null -eq $prev) { $prev = 0 }; $roleCount[$r.ParentNodeId] = $prev + 1 if (-not $roleList[$r.ParentNodeId]) { $roleList[$r.ParentNodeId] = [System.Collections.Generic.List[string]]::new() } $roleList[$r.ParentNodeId].Add($r.DisplayName) | Out-Null } $nodeFilterIds = $null if ($Node) { $resolved = resolveSingleNode $Node if (-not $resolved) { Write-Error "Node '$Node' not found"; return } $nodeFilterIds = Get-EnterpriseNodeAndDescendantIds $ed $resolved.Id } $colSet = @('parent_node', 'user_count', 'team_count', 'role_count') if ($Columns) { $colSet = @($Columns -split ',' | ForEach-Object { $_.Trim() } | Where-Object { $_ -match '^(parent_node|user_count|users|team_count|teams|role_count|roles|provisioning)$' }) if ($colSet.Count -eq 0) { $colSet = @('parent_node', 'user_count', 'team_count', 'role_count') } } $patternLower = if ($Pattern) { $Pattern.Trim().ToLower() } else { '' } $out = [System.Collections.Generic.List[PSCustomObject]]::new() foreach ($n in ($ed.Nodes | Sort-Object { $_.DisplayName })) { if ($nodeFilterIds -and -not $nodeFilterIds.Contains($n.Id)) { continue } $row = [ordered]@{ NodeId = $n.Id; Name = $n.DisplayName } foreach ($c in $colSet) { switch ($c) { 'parent_node' { $row['ParentNode'] = if ($n.ParentNodeId -le 0) { '' } else { Get-KeeperNodePath -NodeId $n.ParentNodeId } } 'user_count' { $row['UserCount'] = $userCount[$n.Id]; if ($null -eq $row['UserCount']) { $row['UserCount'] = 0 } } 'users' { $row['Users'] = ($userList[$n.Id] | Sort-Object) -join ', ' } 'team_count' { $row['TeamCount'] = $teamCount[$n.Id]; if ($null -eq $row['TeamCount']) { $row['TeamCount'] = 0 } } 'teams' { $row['Teams'] = ($teamList[$n.Id] | Sort-Object) -join ', ' } 'role_count' { $row['RoleCount'] = $roleCount[$n.Id]; if ($null -eq $row['RoleCount']) { $row['RoleCount'] = 0 } } 'roles' { $row['Roles'] = ($roleList[$n.Id] | Sort-Object) -join ', ' } 'provisioning' { $parts = @(); if ($n.BridgeId -gt 0) { $parts += 'Bridge' }; if ($n.ScimId -gt 0) { $parts += 'SCIM' }; if ($n.SsoServiceProviderIds -and $n.SsoServiceProviderIds.Length -gt 0) { $parts += 'SSO' }; $row['Provisioning'] = ($parts -join ', ') } } } if ($patternLower) { $text = ($row.Values | ForEach-Object { $_ }) -join ' ' if ($text -notmatch [regex]::Escape($patternLower)) { continue } } $out.Add([PSCustomObject]$row) | Out-Null } $result = @($out | Sort-Object { $_.Name }) if ($Offset -gt 0) { $result = @($result | Select-Object -Skip $Offset) } if ($Limit -gt 0) { $result = @($result | Select-Object -First $Limit) } if ($Format -eq 'table') { $disp = $result | Format-Table -AutoSize } else { $disp = $result } if ($Output) { if ($Format -eq 'json') { Set-Content -Path $Output -Value ($result | ConvertTo-Json -Depth 5) -Encoding utf8 } elseif ($Format -eq 'csv') { Set-Content -Path $Output -Value ($result | ConvertTo-Csv -NoTypeInformation) -Encoding utf8 } else { $result | Format-Table -AutoSize | Out-String | Set-Content -Path $Output -Encoding utf8 } } else { if ($Format -eq 'table') { $disp } else { $disp } } } function Get-KeeperEnterpriseInfoUser { <# .SYNOPSIS Display user information as a table. .DESCRIPTION Outputs users with status, node, roles, teams, and optional columns. .PARAMETER Pattern Optional search pattern to filter users. .PARAMETER Columns Comma-separated columns: name, status, transfer_status, node, role_count, roles, team_count, teams, queued_team_count, queued_teams, alias, 2fa_enabled. Default: name, status, transfer_status, node. .PARAMETER Node Filter by node name or ID: only users in this node or its descendants. .PARAMETER Format Output format: table (default), json, csv. .PARAMETER Output If supplied, write output to this file path. .PARAMETER Offset Number of rows to skip (for pagination). Default 0. .PARAMETER Limit Maximum number of rows to return (0 = no limit). Use with Offset for range/pagination. .EXAMPLE Get-KeeperEnterpriseInfoUser Get-KeeperEnterpriseInfoUser -Columns "name,status,node,roles" -Pattern "admin" -Node "Sales" -Format json -Output users.json -Offset 0 -Limit 100 #> [CmdletBinding()] Param ( [Parameter(Position = 0)][string] $Pattern, [Parameter()][string] $Columns, [Parameter()][string] $Node, [Parameter()][ValidateSet('table', 'json', 'csv')][string] $Format = 'table', [Parameter()][string] $Output, [Parameter()][int] $Offset = 0, [Parameter()][int] $Limit = 0 ) $enterprise = getEnterprise $ed = $enterprise.enterpriseData $rd = $enterprise.roleData $roleUsers = @{} foreach ($r in $rd.Roles) { foreach ($uid in @($rd.GetUsersForRole($r.Id))) { if (-not $roleUsers[$uid]) { $roleUsers[$uid] = [System.Collections.Generic.List[long]]::new() } $roleUsers[$uid].Add($r.Id) | Out-Null } } $teamUsers = @{} foreach ($t in $ed.Teams) { foreach ($uid in @($ed.GetUsersForTeam($t.Uid))) { if (-not $teamUsers[$uid]) { $teamUsers[$uid] = [System.Collections.Generic.List[string]]::new() } $teamUsers[$uid].Add($t.Name) | Out-Null } } $colSet = @('name', 'status', 'transfer_status', 'node') if ($Columns) { $colSet = @($Columns -split ',' | ForEach-Object { $_.Trim() } | Where-Object { $_ -match '^(name|status|transfer_status|node|role_count|roles|team_count|teams|queued_team_count|queued_teams|alias|2fa_enabled)$' }) if ($colSet.Count -eq 0) { $colSet = @('name', 'status', 'transfer_status', 'node') } } $nodeFilterIds = $null if ($Node) { $resolved = resolveSingleNode $Node $nodeFilterIds = Get-EnterpriseNodeAndDescendantIds $ed $resolved.Id } $statusText = $Script:UserStatusText $transferText = $Script:TransferStatusText $patternLower = if ($Pattern) { $Pattern.Trim().ToLower() } else { '' } $out = [System.Collections.Generic.List[PSCustomObject]]::new() foreach ($u in ($ed.Users | Sort-Object { $_.Email })) { $nid = if ($u.ParentNodeId -le 0) { $ed.RootNode.Id } else { $u.ParentNodeId } if ($nodeFilterIds -and -not $nodeFilterIds.Contains($nid)) { continue } $row = [ordered]@{ UserId = $u.Id; Email = $u.Email } foreach ($c in $colSet) { switch ($c) { 'name' { $row['Name'] = $u.DisplayName } 'status' { $row['Status'] = & $statusText $u.UserStatus } 'transfer_status' { $row['TransferStatus'] = & $transferText $u.TransferAcceptanceStatus } 'node' { $row['Node'] = Get-KeeperNodePath -NodeId $u.ParentNodeId -OmitRoot } 'role_count' { $arr = $roleUsers[$u.Id]; if ($null -ne $arr) { $row['RoleCount'] = $arr.Count } else { $row['RoleCount'] = 0 } } 'roles' { $rnames = @($roleUsers[$u.Id] | ForEach-Object { $rr = $null; if ($rd.TryGetRole($_, [ref]$rr)) { $rr.DisplayName } } | Sort-Object); $row['Roles'] = ($rnames -join ', ') } 'team_count' { $arr = $teamUsers[$u.Id]; if ($null -ne $arr) { $row['TeamCount'] = $arr.Count } else { $row['TeamCount'] = 0 } } 'teams' { $row['Teams'] = (($teamUsers[$u.Id] | Sort-Object) -join ', ') } 'queued_team_count' { $row['QueuedTeamCount'] = 0 } 'queued_teams' { $row['QueuedTeams'] = '' } 'alias' { $row['Alias'] = '' } '2fa_enabled' { $row['2FAEnabled'] = $u.TwoFactorEnabled } } } if ($patternLower) { $text = ($row.Values | ForEach-Object { $_ }) -join ' ' if ($text -notmatch [regex]::Escape($patternLower)) { continue } } $out.Add([PSCustomObject]$row) | Out-Null } $result = @($out | Sort-Object { $_.Email }) if ($Offset -gt 0) { $result = @($result | Select-Object -Skip $Offset) } if ($Limit -gt 0) { $result = @($result | Select-Object -First $Limit) } if ($Output) { if ($Format -eq 'json') { Set-Content -Path $Output -Value ($result | ConvertTo-Json -Depth 5) -Encoding utf8 } elseif ($Format -eq 'csv') { Set-Content -Path $Output -Value ($result | ConvertTo-Csv -NoTypeInformation) -Encoding utf8 } else { $result | Format-Table -AutoSize | Out-String | Set-Content -Path $Output -Encoding utf8 } } else { if ($Format -eq 'table') { $result | Format-Table -AutoSize } else { $result } } } function Get-KeeperEnterpriseInfoTeam { <# .SYNOPSIS Display team information as a table. .DESCRIPTION Outputs teams with restricts (Read/Write/Share), node, user/role counts, and optional user/role lists. .PARAMETER Pattern Optional search pattern to filter teams. .PARAMETER Columns Comma-separated columns: restricts, node, user_count, users, queued_user_count, queued_users, role_count, roles. Default: restricts, node, user_count. .PARAMETER Node Filter by node name or ID: only teams in this node or its descendants. .PARAMETER Format Output format: table (default), json, csv. .PARAMETER Output If supplied, write output to this file path. .PARAMETER Offset Number of rows to skip (for pagination). Default 0. .PARAMETER Limit Maximum number of rows to return (0 = no limit). Use with Offset for range/pagination. .EXAMPLE Get-KeeperEnterpriseInfoTeam Get-KeeperEnterpriseInfoTeam -Columns "restricts,node,user_count,users" -Pattern "Eng" -Node "Engineering" -Format json -Output teams.json -Offset 0 -Limit 50 #> [CmdletBinding()] Param ( [Parameter(Position = 0)][string] $Pattern, [Parameter()][string] $Columns, [Parameter()][string] $Node, [Parameter()][switch] $ExactNode, [Parameter()][ValidateSet('table', 'json', 'csv')][string] $Format = 'table', [Parameter()][string] $Output, [Parameter()][int] $Offset = 0, [Parameter()][int] $Limit = 0 ) $enterprise = getEnterprise $ed = $enterprise.enterpriseData $rd = $enterprise.roleData $userCount = @{}; $roleCount = @{} $userList = @{}; $roleList = @{} foreach ($t in $ed.Teams) { $uids = @($ed.GetUsersForTeam($t.Uid)) $userCount[$t.Uid] = $uids.Count $userList[$t.Uid] = @($uids | ForEach-Object { $uu = $null; if ($ed.TryGetUserById($_, [ref]$uu)) { $uu.Email } } | Sort-Object) } foreach ($t in $ed.Teams) { foreach ($rid in @($rd.GetRolesForTeam($t.Uid))) { if (-not $roleList[$t.Uid]) { $roleList[$t.Uid] = [System.Collections.Generic.List[string]]::new() } $rr = $null; if ($rd.TryGetRole($rid, [ref]$rr)) { $roleList[$t.Uid].Add($rr.DisplayName) | Out-Null } } } $nodeFilterIds = $null if ($Node) { $resolved = resolveSingleNode $Node if ($ExactNode) { $nodeFilterIds = [System.Collections.Generic.HashSet[long]]::new() [void]$nodeFilterIds.Add($resolved.Id) } else { $nodeFilterIds = Get-EnterpriseNodeAndDescendantIds $ed $resolved.Id } } $colSet = @('restricts', 'node', 'user_count') if ($Columns) { $colSet = @($Columns -split ',' | ForEach-Object { $_.Trim() } | Where-Object { $_ -match '^(restricts|node|user_count|users|queued_user_count|queued_users|role_count|roles)$' }) if ($colSet.Count -eq 0) { $colSet = @('restricts', 'node', 'user_count') } } $patternLower = if ($Pattern) { $Pattern.Trim().ToLower() } else { '' } $out = [System.Collections.Generic.List[PSCustomObject]]::new() foreach ($t in ($ed.Teams | Sort-Object { $_.Name })) { $nid = if ($t.ParentNodeId -eq 0) { $ed.RootNode.Id } else { $t.ParentNodeId } if ($nodeFilterIds -and -not $nodeFilterIds.Contains($nid)) { continue } $restrictParts = @() if ($t.RestrictView) { $restrictParts += 'Read' } if ($t.RestrictEdit) { $restrictParts += 'Write' } if ($t.RestrictSharing) { $restrictParts += 'Share' } $restricts = $restrictParts -join ', ' $row = [ordered]@{ TeamUid = $t.Uid; Name = $t.Name } foreach ($c in $colSet) { switch ($c) { 'restricts' { $row['Restricts'] = $restricts } 'node' { $row['Node'] = Get-KeeperNodePath -NodeId $t.ParentNodeId -OmitRoot } 'user_count' { $row['UserCount'] = $userCount[$t.Uid]; if ($null -eq $row['UserCount']) { $row['UserCount'] = 0 } } 'users' { $row['Users'] = ($userList[$t.Uid] -join ', ') } 'queued_user_count'{ $row['QueuedUserCount'] = 0 } 'queued_users' { $row['QueuedUsers'] = '' } 'role_count' { $arr = $roleList[$t.Uid]; if ($null -ne $arr) { $row['RoleCount'] = $arr.Count } else { $row['RoleCount'] = 0 } } 'roles' { $row['Roles'] = (($roleList[$t.Uid] | Sort-Object) -join ', ') } } } if ($patternLower) { $text = ($row.Values | ForEach-Object { $_ }) -join ' ' if ($text -notmatch [regex]::Escape($patternLower)) { continue } } $out.Add([PSCustomObject]$row) | Out-Null } $result = @($out | Sort-Object { $_.Name }) if ($Offset -gt 0) { $result = @($result | Select-Object -Skip $Offset) } if ($Limit -gt 0) { $result = @($result | Select-Object -First $Limit) } if ($Output) { if ($Format -eq 'json') { Set-Content -Path $Output -Value ($result | ConvertTo-Json -Depth 5) -Encoding utf8 } elseif ($Format -eq 'csv') { Set-Content -Path $Output -Value ($result | ConvertTo-Csv -NoTypeInformation) -Encoding utf8 } else { $result | Format-Table -AutoSize | Out-String | Set-Content -Path $Output -Encoding utf8 } } else { if ($Format -eq 'table') { $result | Format-Table -AutoSize } else { $result } } } function Get-KeeperEnterpriseInfoRole { <# .SYNOPSIS Display role information as a table. .DESCRIPTION Outputs roles with node, user/team counts, admin flag, and optional user/team lists. .PARAMETER Pattern Optional search pattern to filter roles. .PARAMETER Columns Comma-separated columns: visible_below, default_role, admin, node, user_count, users, team_count, teams. Default: default_role, admin, node, user_count. .PARAMETER Node Filter by node name or ID: only roles in this node or its descendants. .PARAMETER Format Output format: table (default), json, csv. .PARAMETER Output If supplied, write output to this file path. .PARAMETER Offset Number of rows to skip (for pagination). Default 0. .PARAMETER Limit Maximum number of rows to return (0 = no limit). Use with Offset for range/pagination. .EXAMPLE Get-KeeperEnterpriseInfoRole Get-KeeperEnterpriseInfoRole -Columns "visible_below,node,user_count,users" -Pattern "Admin" -Node "Sales" -Format json -Output roles.json -Offset 0 -Limit 50 #> [CmdletBinding()] Param ( [Parameter(Position = 0)][string] $Pattern, [Parameter()][string] $Columns, [Parameter()][string] $Node, [Parameter()][switch] $ExactNode, [Parameter()][ValidateSet('table', 'json', 'csv')][string] $Format = 'table', [Parameter()][string] $Output, [Parameter()][int] $Offset = 0, [Parameter()][int] $Limit = 0 ) $enterprise = getEnterprise $ed = $enterprise.enterpriseData $rd = $enterprise.roleData $userCount = @{}; $teamCount = @{} $userList = @{}; $teamList = @{} foreach ($r in $rd.Roles) { $uids = @($rd.GetUsersForRole($r.Id)) $userCount[$r.Id] = $uids.Count $userList[$r.Id] = @($uids | ForEach-Object { $uu = $null; if ($ed.TryGetUserById($_, [ref]$uu)) { $uu.Email } } | Sort-Object) } foreach ($r in $rd.Roles) { foreach ($tuid in @($rd.GetTeamsForRole($r.Id))) { if (-not $teamList[$r.Id]) { $teamList[$r.Id] = [System.Collections.Generic.List[string]]::new() } $tt = $null; if ($ed.TryGetTeam($tuid, [ref]$tt)) { $teamList[$r.Id].Add($tt.Name) | Out-Null } } } $nodeFilterIds = $null if ($Node) { $resolved = resolveSingleNode $Node if ($ExactNode) { $nodeFilterIds = [System.Collections.Generic.HashSet[long]]::new() [void]$nodeFilterIds.Add($resolved.Id) } else { $nodeFilterIds = Get-EnterpriseNodeAndDescendantIds $ed $resolved.Id } } $managedNodes = @($rd.GetManagedNodes()) $adminRoleIds = [System.Collections.Generic.HashSet[long]]::new() foreach ($mn in $managedNodes) { [void]$adminRoleIds.Add($mn.RoleId) } $colSet = @('visible_below', 'default_role', 'admin', 'node', 'user_count') if ($Columns) { $colSet = @($Columns -split ',' | ForEach-Object { $_.Trim() } | Where-Object { $_ -match '^(visible_below|default_role|admin|node|user_count|users|team_count|teams)$' }) if ($colSet.Count -eq 0) { $colSet = @('default_role', 'admin', 'node', 'user_count') } } $patternLower = if ($Pattern) { $Pattern.Trim().ToLower() } else { '' } $out = [System.Collections.Generic.List[PSCustomObject]]::new() foreach ($r in ($rd.Roles | Sort-Object { $_.DisplayName })) { $nid = if ($r.ParentNodeId -le 0) { $ed.RootNode.Id } else { $r.ParentNodeId } if ($nodeFilterIds -and -not $nodeFilterIds.Contains($nid)) { continue } $row = [ordered]@{ RoleId = $r.Id; Name = $r.DisplayName } foreach ($c in $colSet) { switch ($c) { 'visible_below' { $row['VisibleBelow'] = $r.VisibleBelow } 'default_role' { $row['DefaultRole'] = $r.NewUserInherit } 'admin' { $row['Admin'] = $adminRoleIds.Contains($r.Id) } 'node' { $row['Node'] = Get-KeeperNodePath -NodeId $r.ParentNodeId -OmitRoot } 'user_count' { $row['UserCount'] = $userCount[$r.Id]; if ($null -eq $row['UserCount']) { $row['UserCount'] = 0 } } 'users' { $row['Users'] = ($userList[$r.Id] -join ', ') } 'team_count' { $arr = $teamList[$r.Id]; if ($null -ne $arr) { $row['TeamCount'] = $arr.Count } else { $row['TeamCount'] = 0 } } 'teams' { $row['Teams'] = (($teamList[$r.Id] | Sort-Object) -join ', ') } } } if ($patternLower) { $text = ($row.Values | ForEach-Object { $_ }) -join ' ' if ($text -notmatch [regex]::Escape($patternLower)) { continue } } $out.Add([PSCustomObject]$row) | Out-Null } $result = @($out | Sort-Object { $_.Name }) if ($Offset -gt 0) { $result = @($result | Select-Object -Skip $Offset) } if ($Limit -gt 0) { $result = @($result | Select-Object -First $Limit) } if ($Output) { if ($Format -eq 'json') { Set-Content -Path $Output -Value ($result | ConvertTo-Json -Depth 5) -Encoding utf8 } elseif ($Format -eq 'csv') { Set-Content -Path $Output -Value ($result | ConvertTo-Csv -NoTypeInformation) -Encoding utf8 } else { $result | Format-Table -AutoSize | Out-String | Set-Content -Path $Output -Encoding utf8 } } else { if ($Format -eq 'table') { $result | Format-Table -AutoSize } else { $result } } } function Get-KeeperEnterpriseInfoManagedCompany { <# .SYNOPSIS Display managed company information (MSP only). .DESCRIPTION Outputs managed company information. Available when logged in as MSP. .PARAMETER Pattern Optional search pattern to filter companies. .PARAMETER Node Filter by node name or ID: only managed companies in this node or its descendants. .PARAMETER ExactNode If set, -Node filters to that node only (exclude descendants). .PARAMETER Format Output format: table (default), json, csv. .PARAMETER Output If supplied, write output to this file path. .PARAMETER Offset Number of rows to skip (for pagination). Default 0. .PARAMETER Limit Maximum number of rows to return (0 = no limit). Use with Offset for range/pagination. .EXAMPLE Get-KeeperEnterpriseInfoManagedCompany Get-KeeperEnterpriseInfoManagedCompany -Format json -Output mcs.json -Offset 0 -Limit 20 #> [CmdletBinding()] Param ( [Parameter(Position = 0)][string] $Pattern, [Parameter()][string] $Node, [Parameter()][switch] $ExactNode, [Parameter()][ValidateSet('table', 'json', 'csv')][string] $Format = 'table', [Parameter()][string] $Output, [Parameter()][int] $Offset = 0, [Parameter()][int] $Limit = 0 ) $enterprise = getMspEnterprise $ed = $enterprise.enterpriseData $mcs = $enterprise.mspData.ManagedCompanies if (-not $mcs) { return @() } $nodeFilterIds = $null if ($Node) { $resolved = resolveSingleNode $Node if ($ExactNode) { $nodeFilterIds = [System.Collections.Generic.HashSet[long]]::new() [void]$nodeFilterIds.Add($resolved.Id) } else { $nodeFilterIds = Get-EnterpriseNodeAndDescendantIds $ed $resolved.Id } } $planName = { param($planId) switch ($planId) { 'enterprise' { 'Enterprise' } 'enterprise_plus' { 'Enterprise Plus' } 'business' { 'Business' } 'businessPlus' { 'Business Plus' } default { $planId } } } $patternLower = if ($Pattern) { $Pattern.Trim().ToLower() } else { '' } $out = [System.Collections.Generic.List[PSCustomObject]]::new() foreach ($mc in ($mcs | Sort-Object { $_.EnterpriseName })) { $nid = if ($mc.ParentNodeId -le 0) { $ed.RootNode.Id } else { $mc.ParentNodeId } if ($nodeFilterIds -and -not $nodeFilterIds.Contains($nid)) { continue } $storage = if ($mc.FilePlanType) { $mc.FilePlanType } else { '' } $addons = if ($mc.AddOns) { $mc.AddOns.Count } else { 0 } $allocated = $mc.NumberOfSeats; if ($allocated -eq 2147483647) { $allocated = $null } $nodePath = Get-KeeperNodePath -NodeId $mc.ParentNodeId -OmitRoot $row = [PSCustomObject]@{ CompanyId = $mc.EnterpriseId CompanyName = $mc.EnterpriseName Node = $nodePath Plan = & $planName $mc.ProductId Storage = $storage Addons = $addons Allocated = $allocated Active = $mc.NumberOfUsers } if ($patternLower) { $text = ($row.PSObject.Properties.Value | ForEach-Object { $_ }) -join ' ' if ($text -notmatch [regex]::Escape($patternLower)) { continue } } $out.Add($row) | Out-Null } $result = @($out | Sort-Object { $_.CompanyName }) if ($Offset -gt 0) { $result = @($result | Select-Object -Skip $Offset) } if ($Limit -gt 0) { $result = @($result | Select-Object -First $Limit) } if ($Output) { if ($Format -eq 'json') { Set-Content -Path $Output -Value ($result | ConvertTo-Json -Depth 5) -Encoding utf8 } elseif ($Format -eq 'csv') { Set-Content -Path $Output -Value ($result | ConvertTo-Csv -NoTypeInformation) -Encoding utf8 } else { $result | Format-Table -AutoSize | Out-String | Set-Content -Path $Output -Encoding utf8 } } else { if ($Format -eq 'table') { $result | Format-Table -AutoSize } else { $result } } } function Get-KeeperUserReport { <# .SYNOPSIS Generate an ad-hoc user status report. .DESCRIPTION Generates a comprehensive user status report including email, name, status, transfer status, last login, node, roles, and teams. Queries audit events to determine the last login time for each user. .PARAMETER Format Output format: table (default), json, csv. .PARAMETER Output Output to the given filename. .PARAMETER Days Number of days to look back for last login date. Default: 365. .PARAMETER LastLogin Show only last-login columns (email, name, status, transfer_status, last_login). .EXAMPLE Get-KeeperUserReport Generates a full user report in table format. .EXAMPLE Get-KeeperUserReport -Format json -Output "user_report.json" Exports the full user report to a JSON file. .EXAMPLE Get-KeeperUserReport -Days 30 -LastLogin Shows a compact last-login report looking back 30 days. .EXAMPLE Get-KeeperUserReport -Format csv -Output "report.csv" -Days 90 Exports the full user report as CSV, looking back 90 days. #> [CmdletBinding()] Param ( [Parameter()][ValidateSet('table', 'json', 'csv')][string] $Format = 'table', [Parameter()][string] $Output, [Parameter()][ValidateRange(1, [int]::MaxValue)][int] $Days = 365, [Parameter()][switch] $LastLogin ) [Enterprise]$enterprise = getEnterprise $ed = $enterprise.enterpriseData $rd = $enterprise.roleData $auth = $enterprise.loader.Auth $rolesByUser = @{} foreach ($r in $rd.Roles) { foreach ($uid in @($rd.GetUsersForRole($r.Id))) { if (-not $rolesByUser[$uid]) { $rolesByUser[$uid] = [System.Collections.Generic.List[string]]::new() } $rolesByUser[$uid].Add($r.DisplayName) | Out-Null } } $teamsByUser = @{} foreach ($t in $ed.Teams) { foreach ($uid in @($ed.GetUsersForTeam($t.Uid))) { if (-not $teamsByUser[$uid]) { $teamsByUser[$uid] = [System.Collections.Generic.List[string]]::new() } $teamsByUser[$uid].Add($t.Name) | Out-Null } } $statusText = $Script:UserStatusText $transferText = $Script:TransferStatusText $activeEmails = [System.Collections.Generic.List[string]]::new() foreach ($u in $ed.Users) { if ($u.UserStatus -eq [KeeperSecurity.Enterprise.UserStatus]::Active) { $activeEmails.Add($u.Email.ToLower()) | Out-Null } } $lastLoginMap = @{} $batchLimit = 1000 if ($activeEmails.Count -gt 0) { Write-Verbose "Querying latest login for the last $Days days..." $fromDate = [DateTimeOffset]::UtcNow.AddDays(-$Days) $fromTs = $fromDate.ToUnixTimeSeconds() $offset = 0 while ($offset -lt $activeEmails.Count) { $endIdx = [Math]::Min($offset + $batchLimit, $activeEmails.Count) $batch = @($activeEmails.GetRange($offset, $endIdx - $offset)) $offset = $endIdx $filter = New-Object KeeperSecurity.Enterprise.AuditLogCommands.ReportFilter $filter.EventTypes = @('login', 'login_console', 'chat_login', 'accept_invitation') $filter.Username = $batch $cf = New-Object KeeperSecurity.Enterprise.AuditLogCommands.CreatedFilter $cf.Min = $fromTs $filter.Created = $cf $rq = New-Object KeeperSecurity.Enterprise.AuditLogCommands.GetAuditEventReportsCommand $rq.Filter = $filter $rq.ReportType = 'span' $rq.Limit = $batchLimit $rq.Aggregate = @('last_created') $rq.Columns = @('username') try { $response = $auth.ExecuteAuthCommand( $rq, [KeeperSecurity.Enterprise.AuditLogCommands.GetAuditEventReportsResponse], $true ).GetAwaiter().GetResult() $rs = [KeeperSecurity.Enterprise.AuditLogCommands.GetAuditEventReportsResponse]$response if ($rs.Events) { foreach ($evt in $rs.Events) { $username = '' $lastCreated = 0 if ($evt.ContainsKey('username') -and $null -ne $evt['username']) { $username = $evt['username'].ToString().ToLower() } if ($evt.ContainsKey('last_created') -and $null -ne $evt['last_created']) { $lastCreated = [long]$evt['last_created'] } if ($username -and $lastCreated -gt 0) { $lastLoginMap[$username] = $lastCreated } } } } catch { Write-Warning "Failed to query audit events: $($_.Exception.Message)" } } } $nodePathCache = @{} $out = [System.Collections.Generic.List[PSCustomObject]]::new() foreach ($u in ($ed.Users | Sort-Object { $_.Email.ToLower() })) { $email = $u.Email $name = $u.DisplayName $status = & $statusText $u.UserStatus $transfer = & $transferText $u.TransferAcceptanceStatus $key = $email.ToLower() $lastLoginTs = $lastLoginMap[$key] $lastLog = '' if ($lastLoginTs -and $lastLoginTs -gt 0) { $lastLog = [DateTimeOffset]::FromUnixTimeSeconds($lastLoginTs).UtcDateTime.ToString('yyyy-MM-dd HH:mm:ss UTC') } elseif ($u.UserStatus -ne [KeeperSecurity.Enterprise.UserStatus]::Inactive) { $lastLog = "> $Days DAYS AGO" } else { $lastLog = 'N/A' } $roles = if ($rolesByUser[$u.Id]) { @($rolesByUser[$u.Id] | Sort-Object) } else { @() } $teams = if ($teamsByUser[$u.Id]) { @($teamsByUser[$u.Id] | Sort-Object) } else { @() } if ($LastLogin.IsPresent) { $row = [PSCustomObject][ordered]@{ Email = $email Name = $name Status = $status TransferStatus = $transfer LastLogin = $lastLog } } else { if (-not $nodePathCache.ContainsKey($u.ParentNodeId)) { $nodePathCache[$u.ParentNodeId] = Get-KeeperNodePath -NodeId $u.ParentNodeId } $row = [PSCustomObject][ordered]@{ Email = $email Name = $name Status = $status TransferStatus = $transfer LastLogin = $lastLog Node = $nodePathCache[$u.ParentNodeId] Roles = ($roles -join ",") Teams = ($teams -join ",") } } $out.Add($row) | Out-Null } $result = @($out) $wideWidth = 4096 if ($Output) { switch ($Format) { 'json' { Set-Content -Path $Output -Value ($result | ConvertTo-Json -Depth 5) -Encoding utf8 } 'csv' { $result | Export-Csv -Path $Output -NoTypeInformation -Encoding utf8 } default { $result | Format-Table -AutoSize | Out-String -Width $wideWidth | Set-Content -Path $Output -Encoding utf8 } } Write-Host "Output written to $Output" } else { switch ($Format) { 'json' { $result | ConvertTo-Json -Depth 5 } 'csv' { $result | ConvertTo-Csv -NoTypeInformation } default { $result | Format-Table -AutoSize | Out-String -Width $wideWidth } } } } New-Alias -Name user-report -Value Get-KeeperUserReport -ErrorAction SilentlyContinue New-Alias -Name keitree -Value Get-KeeperEnterpriseInfoTree -ErrorAction SilentlyContinue New-Alias -Name kein -Value Get-KeeperEnterpriseInfoNode -ErrorAction SilentlyContinue New-Alias -Name keiu -Value Get-KeeperEnterpriseInfoUser -ErrorAction SilentlyContinue New-Alias -Name keit -Value Get-KeeperEnterpriseInfoTeam -ErrorAction SilentlyContinue New-Alias -Name keir -Value Get-KeeperEnterpriseInfoRole -ErrorAction SilentlyContinue New-Alias -Name keimc -Value Get-KeeperEnterpriseInfoManagedCompany -ErrorAction SilentlyContinue # SIG # Begin signature block # MIInvgYJKoZIhvcNAQcCoIInrzCCJ6sCAQExDzANBglghkgBZQMEAgEFADB5Bgor # BgEEAYI3AgEEoGswaTA0BgorBgEEAYI3AgEeMCYCAwEAAAQQH8w7YFlLCE63JNLG # KX7zUQIBAAIBAAIBAAIBAAIBADAxMA0GCWCGSAFlAwQCAQUABCChl/sXOiUAXXav # 5s7oZV3vi9+XRbGfbamqOFDMFdP4B6CCITswggWNMIIEdaADAgECAhAOmxiO+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 # oAMCAQICEAe0P3SLJmcoVNrErUyxTt0wDQYJKoZIhvcNAQELBQAwaTELMAkGA1UE # BhMCVVMxFzAVBgNVBAoTDkRpZ2lDZXJ0LCBJbmMuMUEwPwYDVQQDEzhEaWdpQ2Vy # dCBUcnVzdGVkIEc0IENvZGUgU2lnbmluZyBSU0E0MDk2IFNIQTM4NCAyMDIxIENB # MTAeFw0yNTEyMzEwMDAwMDBaFw0yOTAxMDIyMzU5NTlaMIHRMRMwEQYLKwYBBAGC # NzwCAQMTAlVTMRkwFwYLKwYBBAGCNzwCAQITCERlbGF3YXJlMR0wGwYDVQQPDBRQ # cml2YXRlIE9yZ2FuaXphdGlvbjEQMA4GA1UEBRMHMzQwNzk4NTELMAkGA1UEBhMC # VVMxETAPBgNVBAgTCElsbGlub2lzMRAwDgYDVQQHEwdDaGljYWdvMR0wGwYDVQQK # ExRLZWVwZXIgU2VjdXJpdHkgSW5jLjEdMBsGA1UEAxMUS2VlcGVyIFNlY3VyaXR5 # IEluYy4wggGiMA0GCSqGSIb3DQEBAQUAA4IBjwAwggGKAoIBgQCUcNMoSVmxAi0a # vG+StFJMNFFTUIOo3HdBZ+0gqA1XpNgUx11vB1vCZrvFsD9m5oA58tdp4gZN3LmQ # aMvCl2ANUT7MilI02Hf1RWlygBzon6iE0GpU3lgRrwrk1dhtLpGsR6dbMKUUHprc # vKpXk90/VN+vhzY1uik1tCTxkDCPu/AYJg7m9+tR2KqvMuYMaMLhii66eWUAGsBC # h/uZxjkGoJF6qZ0DgFd7rW7VYljbfYSNPeZNGTDgB0J/wOsKl0mn612DTseIvAKt # 4vra/FLFukyEyStnfQ8lWYDcLLCMCjNVrzGipmT5E2iyx7Y1RZCIpNwVogp3Ixbk # Gbq5A/41YNOLLd4cFewyB2F037RevBCRsUODZEt1qBf7Jbu3DiYo1G+zTj9E0R1s # FzyijcfdsTm6X5ble+yCJeGkX5XgsyPnZpyz/FX9Fr0N9pMPGWwW2PKyHEnSytXm # 0Dxdq2P4mA4CBUxq7YoV26L2PF6QEh9BQdXTPcnLysUv7SI/a0ECAwEAAaOCAgIw # ggH+MB8GA1UdIwQYMBaAFGg34Ou2O/hfEYb7/mF7CIhl9E5CMB0GA1UdDgQWBBRG # 4H6CH8pvNX632bsdnrda4MtJLDA9BgNVHSAENjA0MDIGBWeBDAEDMCkwJwYIKwYB # BQUHAgEWG2h0dHA6Ly93d3cuZGlnaWNlcnQuY29tL0NQUzAOBgNVHQ8BAf8EBAMC # B4AwEwYDVR0lBAwwCgYIKwYBBQUHAwMwgbUGA1UdHwSBrTCBqjBToFGgT4ZNaHR0 # cDovL2NybDMuZGlnaWNlcnQuY29tL0RpZ2lDZXJ0VHJ1c3RlZEc0Q29kZVNpZ25p # bmdSU0E0MDk2U0hBMzg0MjAyMUNBMS5jcmwwU6BRoE+GTWh0dHA6Ly9jcmw0LmRp # Z2ljZXJ0LmNvbS9EaWdpQ2VydFRydXN0ZWRHNENvZGVTaWduaW5nUlNBNDA5NlNI # QTM4NDIwMjFDQTEuY3JsMIGUBggrBgEFBQcBAQSBhzCBhDAkBggrBgEFBQcwAYYY # aHR0cDovL29jc3AuZGlnaWNlcnQuY29tMFwGCCsGAQUFBzAChlBodHRwOi8vY2Fj # ZXJ0cy5kaWdpY2VydC5jb20vRGlnaUNlcnRUcnVzdGVkRzRDb2RlU2lnbmluZ1JT # QTQwOTZTSEEzODQyMDIxQ0ExLmNydDAJBgNVHRMEAjAAMA0GCSqGSIb3DQEBCwUA # A4ICAQA1Wlq0WzJa3N6DgjgBU7nagIJBab1prPARXZreX1MOv9VjnS5o0CrfQLr6 # z3bmWHw7xT8dt6bcSwRixqvPJtv4q8Rvo80O3eUMvMxQzqmi7z1zf+HG+/3G4F+2 # IYegvPc8Ui151XCV9rjA8tvFWRLRMX0ZRxY1zfT027HMw0iYL20z44+Cky//FAnL # iRwoNDGiRkZiHbB9YOftPAYNMG3gm1z3zOW5RdfKPrqvMuijE+dfyLIAA6Immpzu # FMH+Wgn8NnSlot9b4YKycaqqdjd7wXDjPub/oQ7VShuCSBWj+UNOTVh0vcZGackc # H1DLVgwp2dcKlxJiQKtkHT/T6LloY6LTe6+8wkVkr8EAv1W+q/+M1a4Ao+ykFbIA # 2LBEmA9qdgoLtenAYIiEg+48SjMPgyBbVPE3bhL1vIqjEIxYCfdmi6wx33oYX7HB # +bJ7zitHw4GgtpfPV8y8QRZImKmeDOKyXjQPDmQM/Eglm/Ns0GzBkVXM8h6UI34b # WZrHz9sbLSE20m5Svmxftvw5zju+I3WsmS/stNfWlOkwU0niUgwPHaz21kjXEA5A # g+aqv26wodqZcnGOlChoWDvSJ8KKgdOFbeAYKAMp1NY7iWV315zpGH19RipCR1NH # 0ND8iIubk3WGNf2rzEfqlOi3h2ywqVkU6AKXHdO5JV4otSKKEDGCBdkwggXVAgEB # MH0waTELMAkGA1UEBhMCVVMxFzAVBgNVBAoTDkRpZ2lDZXJ0LCBJbmMuMUEwPwYD # VQQDEzhEaWdpQ2VydCBUcnVzdGVkIEc0IENvZGUgU2lnbmluZyBSU0E0MDk2IFNI # QTM4NCAyMDIxIENBMQIQB7Q/dIsmZyhU2sStTLFO3TANBglghkgBZQMEAgEFAKCB # hDAYBgorBgEEAYI3AgEMMQowCKACgAChAoAAMBkGCSqGSIb3DQEJAzEMBgorBgEE # AYI3AgEEMBwGCisGAQQBgjcCAQsxDjAMBgorBgEEAYI3AgEVMC8GCSqGSIb3DQEJ # BDEiBCD9/j5+5pGZK0w2Rbvc/OjQl89dewiZK7t+OzPb9sMuWDANBgkqhkiG9w0B # AQEFAASCAYBTG+55ylIVG01faQQPdoBX38JbHphJdHUXztk+rE5TFmlq7MtB40Cx # lPk9jyROMkYyspyI6PEtwTtIsXHHGgyDEuUIwufm/4WW6ti0Jjig+22Y3fQAxiD3 # r4rXELkA8ZKfvPV3YbeUhQdhWKrSCKnNQ+X2aC9v/XiOY0LKxP+vFBZopEmDakvV # K62S2nwghxCDKSawAPgfzeNdv7/TPRjIqNy8e8My2LmWuWaVbO2SGnZcBpmE2Hiw # 3OtO/eJFMtwOvqqoB6eONsCUvsmeE9CfmlLaL4TruznTUTc00oFVO5KsWijERtuv # /GtUq2Yvxdt3JwMIZ89Co69EPXrTaVsNrMTeOnWLWhyIYFz78uh6IOYXuE8TZFDT # mMU2VdYqEpx5CxlxUv+Ku22NfrsA1mmRphWDcg0wGpiNzatFnKnoCMpwjTx9xb2R # U5FPduQUn7szSR+IfBspAMoVGCbNQhtlbRLpnGLvkWt5xWvIKIgW//hT5TkaeZ6C # 9z6PnH9IYDShggMmMIIDIgYJKoZIhvcNAQkGMYIDEzCCAw8CAQEwfTBpMQswCQYD # VQQGEwJVUzEXMBUGA1UEChMORGlnaUNlcnQsIEluYy4xQTA/BgNVBAMTOERpZ2lD # ZXJ0IFRydXN0ZWQgRzQgVGltZVN0YW1waW5nIFJTQTQwOTYgU0hBMjU2IDIwMjUg # Q0ExAhAKgO8YS43xBYLRxHanlXRoMA0GCWCGSAFlAwQCAQUAoGkwGAYJKoZIhvcN # AQkDMQsGCSqGSIb3DQEHATAcBgkqhkiG9w0BCQUxDxcNMjYwNTI2MDQwNjM3WjAv # BgkqhkiG9w0BCQQxIgQgJr5GX2qNbyd4hNRLInujUMP6O+TZOJwWNAOh4bVKtsQw # DQYJKoZIhvcNAQEBBQAEggIAzx3I7KTUSW43WcnOHGJkPdk/3cw4A3r9JoEL1Zqj # 2CfEsHaJY05CT+DHa+f1uI0WcngJpwok0RKLEle9sL8tBcU3lt5V4tjj9/0qgE2M # nWvAW5/oyLpq8KxR/pOqH6c7i0p5Rv62DO5Ne2PDl8/pNAcyVJU7m+zgHnnJHz/v # PPyve4T3dENic2J2y6pfDjfWzQRLhlIASWz4Ddtm/sL/p3mEcTQ/NeNQPOCAgXqM # g9/PgLIRm1HwKEKz3LMiyed+Pr18U5vfjkjrwub1NHN2+6yIZeXpiFzRjDjoS4Mk # 8tlkSs8S7Cq41wAaDQvbklJp/xXpmxv5uT3yiwx1oWys/bRph/AEjYTA4hXCJbmD # wTsip8gKj5+VmkOtKpIi5GQuxPSvSGXkQyvs6u3BBZY+K5N4YcaMbRTeenFtf58C # qDMG2wHa7vu7/1HUDqtNFHLW0uh6NFl9mSyXghQK+XqIcLGwSuu3vCQyZ0ckO5s7 # 4WkPr+NVZTvfkP5TyVTMzZkXN5mV/Vu52A7ftDq78t5KlzrKW7d+evYctTmdBCvB # bJjvgLLvu+aJHRTK1G5oa368uPXqNRJt5nnn+fFC+5i73+cWurrXkwRe4d3UmVgW # 2he1dYZpScDy6zWg7uSu4vGp/6lp1Aj5L2DSo0crmgMG5T3/h+MtlH1ij9OvDE/L # y0E= # SIG # End signature block |