bt-cli 0.4.60__tar.gz → 0.4.62__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 (247) hide show
  1. {bt_cli-0.4.60 → bt_cli-0.4.62}/CLAUDE.md +2 -1
  2. {bt_cli-0.4.60 → bt_cli-0.4.62}/PKG-INFO +1 -1
  3. {bt_cli-0.4.60 → bt_cli-0.4.62}/pyproject.toml +1 -1
  4. {bt_cli-0.4.60 → bt_cli-0.4.62}/src/bt_cli/__init__.py +1 -1
  5. {bt_cli-0.4.60 → bt_cli-0.4.62}/src/bt_cli/data/skills/pws/SKILL.md +39 -0
  6. {bt_cli-0.4.60 → bt_cli-0.4.62}/src/bt_cli/pws/client/passwordsafe.py +29 -0
  7. {bt_cli-0.4.60 → bt_cli-0.4.62}/src/bt_cli/pws/commands/quick.py +340 -72
  8. {bt_cli-0.4.60 → bt_cli-0.4.62}/src/bt_cli/pws/commands/sessions.py +60 -0
  9. {bt_cli-0.4.60 → bt_cli-0.4.62}/tests/pws/test_client.py +24 -0
  10. {bt_cli-0.4.60 → bt_cli-0.4.62}/tests/pws/test_commands.py +70 -1
  11. bt_cli-0.4.62/tests/pws/test_quick_sessions.py +40 -0
  12. {bt_cli-0.4.60 → bt_cli-0.4.62}/.claude/skills/bt/SKILL.md +0 -0
  13. {bt_cli-0.4.60 → bt_cli-0.4.62}/.claude/skills/entitle/SKILL.md +0 -0
  14. {bt_cli-0.4.60 → bt_cli-0.4.62}/.claude/skills/epml/SKILL.md +0 -0
  15. {bt_cli-0.4.60 → bt_cli-0.4.62}/.claude/skills/epmw/SKILL.md +0 -0
  16. {bt_cli-0.4.60 → bt_cli-0.4.62}/.claude/skills/pra/SKILL.md +0 -0
  17. {bt_cli-0.4.60 → bt_cli-0.4.62}/.claude/skills/pws/SKILL.md +0 -0
  18. {bt_cli-0.4.60 → bt_cli-0.4.62}/.env.example +0 -0
  19. {bt_cli-0.4.60 → bt_cli-0.4.62}/.github/workflows/ci.yml +0 -0
  20. {bt_cli-0.4.60 → bt_cli-0.4.62}/.github/workflows/release.yml +0 -0
  21. {bt_cli-0.4.60 → bt_cli-0.4.62}/.gitignore +0 -0
  22. {bt_cli-0.4.60 → bt_cli-0.4.62}/README.md +0 -0
  23. {bt_cli-0.4.60 → bt_cli-0.4.62}/assets/cli-help.png +0 -0
  24. {bt_cli-0.4.60 → bt_cli-0.4.62}/assets/cli-output.png +0 -0
  25. {bt_cli-0.4.60 → bt_cli-0.4.62}/bt-cli.spec +0 -0
  26. {bt_cli-0.4.60 → bt_cli-0.4.62}/bt_entry.py +0 -0
  27. {bt_cli-0.4.60 → bt_cli-0.4.62}/epml-clients-server-side-filters-plan.md +0 -0
  28. {bt_cli-0.4.60 → bt_cli-0.4.62}/epml-implementation-plan.md +0 -0
  29. {bt_cli-0.4.60 → bt_cli-0.4.62}/pf-implementation-plan.md +0 -0
  30. {bt_cli-0.4.60 → bt_cli-0.4.62}/scripts/bt_entry.py +0 -0
  31. {bt_cli-0.4.60 → bt_cli-0.4.62}/scripts/pf_onboard.py +0 -0
  32. {bt_cli-0.4.60 → bt_cli-0.4.62}/scripts/sync-package-data.sh +0 -0
  33. {bt_cli-0.4.60 → bt_cli-0.4.62}/src/bt_cli/cli.py +0 -0
  34. {bt_cli-0.4.60 → bt_cli-0.4.62}/src/bt_cli/commands/__init__.py +0 -0
  35. {bt_cli-0.4.60 → bt_cli-0.4.62}/src/bt_cli/commands/configure.py +0 -0
  36. {bt_cli-0.4.60 → bt_cli-0.4.62}/src/bt_cli/commands/learn.py +0 -0
  37. {bt_cli-0.4.60 → bt_cli-0.4.62}/src/bt_cli/commands/quick.py +0 -0
  38. {bt_cli-0.4.60 → bt_cli-0.4.62}/src/bt_cli/core/__init__.py +0 -0
  39. {bt_cli-0.4.60 → bt_cli-0.4.62}/src/bt_cli/core/auth.py +0 -0
  40. {bt_cli-0.4.60 → bt_cli-0.4.62}/src/bt_cli/core/client.py +0 -0
  41. {bt_cli-0.4.60 → bt_cli-0.4.62}/src/bt_cli/core/config.py +0 -0
  42. {bt_cli-0.4.60 → bt_cli-0.4.62}/src/bt_cli/core/config_file.py +0 -0
  43. {bt_cli-0.4.60 → bt_cli-0.4.62}/src/bt_cli/core/csv_utils.py +0 -0
  44. {bt_cli-0.4.60 → bt_cli-0.4.62}/src/bt_cli/core/errors.py +0 -0
  45. {bt_cli-0.4.60 → bt_cli-0.4.62}/src/bt_cli/core/output.py +0 -0
  46. {bt_cli-0.4.60 → bt_cli-0.4.62}/src/bt_cli/core/prompts.py +0 -0
  47. {bt_cli-0.4.60 → bt_cli-0.4.62}/src/bt_cli/core/rest_debug.py +0 -0
  48. {bt_cli-0.4.60 → bt_cli-0.4.62}/src/bt_cli/data/CLAUDE.md +0 -0
  49. {bt_cli-0.4.60 → bt_cli-0.4.62}/src/bt_cli/data/__init__.py +0 -0
  50. {bt_cli-0.4.60 → bt_cli-0.4.62}/src/bt_cli/data/skills/bt/SKILL.md +0 -0
  51. {bt_cli-0.4.60 → bt_cli-0.4.62}/src/bt_cli/data/skills/entitle/SKILL.md +0 -0
  52. {bt_cli-0.4.60 → bt_cli-0.4.62}/src/bt_cli/data/skills/epml/SKILL.md +0 -0
  53. {bt_cli-0.4.60 → bt_cli-0.4.62}/src/bt_cli/data/skills/epmw/SKILL.md +0 -0
  54. {bt_cli-0.4.60 → bt_cli-0.4.62}/src/bt_cli/data/skills/pf/SKILL.md +0 -0
  55. {bt_cli-0.4.60 → bt_cli-0.4.62}/src/bt_cli/data/skills/pra/SKILL.md +0 -0
  56. {bt_cli-0.4.60 → bt_cli-0.4.62}/src/bt_cli/data/skills/secrets/SKILL.md +0 -0
  57. {bt_cli-0.4.60 → bt_cli-0.4.62}/src/bt_cli/entitle/__init__.py +0 -0
  58. {bt_cli-0.4.60 → bt_cli-0.4.62}/src/bt_cli/entitle/client/__init__.py +0 -0
  59. {bt_cli-0.4.60 → bt_cli-0.4.62}/src/bt_cli/entitle/client/base.py +0 -0
  60. {bt_cli-0.4.60 → bt_cli-0.4.62}/src/bt_cli/entitle/commands/__init__.py +0 -0
  61. {bt_cli-0.4.60 → bt_cli-0.4.62}/src/bt_cli/entitle/commands/accounts.py +0 -0
  62. {bt_cli-0.4.60 → bt_cli-0.4.62}/src/bt_cli/entitle/commands/applications.py +0 -0
  63. {bt_cli-0.4.60 → bt_cli-0.4.62}/src/bt_cli/entitle/commands/auth.py +0 -0
  64. {bt_cli-0.4.60 → bt_cli-0.4.62}/src/bt_cli/entitle/commands/bundles.py +0 -0
  65. {bt_cli-0.4.60 → bt_cli-0.4.62}/src/bt_cli/entitle/commands/integrations.py +0 -0
  66. {bt_cli-0.4.60 → bt_cli-0.4.62}/src/bt_cli/entitle/commands/permissions.py +0 -0
  67. {bt_cli-0.4.60 → bt_cli-0.4.62}/src/bt_cli/entitle/commands/policies.py +0 -0
  68. {bt_cli-0.4.60 → bt_cli-0.4.62}/src/bt_cli/entitle/commands/requests.py +0 -0
  69. {bt_cli-0.4.60 → bt_cli-0.4.62}/src/bt_cli/entitle/commands/resources.py +0 -0
  70. {bt_cli-0.4.60 → bt_cli-0.4.62}/src/bt_cli/entitle/commands/roles.py +0 -0
  71. {bt_cli-0.4.60 → bt_cli-0.4.62}/src/bt_cli/entitle/commands/users.py +0 -0
  72. {bt_cli-0.4.60 → bt_cli-0.4.62}/src/bt_cli/entitle/commands/workflows.py +0 -0
  73. {bt_cli-0.4.60 → bt_cli-0.4.62}/src/bt_cli/entitle/models/__init__.py +0 -0
  74. {bt_cli-0.4.60 → bt_cli-0.4.62}/src/bt_cli/entitle/models/bundle.py +0 -0
  75. {bt_cli-0.4.60 → bt_cli-0.4.62}/src/bt_cli/entitle/models/common.py +0 -0
  76. {bt_cli-0.4.60 → bt_cli-0.4.62}/src/bt_cli/entitle/models/integration.py +0 -0
  77. {bt_cli-0.4.60 → bt_cli-0.4.62}/src/bt_cli/entitle/models/permission.py +0 -0
  78. {bt_cli-0.4.60 → bt_cli-0.4.62}/src/bt_cli/entitle/models/policy.py +0 -0
  79. {bt_cli-0.4.60 → bt_cli-0.4.62}/src/bt_cli/entitle/models/resource.py +0 -0
  80. {bt_cli-0.4.60 → bt_cli-0.4.62}/src/bt_cli/entitle/models/role.py +0 -0
  81. {bt_cli-0.4.60 → bt_cli-0.4.62}/src/bt_cli/entitle/models/user.py +0 -0
  82. {bt_cli-0.4.60 → bt_cli-0.4.62}/src/bt_cli/entitle/models/workflow.py +0 -0
  83. {bt_cli-0.4.60 → bt_cli-0.4.62}/src/bt_cli/epml/__init__.py +0 -0
  84. {bt_cli-0.4.60 → bt_cli-0.4.62}/src/bt_cli/epml/client/__init__.py +0 -0
  85. {bt_cli-0.4.60 → bt_cli-0.4.62}/src/bt_cli/epml/client/base.py +0 -0
  86. {bt_cli-0.4.60 → bt_cli-0.4.62}/src/bt_cli/epml/commands/__init__.py +0 -0
  87. {bt_cli-0.4.60 → bt_cli-0.4.62}/src/bt_cli/epml/commands/audit.py +0 -0
  88. {bt_cli-0.4.60 → bt_cli-0.4.62}/src/bt_cli/epml/commands/auth.py +0 -0
  89. {bt_cli-0.4.60 → bt_cli-0.4.62}/src/bt_cli/epml/commands/client_pkg.py +0 -0
  90. {bt_cli-0.4.60 → bt_cli-0.4.62}/src/bt_cli/epml/commands/clients.py +0 -0
  91. {bt_cli-0.4.60 → bt_cli-0.4.62}/src/bt_cli/epml/commands/external_apis.py +0 -0
  92. {bt_cli-0.4.60 → bt_cli-0.4.62}/src/bt_cli/epml/commands/hosts.py +0 -0
  93. {bt_cli-0.4.60 → bt_cli-0.4.62}/src/bt_cli/epml/commands/iolog.py +0 -0
  94. {bt_cli-0.4.60 → bt_cli-0.4.62}/src/bt_cli/epml/commands/license.py +0 -0
  95. {bt_cli-0.4.60 → bt_cli-0.4.62}/src/bt_cli/epml/commands/quick.py +0 -0
  96. {bt_cli-0.4.60 → bt_cli-0.4.62}/src/bt_cli/epml/commands/rbp_cmdgrps.py +0 -0
  97. {bt_cli-0.4.60 → bt_cli-0.4.62}/src/bt_cli/epml/commands/rbp_entitlement.py +0 -0
  98. {bt_cli-0.4.60 → bt_cli-0.4.62}/src/bt_cli/epml/commands/rbp_hostgrps.py +0 -0
  99. {bt_cli-0.4.60 → bt_cli-0.4.62}/src/bt_cli/epml/commands/rbp_policy.py +0 -0
  100. {bt_cli-0.4.60 → bt_cli-0.4.62}/src/bt_cli/epml/commands/rbp_roles.py +0 -0
  101. {bt_cli-0.4.60 → bt_cli-0.4.62}/src/bt_cli/epml/commands/rbp_tests.py +0 -0
  102. {bt_cli-0.4.60 → bt_cli-0.4.62}/src/bt_cli/epml/commands/rbp_tmdategrps.py +0 -0
  103. {bt_cli-0.4.60 → bt_cli-0.4.62}/src/bt_cli/epml/commands/rbp_tx.py +0 -0
  104. {bt_cli-0.4.60 → bt_cli-0.4.62}/src/bt_cli/epml/commands/rbp_usergrps.py +0 -0
  105. {bt_cli-0.4.60 → bt_cli-0.4.62}/src/bt_cli/epml/commands/settings.py +0 -0
  106. {bt_cli-0.4.60 → bt_cli-0.4.62}/src/bt_cli/epml/commands/siems.py +0 -0
  107. {bt_cli-0.4.60 → bt_cli-0.4.62}/src/bt_cli/epml/commands/users.py +0 -0
  108. {bt_cli-0.4.60 → bt_cli-0.4.62}/src/bt_cli/epml/models/__init__.py +0 -0
  109. {bt_cli-0.4.60 → bt_cli-0.4.62}/src/bt_cli/epmw/__init__.py +0 -0
  110. {bt_cli-0.4.60 → bt_cli-0.4.62}/src/bt_cli/epmw/client/__init__.py +0 -0
  111. {bt_cli-0.4.60 → bt_cli-0.4.62}/src/bt_cli/epmw/client/base.py +0 -0
  112. {bt_cli-0.4.60 → bt_cli-0.4.62}/src/bt_cli/epmw/commands/__init__.py +0 -0
  113. {bt_cli-0.4.60 → bt_cli-0.4.62}/src/bt_cli/epmw/commands/audits.py +0 -0
  114. {bt_cli-0.4.60 → bt_cli-0.4.62}/src/bt_cli/epmw/commands/auth.py +0 -0
  115. {bt_cli-0.4.60 → bt_cli-0.4.62}/src/bt_cli/epmw/commands/computers.py +0 -0
  116. {bt_cli-0.4.60 → bt_cli-0.4.62}/src/bt_cli/epmw/commands/events.py +0 -0
  117. {bt_cli-0.4.60 → bt_cli-0.4.62}/src/bt_cli/epmw/commands/groups.py +0 -0
  118. {bt_cli-0.4.60 → bt_cli-0.4.62}/src/bt_cli/epmw/commands/policies.py +0 -0
  119. {bt_cli-0.4.60 → bt_cli-0.4.62}/src/bt_cli/epmw/commands/quick.py +0 -0
  120. {bt_cli-0.4.60 → bt_cli-0.4.62}/src/bt_cli/epmw/commands/requests.py +0 -0
  121. {bt_cli-0.4.60 → bt_cli-0.4.62}/src/bt_cli/epmw/commands/roles.py +0 -0
  122. {bt_cli-0.4.60 → bt_cli-0.4.62}/src/bt_cli/epmw/commands/tasks.py +0 -0
  123. {bt_cli-0.4.60 → bt_cli-0.4.62}/src/bt_cli/epmw/commands/users.py +0 -0
  124. {bt_cli-0.4.60 → bt_cli-0.4.62}/src/bt_cli/epmw/models/__init__.py +0 -0
  125. {bt_cli-0.4.60 → bt_cli-0.4.62}/src/bt_cli/pf/__init__.py +0 -0
  126. {bt_cli-0.4.60 → bt_cli-0.4.62}/src/bt_cli/pf/client/__init__.py +0 -0
  127. {bt_cli-0.4.60 → bt_cli-0.4.62}/src/bt_cli/pf/client/base.py +0 -0
  128. {bt_cli-0.4.60 → bt_cli-0.4.62}/src/bt_cli/pf/commands/__init__.py +0 -0
  129. {bt_cli-0.4.60 → bt_cli-0.4.62}/src/bt_cli/pf/commands/auth.py +0 -0
  130. {bt_cli-0.4.60 → bt_cli-0.4.62}/src/bt_cli/pf/commands/machines.py +0 -0
  131. {bt_cli-0.4.60 → bt_cli-0.4.62}/src/bt_cli/pf/commands/tokens.py +0 -0
  132. {bt_cli-0.4.60 → bt_cli-0.4.62}/src/bt_cli/pf/commands/user.py +0 -0
  133. {bt_cli-0.4.60 → bt_cli-0.4.62}/src/bt_cli/pf/commands/users.py +0 -0
  134. {bt_cli-0.4.60 → bt_cli-0.4.62}/src/bt_cli/pra/__init__.py +0 -0
  135. {bt_cli-0.4.60 → bt_cli-0.4.62}/src/bt_cli/pra/client/__init__.py +0 -0
  136. {bt_cli-0.4.60 → bt_cli-0.4.62}/src/bt_cli/pra/client/base.py +0 -0
  137. {bt_cli-0.4.60 → bt_cli-0.4.62}/src/bt_cli/pra/commands/__init__.py +0 -0
  138. {bt_cli-0.4.60 → bt_cli-0.4.62}/src/bt_cli/pra/commands/auth.py +0 -0
  139. {bt_cli-0.4.60 → bt_cli-0.4.62}/src/bt_cli/pra/commands/group_policies.py +0 -0
  140. {bt_cli-0.4.60 → bt_cli-0.4.62}/src/bt_cli/pra/commands/import_export.py +0 -0
  141. {bt_cli-0.4.60 → bt_cli-0.4.62}/src/bt_cli/pra/commands/jump_clients.py +0 -0
  142. {bt_cli-0.4.60 → bt_cli-0.4.62}/src/bt_cli/pra/commands/jump_groups.py +0 -0
  143. {bt_cli-0.4.60 → bt_cli-0.4.62}/src/bt_cli/pra/commands/jump_items.py +0 -0
  144. {bt_cli-0.4.60 → bt_cli-0.4.62}/src/bt_cli/pra/commands/jumpoints.py +0 -0
  145. {bt_cli-0.4.60 → bt_cli-0.4.62}/src/bt_cli/pra/commands/policies.py +0 -0
  146. {bt_cli-0.4.60 → bt_cli-0.4.62}/src/bt_cli/pra/commands/quick.py +0 -0
  147. {bt_cli-0.4.60 → bt_cli-0.4.62}/src/bt_cli/pra/commands/teams.py +0 -0
  148. {bt_cli-0.4.60 → bt_cli-0.4.62}/src/bt_cli/pra/commands/users.py +0 -0
  149. {bt_cli-0.4.60 → bt_cli-0.4.62}/src/bt_cli/pra/commands/vault.py +0 -0
  150. {bt_cli-0.4.60 → bt_cli-0.4.62}/src/bt_cli/pra/models/__init__.py +0 -0
  151. {bt_cli-0.4.60 → bt_cli-0.4.62}/src/bt_cli/pra/models/common.py +0 -0
  152. {bt_cli-0.4.60 → bt_cli-0.4.62}/src/bt_cli/pra/models/group_policy.py +0 -0
  153. {bt_cli-0.4.60 → bt_cli-0.4.62}/src/bt_cli/pra/models/jump_client.py +0 -0
  154. {bt_cli-0.4.60 → bt_cli-0.4.62}/src/bt_cli/pra/models/jump_group.py +0 -0
  155. {bt_cli-0.4.60 → bt_cli-0.4.62}/src/bt_cli/pra/models/jump_item.py +0 -0
  156. {bt_cli-0.4.60 → bt_cli-0.4.62}/src/bt_cli/pra/models/jumpoint.py +0 -0
  157. {bt_cli-0.4.60 → bt_cli-0.4.62}/src/bt_cli/pra/models/team.py +0 -0
  158. {bt_cli-0.4.60 → bt_cli-0.4.62}/src/bt_cli/pra/models/user.py +0 -0
  159. {bt_cli-0.4.60 → bt_cli-0.4.62}/src/bt_cli/pra/models/vault.py +0 -0
  160. {bt_cli-0.4.60 → bt_cli-0.4.62}/src/bt_cli/pws/__init__.py +0 -0
  161. {bt_cli-0.4.60 → bt_cli-0.4.62}/src/bt_cli/pws/client/__init__.py +0 -0
  162. {bt_cli-0.4.60 → bt_cli-0.4.62}/src/bt_cli/pws/client/base.py +0 -0
  163. {bt_cli-0.4.60 → bt_cli-0.4.62}/src/bt_cli/pws/client/beyondinsight.py +0 -0
  164. {bt_cli-0.4.60 → bt_cli-0.4.62}/src/bt_cli/pws/commands/__init__.py +0 -0
  165. {bt_cli-0.4.60 → bt_cli-0.4.62}/src/bt_cli/pws/commands/accounts.py +0 -0
  166. {bt_cli-0.4.60 → bt_cli-0.4.62}/src/bt_cli/pws/commands/assets.py +0 -0
  167. {bt_cli-0.4.60 → bt_cli-0.4.62}/src/bt_cli/pws/commands/attributes.py +0 -0
  168. {bt_cli-0.4.60 → bt_cli-0.4.62}/src/bt_cli/pws/commands/audit.py +0 -0
  169. {bt_cli-0.4.60 → bt_cli-0.4.62}/src/bt_cli/pws/commands/auth.py +0 -0
  170. {bt_cli-0.4.60 → bt_cli-0.4.62}/src/bt_cli/pws/commands/clouds.py +0 -0
  171. {bt_cli-0.4.60 → bt_cli-0.4.62}/src/bt_cli/pws/commands/config.py +0 -0
  172. {bt_cli-0.4.60 → bt_cli-0.4.62}/src/bt_cli/pws/commands/credentials.py +0 -0
  173. {bt_cli-0.4.60 → bt_cli-0.4.62}/src/bt_cli/pws/commands/databases.py +0 -0
  174. {bt_cli-0.4.60 → bt_cli-0.4.62}/src/bt_cli/pws/commands/directories.py +0 -0
  175. {bt_cli-0.4.60 → bt_cli-0.4.62}/src/bt_cli/pws/commands/functional.py +0 -0
  176. {bt_cli-0.4.60 → bt_cli-0.4.62}/src/bt_cli/pws/commands/import_export.py +0 -0
  177. {bt_cli-0.4.60 → bt_cli-0.4.62}/src/bt_cli/pws/commands/platforms.py +0 -0
  178. {bt_cli-0.4.60 → bt_cli-0.4.62}/src/bt_cli/pws/commands/search.py +0 -0
  179. {bt_cli-0.4.60 → bt_cli-0.4.62}/src/bt_cli/pws/commands/secrets.py +0 -0
  180. {bt_cli-0.4.60 → bt_cli-0.4.62}/src/bt_cli/pws/commands/systems.py +0 -0
  181. {bt_cli-0.4.60 → bt_cli-0.4.62}/src/bt_cli/pws/commands/users.py +0 -0
  182. {bt_cli-0.4.60 → bt_cli-0.4.62}/src/bt_cli/pws/commands/workgroups.py +0 -0
  183. {bt_cli-0.4.60 → bt_cli-0.4.62}/src/bt_cli/pws/config.py +0 -0
  184. {bt_cli-0.4.60 → bt_cli-0.4.62}/src/bt_cli/pws/models/__init__.py +0 -0
  185. {bt_cli-0.4.60 → bt_cli-0.4.62}/src/bt_cli/pws/models/account.py +0 -0
  186. {bt_cli-0.4.60 → bt_cli-0.4.62}/src/bt_cli/pws/models/asset.py +0 -0
  187. {bt_cli-0.4.60 → bt_cli-0.4.62}/src/bt_cli/pws/models/common.py +0 -0
  188. {bt_cli-0.4.60 → bt_cli-0.4.62}/src/bt_cli/pws/models/system.py +0 -0
  189. {bt_cli-0.4.60 → bt_cli-0.4.62}/src/bt_cli/secrets/__init__.py +0 -0
  190. {bt_cli-0.4.60 → bt_cli-0.4.62}/src/bt_cli/secrets/client/__init__.py +0 -0
  191. {bt_cli-0.4.60 → bt_cli-0.4.62}/src/bt_cli/secrets/client/base.py +0 -0
  192. {bt_cli-0.4.60 → bt_cli-0.4.62}/src/bt_cli/secrets/commands/__init__.py +0 -0
  193. {bt_cli-0.4.60 → bt_cli-0.4.62}/src/bt_cli/secrets/commands/_hints.py +0 -0
  194. {bt_cli-0.4.60 → bt_cli-0.4.62}/src/bt_cli/secrets/commands/auth.py +0 -0
  195. {bt_cli-0.4.60 → bt_cli-0.4.62}/src/bt_cli/secrets/commands/dynamic.py +0 -0
  196. {bt_cli-0.4.60 → bt_cli-0.4.62}/src/bt_cli/secrets/commands/folders.py +0 -0
  197. {bt_cli-0.4.60 → bt_cli-0.4.62}/src/bt_cli/secrets/commands/integrations.py +0 -0
  198. {bt_cli-0.4.60 → bt_cli-0.4.62}/src/bt_cli/secrets/commands/leases.py +0 -0
  199. {bt_cli-0.4.60 → bt_cli-0.4.62}/src/bt_cli/secrets/commands/static.py +0 -0
  200. {bt_cli-0.4.60 → bt_cli-0.4.62}/src/bt_cli/secrets/models/__init__.py +0 -0
  201. {bt_cli-0.4.60 → bt_cli-0.4.62}/tests/__init__.py +0 -0
  202. {bt_cli-0.4.60 → bt_cli-0.4.62}/tests/conftest.py +0 -0
  203. {bt_cli-0.4.60 → bt_cli-0.4.62}/tests/core/__init__.py +0 -0
  204. {bt_cli-0.4.60 → bt_cli-0.4.62}/tests/core/test_auth.py +0 -0
  205. {bt_cli-0.4.60 → bt_cli-0.4.62}/tests/core/test_config.py +0 -0
  206. {bt_cli-0.4.60 → bt_cli-0.4.62}/tests/core/test_config_file.py +0 -0
  207. {bt_cli-0.4.60 → bt_cli-0.4.62}/tests/core/test_errors.py +0 -0
  208. {bt_cli-0.4.60 → bt_cli-0.4.62}/tests/core/test_output.py +0 -0
  209. {bt_cli-0.4.60 → bt_cli-0.4.62}/tests/core/test_rest_debug.py +0 -0
  210. {bt_cli-0.4.60 → bt_cli-0.4.62}/tests/entitle/__init__.py +0 -0
  211. {bt_cli-0.4.60 → bt_cli-0.4.62}/tests/entitle/test_client.py +0 -0
  212. {bt_cli-0.4.60 → bt_cli-0.4.62}/tests/entitle/test_commands.py +0 -0
  213. {bt_cli-0.4.60 → bt_cli-0.4.62}/tests/entitle-smoke-test.sh +0 -0
  214. {bt_cli-0.4.60 → bt_cli-0.4.62}/tests/epml/__init__.py +0 -0
  215. {bt_cli-0.4.60 → bt_cli-0.4.62}/tests/epml/test_client.py +0 -0
  216. {bt_cli-0.4.60 → bt_cli-0.4.62}/tests/epml/test_commands.py +0 -0
  217. {bt_cli-0.4.60 → bt_cli-0.4.62}/tests/epmw/__init__.py +0 -0
  218. {bt_cli-0.4.60 → bt_cli-0.4.62}/tests/epmw/test_client.py +0 -0
  219. {bt_cli-0.4.60 → bt_cli-0.4.62}/tests/epmw/test_commands.py +0 -0
  220. {bt_cli-0.4.60 → bt_cli-0.4.62}/tests/epmw-quick-test-plan.md +0 -0
  221. {bt_cli-0.4.60 → bt_cli-0.4.62}/tests/fixtures/__init__.py +0 -0
  222. {bt_cli-0.4.60 → bt_cli-0.4.62}/tests/fixtures/responses.py +0 -0
  223. {bt_cli-0.4.60 → bt_cli-0.4.62}/tests/integration/__init__.py +0 -0
  224. {bt_cli-0.4.60 → bt_cli-0.4.62}/tests/integration/conftest.py +0 -0
  225. {bt_cli-0.4.60 → bt_cli-0.4.62}/tests/integration/helpers.py +0 -0
  226. {bt_cli-0.4.60 → bt_cli-0.4.62}/tests/integration/test_entitle_integration.py +0 -0
  227. {bt_cli-0.4.60 → bt_cli-0.4.62}/tests/integration/test_epmw_integration.py +0 -0
  228. {bt_cli-0.4.60 → bt_cli-0.4.62}/tests/integration/test_epmw_lifecycle.py +0 -0
  229. {bt_cli-0.4.60 → bt_cli-0.4.62}/tests/integration/test_pra_integration.py +0 -0
  230. {bt_cli-0.4.60 → bt_cli-0.4.62}/tests/integration/test_pra_lifecycle.py +0 -0
  231. {bt_cli-0.4.60 → bt_cli-0.4.62}/tests/integration/test_pws_integration.py +0 -0
  232. {bt_cli-0.4.60 → bt_cli-0.4.62}/tests/integration/test_pws_lifecycle.py +0 -0
  233. {bt_cli-0.4.60 → bt_cli-0.4.62}/tests/pf/__init__.py +0 -0
  234. {bt_cli-0.4.60 → bt_cli-0.4.62}/tests/pf/test_client.py +0 -0
  235. {bt_cli-0.4.60 → bt_cli-0.4.62}/tests/pf/test_commands.py +0 -0
  236. {bt_cli-0.4.60 → bt_cli-0.4.62}/tests/pra/__init__.py +0 -0
  237. {bt_cli-0.4.60 → bt_cli-0.4.62}/tests/pra/test_client.py +0 -0
  238. {bt_cli-0.4.60 → bt_cli-0.4.62}/tests/pra/test_commands.py +0 -0
  239. {bt_cli-0.4.60 → bt_cli-0.4.62}/tests/pra/test_jump_item_fixes.py +0 -0
  240. {bt_cli-0.4.60 → bt_cli-0.4.62}/tests/pra-smoke-test.sh +0 -0
  241. {bt_cli-0.4.60 → bt_cli-0.4.62}/tests/pra-test-plan.md +0 -0
  242. {bt_cli-0.4.60 → bt_cli-0.4.62}/tests/pws/__init__.py +0 -0
  243. {bt_cli-0.4.60 → bt_cli-0.4.62}/tests/pws-quick-test-plan.md +0 -0
  244. {bt_cli-0.4.60 → bt_cli-0.4.62}/tests/pws-smoke-test.sh +0 -0
  245. {bt_cli-0.4.60 → bt_cli-0.4.62}/tests/secrets/__init__.py +0 -0
  246. {bt_cli-0.4.60 → bt_cli-0.4.62}/tests/secrets/test_client.py +0 -0
  247. {bt_cli-0.4.60 → bt_cli-0.4.62}/tests/secrets/test_commands.py +0 -0
