bt-cli 0.4.19__tar.gz → 0.4.22__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (173) hide show
  1. {bt_cli-0.4.19 → bt_cli-0.4.22}/.claude/skills/entitle/SKILL.md +34 -0
  2. {bt_cli-0.4.19 → bt_cli-0.4.22}/.github/workflows/release.yml +3 -0
  3. {bt_cli-0.4.19/src/bt_cli/data → bt_cli-0.4.22}/CLAUDE.md +7 -1
  4. {bt_cli-0.4.19 → bt_cli-0.4.22}/PKG-INFO +2 -2
  5. {bt_cli-0.4.19 → bt_cli-0.4.22}/pyproject.toml +2 -2
  6. {bt_cli-0.4.19 → bt_cli-0.4.22}/src/bt_cli/__init__.py +1 -1
  7. {bt_cli-0.4.19 → bt_cli-0.4.22}/src/bt_cli/cli.py +14 -0
  8. {bt_cli-0.4.19 → bt_cli-0.4.22}/src/bt_cli/data/skills/entitle/SKILL.md +34 -0
  9. {bt_cli-0.4.19 → bt_cli-0.4.22}/src/bt_cli/entitle/client/base.py +3 -3
  10. {bt_cli-0.4.19 → bt_cli-0.4.22}/src/bt_cli/entitle/commands/users.py +7 -3
  11. {bt_cli-0.4.19 → bt_cli-0.4.22}/src/bt_cli/epmw/commands/quick.py +178 -0
  12. {bt_cli-0.4.19 → bt_cli-0.4.22}/src/bt_cli/pra/commands/vault.py +32 -4
  13. {bt_cli-0.4.19 → bt_cli-0.4.22}/.claude/skills/bt/SKILL.md +0 -0
  14. {bt_cli-0.4.19 → bt_cli-0.4.22}/.claude/skills/epmw/SKILL.md +0 -0
  15. {bt_cli-0.4.19 → bt_cli-0.4.22}/.claude/skills/pra/SKILL.md +0 -0
  16. {bt_cli-0.4.19 → bt_cli-0.4.22}/.claude/skills/pws/SKILL.md +0 -0
  17. {bt_cli-0.4.19 → bt_cli-0.4.22}/.env.example +0 -0
  18. {bt_cli-0.4.19 → bt_cli-0.4.22}/.github/workflows/ci.yml +0 -0
  19. {bt_cli-0.4.19 → bt_cli-0.4.22}/.gitignore +0 -0
  20. {bt_cli-0.4.19 → bt_cli-0.4.22}/README.md +0 -0
  21. {bt_cli-0.4.19 → bt_cli-0.4.22}/assets/cli-help.png +0 -0
  22. {bt_cli-0.4.19 → bt_cli-0.4.22}/assets/cli-output.png +0 -0
  23. {bt_cli-0.4.19 → bt_cli-0.4.22}/bt-cli.spec +0 -0
  24. {bt_cli-0.4.19 → bt_cli-0.4.22}/bt_entry.py +0 -0
  25. {bt_cli-0.4.19 → bt_cli-0.4.22}/scripts/bt_entry.py +0 -0
  26. {bt_cli-0.4.19 → bt_cli-0.4.22}/scripts/sync-package-data.sh +0 -0
  27. {bt_cli-0.4.19 → bt_cli-0.4.22}/src/bt_cli/commands/__init__.py +0 -0
  28. {bt_cli-0.4.19 → bt_cli-0.4.22}/src/bt_cli/commands/configure.py +0 -0
  29. {bt_cli-0.4.19 → bt_cli-0.4.22}/src/bt_cli/commands/learn.py +0 -0
  30. {bt_cli-0.4.19 → bt_cli-0.4.22}/src/bt_cli/commands/quick.py +0 -0
  31. {bt_cli-0.4.19 → bt_cli-0.4.22}/src/bt_cli/core/__init__.py +0 -0
  32. {bt_cli-0.4.19 → bt_cli-0.4.22}/src/bt_cli/core/auth.py +0 -0
  33. {bt_cli-0.4.19 → bt_cli-0.4.22}/src/bt_cli/core/client.py +0 -0
  34. {bt_cli-0.4.19 → bt_cli-0.4.22}/src/bt_cli/core/config.py +0 -0
  35. {bt_cli-0.4.19 → bt_cli-0.4.22}/src/bt_cli/core/config_file.py +0 -0
  36. {bt_cli-0.4.19 → bt_cli-0.4.22}/src/bt_cli/core/csv_utils.py +0 -0
  37. {bt_cli-0.4.19 → bt_cli-0.4.22}/src/bt_cli/core/errors.py +0 -0
  38. {bt_cli-0.4.19 → bt_cli-0.4.22}/src/bt_cli/core/output.py +0 -0
  39. {bt_cli-0.4.19 → bt_cli-0.4.22}/src/bt_cli/core/prompts.py +0 -0
  40. {bt_cli-0.4.19 → bt_cli-0.4.22}/src/bt_cli/core/rest_debug.py +0 -0
  41. {bt_cli-0.4.19 → bt_cli-0.4.22/src/bt_cli/data}/CLAUDE.md +0 -0
  42. {bt_cli-0.4.19 → bt_cli-0.4.22}/src/bt_cli/data/__init__.py +0 -0
  43. {bt_cli-0.4.19 → bt_cli-0.4.22}/src/bt_cli/data/skills/bt/SKILL.md +0 -0
  44. {bt_cli-0.4.19 → bt_cli-0.4.22}/src/bt_cli/data/skills/epmw/SKILL.md +0 -0
  45. {bt_cli-0.4.19 → bt_cli-0.4.22}/src/bt_cli/data/skills/pra/SKILL.md +0 -0
  46. {bt_cli-0.4.19 → bt_cli-0.4.22}/src/bt_cli/data/skills/pws/SKILL.md +0 -0
  47. {bt_cli-0.4.19 → bt_cli-0.4.22}/src/bt_cli/entitle/__init__.py +0 -0
  48. {bt_cli-0.4.19 → bt_cli-0.4.22}/src/bt_cli/entitle/client/__init__.py +0 -0
  49. {bt_cli-0.4.19 → bt_cli-0.4.22}/src/bt_cli/entitle/commands/__init__.py +0 -0
  50. {bt_cli-0.4.19 → bt_cli-0.4.22}/src/bt_cli/entitle/commands/accounts.py +0 -0
  51. {bt_cli-0.4.19 → bt_cli-0.4.22}/src/bt_cli/entitle/commands/applications.py +0 -0
  52. {bt_cli-0.4.19 → bt_cli-0.4.22}/src/bt_cli/entitle/commands/auth.py +0 -0
  53. {bt_cli-0.4.19 → bt_cli-0.4.22}/src/bt_cli/entitle/commands/bundles.py +0 -0
  54. {bt_cli-0.4.19 → bt_cli-0.4.22}/src/bt_cli/entitle/commands/integrations.py +0 -0
  55. {bt_cli-0.4.19 → bt_cli-0.4.22}/src/bt_cli/entitle/commands/permissions.py +0 -0
  56. {bt_cli-0.4.19 → bt_cli-0.4.22}/src/bt_cli/entitle/commands/policies.py +0 -0
  57. {bt_cli-0.4.19 → bt_cli-0.4.22}/src/bt_cli/entitle/commands/resources.py +0 -0
  58. {bt_cli-0.4.19 → bt_cli-0.4.22}/src/bt_cli/entitle/commands/roles.py +0 -0
  59. {bt_cli-0.4.19 → bt_cli-0.4.22}/src/bt_cli/entitle/commands/workflows.py +0 -0
  60. {bt_cli-0.4.19 → bt_cli-0.4.22}/src/bt_cli/entitle/models/__init__.py +0 -0
  61. {bt_cli-0.4.19 → bt_cli-0.4.22}/src/bt_cli/entitle/models/bundle.py +0 -0
  62. {bt_cli-0.4.19 → bt_cli-0.4.22}/src/bt_cli/entitle/models/common.py +0 -0
  63. {bt_cli-0.4.19 → bt_cli-0.4.22}/src/bt_cli/entitle/models/integration.py +0 -0
  64. {bt_cli-0.4.19 → bt_cli-0.4.22}/src/bt_cli/entitle/models/permission.py +0 -0
  65. {bt_cli-0.4.19 → bt_cli-0.4.22}/src/bt_cli/entitle/models/policy.py +0 -0
  66. {bt_cli-0.4.19 → bt_cli-0.4.22}/src/bt_cli/entitle/models/resource.py +0 -0
  67. {bt_cli-0.4.19 → bt_cli-0.4.22}/src/bt_cli/entitle/models/role.py +0 -0
  68. {bt_cli-0.4.19 → bt_cli-0.4.22}/src/bt_cli/entitle/models/user.py +0 -0
  69. {bt_cli-0.4.19 → bt_cli-0.4.22}/src/bt_cli/entitle/models/workflow.py +0 -0
  70. {bt_cli-0.4.19 → bt_cli-0.4.22}/src/bt_cli/epmw/__init__.py +0 -0
  71. {bt_cli-0.4.19 → bt_cli-0.4.22}/src/bt_cli/epmw/client/__init__.py +0 -0
  72. {bt_cli-0.4.19 → bt_cli-0.4.22}/src/bt_cli/epmw/client/base.py +0 -0
  73. {bt_cli-0.4.19 → bt_cli-0.4.22}/src/bt_cli/epmw/commands/__init__.py +0 -0
  74. {bt_cli-0.4.19 → bt_cli-0.4.22}/src/bt_cli/epmw/commands/audits.py +0 -0
  75. {bt_cli-0.4.19 → bt_cli-0.4.22}/src/bt_cli/epmw/commands/auth.py +0 -0
  76. {bt_cli-0.4.19 → bt_cli-0.4.22}/src/bt_cli/epmw/commands/computers.py +0 -0
  77. {bt_cli-0.4.19 → bt_cli-0.4.22}/src/bt_cli/epmw/commands/events.py +0 -0
  78. {bt_cli-0.4.19 → bt_cli-0.4.22}/src/bt_cli/epmw/commands/groups.py +0 -0
  79. {bt_cli-0.4.19 → bt_cli-0.4.22}/src/bt_cli/epmw/commands/policies.py +0 -0
  80. {bt_cli-0.4.19 → bt_cli-0.4.22}/src/bt_cli/epmw/commands/requests.py +0 -0
  81. {bt_cli-0.4.19 → bt_cli-0.4.22}/src/bt_cli/epmw/commands/roles.py +0 -0
  82. {bt_cli-0.4.19 → bt_cli-0.4.22}/src/bt_cli/epmw/commands/tasks.py +0 -0
  83. {bt_cli-0.4.19 → bt_cli-0.4.22}/src/bt_cli/epmw/commands/users.py +0 -0
  84. {bt_cli-0.4.19 → bt_cli-0.4.22}/src/bt_cli/epmw/models/__init__.py +0 -0
  85. {bt_cli-0.4.19 → bt_cli-0.4.22}/src/bt_cli/pra/__init__.py +0 -0
  86. {bt_cli-0.4.19 → bt_cli-0.4.22}/src/bt_cli/pra/client/__init__.py +0 -0
  87. {bt_cli-0.4.19 → bt_cli-0.4.22}/src/bt_cli/pra/client/base.py +0 -0
  88. {bt_cli-0.4.19 → bt_cli-0.4.22}/src/bt_cli/pra/commands/__init__.py +0 -0
  89. {bt_cli-0.4.19 → bt_cli-0.4.22}/src/bt_cli/pra/commands/auth.py +0 -0
  90. {bt_cli-0.4.19 → bt_cli-0.4.22}/src/bt_cli/pra/commands/import_export.py +0 -0
  91. {bt_cli-0.4.19 → bt_cli-0.4.22}/src/bt_cli/pra/commands/jump_clients.py +0 -0
  92. {bt_cli-0.4.19 → bt_cli-0.4.22}/src/bt_cli/pra/commands/jump_groups.py +0 -0
  93. {bt_cli-0.4.19 → bt_cli-0.4.22}/src/bt_cli/pra/commands/jump_items.py +0 -0
  94. {bt_cli-0.4.19 → bt_cli-0.4.22}/src/bt_cli/pra/commands/jumpoints.py +0 -0
  95. {bt_cli-0.4.19 → bt_cli-0.4.22}/src/bt_cli/pra/commands/policies.py +0 -0
  96. {bt_cli-0.4.19 → bt_cli-0.4.22}/src/bt_cli/pra/commands/quick.py +0 -0
  97. {bt_cli-0.4.19 → bt_cli-0.4.22}/src/bt_cli/pra/commands/teams.py +0 -0
  98. {bt_cli-0.4.19 → bt_cli-0.4.22}/src/bt_cli/pra/commands/users.py +0 -0
  99. {bt_cli-0.4.19 → bt_cli-0.4.22}/src/bt_cli/pra/models/__init__.py +0 -0
  100. {bt_cli-0.4.19 → bt_cli-0.4.22}/src/bt_cli/pra/models/common.py +0 -0
  101. {bt_cli-0.4.19 → bt_cli-0.4.22}/src/bt_cli/pra/models/jump_client.py +0 -0
  102. {bt_cli-0.4.19 → bt_cli-0.4.22}/src/bt_cli/pra/models/jump_group.py +0 -0
  103. {bt_cli-0.4.19 → bt_cli-0.4.22}/src/bt_cli/pra/models/jump_item.py +0 -0
  104. {bt_cli-0.4.19 → bt_cli-0.4.22}/src/bt_cli/pra/models/jumpoint.py +0 -0
  105. {bt_cli-0.4.19 → bt_cli-0.4.22}/src/bt_cli/pra/models/team.py +0 -0
  106. {bt_cli-0.4.19 → bt_cli-0.4.22}/src/bt_cli/pra/models/user.py +0 -0
  107. {bt_cli-0.4.19 → bt_cli-0.4.22}/src/bt_cli/pra/models/vault.py +0 -0
  108. {bt_cli-0.4.19 → bt_cli-0.4.22}/src/bt_cli/pws/__init__.py +0 -0
  109. {bt_cli-0.4.19 → bt_cli-0.4.22}/src/bt_cli/pws/client/__init__.py +0 -0
  110. {bt_cli-0.4.19 → bt_cli-0.4.22}/src/bt_cli/pws/client/base.py +0 -0
  111. {bt_cli-0.4.19 → bt_cli-0.4.22}/src/bt_cli/pws/client/beyondinsight.py +0 -0
  112. {bt_cli-0.4.19 → bt_cli-0.4.22}/src/bt_cli/pws/client/passwordsafe.py +0 -0
  113. {bt_cli-0.4.19 → bt_cli-0.4.22}/src/bt_cli/pws/commands/__init__.py +0 -0
  114. {bt_cli-0.4.19 → bt_cli-0.4.22}/src/bt_cli/pws/commands/accounts.py +0 -0
  115. {bt_cli-0.4.19 → bt_cli-0.4.22}/src/bt_cli/pws/commands/assets.py +0 -0
  116. {bt_cli-0.4.19 → bt_cli-0.4.22}/src/bt_cli/pws/commands/auth.py +0 -0
  117. {bt_cli-0.4.19 → bt_cli-0.4.22}/src/bt_cli/pws/commands/clouds.py +0 -0
  118. {bt_cli-0.4.19 → bt_cli-0.4.22}/src/bt_cli/pws/commands/config.py +0 -0
  119. {bt_cli-0.4.19 → bt_cli-0.4.22}/src/bt_cli/pws/commands/credentials.py +0 -0
  120. {bt_cli-0.4.19 → bt_cli-0.4.22}/src/bt_cli/pws/commands/databases.py +0 -0
  121. {bt_cli-0.4.19 → bt_cli-0.4.22}/src/bt_cli/pws/commands/directories.py +0 -0
  122. {bt_cli-0.4.19 → bt_cli-0.4.22}/src/bt_cli/pws/commands/functional.py +0 -0
  123. {bt_cli-0.4.19 → bt_cli-0.4.22}/src/bt_cli/pws/commands/import_export.py +0 -0
  124. {bt_cli-0.4.19 → bt_cli-0.4.22}/src/bt_cli/pws/commands/platforms.py +0 -0
  125. {bt_cli-0.4.19 → bt_cli-0.4.22}/src/bt_cli/pws/commands/quick.py +0 -0
  126. {bt_cli-0.4.19 → bt_cli-0.4.22}/src/bt_cli/pws/commands/search.py +0 -0
  127. {bt_cli-0.4.19 → bt_cli-0.4.22}/src/bt_cli/pws/commands/secrets.py +0 -0
  128. {bt_cli-0.4.19 → bt_cli-0.4.22}/src/bt_cli/pws/commands/systems.py +0 -0
  129. {bt_cli-0.4.19 → bt_cli-0.4.22}/src/bt_cli/pws/commands/users.py +0 -0
  130. {bt_cli-0.4.19 → bt_cli-0.4.22}/src/bt_cli/pws/commands/workgroups.py +0 -0
  131. {bt_cli-0.4.19 → bt_cli-0.4.22}/src/bt_cli/pws/config.py +0 -0
  132. {bt_cli-0.4.19 → bt_cli-0.4.22}/src/bt_cli/pws/models/__init__.py +0 -0
  133. {bt_cli-0.4.19 → bt_cli-0.4.22}/src/bt_cli/pws/models/account.py +0 -0
  134. {bt_cli-0.4.19 → bt_cli-0.4.22}/src/bt_cli/pws/models/asset.py +0 -0
  135. {bt_cli-0.4.19 → bt_cli-0.4.22}/src/bt_cli/pws/models/common.py +0 -0
  136. {bt_cli-0.4.19 → bt_cli-0.4.22}/src/bt_cli/pws/models/system.py +0 -0
  137. {bt_cli-0.4.19 → bt_cli-0.4.22}/tests/__init__.py +0 -0
  138. {bt_cli-0.4.19 → bt_cli-0.4.22}/tests/conftest.py +0 -0
  139. {bt_cli-0.4.19 → bt_cli-0.4.22}/tests/core/__init__.py +0 -0
  140. {bt_cli-0.4.19 → bt_cli-0.4.22}/tests/core/test_auth.py +0 -0
  141. {bt_cli-0.4.19 → bt_cli-0.4.22}/tests/core/test_config.py +0 -0
  142. {bt_cli-0.4.19 → bt_cli-0.4.22}/tests/core/test_errors.py +0 -0
  143. {bt_cli-0.4.19 → bt_cli-0.4.22}/tests/core/test_rest_debug.py +0 -0
  144. {bt_cli-0.4.19 → bt_cli-0.4.22}/tests/entitle/__init__.py +0 -0
  145. {bt_cli-0.4.19 → bt_cli-0.4.22}/tests/entitle/test_client.py +0 -0
  146. {bt_cli-0.4.19 → bt_cli-0.4.22}/tests/entitle/test_commands.py +0 -0
  147. {bt_cli-0.4.19 → bt_cli-0.4.22}/tests/entitle-smoke-test.sh +0 -0
  148. {bt_cli-0.4.19 → bt_cli-0.4.22}/tests/epmw/__init__.py +0 -0
  149. {bt_cli-0.4.19 → bt_cli-0.4.22}/tests/epmw/test_client.py +0 -0
  150. {bt_cli-0.4.19 → bt_cli-0.4.22}/tests/epmw/test_commands.py +0 -0
  151. {bt_cli-0.4.19 → bt_cli-0.4.22}/tests/epmw-quick-test-plan.md +0 -0
  152. {bt_cli-0.4.19 → bt_cli-0.4.22}/tests/fixtures/__init__.py +0 -0
  153. {bt_cli-0.4.19 → bt_cli-0.4.22}/tests/fixtures/responses.py +0 -0
  154. {bt_cli-0.4.19 → bt_cli-0.4.22}/tests/integration/__init__.py +0 -0
  155. {bt_cli-0.4.19 → bt_cli-0.4.22}/tests/integration/conftest.py +0 -0
  156. {bt_cli-0.4.19 → bt_cli-0.4.22}/tests/integration/helpers.py +0 -0
  157. {bt_cli-0.4.19 → bt_cli-0.4.22}/tests/integration/test_entitle_integration.py +0 -0
  158. {bt_cli-0.4.19 → bt_cli-0.4.22}/tests/integration/test_epmw_integration.py +0 -0
  159. {bt_cli-0.4.19 → bt_cli-0.4.22}/tests/integration/test_epmw_lifecycle.py +0 -0
  160. {bt_cli-0.4.19 → bt_cli-0.4.22}/tests/integration/test_pra_integration.py +0 -0
  161. {bt_cli-0.4.19 → bt_cli-0.4.22}/tests/integration/test_pra_lifecycle.py +0 -0
  162. {bt_cli-0.4.19 → bt_cli-0.4.22}/tests/integration/test_pws_integration.py +0 -0
  163. {bt_cli-0.4.19 → bt_cli-0.4.22}/tests/integration/test_pws_lifecycle.py +0 -0
  164. {bt_cli-0.4.19 → bt_cli-0.4.22}/tests/pra/__init__.py +0 -0
  165. {bt_cli-0.4.19 → bt_cli-0.4.22}/tests/pra/test_client.py +0 -0
  166. {bt_cli-0.4.19 → bt_cli-0.4.22}/tests/pra/test_commands.py +0 -0
  167. {bt_cli-0.4.19 → bt_cli-0.4.22}/tests/pra-smoke-test.sh +0 -0
  168. {bt_cli-0.4.19 → bt_cli-0.4.22}/tests/pra-test-plan.md +0 -0
  169. {bt_cli-0.4.19 → bt_cli-0.4.22}/tests/pws/__init__.py +0 -0
  170. {bt_cli-0.4.19 → bt_cli-0.4.22}/tests/pws/test_client.py +0 -0
  171. {bt_cli-0.4.19 → bt_cli-0.4.22}/tests/pws/test_commands.py +0 -0
  172. {bt_cli-0.4.19 → bt_cli-0.4.22}/tests/pws-quick-test-plan.md +0 -0
  173. {bt_cli-0.4.19 → bt_cli-0.4.22}/tests/pws-smoke-test.sh +0 -0
