bt-cli 0.4.26__tar.gz → 0.4.28__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 (174) hide show
  1. {bt_cli-0.4.26 → bt_cli-0.4.28}/CLAUDE.md +1 -1
  2. {bt_cli-0.4.26 → bt_cli-0.4.28}/PKG-INFO +1 -1
  3. {bt_cli-0.4.26 → bt_cli-0.4.28}/pyproject.toml +1 -1
  4. {bt_cli-0.4.26 → bt_cli-0.4.28}/src/bt_cli/__init__.py +1 -1
  5. {bt_cli-0.4.26 → bt_cli-0.4.28}/src/bt_cli/pra/client/base.py +12 -4
  6. {bt_cli-0.4.26 → bt_cli-0.4.28}/src/bt_cli/pra/commands/jump_items.py +128 -22
  7. {bt_cli-0.4.26 → bt_cli-0.4.28}/src/bt_cli/pws/client/beyondinsight.py +43 -0
  8. {bt_cli-0.4.26 → bt_cli-0.4.28}/src/bt_cli/pws/commands/attributes.py +90 -0
  9. {bt_cli-0.4.26 → bt_cli-0.4.28}/.claude/skills/bt/SKILL.md +0 -0
  10. {bt_cli-0.4.26 → bt_cli-0.4.28}/.claude/skills/entitle/SKILL.md +0 -0
  11. {bt_cli-0.4.26 → bt_cli-0.4.28}/.claude/skills/epmw/SKILL.md +0 -0
  12. {bt_cli-0.4.26 → bt_cli-0.4.28}/.claude/skills/pra/SKILL.md +0 -0
  13. {bt_cli-0.4.26 → bt_cli-0.4.28}/.claude/skills/pws/SKILL.md +0 -0
  14. {bt_cli-0.4.26 → bt_cli-0.4.28}/.env.example +0 -0
  15. {bt_cli-0.4.26 → bt_cli-0.4.28}/.github/workflows/ci.yml +0 -0
  16. {bt_cli-0.4.26 → bt_cli-0.4.28}/.github/workflows/release.yml +0 -0
  17. {bt_cli-0.4.26 → bt_cli-0.4.28}/.gitignore +0 -0
  18. {bt_cli-0.4.26 → bt_cli-0.4.28}/README.md +0 -0
  19. {bt_cli-0.4.26 → bt_cli-0.4.28}/assets/cli-help.png +0 -0
  20. {bt_cli-0.4.26 → bt_cli-0.4.28}/assets/cli-output.png +0 -0
  21. {bt_cli-0.4.26 → bt_cli-0.4.28}/bt-cli.spec +0 -0
  22. {bt_cli-0.4.26 → bt_cli-0.4.28}/bt_entry.py +0 -0
  23. {bt_cli-0.4.26 → bt_cli-0.4.28}/scripts/bt_entry.py +0 -0
  24. {bt_cli-0.4.26 → bt_cli-0.4.28}/scripts/sync-package-data.sh +0 -0
  25. {bt_cli-0.4.26 → bt_cli-0.4.28}/src/bt_cli/cli.py +0 -0
  26. {bt_cli-0.4.26 → bt_cli-0.4.28}/src/bt_cli/commands/__init__.py +0 -0
  27. {bt_cli-0.4.26 → bt_cli-0.4.28}/src/bt_cli/commands/configure.py +0 -0
  28. {bt_cli-0.4.26 → bt_cli-0.4.28}/src/bt_cli/commands/learn.py +0 -0
  29. {bt_cli-0.4.26 → bt_cli-0.4.28}/src/bt_cli/commands/quick.py +0 -0
  30. {bt_cli-0.4.26 → bt_cli-0.4.28}/src/bt_cli/core/__init__.py +0 -0
  31. {bt_cli-0.4.26 → bt_cli-0.4.28}/src/bt_cli/core/auth.py +0 -0
  32. {bt_cli-0.4.26 → bt_cli-0.4.28}/src/bt_cli/core/client.py +0 -0
  33. {bt_cli-0.4.26 → bt_cli-0.4.28}/src/bt_cli/core/config.py +0 -0
  34. {bt_cli-0.4.26 → bt_cli-0.4.28}/src/bt_cli/core/config_file.py +0 -0
  35. {bt_cli-0.4.26 → bt_cli-0.4.28}/src/bt_cli/core/csv_utils.py +0 -0
  36. {bt_cli-0.4.26 → bt_cli-0.4.28}/src/bt_cli/core/errors.py +0 -0
  37. {bt_cli-0.4.26 → bt_cli-0.4.28}/src/bt_cli/core/output.py +0 -0
  38. {bt_cli-0.4.26 → bt_cli-0.4.28}/src/bt_cli/core/prompts.py +0 -0
  39. {bt_cli-0.4.26 → bt_cli-0.4.28}/src/bt_cli/core/rest_debug.py +0 -0
  40. {bt_cli-0.4.26 → bt_cli-0.4.28}/src/bt_cli/data/CLAUDE.md +0 -0
  41. {bt_cli-0.4.26 → bt_cli-0.4.28}/src/bt_cli/data/__init__.py +0 -0
  42. {bt_cli-0.4.26 → bt_cli-0.4.28}/src/bt_cli/data/skills/bt/SKILL.md +0 -0
  43. {bt_cli-0.4.26 → bt_cli-0.4.28}/src/bt_cli/data/skills/entitle/SKILL.md +0 -0
  44. {bt_cli-0.4.26 → bt_cli-0.4.28}/src/bt_cli/data/skills/epmw/SKILL.md +0 -0
  45. {bt_cli-0.4.26 → bt_cli-0.4.28}/src/bt_cli/data/skills/pra/SKILL.md +0 -0
  46. {bt_cli-0.4.26 → bt_cli-0.4.28}/src/bt_cli/data/skills/pws/SKILL.md +0 -0
  47. {bt_cli-0.4.26 → bt_cli-0.4.28}/src/bt_cli/entitle/__init__.py +0 -0
  48. {bt_cli-0.4.26 → bt_cli-0.4.28}/src/bt_cli/entitle/client/__init__.py +0 -0
  49. {bt_cli-0.4.26 → bt_cli-0.4.28}/src/bt_cli/entitle/client/base.py +0 -0
  50. {bt_cli-0.4.26 → bt_cli-0.4.28}/src/bt_cli/entitle/commands/__init__.py +0 -0
  51. {bt_cli-0.4.26 → bt_cli-0.4.28}/src/bt_cli/entitle/commands/accounts.py +0 -0
  52. {bt_cli-0.4.26 → bt_cli-0.4.28}/src/bt_cli/entitle/commands/applications.py +0 -0
  53. {bt_cli-0.4.26 → bt_cli-0.4.28}/src/bt_cli/entitle/commands/auth.py +0 -0
  54. {bt_cli-0.4.26 → bt_cli-0.4.28}/src/bt_cli/entitle/commands/bundles.py +0 -0
  55. {bt_cli-0.4.26 → bt_cli-0.4.28}/src/bt_cli/entitle/commands/integrations.py +0 -0
  56. {bt_cli-0.4.26 → bt_cli-0.4.28}/src/bt_cli/entitle/commands/permissions.py +0 -0
  57. {bt_cli-0.4.26 → bt_cli-0.4.28}/src/bt_cli/entitle/commands/policies.py +0 -0
  58. {bt_cli-0.4.26 → bt_cli-0.4.28}/src/bt_cli/entitle/commands/resources.py +0 -0
  59. {bt_cli-0.4.26 → bt_cli-0.4.28}/src/bt_cli/entitle/commands/roles.py +0 -0
  60. {bt_cli-0.4.26 → bt_cli-0.4.28}/src/bt_cli/entitle/commands/users.py +0 -0
  61. {bt_cli-0.4.26 → bt_cli-0.4.28}/src/bt_cli/entitle/commands/workflows.py +0 -0
  62. {bt_cli-0.4.26 → bt_cli-0.4.28}/src/bt_cli/entitle/models/__init__.py +0 -0
  63. {bt_cli-0.4.26 → bt_cli-0.4.28}/src/bt_cli/entitle/models/bundle.py +0 -0
  64. {bt_cli-0.4.26 → bt_cli-0.4.28}/src/bt_cli/entitle/models/common.py +0 -0
  65. {bt_cli-0.4.26 → bt_cli-0.4.28}/src/bt_cli/entitle/models/integration.py +0 -0
  66. {bt_cli-0.4.26 → bt_cli-0.4.28}/src/bt_cli/entitle/models/permission.py +0 -0
  67. {bt_cli-0.4.26 → bt_cli-0.4.28}/src/bt_cli/entitle/models/policy.py +0 -0
  68. {bt_cli-0.4.26 → bt_cli-0.4.28}/src/bt_cli/entitle/models/resource.py +0 -0
  69. {bt_cli-0.4.26 → bt_cli-0.4.28}/src/bt_cli/entitle/models/role.py +0 -0
  70. {bt_cli-0.4.26 → bt_cli-0.4.28}/src/bt_cli/entitle/models/user.py +0 -0
  71. {bt_cli-0.4.26 → bt_cli-0.4.28}/src/bt_cli/entitle/models/workflow.py +0 -0
  72. {bt_cli-0.4.26 → bt_cli-0.4.28}/src/bt_cli/epmw/__init__.py +0 -0
  73. {bt_cli-0.4.26 → bt_cli-0.4.28}/src/bt_cli/epmw/client/__init__.py +0 -0
  74. {bt_cli-0.4.26 → bt_cli-0.4.28}/src/bt_cli/epmw/client/base.py +0 -0
  75. {bt_cli-0.4.26 → bt_cli-0.4.28}/src/bt_cli/epmw/commands/__init__.py +0 -0
  76. {bt_cli-0.4.26 → bt_cli-0.4.28}/src/bt_cli/epmw/commands/audits.py +0 -0
  77. {bt_cli-0.4.26 → bt_cli-0.4.28}/src/bt_cli/epmw/commands/auth.py +0 -0
  78. {bt_cli-0.4.26 → bt_cli-0.4.28}/src/bt_cli/epmw/commands/computers.py +0 -0
  79. {bt_cli-0.4.26 → bt_cli-0.4.28}/src/bt_cli/epmw/commands/events.py +0 -0
  80. {bt_cli-0.4.26 → bt_cli-0.4.28}/src/bt_cli/epmw/commands/groups.py +0 -0
  81. {bt_cli-0.4.26 → bt_cli-0.4.28}/src/bt_cli/epmw/commands/policies.py +0 -0
  82. {bt_cli-0.4.26 → bt_cli-0.4.28}/src/bt_cli/epmw/commands/quick.py +0 -0
  83. {bt_cli-0.4.26 → bt_cli-0.4.28}/src/bt_cli/epmw/commands/requests.py +0 -0
  84. {bt_cli-0.4.26 → bt_cli-0.4.28}/src/bt_cli/epmw/commands/roles.py +0 -0
  85. {bt_cli-0.4.26 → bt_cli-0.4.28}/src/bt_cli/epmw/commands/tasks.py +0 -0
  86. {bt_cli-0.4.26 → bt_cli-0.4.28}/src/bt_cli/epmw/commands/users.py +0 -0
  87. {bt_cli-0.4.26 → bt_cli-0.4.28}/src/bt_cli/epmw/models/__init__.py +0 -0
  88. {bt_cli-0.4.26 → bt_cli-0.4.28}/src/bt_cli/pra/__init__.py +0 -0
  89. {bt_cli-0.4.26 → bt_cli-0.4.28}/src/bt_cli/pra/client/__init__.py +0 -0
  90. {bt_cli-0.4.26 → bt_cli-0.4.28}/src/bt_cli/pra/commands/__init__.py +0 -0
  91. {bt_cli-0.4.26 → bt_cli-0.4.28}/src/bt_cli/pra/commands/auth.py +0 -0
  92. {bt_cli-0.4.26 → bt_cli-0.4.28}/src/bt_cli/pra/commands/import_export.py +0 -0
  93. {bt_cli-0.4.26 → bt_cli-0.4.28}/src/bt_cli/pra/commands/jump_clients.py +0 -0
  94. {bt_cli-0.4.26 → bt_cli-0.4.28}/src/bt_cli/pra/commands/jump_groups.py +0 -0
  95. {bt_cli-0.4.26 → bt_cli-0.4.28}/src/bt_cli/pra/commands/jumpoints.py +0 -0
  96. {bt_cli-0.4.26 → bt_cli-0.4.28}/src/bt_cli/pra/commands/policies.py +0 -0
  97. {bt_cli-0.4.26 → bt_cli-0.4.28}/src/bt_cli/pra/commands/quick.py +0 -0
  98. {bt_cli-0.4.26 → bt_cli-0.4.28}/src/bt_cli/pra/commands/teams.py +0 -0
  99. {bt_cli-0.4.26 → bt_cli-0.4.28}/src/bt_cli/pra/commands/users.py +0 -0
  100. {bt_cli-0.4.26 → bt_cli-0.4.28}/src/bt_cli/pra/commands/vault.py +0 -0
  101. {bt_cli-0.4.26 → bt_cli-0.4.28}/src/bt_cli/pra/models/__init__.py +0 -0
  102. {bt_cli-0.4.26 → bt_cli-0.4.28}/src/bt_cli/pra/models/common.py +0 -0
  103. {bt_cli-0.4.26 → bt_cli-0.4.28}/src/bt_cli/pra/models/jump_client.py +0 -0
  104. {bt_cli-0.4.26 → bt_cli-0.4.28}/src/bt_cli/pra/models/jump_group.py +0 -0
  105. {bt_cli-0.4.26 → bt_cli-0.4.28}/src/bt_cli/pra/models/jump_item.py +0 -0
  106. {bt_cli-0.4.26 → bt_cli-0.4.28}/src/bt_cli/pra/models/jumpoint.py +0 -0
  107. {bt_cli-0.4.26 → bt_cli-0.4.28}/src/bt_cli/pra/models/team.py +0 -0
  108. {bt_cli-0.4.26 → bt_cli-0.4.28}/src/bt_cli/pra/models/user.py +0 -0
  109. {bt_cli-0.4.26 → bt_cli-0.4.28}/src/bt_cli/pra/models/vault.py +0 -0
  110. {bt_cli-0.4.26 → bt_cli-0.4.28}/src/bt_cli/pws/__init__.py +0 -0
  111. {bt_cli-0.4.26 → bt_cli-0.4.28}/src/bt_cli/pws/client/__init__.py +0 -0
  112. {bt_cli-0.4.26 → bt_cli-0.4.28}/src/bt_cli/pws/client/base.py +0 -0
  113. {bt_cli-0.4.26 → bt_cli-0.4.28}/src/bt_cli/pws/client/passwordsafe.py +0 -0
  114. {bt_cli-0.4.26 → bt_cli-0.4.28}/src/bt_cli/pws/commands/__init__.py +0 -0
  115. {bt_cli-0.4.26 → bt_cli-0.4.28}/src/bt_cli/pws/commands/accounts.py +0 -0
  116. {bt_cli-0.4.26 → bt_cli-0.4.28}/src/bt_cli/pws/commands/assets.py +0 -0
  117. {bt_cli-0.4.26 → bt_cli-0.4.28}/src/bt_cli/pws/commands/auth.py +0 -0
  118. {bt_cli-0.4.26 → bt_cli-0.4.28}/src/bt_cli/pws/commands/clouds.py +0 -0
  119. {bt_cli-0.4.26 → bt_cli-0.4.28}/src/bt_cli/pws/commands/config.py +0 -0
  120. {bt_cli-0.4.26 → bt_cli-0.4.28}/src/bt_cli/pws/commands/credentials.py +0 -0
  121. {bt_cli-0.4.26 → bt_cli-0.4.28}/src/bt_cli/pws/commands/databases.py +0 -0
  122. {bt_cli-0.4.26 → bt_cli-0.4.28}/src/bt_cli/pws/commands/directories.py +0 -0
  123. {bt_cli-0.4.26 → bt_cli-0.4.28}/src/bt_cli/pws/commands/functional.py +0 -0
  124. {bt_cli-0.4.26 → bt_cli-0.4.28}/src/bt_cli/pws/commands/import_export.py +0 -0
  125. {bt_cli-0.4.26 → bt_cli-0.4.28}/src/bt_cli/pws/commands/platforms.py +0 -0
  126. {bt_cli-0.4.26 → bt_cli-0.4.28}/src/bt_cli/pws/commands/quick.py +0 -0
  127. {bt_cli-0.4.26 → bt_cli-0.4.28}/src/bt_cli/pws/commands/search.py +0 -0
  128. {bt_cli-0.4.26 → bt_cli-0.4.28}/src/bt_cli/pws/commands/secrets.py +0 -0
  129. {bt_cli-0.4.26 → bt_cli-0.4.28}/src/bt_cli/pws/commands/systems.py +0 -0
  130. {bt_cli-0.4.26 → bt_cli-0.4.28}/src/bt_cli/pws/commands/users.py +0 -0
  131. {bt_cli-0.4.26 → bt_cli-0.4.28}/src/bt_cli/pws/commands/workgroups.py +0 -0
  132. {bt_cli-0.4.26 → bt_cli-0.4.28}/src/bt_cli/pws/config.py +0 -0
  133. {bt_cli-0.4.26 → bt_cli-0.4.28}/src/bt_cli/pws/models/__init__.py +0 -0
  134. {bt_cli-0.4.26 → bt_cli-0.4.28}/src/bt_cli/pws/models/account.py +0 -0
  135. {bt_cli-0.4.26 → bt_cli-0.4.28}/src/bt_cli/pws/models/asset.py +0 -0
  136. {bt_cli-0.4.26 → bt_cli-0.4.28}/src/bt_cli/pws/models/common.py +0 -0
  137. {bt_cli-0.4.26 → bt_cli-0.4.28}/src/bt_cli/pws/models/system.py +0 -0
  138. {bt_cli-0.4.26 → bt_cli-0.4.28}/tests/__init__.py +0 -0
  139. {bt_cli-0.4.26 → bt_cli-0.4.28}/tests/conftest.py +0 -0
  140. {bt_cli-0.4.26 → bt_cli-0.4.28}/tests/core/__init__.py +0 -0
  141. {bt_cli-0.4.26 → bt_cli-0.4.28}/tests/core/test_auth.py +0 -0
  142. {bt_cli-0.4.26 → bt_cli-0.4.28}/tests/core/test_config.py +0 -0
  143. {bt_cli-0.4.26 → bt_cli-0.4.28}/tests/core/test_errors.py +0 -0
  144. {bt_cli-0.4.26 → bt_cli-0.4.28}/tests/core/test_rest_debug.py +0 -0
  145. {bt_cli-0.4.26 → bt_cli-0.4.28}/tests/entitle/__init__.py +0 -0
  146. {bt_cli-0.4.26 → bt_cli-0.4.28}/tests/entitle/test_client.py +0 -0
  147. {bt_cli-0.4.26 → bt_cli-0.4.28}/tests/entitle/test_commands.py +0 -0
  148. {bt_cli-0.4.26 → bt_cli-0.4.28}/tests/entitle-smoke-test.sh +0 -0
  149. {bt_cli-0.4.26 → bt_cli-0.4.28}/tests/epmw/__init__.py +0 -0
  150. {bt_cli-0.4.26 → bt_cli-0.4.28}/tests/epmw/test_client.py +0 -0
  151. {bt_cli-0.4.26 → bt_cli-0.4.28}/tests/epmw/test_commands.py +0 -0
  152. {bt_cli-0.4.26 → bt_cli-0.4.28}/tests/epmw-quick-test-plan.md +0 -0
  153. {bt_cli-0.4.26 → bt_cli-0.4.28}/tests/fixtures/__init__.py +0 -0
  154. {bt_cli-0.4.26 → bt_cli-0.4.28}/tests/fixtures/responses.py +0 -0
  155. {bt_cli-0.4.26 → bt_cli-0.4.28}/tests/integration/__init__.py +0 -0
  156. {bt_cli-0.4.26 → bt_cli-0.4.28}/tests/integration/conftest.py +0 -0
  157. {bt_cli-0.4.26 → bt_cli-0.4.28}/tests/integration/helpers.py +0 -0
  158. {bt_cli-0.4.26 → bt_cli-0.4.28}/tests/integration/test_entitle_integration.py +0 -0
  159. {bt_cli-0.4.26 → bt_cli-0.4.28}/tests/integration/test_epmw_integration.py +0 -0
  160. {bt_cli-0.4.26 → bt_cli-0.4.28}/tests/integration/test_epmw_lifecycle.py +0 -0
  161. {bt_cli-0.4.26 → bt_cli-0.4.28}/tests/integration/test_pra_integration.py +0 -0
  162. {bt_cli-0.4.26 → bt_cli-0.4.28}/tests/integration/test_pra_lifecycle.py +0 -0
  163. {bt_cli-0.4.26 → bt_cli-0.4.28}/tests/integration/test_pws_integration.py +0 -0
  164. {bt_cli-0.4.26 → bt_cli-0.4.28}/tests/integration/test_pws_lifecycle.py +0 -0
  165. {bt_cli-0.4.26 → bt_cli-0.4.28}/tests/pra/__init__.py +0 -0
  166. {bt_cli-0.4.26 → bt_cli-0.4.28}/tests/pra/test_client.py +0 -0
  167. {bt_cli-0.4.26 → bt_cli-0.4.28}/tests/pra/test_commands.py +0 -0
  168. {bt_cli-0.4.26 → bt_cli-0.4.28}/tests/pra-smoke-test.sh +0 -0
  169. {bt_cli-0.4.26 → bt_cli-0.4.28}/tests/pra-test-plan.md +0 -0
  170. {bt_cli-0.4.26 → bt_cli-0.4.28}/tests/pws/__init__.py +0 -0
  171. {bt_cli-0.4.26 → bt_cli-0.4.28}/tests/pws/test_client.py +0 -0
  172. {bt_cli-0.4.26 → bt_cli-0.4.28}/tests/pws/test_commands.py +0 -0
  173. {bt_cli-0.4.26 → bt_cli-0.4.28}/tests/pws-quick-test-plan.md +0 -0
  174. {bt_cli-0.4.26 → bt_cli-0.4.28}/tests/pws-smoke-test.sh +0 -0