@@ -1,6 +1,6 @@
1
1
  # BT-CLI
2
2
 
3
- BeyondTrust Platform CLI for Password Safe, Entitle, PRA, EPM Windows, EPM Linux, and the BeyondTrust Secrets API. **Version: 0.4.60**
3
+ BeyondTrust Platform CLI for Password Safe, Entitle, PRA, EPM Windows, EPM Linux, and the BeyondTrust Secrets API. **Version: 0.4.62**
4
4
 
5
5
  ## Setup
6
6
 
@@ -86,6 +86,7 @@ PASSWORD=$(bt pws quick checkout -s server -a admin --raw)
86
86
  - **PRA `export jump-items` defaults to a REAL export** (all types: shell/rdp/vnc/web/tunnel, full type-specific columns) that round-trips through `import jump-items`; `--sample` gives the blank template. `bt pra jump-items web create` exists.
87
87
  - **PRA vault `associate -t protocol_tunnel` is MSSQL-only** (live-verified): server rejects psql/mysql/tcp/k8s with "Only SQL Server Tunnels are allowed" — use tag/criteria association for those. **TCP `--ports` needs listen;destination *pairs*** (even count; odd → 400 "number of parameters is invalid"). Jumpoint listing is `bt pra jumpoints list` (plural).
88
88
  - **PWS `/Sessions` ignores `limit`/`offset`** and returns the full session *history* every call — never `paginate()` it (infinite loop); `list_sessions` does a single GET and `bt pws sessions list` caps display client-side. `Status` codes (live-confirmed): 0=Not started, 1=Active, 2=Completed, 5=Locked, 7=Terminated; `--active`=Status 1. Active sessions have `EndTime`==`StartTime` (placeholder, not null) — identify live sessions by Status, not timestamps. **`terminate` = `POST /Sessions/{id}/Terminate`** (symmetric w/ `/Lock`); `/Sessions/{id}` is GET/PUT only (DELETE → 405).