@@ -14,6 +14,40 @@ description: Entitle commands for JIT access, bundles, workflows, and permission
14
14
 
15
15
  List affected resources first, then ask for explicit confirmation.
16
16
 
17
+ ## Performance Tips
18
+
19
+ **ALWAYS use server-side filters** - never download all data and filter locally.
20
+
21
+ ```bash
22
+ # ✓ FAST - Server-side filtering
23
+ bt entitle permissions list --resource <resource_id>
24
+ bt entitle permissions list --user <user_id>
25
+ bt entitle permissions list --integration <integration_id>
26
+
27
+ # ✗ SLOW - Downloads ALL 23k+ permissions, filters locally
28
+ bt entitle permissions list | jq 'select(...)'
29
+ bt entitle permissions list -o json | grep "something"
30
+ ```
31
+
32
+ **Available filters for permissions list:**
33
+
34
+ | Flag | Purpose |
35
+ |------|---------|
36
+ | `--resource -r` | Filter by resource ID |
37
+ | `--user -u` | Filter by user ID |
38
+ | `--integration -i` | Filter by integration ID |
39
+
40
+ **Dataset size warning:** Entitle can have 20,000+ permissions. Unfiltered queries will be very slow.
41
+
42
+ **Workflow - Check standing access for a resource:**
43
+ ```bash
44
+ # 1. Find resource ID
45
+ bt entitle resources list --integration <integration_id> | grep -i "admin"
46
+
47
+ # 2. Query permissions with resource filter (fast)
48
+ bt entitle permissions list --resource <resource_id> -o json | jq -r '.[] | "\(.actor.name) | \(.actor.email)"'
49
+ ```
50
+
17
51
  ## Integrations & Resources