@@ -1,6 +1,6 @@
1
1
  # BT-CLI
2
2
 
3
- BeyondTrust Platform CLI for Password Safe, Entitle, PRA, and EPM Windows. **Version: 0.4.26**
3
+ BeyondTrust Platform CLI for Password Safe, Entitle, PRA, and EPM Windows. **Version: 0.4.28**
4
4
 
5
5
  ## Setup
6
6
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: bt-cli
3
- Version: 0.4.26
3
+ Version: 0.4.28
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
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "bt-cli"
7
- version = "0.4.26"
7
+ version = "0.4.28"
8
8
  description = "BeyondTrust Platform CLI (unofficial) - Password Safe, Entitle, PRA, EPM"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.9"
@@ -1,3 +1,3 @@
1
1
  """BeyondTrust Unified Admin CLI."""
2
2
 
3
- __version__ = "0.4.26"
3
+ __version__ = "0.4.28"
@@ -489,19 +489,27 @@ class PRAClient:
489
489
  name: Optional[str] = None,
490
490
  hostname: Optional[str] = None,
491
491
  ) -> List[Dict[str, Any]]:
492
- """List Protocol Tunnel Jump items (TCP, MSSQL, K8s) with optional filters."""
492
+ """List Protocol Tunnel Jump items (TCP, MSSQL, K8s) with optional filters.
493
+
494
+ Note: tunnel_type is filtered client-side as the API doesn't support it.
495
+ """
493
496
  params = {}
