bt-cli 0.4.57__tar.gz → 0.4.58__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 (245) hide show
  1. {bt_cli-0.4.57 → bt_cli-0.4.58}/.github/workflows/release.yml +6 -2
  2. {bt_cli-0.4.57 → bt_cli-0.4.58}/CLAUDE.md +3 -2
  3. {bt_cli-0.4.57 → bt_cli-0.4.58}/PKG-INFO +1 -1
  4. {bt_cli-0.4.57 → bt_cli-0.4.58}/pyproject.toml +1 -1
  5. {bt_cli-0.4.57 → bt_cli-0.4.58}/src/bt_cli/__init__.py +1 -1
  6. {bt_cli-0.4.57 → bt_cli-0.4.58}/src/bt_cli/data/skills/pws/SKILL.md +55 -0
  7. {bt_cli-0.4.57 → bt_cli-0.4.58}/src/bt_cli/pws/client/passwordsafe.py +32 -2
  8. {bt_cli-0.4.57 → bt_cli-0.4.58}/src/bt_cli/pws/commands/__init__.py +3 -0
  9. {bt_cli-0.4.57 → bt_cli-0.4.58}/src/bt_cli/pws/commands/accounts.py +47 -0
  10. bt_cli-0.4.58/src/bt_cli/pws/commands/audit.py +117 -0
  11. {bt_cli-0.4.57 → bt_cli-0.4.58}/src/bt_cli/pws/commands/quick.py +92 -0
  12. bt_cli-0.4.58/src/bt_cli/pws/commands/sessions.py +243 -0
  13. {bt_cli-0.4.57 → bt_cli-0.4.58}/.claude/skills/bt/SKILL.md +0 -0
  14. {bt_cli-0.4.57 → bt_cli-0.4.58}/.claude/skills/entitle/SKILL.md +0 -0
  15. {bt_cli-0.4.57 → bt_cli-0.4.58}/.claude/skills/epml/SKILL.md +0 -0
  16. {bt_cli-0.4.57 → bt_cli-0.4.58}/.claude/skills/epmw/SKILL.md +0 -0
  17. {bt_cli-0.4.57 → bt_cli-0.4.58}/.claude/skills/pra/SKILL.md +0 -0
  18. {bt_cli-0.4.57 → bt_cli-0.4.58}/.claude/skills/pws/SKILL.md +0 -0
  19. {bt_cli-0.4.57 → bt_cli-0.4.58}/.env.example +0 -0
  20. {bt_cli-0.4.57 → bt_cli-0.4.58}/.github/workflows/ci.yml +0 -0
  21. {bt_cli-0.4.57 → bt_cli-0.4.58}/.gitignore +0 -0
  22. {bt_cli-0.4.57 → bt_cli-0.4.58}/README.md +0 -0
  23. {bt_cli-0.4.57 → bt_cli-0.4.58}/assets/cli-help.png +0 -0
  24. {bt_cli-0.4.57 → bt_cli-0.4.58}/assets/cli-output.png +0 -0
  25. {bt_cli-0.4.57 → bt_cli-0.4.58}/bt-cli.spec +0 -0
  26. {bt_cli-0.4.57 → bt_cli-0.4.58}/bt_entry.py +0 -0
  27. {bt_cli-0.4.57 → bt_cli-0.4.58}/epml-clients-server-side-filters-plan.md +0 -0
  28. {bt_cli-0.4.57 → bt_cli-0.4.58}/epml-implementation-plan.md +0 -0
  29. {bt_cli-0.4.57 → bt_cli-0.4.58}/pf-implementation-plan.md +0 -0
  30. {bt_cli-0.4.57 → bt_cli-0.4.58}/scripts/bt_entry.py +0 -0
  31. {bt_cli-0.4.57 → bt_cli-0.4.58}/scripts/pf_onboard.py +0 -0
  32. {bt_cli-0.4.57 → bt_cli-0.4.58}/scripts/sync-package-data.sh +0 -0
  33. {bt_cli-0.4.57 → bt_cli-0.4.58}/src/bt_cli/cli.py +0 -0
  34. {bt_cli-0.4.57 → bt_cli-0.4.58}/src/bt_cli/commands/__init__.py +0 -0
  35. {bt_cli-0.4.57 → bt_cli-0.4.58}/src/bt_cli/commands/configure.py +0 -0
  36. {bt_cli-0.4.57 → bt_cli-0.4.58}/src/bt_cli/commands/learn.py +0 -0
  37. {bt_cli-0.4.57 → bt_cli-0.4.58}/src/bt_cli/commands/quick.py +0 -0
  38. {bt_cli-0.4.57 → bt_cli-0.4.58}/src/bt_cli/core/__init__.py +0 -0
  39. {bt_cli-0.4.57 → bt_cli-0.4.58}/src/bt_cli/core/auth.py +0 -0
  40. {bt_cli-0.4.57 → bt_cli-0.4.58}/src/bt_cli/core/client.py +0 -0
  41. {bt_cli-0.4.57 → bt_cli-0.4.58}/src/bt_cli/core/config.py +0 -0
  42. {bt_cli-0.4.57 → bt_cli-0.4.58}/src/bt_cli/core/config_file.py +0 -0
  43. {bt_cli-0.4.57 → bt_cli-0.4.58}/src/bt_cli/core/csv_utils.py +0 -0
  44. {bt_cli-0.4.57 → bt_cli-0.4.58}/src/bt_cli/core/errors.py +0 -0
  45. {bt_cli-0.4.57 → bt_cli-0.4.58}/src/bt_cli/core/output.py +0 -0
  46. {bt_cli-0.4.57 → bt_cli-0.4.58}/src/bt_cli/core/prompts.py +0 -0
  47. {bt_cli-0.4.57 → bt_cli-0.4.58}/src/bt_cli/core/rest_debug.py +0 -0
  48. {bt_cli-0.4.57 → bt_cli-0.4.58}/src/bt_cli/data/CLAUDE.md +0 -0
  49. {bt_cli-0.4.57 → bt_cli-0.4.58}/src/bt_cli/data/__init__.py +0 -0
  50. {bt_cli-0.4.57 → bt_cli-0.4.58}/src/bt_cli/data/skills/bt/SKILL.md +0 -0
  51. {bt_cli-0.4.57 → bt_cli-0.4.58}/src/bt_cli/data/skills/entitle/SKILL.md +0 -0
  52. {bt_cli-0.4.57 → bt_cli-0.4.58}/src/bt_cli/data/skills/epml/SKILL.md +0 -0
  53. {bt_cli-0.4.57 → bt_cli-0.4.58}/src/bt_cli/data/skills/epmw/SKILL.md +0 -0
  54. {bt_cli-0.4.57 → bt_cli-0.4.58}/src/bt_cli/data/skills/pf/SKILL.md +0 -0
  55. {bt_cli-0.4.57 → bt_cli-0.4.58}/src/bt_cli/data/skills/pra/SKILL.md +0 -0
  56. {bt_cli-0.4.57 → bt_cli-0.4.58}/src/bt_cli/data/skills/secrets/SKILL.md +0 -0
  57. {bt_cli-0.4.57 → bt_cli-0.4.58}/src/bt_cli/entitle/__init__.py +0 -0
  58. {bt_cli-0.4.57 → bt_cli-0.4.58}/src/bt_cli/entitle/client/__init__.py +0 -0
  59. {bt_cli-0.4.57 → bt_cli-0.4.58}/src/bt_cli/entitle/client/base.py +0 -0
  60. {bt_cli-0.4.57 → bt_cli-0.4.58}/src/bt_cli/entitle/commands/__init__.py +0 -0
  61. {bt_cli-0.4.57 → bt_cli-0.4.58}/src/bt_cli/entitle/commands/accounts.py +0 -0
  62. {bt_cli-0.4.57 → bt_cli-0.4.58}/src/bt_cli/entitle/commands/applications.py +0 -0
  63. {bt_cli-0.4.57 → bt_cli-0.4.58}/src/bt_cli/entitle/commands/auth.py +0 -0
  64. {bt_cli-0.4.57 → bt_cli-0.4.58}/src/bt_cli/entitle/commands/bundles.py +0 -0
  65. {bt_cli-0.4.57 → bt_cli-0.4.58}/src/bt_cli/entitle/commands/integrations.py +0 -0
  66. {bt_cli-0.4.57 → bt_cli-0.4.58}/src/bt_cli/entitle/commands/permissions.py +0 -0
  67. {bt_cli-0.4.57 → bt_cli-0.4.58}/src/bt_cli/entitle/commands/policies.py +0 -0
  68. {bt_cli-0.4.57 → bt_cli-0.4.58}/src/bt_cli/entitle/commands/requests.py +0 -0
  69. {bt_cli-0.4.57 → bt_cli-0.4.58}/src/bt_cli/entitle/commands/resources.py +0 -0
  70. {bt_cli-0.4.57 → bt_cli-0.4.58}/src/bt_cli/entitle/commands/roles.py +0 -0
  71. {bt_cli-0.4.57 → bt_cli-0.4.58}/src/bt_cli/entitle/commands/users.py +0 -0
  72. {bt_cli-0.4.57 → bt_cli-0.4.58}/src/bt_cli/entitle/commands/workflows.py +0 -0
  73. {bt_cli-0.4.57 → bt_cli-0.4.58}/src/bt_cli/entitle/models/__init__.py +0 -0
  74. {bt_cli-0.4.57 → bt_cli-0.4.58}/src/bt_cli/entitle/models/bundle.py +0 -0
  75. {bt_cli-0.4.57 → bt_cli-0.4.58}/src/bt_cli/entitle/models/common.py +0 -0
  76. {bt_cli-0.4.57 → bt_cli-0.4.58}/src/bt_cli/entitle/models/integration.py +0 -0
  77. {bt_cli-0.4.57 → bt_cli-0.4.58}/src/bt_cli/entitle/models/permission.py +0 -0
  78. {bt_cli-0.4.57 → bt_cli-0.4.58}/src/bt_cli/entitle/models/policy.py +0 -0
  79. {bt_cli-0.4.57 → bt_cli-0.4.58}/src/bt_cli/entitle/models/resource.py +0 -0
  80. {bt_cli-0.4.57 → bt_cli-0.4.58}/src/bt_cli/entitle/models/role.py +0 -0
  81. {bt_cli-0.4.57 → bt_cli-0.4.58}/src/bt_cli/entitle/models/user.py +0 -0
  82. {bt_cli-0.4.57 → bt_cli-0.4.58}/src/bt_cli/entitle/models/workflow.py +0 -0
  83. {bt_cli-0.4.57 → bt_cli-0.4.58}/src/bt_cli/epml/__init__.py +0 -0
  84. {bt_cli-0.4.57 → bt_cli-0.4.58}/src/bt_cli/epml/client/__init__.py +0 -0
  85. {bt_cli-0.4.57 → bt_cli-0.4.58}/src/bt_cli/epml/client/base.py +0 -0
  86. {bt_cli-0.4.57 → bt_cli-0.4.58}/src/bt_cli/epml/commands/__init__.py +0 -0
  87. {bt_cli-0.4.57 → bt_cli-0.4.58}/src/bt_cli/epml/commands/audit.py +0 -0
  88. {bt_cli-0.4.57 → bt_cli-0.4.58}/src/bt_cli/epml/commands/auth.py +0 -0
  89. {bt_cli-0.4.57 → bt_cli-0.4.58}/src/bt_cli/epml/commands/client_pkg.py +0 -0
  90. {bt_cli-0.4.57 → bt_cli-0.4.58}/src/bt_cli/epml/commands/clients.py +0 -0
  91. {bt_cli-0.4.57 → bt_cli-0.4.58}/src/bt_cli/epml/commands/external_apis.py +0 -0
  92. {bt_cli-0.4.57 → bt_cli-0.4.58}/src/bt_cli/epml/commands/hosts.py +0 -0
  93. {bt_cli-0.4.57 → bt_cli-0.4.58}/src/bt_cli/epml/commands/iolog.py +0 -0
  94. {bt_cli-0.4.57 → bt_cli-0.4.58}/src/bt_cli/epml/commands/license.py +0 -0
  95. {bt_cli-0.4.57 → bt_cli-0.4.58}/src/bt_cli/epml/commands/quick.py +0 -0
  96. {bt_cli-0.4.57 → bt_cli-0.4.58}/src/bt_cli/epml/commands/rbp_cmdgrps.py +0 -0
  97. {bt_cli-0.4.57 → bt_cli-0.4.58}/src/bt_cli/epml/commands/rbp_entitlement.py +0 -0
  98. {bt_cli-0.4.57 → bt_cli-0.4.58}/src/bt_cli/epml/commands/rbp_hostgrps.py +0 -0
  99. {bt_cli-0.4.57 → bt_cli-0.4.58}/src/bt_cli/epml/commands/rbp_policy.py +0 -0
  100. {bt_cli-0.4.57 → bt_cli-0.4.58}/src/bt_cli/epml/commands/rbp_roles.py +0 -0
  101. {bt_cli-0.4.57 → bt_cli-0.4.58}/src/bt_cli/epml/commands/rbp_tests.py +0 -0
  102. {bt_cli-0.4.57 → bt_cli-0.4.58}/src/bt_cli/epml/commands/rbp_tmdategrps.py +0 -0
  103. {bt_cli-0.4.57 → bt_cli-0.4.58}/src/bt_cli/epml/commands/rbp_tx.py +0 -0
  104. {bt_cli-0.4.57 → bt_cli-0.4.58}/src/bt_cli/epml/commands/rbp_usergrps.py +0 -0
  105. {bt_cli-0.4.57 → bt_cli-0.4.58}/src/bt_cli/epml/commands/settings.py +0 -0
  106. {bt_cli-0.4.57 → bt_cli-0.4.58}/src/bt_cli/epml/commands/siems.py +0 -0
  107. {bt_cli-0.4.57 → bt_cli-0.4.58}/src/bt_cli/epml/commands/users.py +0 -0
  108. {bt_cli-0.4.57 → bt_cli-0.4.58}/src/bt_cli/epml/models/__init__.py +0 -0
  109. {bt_cli-0.4.57 → bt_cli-0.4.58}/src/bt_cli/epmw/__init__.py +0 -0
  110. {bt_cli-0.4.57 → bt_cli-0.4.58}/src/bt_cli/epmw/client/__init__.py +0 -0
  111. {bt_cli-0.4.57 → bt_cli-0.4.58}/src/bt_cli/epmw/client/base.py +0 -0
  112. {bt_cli-0.4.57 → bt_cli-0.4.58}/src/bt_cli/epmw/commands/__init__.py +0 -0
  113. {bt_cli-0.4.57 → bt_cli-0.4.58}/src/bt_cli/epmw/commands/audits.py +0 -0
  114. {bt_cli-0.4.57 → bt_cli-0.4.58}/src/bt_cli/epmw/commands/auth.py +0 -0
  115. {bt_cli-0.4.57 → bt_cli-0.4.58}/src/bt_cli/epmw/commands/computers.py +0 -0
  116. {bt_cli-0.4.57 → bt_cli-0.4.58}/src/bt_cli/epmw/commands/events.py +0 -0
  117. {bt_cli-0.4.57 → bt_cli-0.4.58}/src/bt_cli/epmw/commands/groups.py +0 -0
  118. {bt_cli-0.4.57 → bt_cli-0.4.58}/src/bt_cli/epmw/commands/policies.py +0 -0
  119. {bt_cli-0.4.57 → bt_cli-0.4.58}/src/bt_cli/epmw/commands/quick.py +0 -0
  120. {bt_cli-0.4.57 → bt_cli-0.4.58}/src/bt_cli/epmw/commands/requests.py +0 -0
  121. {bt_cli-0.4.57 → bt_cli-0.4.58}/src/bt_cli/epmw/commands/roles.py +0 -0
  122. {bt_cli-0.4.57 → bt_cli-0.4.58}/src/bt_cli/epmw/commands/tasks.py +0 -0
  123. {bt_cli-0.4.57 → bt_cli-0.4.58}/src/bt_cli/epmw/commands/users.py +0 -0
  124. {bt_cli-0.4.57 → bt_cli-0.4.58}/src/bt_cli/epmw/models/__init__.py +0 -0
  125. {bt_cli-0.4.57 → bt_cli-0.4.58}/src/bt_cli/pf/__init__.py +0 -0
  126. {bt_cli-0.4.57 → bt_cli-0.4.58}/src/bt_cli/pf/client/__init__.py +0 -0
  127. {bt_cli-0.4.57 → bt_cli-0.4.58}/src/bt_cli/pf/client/base.py +0 -0
  128. {bt_cli-0.4.57 → bt_cli-0.4.58}/src/bt_cli/pf/commands/__init__.py +0 -0
  129. {bt_cli-0.4.57 → bt_cli-0.4.58}/src/bt_cli/pf/commands/auth.py +0 -0
  130. {bt_cli-0.4.57 → bt_cli-0.4.58}/src/bt_cli/pf/commands/machines.py +0 -0
  131. {bt_cli-0.4.57 → bt_cli-0.4.58}/src/bt_cli/pf/commands/tokens.py +0 -0
  132. {bt_cli-0.4.57 → bt_cli-0.4.58}/src/bt_cli/pf/commands/user.py +0 -0
  133. {bt_cli-0.4.57 → bt_cli-0.4.58}/src/bt_cli/pf/commands/users.py +0 -0
  134. {bt_cli-0.4.57 → bt_cli-0.4.58}/src/bt_cli/pra/__init__.py +0 -0
  135. {bt_cli-0.4.57 → bt_cli-0.4.58}/src/bt_cli/pra/client/__init__.py +0 -0
  136. {bt_cli-0.4.57 → bt_cli-0.4.58}/src/bt_cli/pra/client/base.py +0 -0
  137. {bt_cli-0.4.57 → bt_cli-0.4.58}/src/bt_cli/pra/commands/__init__.py +0 -0
  138. {bt_cli-0.4.57 → bt_cli-0.4.58}/src/bt_cli/pra/commands/auth.py +0 -0
  139. {bt_cli-0.4.57 → bt_cli-0.4.58}/src/bt_cli/pra/commands/group_policies.py +0 -0
  140. {bt_cli-0.4.57 → bt_cli-0.4.58}/src/bt_cli/pra/commands/import_export.py +0 -0
  141. {bt_cli-0.4.57 → bt_cli-0.4.58}/src/bt_cli/pra/commands/jump_clients.py +0 -0
  142. {bt_cli-0.4.57 → bt_cli-0.4.58}/src/bt_cli/pra/commands/jump_groups.py +0 -0
  143. {bt_cli-0.4.57 → bt_cli-0.4.58}/src/bt_cli/pra/commands/jump_items.py +0 -0
  144. {bt_cli-0.4.57 → bt_cli-0.4.58}/src/bt_cli/pra/commands/jumpoints.py +0 -0
  145. {bt_cli-0.4.57 → bt_cli-0.4.58}/src/bt_cli/pra/commands/policies.py +0 -0
  146. {bt_cli-0.4.57 → bt_cli-0.4.58}/src/bt_cli/pra/commands/quick.py +0 -0
  147. {bt_cli-0.4.57 → bt_cli-0.4.58}/src/bt_cli/pra/commands/teams.py +0 -0
  148. {bt_cli-0.4.57 → bt_cli-0.4.58}/src/bt_cli/pra/commands/users.py +0 -0
  149. {bt_cli-0.4.57 → bt_cli-0.4.58}/src/bt_cli/pra/commands/vault.py +0 -0
  150. {bt_cli-0.4.57 → bt_cli-0.4.58}/src/bt_cli/pra/models/__init__.py +0 -0
  151. {bt_cli-0.4.57 → bt_cli-0.4.58}/src/bt_cli/pra/models/common.py +0 -0
  152. {bt_cli-0.4.57 → bt_cli-0.4.58}/src/bt_cli/pra/models/group_policy.py +0 -0
  153. {bt_cli-0.4.57 → bt_cli-0.4.58}/src/bt_cli/pra/models/jump_client.py +0 -0
  154. {bt_cli-0.4.57 → bt_cli-0.4.58}/src/bt_cli/pra/models/jump_group.py +0 -0
  155. {bt_cli-0.4.57 → bt_cli-0.4.58}/src/bt_cli/pra/models/jump_item.py +0 -0
  156. {bt_cli-0.4.57 → bt_cli-0.4.58}/src/bt_cli/pra/models/jumpoint.py +0 -0
  157. {bt_cli-0.4.57 → bt_cli-0.4.58}/src/bt_cli/pra/models/team.py +0 -0
  158. {bt_cli-0.4.57 → bt_cli-0.4.58}/src/bt_cli/pra/models/user.py +0 -0
  159. {bt_cli-0.4.57 → bt_cli-0.4.58}/src/bt_cli/pra/models/vault.py +0 -0
  160. {bt_cli-0.4.57 → bt_cli-0.4.58}/src/bt_cli/pws/__init__.py +0 -0
  161. {bt_cli-0.4.57 → bt_cli-0.4.58}/src/bt_cli/pws/client/__init__.py +0 -0
  162. {bt_cli-0.4.57 → bt_cli-0.4.58}/src/bt_cli/pws/client/base.py +0 -0
  163. {bt_cli-0.4.57 → bt_cli-0.4.58}/src/bt_cli/pws/client/beyondinsight.py +0 -0
  164. {bt_cli-0.4.57 → bt_cli-0.4.58}/src/bt_cli/pws/commands/assets.py +0 -0
  165. {bt_cli-0.4.57 → bt_cli-0.4.58}/src/bt_cli/pws/commands/attributes.py +0 -0
  166. {bt_cli-0.4.57 → bt_cli-0.4.58}/src/bt_cli/pws/commands/auth.py +0 -0
  167. {bt_cli-0.4.57 → bt_cli-0.4.58}/src/bt_cli/pws/commands/clouds.py +0 -0
  168. {bt_cli-0.4.57 → bt_cli-0.4.58}/src/bt_cli/pws/commands/config.py +0 -0
  169. {bt_cli-0.4.57 → bt_cli-0.4.58}/src/bt_cli/pws/commands/credentials.py +0 -0
  170. {bt_cli-0.4.57 → bt_cli-0.4.58}/src/bt_cli/pws/commands/databases.py +0 -0
  171. {bt_cli-0.4.57 → bt_cli-0.4.58}/src/bt_cli/pws/commands/directories.py +0 -0
  172. {bt_cli-0.4.57 → bt_cli-0.4.58}/src/bt_cli/pws/commands/functional.py +0 -0
  173. {bt_cli-0.4.57 → bt_cli-0.4.58}/src/bt_cli/pws/commands/import_export.py +0 -0
  174. {bt_cli-0.4.57 → bt_cli-0.4.58}/src/bt_cli/pws/commands/platforms.py +0 -0
  175. {bt_cli-0.4.57 → bt_cli-0.4.58}/src/bt_cli/pws/commands/search.py +0 -0
  176. {bt_cli-0.4.57 → bt_cli-0.4.58}/src/bt_cli/pws/commands/secrets.py +0 -0
  177. {bt_cli-0.4.57 → bt_cli-0.4.58}/src/bt_cli/pws/commands/systems.py +0 -0
  178. {bt_cli-0.4.57 → bt_cli-0.4.58}/src/bt_cli/pws/commands/users.py +0 -0
  179. {bt_cli-0.4.57 → bt_cli-0.4.58}/src/bt_cli/pws/commands/workgroups.py +0 -0
  180. {bt_cli-0.4.57 → bt_cli-0.4.58}/src/bt_cli/pws/config.py +0 -0
  181. {bt_cli-0.4.57 → bt_cli-0.4.58}/src/bt_cli/pws/models/__init__.py +0 -0
  182. {bt_cli-0.4.57 → bt_cli-0.4.58}/src/bt_cli/pws/models/account.py +0 -0
  183. {bt_cli-0.4.57 → bt_cli-0.4.58}/src/bt_cli/pws/models/asset.py +0 -0
  184. {bt_cli-0.4.57 → bt_cli-0.4.58}/src/bt_cli/pws/models/common.py +0 -0
  185. {bt_cli-0.4.57 → bt_cli-0.4.58}/src/bt_cli/pws/models/system.py +0 -0
  186. {bt_cli-0.4.57 → bt_cli-0.4.58}/src/bt_cli/secrets/__init__.py +0 -0
  187. {bt_cli-0.4.57 → bt_cli-0.4.58}/src/bt_cli/secrets/client/__init__.py +0 -0
  188. {bt_cli-0.4.57 → bt_cli-0.4.58}/src/bt_cli/secrets/client/base.py +0 -0
  189. {bt_cli-0.4.57 → bt_cli-0.4.58}/src/bt_cli/secrets/commands/__init__.py +0 -0
  190. {bt_cli-0.4.57 → bt_cli-0.4.58}/src/bt_cli/secrets/commands/_hints.py +0 -0
  191. {bt_cli-0.4.57 → bt_cli-0.4.58}/src/bt_cli/secrets/commands/auth.py +0 -0
  192. {bt_cli-0.4.57 → bt_cli-0.4.58}/src/bt_cli/secrets/commands/dynamic.py +0 -0
  193. {bt_cli-0.4.57 → bt_cli-0.4.58}/src/bt_cli/secrets/commands/folders.py +0 -0
  194. {bt_cli-0.4.57 → bt_cli-0.4.58}/src/bt_cli/secrets/commands/integrations.py +0 -0
  195. {bt_cli-0.4.57 → bt_cli-0.4.58}/src/bt_cli/secrets/commands/leases.py +0 -0
  196. {bt_cli-0.4.57 → bt_cli-0.4.58}/src/bt_cli/secrets/commands/static.py +0 -0
  197. {bt_cli-0.4.57 → bt_cli-0.4.58}/src/bt_cli/secrets/models/__init__.py +0 -0
  198. {bt_cli-0.4.57 → bt_cli-0.4.58}/tests/__init__.py +0 -0
  199. {bt_cli-0.4.57 → bt_cli-0.4.58}/tests/conftest.py +0 -0
  200. {bt_cli-0.4.57 → bt_cli-0.4.58}/tests/core/__init__.py +0 -0
  201. {bt_cli-0.4.57 → bt_cli-0.4.58}/tests/core/test_auth.py +0 -0
  202. {bt_cli-0.4.57 → bt_cli-0.4.58}/tests/core/test_config.py +0 -0
  203. {bt_cli-0.4.57 → bt_cli-0.4.58}/tests/core/test_config_file.py +0 -0
  204. {bt_cli-0.4.57 → bt_cli-0.4.58}/tests/core/test_errors.py +0 -0
  205. {bt_cli-0.4.57 → bt_cli-0.4.58}/tests/core/test_output.py +0 -0
  206. {bt_cli-0.4.57 → bt_cli-0.4.58}/tests/core/test_rest_debug.py +0 -0
  207. {bt_cli-0.4.57 → bt_cli-0.4.58}/tests/entitle/__init__.py +0 -0
  208. {bt_cli-0.4.57 → bt_cli-0.4.58}/tests/entitle/test_client.py +0 -0
  209. {bt_cli-0.4.57 → bt_cli-0.4.58}/tests/entitle/test_commands.py +0 -0
  210. {bt_cli-0.4.57 → bt_cli-0.4.58}/tests/entitle-smoke-test.sh +0 -0
  211. {bt_cli-0.4.57 → bt_cli-0.4.58}/tests/epml/__init__.py +0 -0
  212. {bt_cli-0.4.57 → bt_cli-0.4.58}/tests/epml/test_client.py +0 -0
  213. {bt_cli-0.4.57 → bt_cli-0.4.58}/tests/epml/test_commands.py +0 -0
  214. {bt_cli-0.4.57 → bt_cli-0.4.58}/tests/epmw/__init__.py +0 -0
  215. {bt_cli-0.4.57 → bt_cli-0.4.58}/tests/epmw/test_client.py +0 -0
  216. {bt_cli-0.4.57 → bt_cli-0.4.58}/tests/epmw/test_commands.py +0 -0
  217. {bt_cli-0.4.57 → bt_cli-0.4.58}/tests/epmw-quick-test-plan.md +0 -0
  218. {bt_cli-0.4.57 → bt_cli-0.4.58}/tests/fixtures/__init__.py +0 -0
  219. {bt_cli-0.4.57 → bt_cli-0.4.58}/tests/fixtures/responses.py +0 -0
  220. {bt_cli-0.4.57 → bt_cli-0.4.58}/tests/integration/__init__.py +0 -0
  221. {bt_cli-0.4.57 → bt_cli-0.4.58}/tests/integration/conftest.py +0 -0
  222. {bt_cli-0.4.57 → bt_cli-0.4.58}/tests/integration/helpers.py +0 -0
  223. {bt_cli-0.4.57 → bt_cli-0.4.58}/tests/integration/test_entitle_integration.py +0 -0
  224. {bt_cli-0.4.57 → bt_cli-0.4.58}/tests/integration/test_epmw_integration.py +0 -0
  225. {bt_cli-0.4.57 → bt_cli-0.4.58}/tests/integration/test_epmw_lifecycle.py +0 -0
  226. {bt_cli-0.4.57 → bt_cli-0.4.58}/tests/integration/test_pra_integration.py +0 -0
  227. {bt_cli-0.4.57 → bt_cli-0.4.58}/tests/integration/test_pra_lifecycle.py +0 -0
  228. {bt_cli-0.4.57 → bt_cli-0.4.58}/tests/integration/test_pws_integration.py +0 -0
  229. {bt_cli-0.4.57 → bt_cli-0.4.58}/tests/integration/test_pws_lifecycle.py +0 -0
  230. {bt_cli-0.4.57 → bt_cli-0.4.58}/tests/pf/__init__.py +0 -0
  231. {bt_cli-0.4.57 → bt_cli-0.4.58}/tests/pf/test_client.py +0 -0
  232. {bt_cli-0.4.57 → bt_cli-0.4.58}/tests/pf/test_commands.py +0 -0
  233. {bt_cli-0.4.57 → bt_cli-0.4.58}/tests/pra/__init__.py +0 -0
  234. {bt_cli-0.4.57 → bt_cli-0.4.58}/tests/pra/test_client.py +0 -0
  235. {bt_cli-0.4.57 → bt_cli-0.4.58}/tests/pra/test_commands.py +0 -0
  236. {bt_cli-0.4.57 → bt_cli-0.4.58}/tests/pra-smoke-test.sh +0 -0
  237. {bt_cli-0.4.57 → bt_cli-0.4.58}/tests/pra-test-plan.md +0 -0
  238. {bt_cli-0.4.57 → bt_cli-0.4.58}/tests/pws/__init__.py +0 -0
  239. {bt_cli-0.4.57 → bt_cli-0.4.58}/tests/pws/test_client.py +0 -0
  240. {bt_cli-0.4.57 → bt_cli-0.4.58}/tests/pws/test_commands.py +0 -0
  241. {bt_cli-0.4.57 → bt_cli-0.4.58}/tests/pws-quick-test-plan.md +0 -0
  242. {bt_cli-0.4.57 → bt_cli-0.4.58}/tests/pws-smoke-test.sh +0 -0
  243. {bt_cli-0.4.57 → bt_cli-0.4.58}/tests/secrets/__init__.py +0 -0
  244. {bt_cli-0.4.57 → bt_cli-0.4.58}/tests/secrets/test_client.py +0 -0
  245. {bt_cli-0.4.57 → bt_cli-0.4.58}/tests/secrets/test_commands.py +0 -0