18
52
 
19
53
  ```bash
@@ -236,6 +236,9 @@ jobs:
236
236
  (github.event_name == 'push' && startsWith(github.ref, 'refs/tags/v')) ||
237
237
  (github.event_name == 'workflow_dispatch' && inputs.skip_pypi == false)
238
238
 
239
+ permissions:
240
+ id-token: write # Required for trusted publishing to PyPI
241
+
239
242
  environment:
240
243
  name: pypi
241
244
  url: https://pypi.org/project/bt-cli/
@@ -1,6 +1,6 @@
1
1
  # BT-CLI
2
2
 
3
- BeyondTrust Platform CLI for Password Safe, Entitle, PRA, and EPM Windows.
3
+ BeyondTrust Platform CLI for Password Safe, Entitle, PRA, and EPM Windows. **Version: 0.4.22**
4
4
 
5
5
  ## Setup
6
6
 
@@ -35,6 +35,9 @@ Use these slash commands for detailed product guidance:
35
35
  ## Common Patterns
36
36
 
37
37
  ```bash
38
+ # Version info
39
+ bt version # or bt --version or bt -V
40
+
38
41
  # All list commands support JSON output
39
42
  bt pws systems list -o json
40
43
  bt pra jump-items shell list -o json
@@ -61,6 +64,9 @@ PASSWORD=$(bt pws quick checkout -s server -a admin --raw)
61
64
  - PRA K8s tunnels: Require Linux jumpoint