494
497
  if jump_group_id:
495
498
  params["jump_group_id"] = jump_group_id
496
- if tunnel_type:
497
- params["tunnel_type"] = tunnel_type
498
499
  if tag:
499
500
  params["tag"] = tag
500
501
  if name:
501
502
  params["name"] = name
502
503
  if hostname:
503
504
  params["hostname"] = hostname
504
- return self.get_paginated("/jump-item/protocol-tunnel-jump", params)
505
+
506
+ items = self.get_paginated("/jump-item/protocol-tunnel-jump", params)
507
+
508
+ # Filter tunnel_type client-side (API doesn't support this filter)
509
+ if tunnel_type:
510
+ items = [i for i in items if i.get("tunnel_type") == tunnel_type]
511
+
512
+ return items
505
513
 
506
514
  def get_protocol_tunnel(self, item_id: int) -> Dict[str, Any]:
507
515
  """Get a Protocol Tunnel Jump item."""
@@ -1,12 +1,29 @@
1
1
  """Jump Item commands (shell, RDP, VNC, web, tunnels)."""
2
2
 
3
- from typing import Optional
3
+ from fnmatch import fnmatch
4
+ from typing import List, Optional
4
5
 
5
6
  import httpx
6
7
  import typer
7
8
 
8
9
  from bt_cli.core.output import OutputFormat, print_table, print_json, print_error, print_success, print_api_error