89
+ - **PWS SSH/RDP session establishment = `POST /Requests/{id}/Sessions`** (`{"SessionType":"SSH"|"RDP"}`), NOT `POST /Sessions` (collection is GET-only → 405). Create the request with matching `AccessType` first. **SSH** returns a ready `Command` (`ssh -p 4422 <ticket>@<proxyhost>`) + `Link`; **RDP** returns `Host`/`Port`/`Ticket` only — the `.rdp` is built client-side (`full address` = proxy `Host:Port`, `username` = `Ticket`); there's no server-side launch file (`/Sessions/RDPFile` is audit-playback → 403). Ticket is the proxy username; password is proxy-injected. `bt pws quick checkout --access-type ssh|rdp` and `bt pws quick ssh`/`quick rdp` (with `--launch`); session commands do **not** auto-checkin.
89
90
  - Entitle permissions: Use server-side filters (`--resource`, `--user`, `--integration`) - dataset can be 20k+ records
90
91
  - Entitle users: Use `--all` flag only when needed - default fetches first page only
91
92
  - Windows builds: Rich must be pinned to `<14.0.0` (Rich 14 has PyInstaller unicode issues)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: bt-cli
3
- Version: 0.4.60
3
+ Version: 0.4.62
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.60"
7
+ version = "0.4.62"
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.60"
3
+ __version__ = "0.4.62"
@@ -150,6 +150,36 @@ bt pws credentials show <request_id>
150
150
  bt pws credentials checkin <request_id>
151
151
  ```
152
152
 
153
+ ## SSH / RDP session checkout
154
+
155
+ `bt pws quick checkout` handles three access types. Interactive runs prompt for
156
+ the access type; scriptable via `--access-type`. There are also dedicated
157
+ `quick ssh` / `quick rdp` wrappers.
158
+
159
+ ```bash
160
+ # Credential (default) - password / dsskey (SSH key) / passphrase
161
+ bt pws quick checkout -s server -a admin
162
+ bt pws quick checkout -s server -a admin --credential-type dsskey --raw
163
+
164
+ # SSH session - prints a ready-to-run ssh command (proxy injects the password)
165
+ bt pws quick ssh -s linhost2 -a app_account02
166
+ bt pws quick checkout -s linhost2 -a app_account02 --access-type ssh
167
+ bt pws quick ssh -s linhost2 -a app --launch # exec ssh in this terminal
168
+ eval "$(bt pws quick ssh -s linhost2 -a app --raw)" # raw = just the ssh command
169
+
170
+ # RDP session - writes a launchable .rdp file (proxy target + one-time ticket)
171
+ bt pws quick rdp -s WIN03-APP -a Administrator
172
+ bt pws quick rdp -s WIN03-APP -a Administrator --out ~/win03.rdp
173
+ bt pws quick rdp -s WIN03-APP -a Administrator --launch # mstsc / xfreerdp / open
174
+ ```
175
+
176
+ **How it works (live-verified):**
177
+ - Session establishment is **`POST /Requests/{id}/Sessions`** (`{"SessionType":"SSH"|"RDP"}`), created after a request with the matching `AccessType`. The `/Sessions` collection is GET-only (POST → 405).
178
+ - **SSH** response carries a ready `Command` (`ssh -p 4422 <ticket>@<proxyhost>`) and `Link` — just run it. The `<ticket>` is the proxy username; the password is injected by the proxy, so you never handle a credential.
179
+ - **RDP** response gives `Host`/`Port`/`Ticket` only. There is **no server-side `.rdp`** (`/Sessions/RDPFile` is audit-playback → 403), so the CLI builds the file client-side (`full address:s:<Host>:<Port>` + `username:s:<Ticket>`).
180
+ - `Host` is the **session proxy node** (same for every target), not the managed system's own host.
181
+ - Session checkout does **not** auto-checkin — the session needs the request open. Check in when done: `bt pws credentials checkin <request_id>`.
182
+
153
183
  ## Sessions (view / suspend / terminate)
154
184
 
155
185
  ```bash
@@ -165,8 +195,17 @@ bt pws sessions suspend <session_id>
165
195
  # Terminate = end permanently (cannot be resumed) - prompts unless --force
166
196
  bt pws sessions terminate <session_id>
167
197
  bt pws sessions terminate <session_id> --force
198
+
199
+ # Export session history as evidence (same filters as list, no display cap)
200
+ bt pws sessions export --file sessions.json
201
+ bt pws sessions export --active --file active-sessions.csv --format csv
202
+ bt pws sessions export --system 13 -o json
168
203
  ```
169
204
 
205
+ `sessions export` is a sessions-only shortcut; use `bt pws audit export` to pull
206
+ requests + sessions together. Because `/Sessions` ignores server-side filters,
207
+ `--system` and `--active` are applied **client-side** after the full fetch.
208
+
170
209
  **Gotchas:**
171
210
  - `/Sessions` returns session **history**, not just active sessions, and **ignores `limit`/`offset`** (returns the full set every call - the client does a single GET, the CLI caps display client-side). Use `--active` for the live ones.
172
211
  - `Status` codes confirmed live: **0 = Not started, 1 = Active, 2 = Completed, 5 = Locked, 7 = Terminated** (3/9 are other terminal states). `--active` filters on Status == 1.
@@ -429,6 +429,35 @@ class PasswordSafeMixin:
429
429
  access_type=access_type,
430
430
  )
431
431
 
432
+ def create_session(
433
+ self: "PasswordSafeClient",
434
+ request_id: int,
435
+ session_type: str = "SSH",
436
+ node_id: Optional[str] = None,
437
+ ) -> dict[str, Any]:
438
+ """Establish an SSH/RDP session for an approved request.
439
+
440
+ Live-verified: session creation is POST /Requests/{id}/Sessions (the
441
+ /Sessions collection is GET-only - a POST there returns 405). Create the
442
+ request first with a matching AccessType (SSH/RDP), then call this.
443
+
444
+ Args:
445
+ request_id: Request ID (must have AccessType SSH or RDP)
446
+ session_type: "SSH", "RDP", or "App"
447
+ node_id: Optional session node (proxy) ID
448
+
449
+ Returns:
450
+ Session dict. Common keys: Host, Port, Ticket, SessionID, NodeID.
451
+ SSH also returns a ready-to-use Command ("ssh -p <port> <ticket>@<host>")
452
+ and Link ("ssh://..."). RDP returns Host/Port/Ticket only - the .rdp
453
+ file is built client-side (there is no server-side launch file; the
454
+ password is injected by the proxy, with Ticket as the proxy username).
455
+ """
456
+ data: dict[str, Any] = {"SessionType": session_type}
457
+ if node_id:
458
+ data["NodeID"] = node_id
459
+ return self.post(f"/Requests/{request_id}/Sessions", json=data)
460
+
432
461
  def get_credential(
433
462
  self: "PasswordSafeClient",
434
463
  request_id: int,
@@ -2,6 +2,11 @@
2
2
 
3
3
  from typing import Optional
4
4
  import json
5
+ import os
6
+ import shutil
7
+ import subprocess
8
+ import sys
9
+ from pathlib import Path
5
10
 
6
11
  import httpx
7
12
  import typer
@@ -18,6 +23,129 @@ from ..models.common import CredentialType, CREDENTIAL_TYPE_META
18
23
  app = typer.Typer(no_args_is_help=True, help="Quick commands - common multi-step operations in one command")
19
24
  console = Console()
20
25
 
26
+ # Access types accepted by POST /Requests (AccessType). "view" returns a
27
+ # credential; "rdp"/"ssh" establish a proxied session.
28
+ ACCESS_TYPES = {"view": "View", "rdp": "RDP", "ssh": "SSH"}
29
+
30
+
31
+ def _resolve_target(client, system: Optional[str], account: Optional[str], quiet: bool) -> dict:
32
+ """Find a managed system + account by name, prompting when interactive.
33
+
34
+ Shared by checkout / ssh / rdp. Returns a dict with system_id, system_name,
35
+ account_id, account_name. Exits (typer.Exit) with a friendly message when a
36
+ system or account can't be resolved. `quiet` suppresses progress lines (set
37
+ for --raw and -o json so stdout stays clean for scripts/jq).
38
+ """
39
+ if not system:
40
+ system = prompt_if_missing(system, "System name (or partial match)")
41
+
42
+ console.print(f"[dim]Finding system '{system}'...[/dim]") if not quiet else None
43
+ systems = client.list_managed_systems(search=system)
44
+ if not systems:
45
+ print_error(f"No system found matching '{system}'")
46
+ raise typer.Exit(1)
47
+
48
+ matched_system = None
49
+ for s in systems:
50
+ if s.get("SystemName", "").lower() == system.lower():
51
+ matched_system = s
52
+ break
53
+ if not matched_system:
54
+ matched_system = systems[0]
55
+ if len(systems) > 1 and not quiet:
56
+ console.print(f"[yellow]Multiple matches found, using: {matched_system.get('SystemName')}[/yellow]")
57
+
58
+ system_id = matched_system.get("ManagedSystemID")
59
+ system_name = matched_system.get("SystemName")
60
+
61
+ accounts = client.list_managed_accounts(system_id=system_id)
62
+ if not account:
63
+ if not accounts:
64
+ print_error(f"No accounts found on system '{system_name}'")
65
+ raise typer.Exit(1)
66
+ account = prompt_from_list(
67
+ accounts, "Account name", "AccountName", "AccountName",
68
+ f"Accounts on {system_name}", str
69
+ )
70
+
71
+ console.print(f"[dim]Finding account '{account}' on {system_name}...[/dim]") if not quiet else None
72
+ matched_account = None
73
+ for acc in accounts:
74
+ if acc.get("AccountName", "").lower() == account.lower():
75
+ matched_account = acc
76
+ break
77
+ if not matched_account:
78
+ print_error(f"Account '{account}' not found on system '{system_name}'")
79
+ if accounts and not quiet:
80
+ console.print("[dim]Available accounts:[/dim]")
81
+ for acc in accounts[:10]:
82
+ console.print(f" - {acc.get('AccountName')}")
83
+ raise typer.Exit(1)
84
+
85
+ return {
86
+ "system_id": system_id,
87
+ "system_name": system_name,
88
+ "account_id": matched_account.get("AccountId", matched_account.get("ManagedAccountID")),
89
+ "account_name": matched_account.get("AccountName"),
90
+ }
91
+
92
+
93
+ def _build_rdp_file(session: dict) -> str:
94
+ """Build .rdp file content from a PWS RDP session response.
95
+
96
+ The proxy Host:Port is the connection target and Ticket is the proxy
97
+ username; the password is injected by the proxy. There is no server-side
98
+ .rdp file (verified), so we assemble the minimal working file client-side.
99
+ """
100
+ host = session.get("Host", "")
101
+ port = session.get("Port", "")
102
+ ticket = session.get("Ticket") or session.get("ID", "")
103
+ return (
104
+ f"full address:s:{host}:{port}\n"
105
+ f"username:s:{ticket}\n"
106
+ "prompt for credentials:i:0\n"
107
+ "administrative session:i:0\n"
108
+ )
109
+
110
+
111
+ def _ssh_command(session: dict) -> str:
112
+ """Ready-to-run ssh command for a PWS SSH session (server usually provides it)."""
113
+ cmd = session.get("Command")
114
+ if cmd:
115
+ return cmd
116
+ return f"ssh -p {session.get('Port')} {session.get('Ticket') or session.get('ID')}@{session.get('Host')}"
117
+
118
+
119
+ def _launch_ssh(command: str) -> None:
120
+ """Exec an ssh session in the current terminal (replaces the process)."""
121
+ parts = command.split()
122
+ exe = shutil.which(parts[0])
123
+ if not exe:
124
+ print_warning(f"'{parts[0]}' not found on PATH - run the command above manually.")
125
+ return
126
+ console.print(f"[dim]Launching: {command}[/dim]")
127
+ os.execvp(exe, parts) # replaces this process with the ssh client
128
+
129
+
130
+ def _launch_rdp(path: Path) -> None:
131
+ """Open a .rdp file with the platform RDP client."""
132
+ if sys.platform.startswith("win"):
133
+ candidates = [["mstsc", str(path)]]
134
+ elif sys.platform == "darwin":
135
+ candidates = [["open", str(path)]]
136
+ else:
137
+ candidates = [["xfreerdp", str(path)], ["xdg-open", str(path)]]
138
+
139
+ for cmd in candidates:
140
+ if shutil.which(cmd[0]):
141
+ console.print(f"[dim]Launching: {' '.join(cmd)}[/dim]")
142
+ subprocess.Popen(cmd, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
143
+ return
144
+ print_warning(
145
+ f"No RDP client found ({', '.join(c[0] for c in candidates)}). "
146
+ f"Open the file manually: {path}"
147
+ )
148
+
21
149
 
22
150
  @app.command("checkout")
23
151
  def quick_checkout(
@@ -25,90 +153,62 @@ def quick_checkout(
25
153
  account: Optional[str] = typer.Option(None, "--account", "-a", help="Account name"),
26
154
  duration: int = typer.Option(60, "--duration", "-d", help="Duration in minutes"),
27
155
  reason: Optional[str] = typer.Option(None, "--reason", "-r", help="Reason for checkout"),
28
- credential_type: str = typer.Option("password", "--credential-type", help="Credential type: password, dsskey, passphrase"),
29
- raw: bool = typer.Option(False, "--raw", help="Output only the credential value (for scripts)"),
156
+ access_type: Optional[str] = typer.Option(None, "--access-type", help="Access type: view (credential), rdp, or ssh session. Prompts interactively; defaults to view."),
157
+ credential_type: str = typer.Option("password", "--credential-type", help="For view access: password, dsskey, passphrase"),
158
+ launch: bool = typer.Option(False, "--launch", help="For rdp/ssh: launch the local client after establishing the session"),
159
+ out: Optional[str] = typer.Option(None, "--out", help="For rdp: path to write the .rdp file (default ./<account>@<system>.rdp)"),
160
+ raw: bool = typer.Option(False, "--raw", help="Output only the value (credential / ssh command / rdp path)"),
30
161
  output: str = typer.Option("table", "--output", "-o", help="Output format: table or json"),
31
162
  ) -> None:
32
- """Checkout credentials and show password/SSH key in one step.
163
+ """Checkout a credential, or establish an RDP/SSH session, in one step.
33
164
 
34
- Combines: find system -> find account -> checkout -> show credential
165
+ Combines: find system -> find account -> request -> deliver.
35
166
 
36
- If system or account not provided, prompts interactively.
167
+ Access types:
168
+ view Return the credential (password / dsskey SSH key / passphrase).
169
+ ssh Establish an SSH proxy session and print the ready ssh command.
170
+ rdp Establish an RDP proxy session and write a launchable .rdp file.
171
+
172
+ If system, account, or access type aren't provided, prompts interactively.
37
173
 
38
174
  Examples:
39
- bt pws quick checkout # Interactive mode
40
- bt pws quick checkout -s "axion-finapp-01" -a "root"
41
- bt pws quick checkout -s axion -a root --duration 30
175
+ bt pws quick checkout # interactive (prompts for access type)
176
+ bt pws quick checkout -s axion -a root
42
177
  PASSWORD=$(bt pws quick checkout -s server -a admin --raw)
43
178
  KEY=$(bt pws quick checkout -s server -a admin --credential-type dsskey --raw)
179
+ bt pws quick checkout -s linhost -a app --access-type ssh --launch
180
+ bt pws quick checkout -s win03 -a Administrator --access-type rdp --out win03.rdp
44
181
  """
182
+ # Resolve access type (prompt when interactive, default to view)
183
+ if access_type is None:
184
+ access_type = "view" if raw else prompt_choice(
185
+ "Access type",
186
+ [("view", "credential"), ("ssh", "SSH session"), ("rdp", "RDP session")],
187
+ default="view",
188
+ )
189
+ access_type = access_type.lower()
190
+ if access_type not in ACCESS_TYPES:
191
+ print_error(f"Invalid access type: {access_type}. Must be: view, rdp, ssh")
192
+ raise typer.Exit(1)
193
+
194
+ if access_type in ("ssh", "rdp"):
195
+ _do_session_checkout(
196
+ system=system, account=account, session_type=access_type,
197
+ duration=duration, reason=reason, launch=launch, out=out,
198
+ raw=raw, output=output,
199
+ )
200
+ return
201
+
202
+ quiet = raw or output == "json"
45
203
  try:
46
204
  with get_client() as client:
47
205
  client.authenticate()
206
+ target = _resolve_target(client, system, account, quiet)
207
+ system_id, system_name = target["system_id"], target["system_name"]
208
+ account_id, account_name = target["account_id"], target["account_name"]
48
209
 
49
- # Interactive prompt for system if not provided
50
- if not system:
51
- system = prompt_if_missing(system, "System name (or partial match)")
52
-
53
- # Step 1: Find the system
54
- console.print(f"[dim]Finding system '{system}'...[/dim]") if not raw else None
55
- systems = client.list_managed_systems(search=system)
56
-
57
- if not systems:
58
- print_error(f"No system found matching '{system}'")
59
- raise typer.Exit(1)
60
-
61
- # Try exact match first, then partial
62
- matched_system = None
63
- for s in systems:
64
- if s.get("SystemName", "").lower() == system.lower():
65
- matched_system = s
66
- break
67
-
68
- if not matched_system:
69
- # Use first partial match
70
- matched_system = systems[0]
71
- if len(systems) > 1 and not raw:
72
- console.print(f"[yellow]Multiple matches found, using: {matched_system.get('SystemName')}[/yellow]")
73
-
74
- system_id = matched_system.get("ManagedSystemID")
75
- system_name = matched_system.get("SystemName")
76
-
77
- # Step 2: Find the account
78
- accounts = client.list_managed_accounts(system_id=system_id)
79
-
80
- # Interactive prompt for account if not provided
81
- if not account:
82
- if not accounts:
83
- print_error(f"No accounts found on system '{system_name}'")
84
- raise typer.Exit(1)
85
- account = prompt_from_list(
86
- accounts, "Account name", "AccountName", "AccountName",
87
- f"Accounts on {system_name}", str
88
- )
89
-
90
- console.print(f"[dim]Finding account '{account}' on {system_name}...[/dim]") if not raw else None
91
-
92
- matched_account = None
93
- for acc in accounts:
94
- if acc.get("AccountName", "").lower() == account.lower():
95
- matched_account = acc
96
- break
97
-
98
- if not matched_account:
99
- print_error(f"Account '{account}' not found on system '{system_name}'")
100
- # Show available accounts
101
- if accounts and not raw:
102
- console.print("[dim]Available accounts:[/dim]")
103
- for acc in accounts[:10]:
104
- console.print(f" - {acc.get('AccountName')}")
105
- raise typer.Exit(1)
106
-
107
- account_id = matched_account.get("AccountId", matched_account.get("ManagedAccountID"))
108
- account_name = matched_account.get("AccountName")
109
-
110
- # Step 3: Checkout
111
- console.print(f"[dim]Checking out {account_name}@{system_name}...[/dim]") if not raw else None
210
+ # Checkout
211
+ console.print(f"[dim]Checking out {account_name}@{system_name}...[/dim]") if not quiet else None
112
212
  request = client.create_request(
113
213
  account_id=account_id,
114
214
  system_id=system_id,
@@ -118,7 +218,7 @@ def quick_checkout(
118
218
  )
119
219
  request_id = request.get("RequestID")
120
220
 
121
- # Step 4: Get the credential
221
+ # Get the credential
122
222
  try:
123
223
  ctype = CredentialType(credential_type)
124
224
  except ValueError:
@@ -169,6 +269,174 @@ def quick_checkout(
169
269
  raise typer.Exit(1)
170
270
 
171
271
 
272
+ def _do_session_checkout(
273
+ system: Optional[str],
274
+ account: Optional[str],
275
+ session_type: str,
276
+ duration: int,
277
+ reason: Optional[str],
278
+ launch: bool,
279
+ out: Optional[str],
280
+ raw: bool,
281
+ output: str,
282
+ ) -> None:
283
+ """Shared RDP/SSH session establishment for checkout / ssh / rdp commands.
284
+
285
+ Creates a request with the matching AccessType, establishes the proxy
286
+ session, then delivers the artifact: an ssh command (SSH) or a .rdp file
287
+ (RDP). Does NOT check the request in - the session needs it to stay open.
288
+ """
289
+ st = session_type.lower()
290
+ access = ACCESS_TYPES[st]
291
+ quiet = raw or output == "json"
292
+ try:
293
+ with get_client() as client:
294
+ client.authenticate()
295
+ target = _resolve_target(client, system, account, quiet)
296
+ system_id, system_name = target["system_id"], target["system_name"]
297
+ account_id, account_name = target["account_id"], target["account_name"]
298
+
299
+ console.print(f"[dim]Establishing {access} session to {account_name}@{system_name}...[/dim]") if not quiet else None
300
+ request = client.create_request(
301
+ account_id=account_id,
302
+ system_id=system_id,
303
+ duration_minutes=duration,
304
+ reason=reason,
305
+ access_type=access,
306
+ )
307
+ request_id = request.get("RequestID")
308
+ session = client.create_session(request_id, session_type=access)
309
+
310
+ session_id = session.get("SessionID")
311
+
312
+ if st == "ssh":
313
+ command = _ssh_command(session)
314
+ if raw:
315
+ print(command, end="")
316
+ elif output == "json":
317
+ console.print_json(json.dumps({
318
+ "request_id": request_id,
319
+ "session_id": session_id,
320
+ "system": system_name,
321
+ "account": account_name,
322
+ "command": command,
323
+ "link": session.get("Link"),
324
+ "host": session.get("Host"),
325
+ "port": session.get("Port"),
326
+ }, default=str))
327
+ else:
328
+ console.print(Panel(
329
+ f"[green]SSH session established![/green]\n\n"
330
+ f"System: [cyan]{system_name}[/cyan] Account: [cyan]{account_name}[/cyan]\n"
331
+ f"Request ID: [bold yellow]{request_id}[/bold yellow] Session ID: {session_id}\n\n"
332
+ f"Connect:\n [bold green]{rich_escape(command)}[/bold green]\n\n"
333
+ f"[dim]The proxy injects the password - no credential needed.\n"
334
+ f"Checkin: bt pws credentials checkin {request_id}[/dim]",
335
+ title="Quick SSH",
336
+ ))
337
+ if launch:
338
+ _launch_ssh(command)
339
+ return
340
+
341
+ # RDP
342
+ content = _build_rdp_file(session)
343
+ path = Path(out) if out else Path(f"{account_name}@{system_name}.rdp")
344
+ path.write_text(content)
345
+ abspath = str(path.resolve())
346
+
347
+ if raw:
348
+ print(abspath, end="")
349
+ elif output == "json":
350
+ console.print_json(json.dumps({
351
+ "request_id": request_id,
352
+ "session_id": session_id,
353
+ "system": system_name,
354
+ "account": account_name,
355
+ "rdp_file": abspath,
356
+ "host": session.get("Host"),
357
+ "port": session.get("Port"),
358
+ }, default=str))
359
+ else:
360
+ console.print(Panel(
361
+ f"[green]RDP session established![/green]\n\n"
362
+ f"System: [cyan]{system_name}[/cyan] Account: [cyan]{account_name}[/cyan]\n"
363
+ f"Request ID: [bold yellow]{request_id}[/bold yellow] Session ID: {session_id}\n\n"
364
+ f"RDP file: [bold green]{rich_escape(abspath)}[/bold green]\n"
365
+ f"Proxy: {session.get('Host')}:{session.get('Port')}\n\n"
366
+ f"[dim]Open the file with your RDP client (or pass --launch).\n"
367
+ f"Checkin: bt pws credentials checkin {request_id}[/dim]",
368
+ title="Quick RDP",
369
+ ))
370
+ if launch:
371
+ _launch_rdp(path)
372
+
373
+ except httpx.HTTPStatusError as e:
374
+ print_api_error(e, f"quick {st}")
375
+ raise typer.Exit(1)
376
+ except httpx.RequestError as e:
377
+ print_api_error(e, f"quick {st}")
378
+ raise typer.Exit(1)
379
+ except typer.Exit:
380
+ raise
381
+ except Exception as e:
382
+ print_api_error(e, f"quick {st}")
383
+ raise typer.Exit(1)
384
+
385
+
386
+ @app.command("ssh")
387
+ def quick_ssh(
388
+ system: Optional[str] = typer.Option(None, "--system", "-s", help="System name (partial match supported)"),
389
+ account: Optional[str] = typer.Option(None, "--account", "-a", help="Account name"),
390
+ duration: int = typer.Option(60, "--duration", "-d", help="Duration in minutes"),
391
+ reason: Optional[str] = typer.Option(None, "--reason", "-r", help="Reason for the session"),
392
+ launch: bool = typer.Option(False, "--launch", help="Launch the ssh client after establishing the session"),
393
+ raw: bool = typer.Option(False, "--raw", help="Output only the ssh command (for scripts)"),
394
+ output: str = typer.Option("table", "--output", "-o", help="Output format: table or json"),
395
+ ) -> None:
396
+ """Establish an SSH proxy session and print a ready-to-run ssh command.
397
+
398
+ The Password Safe proxy injects the credential - you never see the password.
399
+
400
+ Examples:
401
+ bt pws quick ssh -s linhost -a app_account02
402
+ bt pws quick ssh -s linhost -a app --launch
403
+ eval "$(bt pws quick ssh -s linhost -a app --raw)"
404
+ """
405
+ _do_session_checkout(
406
+ system=system, account=account, session_type="ssh",
407
+ duration=duration, reason=reason, launch=launch, out=None,
408
+ raw=raw, output=output,
409
+ )
410
+
411
+
412
+ @app.command("rdp")
413
+ def quick_rdp(
414
+ system: Optional[str] = typer.Option(None, "--system", "-s", help="System name (partial match supported)"),
415
+ account: Optional[str] = typer.Option(None, "--account", "-a", help="Account name"),
416
+ duration: int = typer.Option(60, "--duration", "-d", help="Duration in minutes"),
417
+ reason: Optional[str] = typer.Option(None, "--reason", "-r", help="Reason for the session"),
418
+ launch: bool = typer.Option(False, "--launch", help="Launch the RDP client on the generated file"),
419
+ out: Optional[str] = typer.Option(None, "--out", help="Path to write the .rdp file (default ./<account>@<system>.rdp)"),
420
+ raw: bool = typer.Option(False, "--raw", help="Output only the .rdp file path (for scripts)"),
421
+ output: str = typer.Option("table", "--output", "-o", help="Output format: table or json"),
422
+ ) -> None:
423
+ """Establish an RDP proxy session and write a launchable .rdp file.
424
+
425
+ The Password Safe proxy injects the credential; the .rdp file targets the
426
+ proxy with the one-time ticket as the username.
427
+
428
+ Examples:
429
+ bt pws quick rdp -s win03 -a Administrator
430
+ bt pws quick rdp -s win03 -a Administrator --launch
431
+ bt pws quick rdp -s win03 -a Administrator --out ~/sessions/win03.rdp
432
+ """
433
+ _do_session_checkout(
434
+ system=system, account=account, session_type="rdp",
435
+ duration=duration, reason=reason, launch=launch, out=out,
436
+ raw=raw, output=output,
437
+ )
438
+
439
+
172
440
  @app.command("checkin")
173
441
  def quick_checkin(
174
442
  request_id: int = typer.Argument(..., help="Request ID to check in"),
@@ -9,6 +9,7 @@ from rich.table import Table
9
9
 
10
10
  from ...core.output import print_api_error
11
11
  from ..client.base import get_client
12
+ from .audit import _write_csv
12
13
 
13
14
  app = typer.Typer(no_args_is_help=True, help="View and control active Password Safe sessions")
14
15
  console = Console()
@@ -167,6 +168,65 @@ def get_session(
167
168
  raise typer.Exit(1)
168
169
 
169
170
 
171
+ @app.command("export")
172
+ def export_sessions(
173
+ system: int = typer.Option(None, "--system", help="Filter by managed system ID"),
174
+ active: bool = typer.Option(False, "--active", help="Export only active (Status 1) sessions"),
175
+ status: str = typer.Option(None, "--status", "-s", help="Filter by raw numeric status code"),
176
+ file: str = typer.Option(None, "--file", help="Write to this file (otherwise prints to stdout)"),
177
+ fmt: str = typer.Option("json", "--format", help="File format: json or csv"),
178
+ output: str = typer.Option("json", "--output", "-o", help="Stdout format when no --file: json or table"),
179
+ ) -> None:
180
+ """Export session history as an evidence artifact.
181
+
182
+ Same filters as `sessions list`, but exports ALL matching rows (no display
183
+ cap). SIEM is the primary audit channel - this is for ad-hoc evidence pulls.
184
+ To export requests and sessions together, see `bt pws audit export`.
185
+
186
+ Examples:
187
+ bt pws sessions export --file sessions.json
188
+ bt pws sessions export --active --file active-sessions.csv --format csv
189
+ bt pws sessions export --system 13 -o json
190
+ """
191
+ try:
192
+ with get_client() as client:
193
+ client.authenticate()
194
+ sessions = client.list_sessions(system_id=system, status=status)
195
+
196
+ # /Sessions ignores server-side filters (returns full history every call),
197
+ # so filter client-side to make --system/--active actually apply.
198
+ if system is not None:
199
+ sessions = [s for s in sessions if s.get("ManagedSystemID") == system]
200
+ if active:
201
+ sessions = [s for s in sessions if is_active(s)]
202
+
203
+ if file:
204
+ if fmt == "csv":
205
+ _write_csv(file, sessions)
206
+ else:
207
+ with open(file, "w", encoding="utf-8") as f:
208
+ json.dump(sessions, f, indent=2, default=str)
209
+ console.print(f"[green]Exported {len(sessions)} session(s).[/green]")
210
+ console.print(f" [cyan]{file}[/cyan]")
211
+ return
212
+
213
+ # No file: print to stdout
214
+ if output == "json":
215
+ console.print_json(json.dumps(sessions, default=str))
216
+ else:
217
+ print_sessions_table(sessions, title=f"Sessions ({len(sessions)})")
218
+
219
+ except httpx.HTTPStatusError as e:
220
+ print_api_error(e, "export sessions")
221
+ raise typer.Exit(1)
222
+ except httpx.RequestError as e:
223
+ print_api_error(e, "export sessions")
224
+ raise typer.Exit(1)
225
+ except Exception as e:
226
+ print_api_error(e, "export sessions")
227
+ raise typer.Exit(1)
228
+
229
+
170
230
  @app.command("suspend")
171
231
  def suspend_session(
172
232
  session_id: int = typer.Argument(..., help="Session ID to lock/suspend"),