62
65
  - PWS assets: Must create via workgroup endpoint
63
66
  - ECM integration: PWS system name must match PRA jump item name
67
+ - Entitle permissions: Use server-side filters (`--resource`, `--user`, `--integration`) - dataset can be 20k+ records
68
+ - Entitle users: Use `--all` flag only when needed - default fetches first page only
69
+ - Windows builds: Rich must be pinned to `<14.0.0` (Rich 14 has PyInstaller unicode issues)
64
70
 
65
71
  ## Functional vs Managed Accounts
66
72
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: bt-cli
3
- Version: 0.4.19
3
+ Version: 0.4.22
4
4
  Summary: BeyondTrust Platform CLI (unofficial) - Password Safe, Entitle, PRA, EPM
5
5
  Author-email: Dave Grendysz <dgrendysz@beyondtrust.com>
6
6
  License: MIT
@@ -23,7 +23,7 @@ Requires-Dist: httpx>=0.27.0
23
23
  Requires-Dist: pydantic>=2.0.0
24
24
  Requires-Dist: python-dotenv>=1.0.0
25
25
  Requires-Dist: pyyaml>=6.0.0
26
- Requires-Dist: rich<15.0.0,>=13.7.0
26
+ Requires-Dist: rich<14.0.0,>=13.7.0
27
27
  Requires-Dist: shellingham>=1.5.0