@@ -48,6 +48,7 @@ jobs:
48
48
  with:
49
49
  name: bt-linux-amd64
50
50
  path: dist/bt-linux-amd64
51
+ retention-days: 7
51
52
 
52
53
  # Build macOS and Windows binaries
53
54
  build-binaries:
@@ -96,6 +97,7 @@ jobs:
96
97
  with:
97
98
  name: ${{ matrix.asset_name }}
98
99
  path: dist/${{ matrix.asset_name }}
100
+ retention-days: 7
99
101
 
100
102
  # Build Python package
101
103
  build-package:
@@ -125,6 +127,7 @@ jobs:
125
127
  with:
126
128
  name: python-package
127
129
  path: dist/*
130
+ retention-days: 7
128
131
 
129
132
  # Create GitHub release with all artifacts
130
133
  release:
@@ -251,6 +254,7 @@ jobs:
251
254
  path: dist
252
255
 
253
256
  - name: Publish to PyPI
257
+ # No password — uses Trusted Publishing (OIDC) via the registered
258
+ # PyPI publisher for this repo/workflow/environment; also enables
259
+ # PEP 740 attestations, which an explicit password would disable.
254
260
  uses: pypa/gh-action-pypi-publish@release/v1
255
- with:
256
- password: ${{ secrets.PYPI_API_TOKEN }}
@@ -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.57**
3
+ BeyondTrust Platform CLI for Password Safe, Entitle, PRA, EPM Windows, EPM Linux, and the BeyondTrust Secrets API. **Version: 0.4.58**
4
4
 
5
5
  ## Setup
6
6
 
@@ -39,7 +39,7 @@ Use these slash commands for detailed product guidance:
39
39
  | Product | Command | Key Operations |
40
40
  |---------|---------|----------------|
41
41
  | Cross-product | `bt quick` | `pasm-onboard`, `pasm-offboard`, `pasm-search` |
42
- | Password Safe | `bt pws` | `systems`, `accounts`, `credentials`, `secrets`, `quick` |
42
+ | Password Safe | `bt pws` | `systems`, `accounts`, `credentials`, `sessions`, `secrets`, `audit`, `quick` |
43
43
  | PRA | `bt pra` | `jump-items`, `vault`, `jump-groups`, `quick` |
44
44
  | Entitle | `bt entitle` | `integrations`, `resources`, `bundles`, `permissions` |
45
45
  | EPM Windows | `bt epmw` | `computers`, `groups`, `policies`, `requests`, `quick` |
@@ -81,6 +81,7 @@ PASSWORD=$(bt pws quick checkout -s server -a admin --raw)
81
81
  - PRA K8s tunnels: Require Linux jumpoint
82
82
  - PWS assets: Must create via workgroup endpoint
83
83
  - ECM integration: PWS system name must match PRA jump item name
84
+ - **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).
84
85
  - Entitle permissions: Use server-side filters (`--resource`, `--user`, `--integration`) - dataset can be 20k+ records
85
86
  - Entitle users: Use `--all` flag only when needed - default fetches first page only
86
87
  - 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.57
3
+ Version: 0.4.58
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.57"
7
+ version = "0.4.58"
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.57"
3
+ __version__ = "0.4.58"
@@ -13,6 +13,7 @@ description: Password Safe commands for credentials, systems, accounts, secrets,
13
13
  - `bt pws secrets safes delete` - Deletes entire safe
14
14
  - `bt pws secrets folders delete` - Deletes folder and contents
15
15
  - `bt pws quick offboard` - Removes system + accounts + asset
16
+ - `bt pws sessions terminate` - Ends an active session (cannot be resumed; use `suspend` to lock instead)
16
17
 
17
18
  List affected resources first, then ask for explicit confirmation.
18
19
 
@@ -38,6 +39,11 @@ bt pws quick search root -o json
38
39
  # Rotate password
39
40
  bt pws quick rotate -s "axion-finapp-01" -a "root"
40
41
 
42
+ # Test stored credential (non-destructive - does NOT rotate)
43
+ # Exit 0 = valid, 2 = failed the test
44
+ bt pws quick test -s "axion-finapp-01" -a "root"
45
+ bt pws accounts test <account_id>
46
+
41
47
  # Onboard system (asset + system + account)
42
48
  # First find your workgroup and functional account IDs (see "Discover IDs" below)
43
49
  bt pws quick onboard -n "my-server" -i "10.0.1.50" -w <workgroup_id>
@@ -144,6 +150,55 @@ bt pws credentials show <request_id>
144
150
  bt pws credentials checkin <request_id>
145
151
  ```
146
152
 
153
+ ## Sessions (view / suspend / terminate)
154
+
155
+ ```bash
156
+ # List sessions - newest first, capped at 25 by default
157
+ bt pws sessions list
158
+ bt pws sessions list --active # only live sessions (started, not ended)
159
+ bt pws sessions list --all -o json # full history
160
+ bt pws sessions get <session_id>
161
+
162
+ # Suspend = lock (user disconnected, session can be resumed)
163
+ bt pws sessions suspend <session_id>
164
+
165
+ # Terminate = end permanently (cannot be resumed) - prompts unless --force
166
+ bt pws sessions terminate <session_id>
167
+ bt pws sessions terminate <session_id> --force
168
+ ```
169
+
170
+ **Gotchas:**
171
+ - `/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
+ - `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.
173
+ - An **active session's `EndTime` is a placeholder equal to `StartTime`** (not null), so you can't identify live sessions by timestamps alone - use the Status code. `suspend` moves Status 1 → 5 (Locked); `terminate` moves it → 7 (Terminated).
174
+ - **`terminate` is `POST /Sessions/{id}/Terminate`** (symmetric with `/Lock`). `/Sessions/{id}` only allows GET/PUT - a DELETE returns 405.
175
+
176
+ ## Permissions (Secrets Safe)
177
+
178
+ Safe permission grant/revoke is already supported (this is the "assign permissions via API" capability):
179
+
180
+ ```bash
181
+ bt pws secrets safes permissions-options # list valid flags
182
+ bt pws secrets safes permissions <safe_id> # show current grants
183
+ bt pws secrets safes grant <safe_id> --user <uid> --permissions "Read,Edit"
184
+ bt pws secrets safes grant <safe_id> --group <gid> --permissions "Manage" --expires <iso8601>
185
+ bt pws secrets safes revoke <safe_id> --user <uid>
186
+ ```
187
+
188
+ **Valid permission flags:** `None, Read, Create, Edit, Delete, Share, Manage` (`Manage` = full control / manage permissions).
189
+
190
+ ## Audit Evidence Export
191
+
192
+ SIEM is the primary audit channel; these commands pull ad-hoc evidence (request + session history):
193
+
194
+ ```bash
195
+ bt pws audit export --file evidence.json # both, JSON
196
+ bt pws audit export --requests --status approved --file reqs.csv --format csv
197
+ bt pws audit export --sessions -o json # stdout
198
+ ```
199
+
200
+ **Note:** with `--format csv` and both sections, files are split (`evidence-requests.csv`, `evidence-sessions.csv`) since CSV is single-table.
201
+
147
202
  ## CSV Import/Export
148
203
 
149
204
  ```bash
@@ -288,6 +288,26 @@ class PasswordSafeMixin:
288
288
  # Use POST to trigger immediate password change
289
289
  return self.post(f"/ManagedAccounts/{account_id}/Credentials/Change")
290
290
 
291
+ def test_managed_account_credential(
292
+ self: "PasswordSafeClient",
293
+ account_id: int,
294
+ ) -> dict[str, Any]:
295
+ """Test the stored credential for a managed account.
296
+
297
+ Tests whether the credential currently stored in Password Safe still
298
+ authenticates against the target system, using the configured
299
+ functional account. This is non-destructive — it does not rotate or
300
+ change the password.
301
+
302
+ Args:
303
+ account_id: Managed account ID to test
304
+
305
+ Returns:
306
+ Response with a ``Success`` boolean indicating whether the
307
+ credential authenticated successfully.
308
+ """
309
+ return self.post(f"/ManagedAccounts/{account_id}/Credentials/Test")
310
+
291
311
  # =========================================================================
292
312
  # Credential Requests (Checkout/Checkin)
293
313
  # =========================================================================
@@ -525,7 +545,14 @@ class PasswordSafeMixin:
525
545
  if status:
526
546
  params["status"] = status
527
547
 
528
- return self.paginate("/Sessions", params=params)
548
+ # NOTE: /Sessions ignores limit/offset and returns the full set on every
549
+ # call, so paginate() would loop forever. Use a single GET and normalize.
550
+ result = self.get("/Sessions", params=params)
551
+ if isinstance(result, list):
552
+ return result
553
+ if isinstance(result, dict) and "Data" in result:
554
+ return result["Data"]
555
+ return []
529
556
 
530
557
  def get_session(self: "PasswordSafeClient", session_id: int) -> dict[str, Any]:
531
558
  """Get a session by ID.
@@ -552,13 +579,16 @@ class PasswordSafeMixin:
552
579
  def terminate_session(self: "PasswordSafeClient", session_id: int) -> dict[str, Any]:
553
580
  """Terminate an active session.
554
581
 
582
+ Note: /Sessions/{id} only allows GET/PUT (DELETE returns 405). Termination
583
+ is a POST to the /Terminate sub-resource, symmetric with /Lock.
584
+
555
585
  Args:
556
586
  session_id: Session ID to terminate
557
587
 
558
588
  Returns:
559
589
  Response from termination
560
590
  """
561
- return self.delete(f"/Sessions/{session_id}")
591
+ return self.post(f"/Sessions/{session_id}/Terminate")
562
592
 
563
593
  # =========================================================================
564
594
  # Applications
@@ -11,6 +11,7 @@ app = typer.Typer(
11
11
  # Import and register command groups
12
12
  from . import auth, systems, accounts, assets, workgroups, platforms, users
13
13
  from . import credentials, config, databases, directories, clouds, secrets, quick, functional, search, attributes
14
+ from . import sessions, audit
14
15
  from .import_export import import_app, export_app
15
16
 
16
17
  app.add_typer(auth.app, name="auth", help="Authentication commands")
@@ -21,6 +22,8 @@ app.add_typer(assets.app, name="assets", help="BeyondInsight assets")
21
22
  app.add_typer(workgroups.app, name="workgroups", help="Manage workgroups")
22
23
  app.add_typer(platforms.app, name="platforms", help="View available platforms")
23
24
  app.add_typer(credentials.app, name="credentials", help="Credential checkout/checkin")
25
+ app.add_typer(sessions.app, name="sessions", help="View and control active sessions")
26
+ app.add_typer(audit.app, name="audit", help="Export audit evidence (request/session history)")
24
27
  app.add_typer(config.app, name="config", help="View Password Safe configuration")
25
28
  app.add_typer(databases.app, name="databases", help="Manage database instances")
26
29
  app.add_typer(directories.app, name="directories", help="Manage directories (Entra ID, AD)")
@@ -372,6 +372,53 @@ def rotate_password(
372
372
  raise typer.Exit(1)
373
373
 
374
374
 
375
+ @app.command("test")
376
+ def test_credential(
377
+ account_id: int = typer.Argument(..., help="Managed account ID to test"),
378
+ ) -> None:
379
+ """Test the stored credential for a managed account.
380
+
381
+ Checks whether the password Password Safe currently holds still
382
+ authenticates against the target system (using the functional account).
383
+ This is non-destructive — it does NOT rotate or change the password.
384
+
385
+ Exit code 0 = credential valid, 2 = credential failed the test.
386
+ """
387
+ try:
388
+ with get_client() as client:
389
+ client.authenticate()
390
+
391
+ # Get account details for display
392
+ account = client.get_managed_account(account_id)
393
+ account_name = account.get("AccountName", "Unknown")
394
+ system_name = account.get("SystemName", str(account.get("ManagedSystemID", "?")))
395
+
396
+ console.print(f"Testing credential for [cyan]{account_name}[/cyan] on [yellow]{system_name}[/yellow]...")
397
+
398
+ result = client.test_managed_account_credential(account_id)
399
+
400
+ success = bool(result.get("Success")) if isinstance(result, dict) else False
401
+
402
+ if success:
403
+ console.print(f"[green]Credential test PASSED for {account_name}@{system_name}[/green]")
404
+ else:
405
+ console.print(f"[red]Credential test FAILED for {account_name}@{system_name}[/red]")
406
+ console.print("[dim]The stored password no longer authenticates. Consider rotating: bt pws accounts rotate {0}[/dim]".format(account_id))
407
+ raise typer.Exit(2)
408
+
409
+ except typer.Exit:
410
+ raise
411
+ except httpx.HTTPStatusError as e:
412
+ print_api_error(e, "test credential")
413
+ raise typer.Exit(1)
414
+ except httpx.RequestError as e:
415
+ print_api_error(e, "test credential")
416
+ raise typer.Exit(1)
417
+ except Exception as e:
418
+ print_api_error(e, "test credential")
419
+ raise typer.Exit(1)
420
+
421
+
375
422
  # =============================================================================
376
423
  # Synced Accounts
377
424
  # =============================================================================
@@ -0,0 +1,117 @@
1
+ """CLI commands for exporting Password Safe audit evidence.
2
+
3
+ These commands export request and session history as evidence artifacts. They
4
+ are NOT a replacement for the SIEM audit stream - the platform's SIEM output is
5
+ the primary, tamper-evident audit channel. Use this for ad-hoc evidence pulls
6
+ (e.g. attaching request/session history to a ticket or access review).
7
+ """
8
+
9
+ import csv
10
+ import json
11
+
12
+ import httpx
13
+ import typer
14
+ from rich.console import Console
15
+
16
+ from ...core.output import print_api_error
17
+ from ..client.base import get_client
18
+
19
+ app = typer.Typer(no_args_is_help=True, help="Export audit evidence (request/session history)")
20
+ console = Console()
21
+
22
+
23
+ def _flatten(rows: list[dict]) -> list[dict]:
24
+ """Stable union of keys across rows for CSV columns."""
25
+ cols: list[str] = []
26
+ for r in rows:
27
+ for k in r.keys():
28
+ if k not in cols:
29
+ cols.append(k)
30
+ return cols
31
+
32
+
33
+ def _write_csv(path: str, rows: list[dict]) -> None:
34
+ cols = _flatten(rows)
35
+ with open(path, "w", newline="", encoding="utf-8") as f:
36
+ writer = csv.DictWriter(f, fieldnames=cols, extrasaction="ignore")
37
+ writer.writeheader()
38
+ for r in rows:
39
+ writer.writerow({k: ("" if r.get(k) is None else r.get(k)) for k in cols})
40
+
41
+
42
+ @app.command("export")
43
+ def export_audit(
44
+ requests: bool = typer.Option(False, "--requests", help="Include credential request history"),
45
+ sessions: bool = typer.Option(False, "--sessions", help="Include session history"),
46
+ status: str = typer.Option(None, "--status", "-s", help="Filter requests by status"),
47
+ file: str = typer.Option(None, "--file", help="Write evidence to this file (otherwise prints to stdout)"),
48
+ fmt: str = typer.Option("json", "--format", help="Output format when writing a file: json or csv"),
49
+ output: str = typer.Option("json", "--output", "-o", help="Stdout format when no --file: json or table"),
50
+ ) -> None:
51
+ """Export request and/or session history as audit evidence.
52
+
53
+ Defaults to BOTH requests and sessions when neither flag is given.
54
+ Note: SIEM is the primary audit channel - this is for ad-hoc evidence pulls.
55
+
56
+ Examples:
57
+ bt pws audit export --file evidence.json
58
+ bt pws audit export --requests --status approved --file requests.csv --format csv
59
+ bt pws audit export --sessions -o json
60
+ """
61
+ # Default: include both when neither is specified
62
+ if not requests and not sessions:
63
+ requests = sessions = True
64
+
65
+ try:
66
+ with get_client() as client:
67
+ client.authenticate()
68
+
69
+ evidence: dict[str, list[dict]] = {}
70
+ if requests:
71
+ evidence["requests"] = client.list_requests(status=status, limit=None)
72
+ if sessions:
73
+ evidence["sessions"] = client.list_sessions()
74
+
75
+ # Write to file
76
+ if file:
77
+ if fmt == "csv":
78
+ # CSV is single-table; pick the one section requested, or split into two files.
79
+ sections = [k for k in ("requests", "sessions") if k in evidence]
80
+ if len(sections) == 1:
81
+ _write_csv(file, evidence[sections[0]])
82
+ written = [file]
83
+ else:
84
+ # Two sections: derive per-section filenames (foo.csv -> foo-requests.csv)
85
+ base = file[:-4] if file.lower().endswith(".csv") else file
86
+ written = []
87
+ for sec in sections:
88
+ path = f"{base}-{sec}.csv"
89
+ _write_csv(path, evidence[sec])
90
+ written.append(path)
91
+ else:
92
+ with open(file, "w", encoding="utf-8") as f:
93
+ json.dump(evidence, f, indent=2, default=str)
94
+ written = [file]
95
+
96
+ counts = ", ".join(f"{k}={len(v)}" for k, v in evidence.items())
97
+ console.print(f"[green]Audit evidence exported ({counts}).[/green]")
98
+ for p in written:
99
+ console.print(f" [cyan]{p}[/cyan]")
100
+ return
101
+
102
+ # No file: print to stdout
103
+ if output == "json":
104
+ console.print_json(json.dumps(evidence, default=str))
105
+ else:
106
+ for section, rows in evidence.items():
107
+ console.print(f"[bold]{section}[/bold]: {len(rows)} records")
108
+
109
+ except httpx.HTTPStatusError as e:
110
+ print_api_error(e, "export audit evidence")
111
+ raise typer.Exit(1)
112
+ except httpx.RequestError as e:
113
+ print_api_error(e, "export audit evidence")
114
+ raise typer.Exit(1)
115
+ except Exception as e:
116
+ print_api_error(e, "export audit evidence")
117
+ raise typer.Exit(1)
@@ -492,6 +492,98 @@ def quick_rotate(
492
492
  raise typer.Exit(1)
493
493
 
494
494
 
495
+ @app.command("test")
496
+ def quick_test(
497
+ system: str = typer.Option(..., "--system", "-s", help="System name (partial match supported)"),
498
+ account: str = typer.Option(..., "--account", "-a", help="Account name"),
499
+ ) -> None:
500
+ """Find an account and test its stored credential.
501
+
502
+ Combines: find system -> find account -> test credential
503
+
504
+ Non-destructive — checks whether the password Password Safe holds still
505
+ authenticates against the system, without rotating it.
506
+
507
+ Exit code 0 = credential valid, 2 = credential failed the test.
508
+
509
+ Examples:
510
+ bt pws quick test -s "axion-finapp-01" -a "root"
511
+ bt pws quick test -s axion -a svc-backup
512
+ """
513
+ try:
514
+ with get_client() as client:
515
+ client.authenticate()
516
+
517
+ # Step 1: Find the system
518
+ console.print(f"[dim]Finding system '{system}'...[/dim]")
519
+ systems = client.list_managed_systems(search=system)
520
+
521
+ if not systems:
522
+ print_error(f"No system found matching '{system}'")
523
+ raise typer.Exit(1)
524
+
525
+ # Try exact match first, then partial
526
+ matched_system = None
527
+ for s in systems:
528
+ if s.get("SystemName", "").lower() == system.lower():
529
+ matched_system = s
530
+ break
531
+
532
+ if not matched_system:
533
+ matched_system = systems[0]
534
+ if len(systems) > 1:
535
+ console.print(f"[yellow]Multiple matches found, using: {matched_system.get('SystemName')}[/yellow]")
536
+
537
+ system_id = matched_system.get("ManagedSystemID")
538
+ system_name = matched_system.get("SystemName")
539
+
540
+ # Step 2: Find the account
541
+ console.print(f"[dim]Finding account '{account}' on {system_name}...[/dim]")
542
+ accounts = client.list_managed_accounts(system_id=system_id)
543
+
544
+ matched_account = None
545
+ for acc in accounts:
546
+ if acc.get("AccountName", "").lower() == account.lower():
547
+ matched_account = acc
548
+ break
549
+
550
+ if not matched_account:
551
+ print_error(f"Account '{account}' not found on system '{system_name}'")
552
+ if accounts:
553
+ console.print("[dim]Available accounts:[/dim]")
554
+ for acc in accounts[:10]:
555
+ console.print(f" - {acc.get('AccountName')}")
556
+ raise typer.Exit(1)
557
+
558
+ account_id = matched_account.get("AccountId", matched_account.get("ManagedAccountID"))
559
+ account_name = matched_account.get("AccountName")
560
+
561
+ # Step 3: Test the credential
562
+ console.print(f"[dim]Testing credential for {account_name}@{system_name}...[/dim]")
563
+ result = client.test_managed_account_credential(account_id)
564
+
565
+ success = bool(result.get("Success")) if isinstance(result, dict) else False
566
+
567
+ if success:
568
+ print_success(f"Credential test PASSED for {account_name}@{system_name}")
569
+ else:
570
+ print_error(f"Credential test FAILED for {account_name}@{system_name}")
571
+ console.print("[dim]The stored password no longer authenticates. Consider: bt pws quick rotate -s ... -a ...[/dim]")
572
+ raise typer.Exit(2)
573
+
574
+ except typer.Exit:
575
+ raise
576
+ except httpx.HTTPStatusError as e:
577
+ print_api_error(e, "quick test")
578
+ raise typer.Exit(1)
579
+ except httpx.RequestError as e:
580
+ print_api_error(e, "quick test")
581
+ raise typer.Exit(1)
582
+ except Exception as e:
583
+ print_api_error(e, "quick test")
584
+ raise typer.Exit(1)
585
+
586
+
495
587
  @app.command("find-secret")
496
588
  def quick_find_secret(
497
589
  query: str = typer.Argument(..., help="Search term (searches folders and secrets)"),