9
10
 
11
+
12
+ def _filter_by_pattern(items: List[dict], pattern: str, field: str = "name") -> List[dict]:
13
+ """Filter items by fnmatch pattern on a field."""
14
+ return [item for item in items if fnmatch(item.get(field, "").lower(), pattern.lower())]
15
+
16
+
17
+ def _find_by_name(items: List[dict], name: str) -> Optional[dict]:
18
+ """Find a single item by exact name match (case-insensitive)."""
19
+ name_lower = name.lower()
20
+ matches = [item for item in items if item.get("name", "").lower() == name_lower]
21
+ if len(matches) == 1:
22
+ return matches[0]
23
+ elif len(matches) > 1:
24
+ raise ValueError(f"Multiple items found with name '{name}'. Use ID instead.")
25
+ return None
26
+
10
27
  app = typer.Typer(no_args_is_help=True)
11
28
 
12
29
  # Shell Jump subcommands
@@ -20,6 +37,7 @@ def list_shell_jumps(
20
37
  jumpoint_id: Optional[int] = typer.Option(None, "--jumpoint", "-j", help="Filter by Jumpoint"),
21
38
  tag: Optional[str] = typer.Option(None, "--tag", "-t", help="Filter by tag (exact match)"),
22
39
  name: Optional[str] = typer.Option(None, "--name", "-n", help="Filter by name (exact match)"),
40
+ name_pattern: Optional[str] = typer.Option(None, "--name-pattern", help="Filter by name pattern (e.g., 'prod-*')"),
23
41
  hostname: Optional[str] = typer.Option(None, "--hostname", help="Filter by hostname (exact match)"),
24
42
  output: OutputFormat = typer.Option(OutputFormat.TABLE, "--output", "-o"),
25
43
  ):
@@ -36,6 +54,10 @@ def list_shell_jumps(
36
54
  hostname=hostname,
37
55
  )
38
56
 
57
+ # Client-side pattern filtering
58
+ if name_pattern:
59
+ items = _filter_by_pattern(items, name_pattern)
60
+
39
61
  if output == OutputFormat.JSON:
40
62
  print_json(items)
41
63
  else:
@@ -206,27 +228,44 @@ def update_shell_jump(
206
228
 
207
229
  @shell_app.command("delete")
208
230
  def delete_shell_jump(
209
- item_id: int = typer.Argument(..., help="Shell Jump ID"),
231
+ item_id: Optional[int] = typer.Argument(None, help="Shell Jump ID"),
232
+ name: Optional[str] = typer.Option(None, "--name", "-n", help="Delete by name instead of ID"),
210
233
  force: bool = typer.Option(False, "--force", "-f", help="Skip confirmation"),
211
234
  dry_run: bool = typer.Option(False, "--dry-run", help="Show what would be deleted without deleting"),
212
235
  ):
213
- """Delete a Shell Jump item."""
236
+ """Delete a Shell Jump item by ID or name."""
214
237
  from bt_cli.pra.client import get_client
215
238
 
239
+ if not item_id and not name:
240
+ print_error("Either ITEM_ID or --name is required")
241
+ raise typer.Exit(1)
242
+
216
243
  try:
217
244
  client = get_client()
218
- # Fetch item details for display
219
- if dry_run:
245
+
246
+ # Look up by name if provided
247
+ if name and not item_id:
248
+ items = client.list_shell_jumps(name=name)
249
+ if not items:
250
+ print_error(f"No shell jump found with name '{name}'")
251
+ raise typer.Exit(1)
252
+ if len(items) > 1:
253
+ print_error(f"Multiple shell jumps found with name '{name}'. Use ID instead.")
254
+ raise typer.Exit(1)
255
+ item = items[0]
256
+ item_id = item.get("id")
257
+ else:
220
258
  item = client.get_shell_jump(item_id)
259
+
260
+ if dry_run:
221
261
  print_success(f"[DRY-RUN] Would delete shell jump: {item.get('name')} (ID: {item_id})")
222
262
  return
223
263
 
224
264
  if not force:
225
- item = client.get_shell_jump(item_id)
226
265
  typer.confirm(f"Delete shell jump '{item.get('name')}' (ID: {item_id})?", abort=True)
227
266
 
228
267
  client.delete_shell_jump(item_id)
229
- print_success(f"Deleted shell jump {item_id}")
268
+ print_success(f"Deleted shell jump '{item.get('name')}' (ID: {item_id})")
230
269
  except httpx.HTTPStatusError as e:
231
270
  print_api_error(e, "delete shell jump")
232
271
  raise typer.Exit(1)
@@ -249,6 +288,7 @@ def list_rdp_jumps(
249
288
  jumpoint_id: Optional[int] = typer.Option(None, "--jumpoint", "-j", help="Filter by Jumpoint"),
250
289
  tag: Optional[str] = typer.Option(None, "--tag", "-t", help="Filter by tag (exact match)"),
251
290
  name: Optional[str] = typer.Option(None, "--name", "-n", help="Filter by name (exact match)"),
291
+ name_pattern: Optional[str] = typer.Option(None, "--name-pattern", help="Filter by name pattern (e.g., 'prod-*')"),
252
292
  hostname: Optional[str] = typer.Option(None, "--hostname", help="Filter by hostname (exact match)"),
253
293
  output: OutputFormat = typer.Option(OutputFormat.TABLE, "--output", "-o"),
254
294
  ):
@@ -265,6 +305,9 @@ def list_rdp_jumps(
265
305
  hostname=hostname,
266
306
  )
267
307
 
308
+ if name_pattern:
309
+ items = _filter_by_pattern(items, name_pattern)
310
+
268
311
  if output == OutputFormat.JSON:
269
312
  print_json(items)
270
313
  else:
@@ -382,26 +425,43 @@ def create_rdp_jump(
382
425
 
383
426
  @rdp_app.command("delete")
384
427
  def delete_rdp_jump(
385
- item_id: int = typer.Argument(..., help="RDP Jump ID"),
428
+ item_id: Optional[int] = typer.Argument(None, help="RDP Jump ID"),
429
+ name: Optional[str] = typer.Option(None, "--name", "-n", help="Delete by name instead of ID"),
386
430
  force: bool = typer.Option(False, "--force", "-f", help="Skip confirmation"),
387
431
  dry_run: bool = typer.Option(False, "--dry-run", help="Show what would be deleted without deleting"),
388
432
  ):
389
- """Delete a Remote RDP Jump item."""
433
+ """Delete a Remote RDP Jump item by ID or name."""
390
434
  from bt_cli.pra.client import get_client
391
435
 
436
+ if not item_id and not name:
437
+ print_error("Either ITEM_ID or --name is required")
438
+ raise typer.Exit(1)
439
+
392
440
  try:
393
441
  client = get_client()
394
- if dry_run:
442
+
443
+ if name and not item_id:
444
+ items = client.list_rdp_jumps(name=name)
445
+ if not items:
446
+ print_error(f"No RDP jump found with name '{name}'")
447
+ raise typer.Exit(1)
448
+ if len(items) > 1:
449
+ print_error(f"Multiple RDP jumps found with name '{name}'. Use ID instead.")
450
+ raise typer.Exit(1)
451
+ item = items[0]
452
+ item_id = item.get("id")
453
+ else:
395
454
  item = client.get_rdp_jump(item_id)
455
+
456
+ if dry_run:
396
457
  print_success(f"[DRY-RUN] Would delete RDP jump: {item.get('name')} (ID: {item_id})")
397
458
  return
398
459
 
399
460
  if not force:
400
- item = client.get_rdp_jump(item_id)
401
461
  typer.confirm(f"Delete RDP jump '{item.get('name')}' (ID: {item_id})?", abort=True)
402
462
 
403
463
  client.delete_rdp_jump(item_id)
404
- print_success(f"Deleted RDP jump {item_id}")
464
+ print_success(f"Deleted RDP jump '{item.get('name')}' (ID: {item_id})")
405
465
  except httpx.HTTPStatusError as e:
406
466
  print_api_error(e, "delete RDP jump")
407
467
  raise typer.Exit(1)
@@ -423,6 +483,7 @@ def list_vnc_jumps(
423
483
  jump_group_id: Optional[int] = typer.Option(None, "--jump-group", "-g", help="Filter by Jump Group"),
424
484
  tag: Optional[str] = typer.Option(None, "--tag", "-t", help="Filter by tag (exact match)"),
425
485
  name: Optional[str] = typer.Option(None, "--name", "-n", help="Filter by name (exact match)"),
486
+ name_pattern: Optional[str] = typer.Option(None, "--name-pattern", help="Filter by name pattern (e.g., 'prod-*')"),
426
487
  hostname: Optional[str] = typer.Option(None, "--hostname", help="Filter by hostname (exact match)"),
427
488
  output: OutputFormat = typer.Option(OutputFormat.TABLE, "--output", "-o"),
428
489
  ):
@@ -438,6 +499,9 @@ def list_vnc_jumps(
438
499
  hostname=hostname,
439
500
  )
440
501
 
502
+ if name_pattern:
503
+ items = _filter_by_pattern(items, name_pattern)
504
+
441
505
  if output == OutputFormat.JSON:
442
506
  print_json(items)
443
507
  else:
@@ -470,6 +534,7 @@ def list_web_jumps(
470
534
  jump_group_id: Optional[int] = typer.Option(None, "--jump-group", "-g", help="Filter by Jump Group"),
471
535
  tag: Optional[str] = typer.Option(None, "--tag", "-t", help="Filter by tag (exact match)"),
472
536
  name: Optional[str] = typer.Option(None, "--name", "-n", help="Filter by name (exact match)"),
537
+ name_pattern: Optional[str] = typer.Option(None, "--name-pattern", help="Filter by name pattern (e.g., 'prod-*')"),
473
538
  output: OutputFormat = typer.Option(OutputFormat.TABLE, "--output", "-o"),
474
539
  ):
475
540
  """List Web Jump items."""
@@ -483,6 +548,9 @@ def list_web_jumps(
483
548
  name=name,
484
549
  )
485
550
 
551
+ if name_pattern:
552
+ items = _filter_by_pattern(items, name_pattern)
553
+
486
554
  if output == OutputFormat.JSON:
487
555
  print_json(items)
488
556
  else:
@@ -545,26 +613,43 @@ def get_web_jump(
545
613
 
546
614
  @web_app.command("delete")
547
615
  def delete_web_jump(
548
- item_id: int = typer.Argument(..., help="Web Jump ID"),
616
+ item_id: Optional[int] = typer.Argument(None, help="Web Jump ID"),
617
+ name: Optional[str] = typer.Option(None, "--name", "-n", help="Delete by name instead of ID"),
549
618
  force: bool = typer.Option(False, "--force", "-f", help="Skip confirmation"),
550
619
  dry_run: bool = typer.Option(False, "--dry-run", help="Show what would be deleted without deleting"),
551
620
  ):
552
- """Delete a Web Jump item."""
621
+ """Delete a Web Jump item by ID or name."""
553
622
  from bt_cli.pra.client import get_client
554
623
 
624
+ if not item_id and not name:
625
+ print_error("Either ITEM_ID or --name is required")
626
+ raise typer.Exit(1)
627
+
555
628
  try:
556
629
  client = get_client()
557
- if dry_run:
630
+
631
+ if name and not item_id:
632
+ items = client.list_web_jumps(name=name)
633
+ if not items:
634
+ print_error(f"No web jump found with name '{name}'")
635
+ raise typer.Exit(1)
636
+ if len(items) > 1:
637
+ print_error(f"Multiple web jumps found with name '{name}'. Use ID instead.")
638
+ raise typer.Exit(1)
639
+ item = items[0]
640
+ item_id = item.get("id")
641
+ else:
558
642
  item = client.get_web_jump(item_id)
643
+
644
+ if dry_run:
559
645
  print_success(f"[DRY-RUN] Would delete web jump: {item.get('name')} (ID: {item_id})")
560
646
  return
561
647
 
562
648
  if not force:
563
- item = client.get_web_jump(item_id)
564
649
  typer.confirm(f"Delete web jump '{item.get('name')}' (ID: {item_id})?", abort=True)
565
650
 
566
651
  client.delete_web_jump(item_id)
567
- print_success(f"Deleted web jump {item_id}")
652
+ print_success(f"Deleted web jump '{item.get('name')}' (ID: {item_id})")
568
653
  except httpx.HTTPStatusError as e:
569
654
  print_api_error(e, "delete web jump")
570
655
  raise typer.Exit(1)
@@ -587,6 +672,7 @@ def list_protocol_tunnels(
587
672
  tunnel_type: Optional[str] = typer.Option(None, "--type", help="Filter by type: tcp, mssql, psql, mysql, k8s"),
588
673
  tag: Optional[str] = typer.Option(None, "--tag", "-t", help="Filter by tag (exact match)"),
589
674
  name: Optional[str] = typer.Option(None, "--name", "-n", help="Filter by name (exact match)"),
675
+ name_pattern: Optional[str] = typer.Option(None, "--name-pattern", help="Filter by name pattern (e.g., 'prod-*')"),
590
676
  hostname: Optional[str] = typer.Option(None, "--hostname", help="Filter by hostname (exact match)"),
591
677
  output: OutputFormat = typer.Option(OutputFormat.TABLE, "--output", "-o"),
592
678
  ):
@@ -603,6 +689,9 @@ def list_protocol_tunnels(
603
689
  hostname=hostname,
604
690
  )
605
691
 
692
+ if name_pattern:
693
+ items = _filter_by_pattern(items, name_pattern)
694
+
606
695
  if output == OutputFormat.JSON:
607
696
  print_json(items)
608
697
  else:
@@ -741,26 +830,43 @@ def create_protocol_tunnel(
741
830
 
742
831
  @tunnel_app.command("delete")
743
832
  def delete_protocol_tunnel(
744
- item_id: int = typer.Argument(..., help="Protocol Tunnel ID"),
833
+ item_id: Optional[int] = typer.Argument(None, help="Protocol Tunnel ID"),
834
+ name: Optional[str] = typer.Option(None, "--name", "-n", help="Delete by name instead of ID"),
745
835
  force: bool = typer.Option(False, "--force", "-f", help="Skip confirmation"),
746
836
  dry_run: bool = typer.Option(False, "--dry-run", help="Show what would be deleted without deleting"),
747
837
  ):
748
- """Delete a Protocol Tunnel Jump item."""
838
+ """Delete a Protocol Tunnel Jump item by ID or name."""
749
839
  from bt_cli.pra.client import get_client
750
840
 
841
+ if not item_id and not name:
842
+ print_error("Either ITEM_ID or --name is required")
843
+ raise typer.Exit(1)
844
+
751
845
  try:
752
846
  client = get_client()
753
- if dry_run:
847
+
848
+ if name and not item_id:
849
+ items = client.list_protocol_tunnels(name=name)
850
+ if not items:
851
+ print_error(f"No protocol tunnel found with name '{name}'")
852
+ raise typer.Exit(1)
853
+ if len(items) > 1:
854
+ print_error(f"Multiple protocol tunnels found with name '{name}'. Use ID instead.")
855
+ raise typer.Exit(1)
856
+ item = items[0]
857
+ item_id = item.get("id")
858
+ else:
754
859
  item = client.get_protocol_tunnel(item_id)
860
+
861
+ if dry_run:
755
862
  print_success(f"[DRY-RUN] Would delete protocol tunnel: {item.get('name')} (ID: {item_id})")
756
863
  return
757
864
 
758
865
  if not force:
759
- item = client.get_protocol_tunnel(item_id)
760
866
  typer.confirm(f"Delete protocol tunnel '{item.get('name')}' (ID: {item_id})?", abort=True)
761
867
 
762
868
  client.delete_protocol_tunnel(item_id)
763
- print_success(f"Deleted protocol tunnel {item_id}")
869
+ print_success(f"Deleted protocol tunnel '{item.get('name')}' (ID: {item_id})")
764
870
  except httpx.HTTPStatusError as e:
765
871
  print_api_error(e, "delete protocol tunnel")
766
872
  raise typer.Exit(1)
@@ -910,3 +910,46 @@ class BeyondInsightMixin:
910
910
  attribute_id: Attribute ID to remove
911
911
  """
912
912
  self.delete(f"/ManagedSystems/{managed_system_id}/Attributes/{attribute_id}")
913
+
914
+ def get_managed_account_attributes(
915
+ self: "PasswordSafeClient",
916
+ account_id: int,
917
+ ) -> list[dict[str, Any]]:
918
+ """Get attributes for a managed account.
919
+
920
+ Args:
921
+ account_id: Managed account ID
922
+
923
+ Returns:
924
+ List of attribute objects assigned to this account
925
+ """
926
+ return self.get(f"/ManagedAccounts/{account_id}/Attributes")
927
+
928
+ def assign_managed_account_attribute(
929
+ self: "PasswordSafeClient",
930
+ account_id: int,
931
+ attribute_id: int,
932
+ ) -> dict[str, Any]:
933
+ """Assign an attribute to a managed account.
934
+
935
+ Args:
936
+ account_id: Managed account ID
937
+ attribute_id: Attribute ID to assign
938
+
939
+ Returns:
940
+ The created assignment
941
+ """
942
+ return self.post(f"/ManagedAccounts/{account_id}/Attributes/{attribute_id}")
943
+
944
+ def remove_managed_account_attribute(
945
+ self: "PasswordSafeClient",
946
+ account_id: int,
947
+ attribute_id: int,
948
+ ) -> None:
949
+ """Remove an attribute from a managed account.
950
+
951
+ Args:
952
+ account_id: Managed account ID
953
+ attribute_id: Attribute ID to remove
954
+ """
955
+ self.delete(f"/ManagedAccounts/{account_id}/Attributes/{attribute_id}")
@@ -174,3 +174,93 @@ def remove_attribute(
174
174
  except Exception as e:
175
175
  print_api_error(e, "remove attribute")
176
176
  raise typer.Exit(1)
177
+
178
+
179
+ # Account attribute commands
180
+ @app.command("show-for-account")
181
+ def show_account_attributes(
182
+ account_id: int = typer.Argument(..., help="Managed Account ID"),
183
+ output: OutputFormat = typer.Option(OutputFormat.TABLE, "--output", "-o"),
184
+ ):
185
+ """Show attributes assigned to a managed account."""
186
+ from bt_cli.pws.client import get_client
187
+
188
+ try:
189
+ with get_client() as client:
190
+ attributes = client.get_managed_account_attributes(account_id)
191
+
192
+ if output == OutputFormat.JSON:
193
+ print_json(attributes)
194
+ else:
195
+ if not attributes:
196
+ print_error(f"No attributes assigned to managed account {account_id}")
197
+ return
198
+
199
+ columns = [
200
+ ("ID", "AttributeID"),
201
+ ("Name", "AttributeName"),
202
+ ("Type", "AttributeType"),
203
+ ]
204
+ print_table(attributes, columns, title=f"Attributes for Managed Account {account_id}")
205
+ except httpx.HTTPStatusError as e:
206
+ print_api_error(e, "show account attributes")
207
+ raise typer.Exit(1)
208
+ except httpx.RequestError as e:
209
+ print_api_error(e, "show account attributes")
210
+ raise typer.Exit(1)
211
+ except Exception as e:
212
+ print_api_error(e, "show account attributes")
213
+ raise typer.Exit(1)
214
+
215
+
216
+ @app.command("assign-to-account")
217
+ def assign_account_attribute(
218
+ account_id: int = typer.Argument(..., help="Managed Account ID"),
219
+ attribute_id: int = typer.Argument(..., help="Attribute ID to assign"),
220
+ ):
221
+ """Assign an attribute to a managed account."""
222
+ from bt_cli.pws.client import get_client
223
+
224
+ try:
225
+ with get_client() as client:
226
+ client.assign_managed_account_attribute(account_id, attribute_id)
227
+ print_success(f"Assigned attribute {attribute_id} to managed account {account_id}")
228
+ except httpx.HTTPStatusError as e:
229
+ print_api_error(e, "assign attribute to account")
230
+ raise typer.Exit(1)
231
+ except httpx.RequestError as e:
232
+ print_api_error(e, "assign attribute to account")
233
+ raise typer.Exit(1)
234
+ except Exception as e:
235
+ print_api_error(e, "assign attribute to account")
236
+ raise typer.Exit(1)
237
+
238
+
239
+ @app.command("remove-from-account")
240
+ def remove_account_attribute(
241
+ account_id: int = typer.Argument(..., help="Managed Account ID"),
242
+ attribute_id: int = typer.Argument(..., help="Attribute ID to remove"),
243
+ force: bool = typer.Option(False, "--force", "-f", help="Skip confirmation"),
244
+ ):
245
+ """Remove an attribute from a managed account."""
246
+ from bt_cli.pws.client import get_client
247
+
248
+ if not force:
249
+ typer.confirm(
250
+ f"Remove attribute {attribute_id} from managed account {account_id}?",
251
+ abort=True
252
+ )
253
+
254
+ try:
255
+ with get_client() as client:
256
+ client.remove_managed_account_attribute(account_id, attribute_id)
257
+ print_success(f"Removed attribute {attribute_id} from managed account {account_id}")
258
+ except httpx.HTTPStatusError as e:
259
+ print_api_error(e, "remove attribute from account")
260
+ raise typer.Exit(1)
261
+ except httpx.RequestError as e:
262
+ print_api_error(e, "remove attribute from account")
263
+ raise typer.Exit(1)
264
+ except Exception as e:
265
+ print_api_error(e, "remove attribute from account")
266
+ raise typer.Exit(1)
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
File without changes