28
28
  Requires-Dist: truststore>=0.8.0; python_version >= '3.10'
29
29
  Requires-Dist: typer<1.0.0,>=0.12.0
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "bt-cli"
7
- version = "0.4.19"
7
+ version = "0.4.22"
8
8
  description = "BeyondTrust Platform CLI (unofficial) - Password Safe, Entitle, PRA, EPM"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.9"
@@ -39,7 +39,7 @@ dependencies = [
39
39
  "typer>=0.12.0,<1.0.0",
40
40
  "httpx>=0.27.0",
41
41
  "pydantic>=2.0.0",
42
- "rich>=13.7.0,<15.0.0",
42
+ "rich>=13.7.0,<14.0.0", # Pin to 13.x - Rich 14 has PyInstaller issues
43
43
  "python-dotenv>=1.0.0",
44
44
  "pyyaml>=6.0.0",
45
45
  "shellingham>=1.5.0",
@@ -1,3 +1,3 @@
1
1
  """BeyondTrust Unified Admin CLI."""
2
2
 
3
- __version__ = "0.4.19"
3
+ __version__ = "0.4.21"
@@ -139,6 +139,13 @@ except Exception:
139
139
  pass # Quick module not ready yet
140
140
 
141
141
 
142
+ def _version_callback(value: bool) -> None:
143
+ """Print version and exit."""
144
+ if value:
145
+ print(f"bt-cli version {__version__}")
146
+ raise typer.Exit()
147
+
148
+
142
149
  @app.callback()
143
150
  def main_callback(
144
151
  profile: Optional[str] = typer.Option(
@@ -153,6 +160,13 @@ def main_callback(
153
160
  help="Show REST API calls (method, URL, headers, body)",
154
161
  envvar="BT_SHOW_REST",
155
162
  ),
163
+ version: bool = typer.Option(
164
+ False,
165
+ "--version", "-V",
166
+ help="Show version and exit",
167
+ callback=_version_callback,
168
+ is_eager=True,
169
+ ),
156
170
  ) -> None:
157
171
  """BeyondTrust Platform CLI.
158
172
 
@@ -14,6 +14,40 @@ description: Entitle commands for JIT access, bundles, workflows, and permission
14
14
 
15
15
  List affected resources first, then ask for explicit confirmation.
16
16
 
17
+ ## Performance Tips
18
+
19
+ **ALWAYS use server-side filters** - never download all data and filter locally.
20
+
21
+ ```bash
22
+ # ✓ FAST - Server-side filtering
23
+ bt entitle permissions list --resource <resource_id>
24
+ bt entitle permissions list --user <user_id>
25
+ bt entitle permissions list --integration <integration_id>
26
+
27
+ # ✗ SLOW - Downloads ALL 23k+ permissions, filters locally
28
+ bt entitle permissions list | jq 'select(...)'
29
+ bt entitle permissions list -o json | grep "something"
30
+ ```
31
+
32
+ **Available filters for permissions list:**
33
+
34
+ | Flag | Purpose |
35
+ |------|---------|
36
+ | `--resource -r` | Filter by resource ID |
37
+ | `--user -u` | Filter by user ID |
38
+ | `--integration -i` | Filter by integration ID |
39
+
40
+ **Dataset size warning:** Entitle can have 20,000+ permissions. Unfiltered queries will be very slow.
41
+
42
+ **Workflow - Check standing access for a resource:**
43
+ ```bash
44
+ # 1. Find resource ID
45
+ bt entitle resources list --integration <integration_id> | grep -i "admin"
46
+
47
+ # 2. Query permissions with resource filter (fast)
48
+ bt entitle permissions list --resource <resource_id> -o json | jq -r '.[] | "\(.actor.name) | \(.actor.email)"'
49
+ ```
50
+
17
51
  ## Integrations & Resources
18
52
 
19
53
  ```bash
@@ -351,10 +351,10 @@ class EntitleClient:
351
351
  # =========================================================================
352
352
 
353
353
  def list_users(
354
- self, search: Optional[str] = None, limit: int = 100
354
+ self, search: Optional[str] = None, limit: int = 100, max_pages: Optional[int] = None
355
355
  ) -> list[dict[str, Any]]:
356
- """List all users."""
357
- return self.paginate("/users", {"search": search}, page_size=limit)
356
+ """List users with optional pagination limit."""
357
+ return self.paginate("/users", {"search": search}, page_size=limit, max_pages=max_pages)
358
358
 
359
359
  def get_user(self, user_id: str) -> dict[str, Any]:
360
360
  """Get a user by ID."""
@@ -14,7 +14,8 @@ app = typer.Typer(no_args_is_help=True, help="Manage users")
14
14
  @app.command("list")
15
15
  def list_users(
16
16
  search: Optional[str] = typer.Option(None, "--search", "-s", help="Search by email or name"),
17
- limit: int = typer.Option(100, "--limit", "-l", help="Maximum results to return"),
17
+ limit: int = typer.Option(50, "--limit", "-l", help="Results per page"),
18
+ fetch_all: bool = typer.Option(False, "--all", help="Fetch all results (may be slow for large datasets)"),
18
19
  output: str = typer.Option("table", "--output", "-o", help="Output format: table, json"),
19
20
  ) -> None:
20
21
  """List users in Entitle.
@@ -22,11 +23,14 @@ def list_users(
22
23
  Examples:
23
24
  bt entitle users list
24
25
  bt entitle users list -s "john"
25
- bt entitle users list -l 50 -o json
26
+ bt entitle users list -l 100 --all
27
+ bt entitle users list -o json
26
28
  """
27
29
  try:
28
30
  with get_client() as client:
29
- data = client.list_users(search=search, limit=limit)
31
+ # Only fetch first page by default, use --all for everything
32
+ max_pages = None if fetch_all else 1
33
+ data = client.list_users(search=search, limit=limit, max_pages=max_pages)
30
34
 
31
35
  if output == "json":
32
36
  print_json(data)
@@ -10,6 +10,7 @@ from rich.console import Console
10
10
  from rich.table import Table
11
11
 
12
12
  from ...core.output import print_api_error, print_error, print_warning, print_success
13
+ from ...core.prompts import prompt_from_list
13
14
 
14
15
  app = typer.Typer(no_args_is_help=True, help="Quick commands - common multi-step operations in one command")
15
16
  console = Console()
@@ -346,3 +347,180 @@ def group_status(
346
347
  except Exception as e:
347
348
  print_api_error(e, "quick status")
348
349
  raise typer.Exit(1)
350
+
351
+
352
+ @app.command("move-computer")
353
+ def move_computer(
354
+ computer: Optional[str] = typer.Option(None, "--computer", "-c", help="Computer name or ID (partial match for name)"),
355
+ group: Optional[str] = typer.Option(None, "--group", "-g", help="Target group name or ID"),
356
+ force: bool = typer.Option(False, "--force", "-f", help="Skip confirmation"),
357
+ output: str = typer.Option("table", "--output", "-o", help="Output format: table or json"),
358
+ ) -> None:
359
+ """Move a computer to a different group.
360
+
361
+ Interactive workflow to change a computer's group membership.
362
+ Lists computers (optionally filtered), then lists available groups,
363
+ and moves the selected computer to the selected group.
364
+
365
+ Examples:
366
+ bt epmw quick move-computer # Interactive selection
367
+ bt epmw quick move-computer -c "CorpWS01" # Filter by computer name
368
+ bt epmw quick move-computer -c "CorpWS01" -g "Servers" # Specify both
369
+ bt epmw quick move-computer -c "CorpWS01" -g "Servers" -f # Skip confirmation
370
+ """
371
+ from ..client import get_client
372
+
373
+ try:
374
+ client = get_client()
375
+
376
+ # Step 1: Get and filter computers
377
+ console.print("[dim]Fetching computers...[/dim]")
378
+ computers = client.list_computers()
379
+
380
+ if not computers:
381
+ print_error("No computers found")
382
+ raise typer.Exit(1)
383
+
384
+ # Filter by computer name/ID if provided
385
+ if computer:
386
+ computer_lower = computer.lower()
387
+ filtered = [
388
+ c for c in computers
389
+ if computer_lower in (c.get("host") or "").lower()
390
+ or computer_lower in (c.get("id") or "").lower()
391
+ ]
392
+ if not filtered:
393
+ print_error(f"No computers matching '{computer}'")
394
+ raise typer.Exit(1)
395
+ computers = filtered
396
+
397
+ # Show computers and select one
398
+ if len(computers) == 1:
399
+ selected_computer = computers[0]
400
+ console.print(f"[dim]Found computer: {selected_computer.get('host')}[/dim]")
401
+ else:
402
+ # Show list and prompt for selection
403
+ table = Table(title="Available Computers")
404
+ table.add_column("#", style="dim", justify="right")
405
+ table.add_column("Host", style="cyan")
406
+ table.add_column("Domain", style="yellow")
407
+ table.add_column("Current Group", style="green")
408
+ table.add_column("Status", style="magenta")
409
+
410
+ for idx, comp in enumerate(computers, 1):
411
+ table.add_row(
412
+ str(idx),
413
+ comp.get("host", ""),
414
+ comp.get("domain", "") or "-",
415
+ comp.get("groupName", "") or "-",
416
+ comp.get("connectionStatus", ""),
417
+ )
418
+ console.print(table)
419
+
420
+ selection = typer.prompt("Select computer number", type=int)
421
+ if selection < 1 or selection > len(computers):
422
+ print_error(f"Invalid selection. Choose 1-{len(computers)}")
423
+ raise typer.Exit(1)
424
+ selected_computer = computers[selection - 1]
425
+
426
+ computer_id = selected_computer.get("id")
427
+ computer_host = selected_computer.get("host")
428
+ current_group = selected_computer.get("groupName") or "(No Group)"
429
+
430
+ # Step 2: Get and filter groups
431
+ console.print("[dim]Fetching groups...[/dim]")
432
+ groups = client.list_groups()
433
+
434
+ if not groups:
435
+ print_error("No groups found")
436
+ raise typer.Exit(1)
437
+
438
+ # Filter by group name/ID if provided
439
+ if group:
440
+ group_lower = group.lower()
441
+ filtered = [
442
+ g for g in groups
443
+ if group_lower in (g.get("name") or "").lower()
444
+ or group_lower in (g.get("id") or "").lower()
445
+ ]
446
+ if not filtered:
447
+ print_error(f"No groups matching '{group}'")
448
+ raise typer.Exit(1)
449
+ groups = filtered
450
+
451
+ # Show groups and select one
452
+ if len(groups) == 1:
453
+ selected_group = groups[0]
454
+ console.print(f"[dim]Target group: {selected_group.get('name')}[/dim]")
455
+ else:
456
+ # Show list and prompt for selection
457
+ table = Table(title="Available Groups")
458
+ table.add_column("#", style="dim", justify="right")
459
+ table.add_column("Name", style="cyan")
460
+ table.add_column("Computers", style="green", justify="right")
461
+ table.add_column("Policy", style="yellow")
462
+
463
+ for idx, grp in enumerate(groups, 1):
464
+ table.add_row(
465
+ str(idx),
466
+ grp.get("name", ""),
467
+ str(grp.get("computerCount", 0)),
468
+ grp.get("policyName", "") or "-",
469
+ )
470
+ console.print(table)
471
+
472
+ selection = typer.prompt("Select target group number", type=int)
473
+ if selection < 1 or selection > len(groups):
474
+ print_error(f"Invalid selection. Choose 1-{len(groups)}")
475
+ raise typer.Exit(1)
476
+ selected_group = groups[selection - 1]
477
+
478
+ group_id = selected_group.get("id")
479
+ group_name = selected_group.get("name")
480
+
481
+ # Check if already in target group
482
+ if current_group == group_name:
483
+ console.print(f"[yellow]Computer '{computer_host}' is already in group '{group_name}'[/yellow]")
484
+ raise typer.Exit(0)
485
+
486
+ # Step 3: Confirm and move
487
+ if not force:
488
+ console.print()
489
+ console.print(f"[bold]Move computer:[/bold] {computer_host}")
490
+ console.print(f"[bold]From group:[/bold] {current_group}")
491
+ console.print(f"[bold]To group:[/bold] {group_name}")
492
+ console.print()
493
+ confirm = typer.confirm("Proceed with move?")
494
+ if not confirm:
495
+ console.print("[yellow]Cancelled.[/yellow]")
496
+ raise typer.Exit(0)
497
+
498
+ # Perform the move
499
+ console.print("[dim]Moving computer to group...[/dim]")
500
+ client.assign_computers_to_group(group_id, [computer_id])
501
+
502
+ if output == "json":
503
+ result = {
504
+ "computer": {
505
+ "id": computer_id,
506
+ "host": computer_host,
507
+ },
508
+ "previousGroup": current_group,
509
+ "newGroup": group_name,
510
+ "success": True,
511
+ }
512
+ console.print_json(json.dumps(result, default=str))
513
+ else:
514
+ print_success(f"Moved '{computer_host}' from '{current_group}' to '{group_name}'")
515
+
516
+ except httpx.HTTPStatusError as e:
517
+ print_api_error(e, "quick move-computer")
518
+ raise typer.Exit(1)
519
+ except httpx.RequestError as e:
520
+ print_api_error(e, "quick move-computer")
521
+ raise typer.Exit(1)
522
+ except typer.Exit:
523
+ raise
524
+ except Exception as e:
525
+ print_api_error(e, "quick move-computer")
526
+ raise typer.Exit(1)
@@ -137,13 +137,16 @@ def create_vault_account(
137
137
  name: str = typer.Option(..., "--name", "-n", help="Account name"),
138
138
  account_type: str = typer.Option(
139
139
  ..., "--type", "-t",
140
- help="Account type: username_password, ssh, or ssh_ca"
140
+ help="Account type: username_password, ssh, ssh_ca, or token"
141
141
  ),
142
142
  username: Optional[str] = typer.Option(None, "--username", "-u", help="Username (for username_password)"),
143
143
  password: Optional[str] = typer.Option(None, "--password", "-p", help="Password (for username_password)"),
144
+ token: Optional[str] = typer.Option(None, "--token", help="Token value (for token type)"),
145
+ token_file: Optional[str] = typer.Option(None, "--token-file", help="Path to file containing token"),
144
146
  private_key: Optional[str] = typer.Option(None, "--private-key", help="SSH private key (for ssh type)"),
145
147
  private_key_file: Optional[str] = typer.Option(None, "--private-key-file", help="Path to SSH private key file"),
146
148
  description: Optional[str] = typer.Option(None, "--description", "-d", help="Account description"),
149
+ account_group_id: Optional[int] = typer.Option(None, "--group-id", "-g", help="Vault Account Group ID"),
147
150
  personal: bool = typer.Option(False, "--personal", help="Mark as personal account"),
148
151
  output: OutputFormat = typer.Option(OutputFormat.JSON, "--output", "-o"),
149
152
  ):
@@ -158,20 +161,29 @@ def create_vault_account(
158
161
 
159
162
  # Create SSH CA account (for certificate-based auth)
160
163
  bt pra vault accounts create -n "ssh-ca" -t ssh_ca --private-key-file /path/to/ca_key
164
+
165
+ # Create token account (API keys, bearer tokens, etc.)
166
+ bt pra vault accounts create -n "api-key" -t token --token "sk-abc123..."
167
+
168
+ # Create token account from file
169
+ bt pra vault accounts create -n "service-token" -t token --token-file /path/to/token.txt
161
170
  """
162
171
  from bt_cli.pra.client import get_client
163
172
  from pathlib import Path
164
173
 
165
- # Validate account type
166
- valid_types = ["username_password", "ssh", "ssh_ca"]
174
+ # Validate account type (token maps to opaque_token in API)
175
+ valid_types = ["username_password", "ssh", "ssh_ca", "token"]
167
176
  if account_type not in valid_types:
168
177
  print_error(f"Invalid account type '{account_type}'. Must be one of: {', '.join(valid_types)}")
169
178
  raise typer.Exit(1)
170
179
 
180
+ # Map 'token' to API type 'opaque_token'
181
+ api_type = "opaque_token" if account_type == "token" else account_type
182
+
171
183
  # Build account data
172
184
  data = {
173
185
  "name": name,
174
- "type": account_type,
186
+ "type": api_type,
175
187
  }
176
188
 
177
189
  if username:
@@ -180,9 +192,21 @@ def create_vault_account(
180
192
  data["password"] = password
181
193
  if description:
182
194
  data["description"] = description
195
+ if account_group_id:
196
+ data["account_group_id"] = account_group_id
183
197
  if personal:
184
198
  data["personal"] = True
185
199
 
200
+ # Handle token (from file or direct)
201
+ if token_file:
202
+ token_path = Path(token_file).expanduser()
203
+ if not token_path.exists():
204
+ print_error(f"Token file not found: {token_file}")
205
+ raise typer.Exit(1)
206
+ data["token"] = token_path.read_text().strip()
207
+ elif token:
208
+ data["token"] = token
209
+
186
210
  # Handle private key (from file or direct)
187
211
  if private_key_file:
188
212
  key_path = Path(private_key_file).expanduser()
@@ -202,6 +226,10 @@ def create_vault_account(
202
226
  if not data.get("private_key"):
203
227
  print_error(f"Private key is required for {account_type} type (use --private-key or --private-key-file)")
204
228
  raise typer.Exit(1)
229
+ elif account_type == "token":
230
+ if not data.get("token"):
231
+ print_error("Token is required for token type (use --token or --token-file)")
232
+ raise typer.Exit(1)
205
233
 
206
234
  try:
207
235
  client = get_client()
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes