bt-cli 0.4.56__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 (248) hide show
  1. {bt_cli-0.4.56 → bt_cli-0.4.58}/.github/workflows/release.yml +6 -2
  2. {bt_cli-0.4.56 → bt_cli-0.4.58}/CLAUDE.md +5 -4
  3. {bt_cli-0.4.56 → bt_cli-0.4.58}/PKG-INFO +1 -1
  4. {bt_cli-0.4.56 → bt_cli-0.4.58}/pyproject.toml +1 -1
  5. {bt_cli-0.4.56 → bt_cli-0.4.58}/src/bt_cli/__init__.py +1 -1
  6. {bt_cli-0.4.56 → bt_cli-0.4.58}/src/bt_cli/data/skills/pf/SKILL.md +46 -6
  7. {bt_cli-0.4.56 → bt_cli-0.4.58}/src/bt_cli/data/skills/pws/SKILL.md +55 -0
  8. bt_cli-0.4.58/src/bt_cli/pf/client/base.py +225 -0
  9. {bt_cli-0.4.56 → bt_cli-0.4.58}/src/bt_cli/pf/commands/__init__.py +2 -0
  10. bt_cli-0.4.58/src/bt_cli/pf/commands/users.py +298 -0
  11. {bt_cli-0.4.56 → bt_cli-0.4.58}/src/bt_cli/pws/client/passwordsafe.py +32 -2
  12. {bt_cli-0.4.56 → bt_cli-0.4.58}/src/bt_cli/pws/commands/__init__.py +3 -0
  13. {bt_cli-0.4.56 → bt_cli-0.4.58}/src/bt_cli/pws/commands/accounts.py +47 -0
  14. bt_cli-0.4.58/src/bt_cli/pws/commands/audit.py +117 -0
  15. {bt_cli-0.4.56 → bt_cli-0.4.58}/src/bt_cli/pws/commands/quick.py +92 -0
  16. bt_cli-0.4.58/src/bt_cli/pws/commands/sessions.py +243 -0
  17. bt_cli-0.4.58/tests/pf/test_client.py +159 -0
  18. bt_cli-0.4.58/tests/pf/test_commands.py +266 -0
  19. bt_cli-0.4.56/src/bt_cli/pf/client/base.py +0 -133
  20. bt_cli-0.4.56/tests/pf/test_client.py +0 -79
  21. bt_cli-0.4.56/tests/pf/test_commands.py +0 -121
  22. {bt_cli-0.4.56 → bt_cli-0.4.58}/.claude/skills/bt/SKILL.md +0 -0
  23. {bt_cli-0.4.56 → bt_cli-0.4.58}/.claude/skills/entitle/SKILL.md +0 -0
  24. {bt_cli-0.4.56 → bt_cli-0.4.58}/.claude/skills/epml/SKILL.md +0 -0
  25. {bt_cli-0.4.56 → bt_cli-0.4.58}/.claude/skills/epmw/SKILL.md +0 -0
  26. {bt_cli-0.4.56 → bt_cli-0.4.58}/.claude/skills/pra/SKILL.md +0 -0
  27. {bt_cli-0.4.56 → bt_cli-0.4.58}/.claude/skills/pws/SKILL.md +0 -0
  28. {bt_cli-0.4.56 → bt_cli-0.4.58}/.env.example +0 -0
  29. {bt_cli-0.4.56 → bt_cli-0.4.58}/.github/workflows/ci.yml +0 -0
  30. {bt_cli-0.4.56 → bt_cli-0.4.58}/.gitignore +0 -0
  31. {bt_cli-0.4.56 → bt_cli-0.4.58}/README.md +0 -0
  32. {bt_cli-0.4.56 → bt_cli-0.4.58}/assets/cli-help.png +0 -0
  33. {bt_cli-0.4.56 → bt_cli-0.4.58}/assets/cli-output.png +0 -0
  34. {bt_cli-0.4.56 → bt_cli-0.4.58}/bt-cli.spec +0 -0
  35. {bt_cli-0.4.56 → bt_cli-0.4.58}/bt_entry.py +0 -0
  36. {bt_cli-0.4.56 → bt_cli-0.4.58}/epml-clients-server-side-filters-plan.md +0 -0
  37. {bt_cli-0.4.56 → bt_cli-0.4.58}/epml-implementation-plan.md +0 -0
  38. {bt_cli-0.4.56 → bt_cli-0.4.58}/pf-implementation-plan.md +0 -0
  39. {bt_cli-0.4.56 → bt_cli-0.4.58}/scripts/bt_entry.py +0 -0
  40. {bt_cli-0.4.56 → bt_cli-0.4.58}/scripts/pf_onboard.py +0 -0
  41. {bt_cli-0.4.56 → bt_cli-0.4.58}/scripts/sync-package-data.sh +0 -0
  42. {bt_cli-0.4.56 → bt_cli-0.4.58}/src/bt_cli/cli.py +0 -0
  43. {bt_cli-0.4.56 → bt_cli-0.4.58}/src/bt_cli/commands/__init__.py +0 -0
  44. {bt_cli-0.4.56 → bt_cli-0.4.58}/src/bt_cli/commands/configure.py +0 -0
  45. {bt_cli-0.4.56 → bt_cli-0.4.58}/src/bt_cli/commands/learn.py +0 -0
  46. {bt_cli-0.4.56 → bt_cli-0.4.58}/src/bt_cli/commands/quick.py +0 -0
  47. {bt_cli-0.4.56 → bt_cli-0.4.58}/src/bt_cli/core/__init__.py +0 -0
  48. {bt_cli-0.4.56 → bt_cli-0.4.58}/src/bt_cli/core/auth.py +0 -0
  49. {bt_cli-0.4.56 → bt_cli-0.4.58}/src/bt_cli/core/client.py +0 -0
  50. {bt_cli-0.4.56 → bt_cli-0.4.58}/src/bt_cli/core/config.py +0 -0
  51. {bt_cli-0.4.56 → bt_cli-0.4.58}/src/bt_cli/core/config_file.py +0 -0
  52. {bt_cli-0.4.56 → bt_cli-0.4.58}/src/bt_cli/core/csv_utils.py +0 -0
  53. {bt_cli-0.4.56 → bt_cli-0.4.58}/src/bt_cli/core/errors.py +0 -0
  54. {bt_cli-0.4.56 → bt_cli-0.4.58}/src/bt_cli/core/output.py +0 -0
  55. {bt_cli-0.4.56 → bt_cli-0.4.58}/src/bt_cli/core/prompts.py +0 -0
  56. {bt_cli-0.4.56 → bt_cli-0.4.58}/src/bt_cli/core/rest_debug.py +0 -0
  57. {bt_cli-0.4.56 → bt_cli-0.4.58}/src/bt_cli/data/CLAUDE.md +0 -0
  58. {bt_cli-0.4.56 → bt_cli-0.4.58}/src/bt_cli/data/__init__.py +0 -0
  59. {bt_cli-0.4.56 → bt_cli-0.4.58}/src/bt_cli/data/skills/bt/SKILL.md +0 -0
  60. {bt_cli-0.4.56 → bt_cli-0.4.58}/src/bt_cli/data/skills/entitle/SKILL.md +0 -0
  61. {bt_cli-0.4.56 → bt_cli-0.4.58}/src/bt_cli/data/skills/epml/SKILL.md +0 -0
  62. {bt_cli-0.4.56 → bt_cli-0.4.58}/src/bt_cli/data/skills/epmw/SKILL.md +0 -0
  63. {bt_cli-0.4.56 → bt_cli-0.4.58}/src/bt_cli/data/skills/pra/SKILL.md +0 -0
  64. {bt_cli-0.4.56 → bt_cli-0.4.58}/src/bt_cli/data/skills/secrets/SKILL.md +0 -0
  65. {bt_cli-0.4.56 → bt_cli-0.4.58}/src/bt_cli/entitle/__init__.py +0 -0
  66. {bt_cli-0.4.56 → bt_cli-0.4.58}/src/bt_cli/entitle/client/__init__.py +0 -0
  67. {bt_cli-0.4.56 → bt_cli-0.4.58}/src/bt_cli/entitle/client/base.py +0 -0
  68. {bt_cli-0.4.56 → bt_cli-0.4.58}/src/bt_cli/entitle/commands/__init__.py +0 -0
  69. {bt_cli-0.4.56 → bt_cli-0.4.58}/src/bt_cli/entitle/commands/accounts.py +0 -0
  70. {bt_cli-0.4.56 → bt_cli-0.4.58}/src/bt_cli/entitle/commands/applications.py +0 -0
  71. {bt_cli-0.4.56 → bt_cli-0.4.58}/src/bt_cli/entitle/commands/auth.py +0 -0
  72. {bt_cli-0.4.56 → bt_cli-0.4.58}/src/bt_cli/entitle/commands/bundles.py +0 -0
  73. {bt_cli-0.4.56 → bt_cli-0.4.58}/src/bt_cli/entitle/commands/integrations.py +0 -0
  74. {bt_cli-0.4.56 → bt_cli-0.4.58}/src/bt_cli/entitle/commands/permissions.py +0 -0
  75. {bt_cli-0.4.56 → bt_cli-0.4.58}/src/bt_cli/entitle/commands/policies.py +0 -0
  76. {bt_cli-0.4.56 → bt_cli-0.4.58}/src/bt_cli/entitle/commands/requests.py +0 -0
  77. {bt_cli-0.4.56 → bt_cli-0.4.58}/src/bt_cli/entitle/commands/resources.py +0 -0
  78. {bt_cli-0.4.56 → bt_cli-0.4.58}/src/bt_cli/entitle/commands/roles.py +0 -0
  79. {bt_cli-0.4.56 → bt_cli-0.4.58}/src/bt_cli/entitle/commands/users.py +0 -0
  80. {bt_cli-0.4.56 → bt_cli-0.4.58}/src/bt_cli/entitle/commands/workflows.py +0 -0
  81. {bt_cli-0.4.56 → bt_cli-0.4.58}/src/bt_cli/entitle/models/__init__.py +0 -0
  82. {bt_cli-0.4.56 → bt_cli-0.4.58}/src/bt_cli/entitle/models/bundle.py +0 -0
  83. {bt_cli-0.4.56 → bt_cli-0.4.58}/src/bt_cli/entitle/models/common.py +0 -0
  84. {bt_cli-0.4.56 → bt_cli-0.4.58}/src/bt_cli/entitle/models/integration.py +0 -0
  85. {bt_cli-0.4.56 → bt_cli-0.4.58}/src/bt_cli/entitle/models/permission.py +0 -0
  86. {bt_cli-0.4.56 → bt_cli-0.4.58}/src/bt_cli/entitle/models/policy.py +0 -0
  87. {bt_cli-0.4.56 → bt_cli-0.4.58}/src/bt_cli/entitle/models/resource.py +0 -0
  88. {bt_cli-0.4.56 → bt_cli-0.4.58}/src/bt_cli/entitle/models/role.py +0 -0
  89. {bt_cli-0.4.56 → bt_cli-0.4.58}/src/bt_cli/entitle/models/user.py +0 -0
  90. {bt_cli-0.4.56 → bt_cli-0.4.58}/src/bt_cli/entitle/models/workflow.py +0 -0
  91. {bt_cli-0.4.56 → bt_cli-0.4.58}/src/bt_cli/epml/__init__.py +0 -0
  92. {bt_cli-0.4.56 → bt_cli-0.4.58}/src/bt_cli/epml/client/__init__.py +0 -0
  93. {bt_cli-0.4.56 → bt_cli-0.4.58}/src/bt_cli/epml/client/base.py +0 -0
  94. {bt_cli-0.4.56 → bt_cli-0.4.58}/src/bt_cli/epml/commands/__init__.py +0 -0
  95. {bt_cli-0.4.56 → bt_cli-0.4.58}/src/bt_cli/epml/commands/audit.py +0 -0
  96. {bt_cli-0.4.56 → bt_cli-0.4.58}/src/bt_cli/epml/commands/auth.py +0 -0
  97. {bt_cli-0.4.56 → bt_cli-0.4.58}/src/bt_cli/epml/commands/client_pkg.py +0 -0
  98. {bt_cli-0.4.56 → bt_cli-0.4.58}/src/bt_cli/epml/commands/clients.py +0 -0
  99. {bt_cli-0.4.56 → bt_cli-0.4.58}/src/bt_cli/epml/commands/external_apis.py +0 -0
  100. {bt_cli-0.4.56 → bt_cli-0.4.58}/src/bt_cli/epml/commands/hosts.py +0 -0
  101. {bt_cli-0.4.56 → bt_cli-0.4.58}/src/bt_cli/epml/commands/iolog.py +0 -0
  102. {bt_cli-0.4.56 → bt_cli-0.4.58}/src/bt_cli/epml/commands/license.py +0 -0
  103. {bt_cli-0.4.56 → bt_cli-0.4.58}/src/bt_cli/epml/commands/quick.py +0 -0
  104. {bt_cli-0.4.56 → bt_cli-0.4.58}/src/bt_cli/epml/commands/rbp_cmdgrps.py +0 -0
  105. {bt_cli-0.4.56 → bt_cli-0.4.58}/src/bt_cli/epml/commands/rbp_entitlement.py +0 -0
  106. {bt_cli-0.4.56 → bt_cli-0.4.58}/src/bt_cli/epml/commands/rbp_hostgrps.py +0 -0
  107. {bt_cli-0.4.56 → bt_cli-0.4.58}/src/bt_cli/epml/commands/rbp_policy.py +0 -0
  108. {bt_cli-0.4.56 → bt_cli-0.4.58}/src/bt_cli/epml/commands/rbp_roles.py +0 -0
  109. {bt_cli-0.4.56 → bt_cli-0.4.58}/src/bt_cli/epml/commands/rbp_tests.py +0 -0
  110. {bt_cli-0.4.56 → bt_cli-0.4.58}/src/bt_cli/epml/commands/rbp_tmdategrps.py +0 -0
  111. {bt_cli-0.4.56 → bt_cli-0.4.58}/src/bt_cli/epml/commands/rbp_tx.py +0 -0
  112. {bt_cli-0.4.56 → bt_cli-0.4.58}/src/bt_cli/epml/commands/rbp_usergrps.py +0 -0
  113. {bt_cli-0.4.56 → bt_cli-0.4.58}/src/bt_cli/epml/commands/settings.py +0 -0
  114. {bt_cli-0.4.56 → bt_cli-0.4.58}/src/bt_cli/epml/commands/siems.py +0 -0
  115. {bt_cli-0.4.56 → bt_cli-0.4.58}/src/bt_cli/epml/commands/users.py +0 -0
  116. {bt_cli-0.4.56 → bt_cli-0.4.58}/src/bt_cli/epml/models/__init__.py +0 -0
  117. {bt_cli-0.4.56 → bt_cli-0.4.58}/src/bt_cli/epmw/__init__.py +0 -0
  118. {bt_cli-0.4.56 → bt_cli-0.4.58}/src/bt_cli/epmw/client/__init__.py +0 -0
  119. {bt_cli-0.4.56 → bt_cli-0.4.58}/src/bt_cli/epmw/client/base.py +0 -0
  120. {bt_cli-0.4.56 → bt_cli-0.4.58}/src/bt_cli/epmw/commands/__init__.py +0 -0
  121. {bt_cli-0.4.56 → bt_cli-0.4.58}/src/bt_cli/epmw/commands/audits.py +0 -0
  122. {bt_cli-0.4.56 → bt_cli-0.4.58}/src/bt_cli/epmw/commands/auth.py +0 -0
  123. {bt_cli-0.4.56 → bt_cli-0.4.58}/src/bt_cli/epmw/commands/computers.py +0 -0
  124. {bt_cli-0.4.56 → bt_cli-0.4.58}/src/bt_cli/epmw/commands/events.py +0 -0
  125. {bt_cli-0.4.56 → bt_cli-0.4.58}/src/bt_cli/epmw/commands/groups.py +0 -0
  126. {bt_cli-0.4.56 → bt_cli-0.4.58}/src/bt_cli/epmw/commands/policies.py +0 -0
  127. {bt_cli-0.4.56 → bt_cli-0.4.58}/src/bt_cli/epmw/commands/quick.py +0 -0
  128. {bt_cli-0.4.56 → bt_cli-0.4.58}/src/bt_cli/epmw/commands/requests.py +0 -0
  129. {bt_cli-0.4.56 → bt_cli-0.4.58}/src/bt_cli/epmw/commands/roles.py +0 -0
  130. {bt_cli-0.4.56 → bt_cli-0.4.58}/src/bt_cli/epmw/commands/tasks.py +0 -0
  131. {bt_cli-0.4.56 → bt_cli-0.4.58}/src/bt_cli/epmw/commands/users.py +0 -0
  132. {bt_cli-0.4.56 → bt_cli-0.4.58}/src/bt_cli/epmw/models/__init__.py +0 -0
  133. {bt_cli-0.4.56 → bt_cli-0.4.58}/src/bt_cli/pf/__init__.py +0 -0
  134. {bt_cli-0.4.56 → bt_cli-0.4.58}/src/bt_cli/pf/client/__init__.py +0 -0
  135. {bt_cli-0.4.56 → bt_cli-0.4.58}/src/bt_cli/pf/commands/auth.py +0 -0
  136. {bt_cli-0.4.56 → bt_cli-0.4.58}/src/bt_cli/pf/commands/machines.py +0 -0
  137. {bt_cli-0.4.56 → bt_cli-0.4.58}/src/bt_cli/pf/commands/tokens.py +0 -0
  138. {bt_cli-0.4.56 → bt_cli-0.4.58}/src/bt_cli/pf/commands/user.py +0 -0
  139. {bt_cli-0.4.56 → bt_cli-0.4.58}/src/bt_cli/pra/__init__.py +0 -0
  140. {bt_cli-0.4.56 → bt_cli-0.4.58}/src/bt_cli/pra/client/__init__.py +0 -0
  141. {bt_cli-0.4.56 → bt_cli-0.4.58}/src/bt_cli/pra/client/base.py +0 -0
  142. {bt_cli-0.4.56 → bt_cli-0.4.58}/src/bt_cli/pra/commands/__init__.py +0 -0
  143. {bt_cli-0.4.56 → bt_cli-0.4.58}/src/bt_cli/pra/commands/auth.py +0 -0
  144. {bt_cli-0.4.56 → bt_cli-0.4.58}/src/bt_cli/pra/commands/group_policies.py +0 -0
  145. {bt_cli-0.4.56 → bt_cli-0.4.58}/src/bt_cli/pra/commands/import_export.py +0 -0
  146. {bt_cli-0.4.56 → bt_cli-0.4.58}/src/bt_cli/pra/commands/jump_clients.py +0 -0
  147. {bt_cli-0.4.56 → bt_cli-0.4.58}/src/bt_cli/pra/commands/jump_groups.py +0 -0
  148. {bt_cli-0.4.56 → bt_cli-0.4.58}/src/bt_cli/pra/commands/jump_items.py +0 -0
  149. {bt_cli-0.4.56 → bt_cli-0.4.58}/src/bt_cli/pra/commands/jumpoints.py +0 -0
  150. {bt_cli-0.4.56 → bt_cli-0.4.58}/src/bt_cli/pra/commands/policies.py +0 -0
  151. {bt_cli-0.4.56 → bt_cli-0.4.58}/src/bt_cli/pra/commands/quick.py +0 -0
  152. {bt_cli-0.4.56 → bt_cli-0.4.58}/src/bt_cli/pra/commands/teams.py +0 -0
  153. {bt_cli-0.4.56 → bt_cli-0.4.58}/src/bt_cli/pra/commands/users.py +0 -0
  154. {bt_cli-0.4.56 → bt_cli-0.4.58}/src/bt_cli/pra/commands/vault.py +0 -0
  155. {bt_cli-0.4.56 → bt_cli-0.4.58}/src/bt_cli/pra/models/__init__.py +0 -0
  156. {bt_cli-0.4.56 → bt_cli-0.4.58}/src/bt_cli/pra/models/common.py +0 -0
  157. {bt_cli-0.4.56 → bt_cli-0.4.58}/src/bt_cli/pra/models/group_policy.py +0 -0
  158. {bt_cli-0.4.56 → bt_cli-0.4.58}/src/bt_cli/pra/models/jump_client.py +0 -0
  159. {bt_cli-0.4.56 → bt_cli-0.4.58}/src/bt_cli/pra/models/jump_group.py +0 -0
  160. {bt_cli-0.4.56 → bt_cli-0.4.58}/src/bt_cli/pra/models/jump_item.py +0 -0
  161. {bt_cli-0.4.56 → bt_cli-0.4.58}/src/bt_cli/pra/models/jumpoint.py +0 -0
  162. {bt_cli-0.4.56 → bt_cli-0.4.58}/src/bt_cli/pra/models/team.py +0 -0
  163. {bt_cli-0.4.56 → bt_cli-0.4.58}/src/bt_cli/pra/models/user.py +0 -0
  164. {bt_cli-0.4.56 → bt_cli-0.4.58}/src/bt_cli/pra/models/vault.py +0 -0
  165. {bt_cli-0.4.56 → bt_cli-0.4.58}/src/bt_cli/pws/__init__.py +0 -0
  166. {bt_cli-0.4.56 → bt_cli-0.4.58}/src/bt_cli/pws/client/__init__.py +0 -0
  167. {bt_cli-0.4.56 → bt_cli-0.4.58}/src/bt_cli/pws/client/base.py +0 -0
  168. {bt_cli-0.4.56 → bt_cli-0.4.58}/src/bt_cli/pws/client/beyondinsight.py +0 -0
  169. {bt_cli-0.4.56 → bt_cli-0.4.58}/src/bt_cli/pws/commands/assets.py +0 -0
  170. {bt_cli-0.4.56 → bt_cli-0.4.58}/src/bt_cli/pws/commands/attributes.py +0 -0
  171. {bt_cli-0.4.56 → bt_cli-0.4.58}/src/bt_cli/pws/commands/auth.py +0 -0
  172. {bt_cli-0.4.56 → bt_cli-0.4.58}/src/bt_cli/pws/commands/clouds.py +0 -0
  173. {bt_cli-0.4.56 → bt_cli-0.4.58}/src/bt_cli/pws/commands/config.py +0 -0
  174. {bt_cli-0.4.56 → bt_cli-0.4.58}/src/bt_cli/pws/commands/credentials.py +0 -0
  175. {bt_cli-0.4.56 → bt_cli-0.4.58}/src/bt_cli/pws/commands/databases.py +0 -0
  176. {bt_cli-0.4.56 → bt_cli-0.4.58}/src/bt_cli/pws/commands/directories.py +0 -0
  177. {bt_cli-0.4.56 → bt_cli-0.4.58}/src/bt_cli/pws/commands/functional.py +0 -0
  178. {bt_cli-0.4.56 → bt_cli-0.4.58}/src/bt_cli/pws/commands/import_export.py +0 -0
  179. {bt_cli-0.4.56 → bt_cli-0.4.58}/src/bt_cli/pws/commands/platforms.py +0 -0
  180. {bt_cli-0.4.56 → bt_cli-0.4.58}/src/bt_cli/pws/commands/search.py +0 -0
  181. {bt_cli-0.4.56 → bt_cli-0.4.58}/src/bt_cli/pws/commands/secrets.py +0 -0
  182. {bt_cli-0.4.56 → bt_cli-0.4.58}/src/bt_cli/pws/commands/systems.py +0 -0
  183. {bt_cli-0.4.56 → bt_cli-0.4.58}/src/bt_cli/pws/commands/users.py +0 -0
  184. {bt_cli-0.4.56 → bt_cli-0.4.58}/src/bt_cli/pws/commands/workgroups.py +0 -0
  185. {bt_cli-0.4.56 → bt_cli-0.4.58}/src/bt_cli/pws/config.py +0 -0
  186. {bt_cli-0.4.56 → bt_cli-0.4.58}/src/bt_cli/pws/models/__init__.py +0 -0
  187. {bt_cli-0.4.56 → bt_cli-0.4.58}/src/bt_cli/pws/models/account.py +0 -0
  188. {bt_cli-0.4.56 → bt_cli-0.4.58}/src/bt_cli/pws/models/asset.py +0 -0
  189. {bt_cli-0.4.56 → bt_cli-0.4.58}/src/bt_cli/pws/models/common.py +0 -0
  190. {bt_cli-0.4.56 → bt_cli-0.4.58}/src/bt_cli/pws/models/system.py +0 -0
  191. {bt_cli-0.4.56 → bt_cli-0.4.58}/src/bt_cli/secrets/__init__.py +0 -0
  192. {bt_cli-0.4.56 → bt_cli-0.4.58}/src/bt_cli/secrets/client/__init__.py +0 -0
  193. {bt_cli-0.4.56 → bt_cli-0.4.58}/src/bt_cli/secrets/client/base.py +0 -0
  194. {bt_cli-0.4.56 → bt_cli-0.4.58}/src/bt_cli/secrets/commands/__init__.py +0 -0
  195. {bt_cli-0.4.56 → bt_cli-0.4.58}/src/bt_cli/secrets/commands/_hints.py +0 -0
  196. {bt_cli-0.4.56 → bt_cli-0.4.58}/src/bt_cli/secrets/commands/auth.py +0 -0
  197. {bt_cli-0.4.56 → bt_cli-0.4.58}/src/bt_cli/secrets/commands/dynamic.py +0 -0
  198. {bt_cli-0.4.56 → bt_cli-0.4.58}/src/bt_cli/secrets/commands/folders.py +0 -0
  199. {bt_cli-0.4.56 → bt_cli-0.4.58}/src/bt_cli/secrets/commands/integrations.py +0 -0
  200. {bt_cli-0.4.56 → bt_cli-0.4.58}/src/bt_cli/secrets/commands/leases.py +0 -0
  201. {bt_cli-0.4.56 → bt_cli-0.4.58}/src/bt_cli/secrets/commands/static.py +0 -0
  202. {bt_cli-0.4.56 → bt_cli-0.4.58}/src/bt_cli/secrets/models/__init__.py +0 -0
  203. {bt_cli-0.4.56 → bt_cli-0.4.58}/tests/__init__.py +0 -0
  204. {bt_cli-0.4.56 → bt_cli-0.4.58}/tests/conftest.py +0 -0
  205. {bt_cli-0.4.56 → bt_cli-0.4.58}/tests/core/__init__.py +0 -0
  206. {bt_cli-0.4.56 → bt_cli-0.4.58}/tests/core/test_auth.py +0 -0
  207. {bt_cli-0.4.56 → bt_cli-0.4.58}/tests/core/test_config.py +0 -0
  208. {bt_cli-0.4.56 → bt_cli-0.4.58}/tests/core/test_config_file.py +0 -0
  209. {bt_cli-0.4.56 → bt_cli-0.4.58}/tests/core/test_errors.py +0 -0
  210. {bt_cli-0.4.56 → bt_cli-0.4.58}/tests/core/test_output.py +0 -0
  211. {bt_cli-0.4.56 → bt_cli-0.4.58}/tests/core/test_rest_debug.py +0 -0
  212. {bt_cli-0.4.56 → bt_cli-0.4.58}/tests/entitle/__init__.py +0 -0
  213. {bt_cli-0.4.56 → bt_cli-0.4.58}/tests/entitle/test_client.py +0 -0
  214. {bt_cli-0.4.56 → bt_cli-0.4.58}/tests/entitle/test_commands.py +0 -0
  215. {bt_cli-0.4.56 → bt_cli-0.4.58}/tests/entitle-smoke-test.sh +0 -0
  216. {bt_cli-0.4.56 → bt_cli-0.4.58}/tests/epml/__init__.py +0 -0
  217. {bt_cli-0.4.56 → bt_cli-0.4.58}/tests/epml/test_client.py +0 -0
  218. {bt_cli-0.4.56 → bt_cli-0.4.58}/tests/epml/test_commands.py +0 -0
  219. {bt_cli-0.4.56 → bt_cli-0.4.58}/tests/epmw/__init__.py +0 -0
  220. {bt_cli-0.4.56 → bt_cli-0.4.58}/tests/epmw/test_client.py +0 -0
  221. {bt_cli-0.4.56 → bt_cli-0.4.58}/tests/epmw/test_commands.py +0 -0
  222. {bt_cli-0.4.56 → bt_cli-0.4.58}/tests/epmw-quick-test-plan.md +0 -0
  223. {bt_cli-0.4.56 → bt_cli-0.4.58}/tests/fixtures/__init__.py +0 -0
  224. {bt_cli-0.4.56 → bt_cli-0.4.58}/tests/fixtures/responses.py +0 -0
  225. {bt_cli-0.4.56 → bt_cli-0.4.58}/tests/integration/__init__.py +0 -0
  226. {bt_cli-0.4.56 → bt_cli-0.4.58}/tests/integration/conftest.py +0 -0
  227. {bt_cli-0.4.56 → bt_cli-0.4.58}/tests/integration/helpers.py +0 -0
  228. {bt_cli-0.4.56 → bt_cli-0.4.58}/tests/integration/test_entitle_integration.py +0 -0
  229. {bt_cli-0.4.56 → bt_cli-0.4.58}/tests/integration/test_epmw_integration.py +0 -0
  230. {bt_cli-0.4.56 → bt_cli-0.4.58}/tests/integration/test_epmw_lifecycle.py +0 -0
  231. {bt_cli-0.4.56 → bt_cli-0.4.58}/tests/integration/test_pra_integration.py +0 -0
  232. {bt_cli-0.4.56 → bt_cli-0.4.58}/tests/integration/test_pra_lifecycle.py +0 -0
  233. {bt_cli-0.4.56 → bt_cli-0.4.58}/tests/integration/test_pws_integration.py +0 -0
  234. {bt_cli-0.4.56 → bt_cli-0.4.58}/tests/integration/test_pws_lifecycle.py +0 -0
  235. {bt_cli-0.4.56 → bt_cli-0.4.58}/tests/pf/__init__.py +0 -0
  236. {bt_cli-0.4.56 → bt_cli-0.4.58}/tests/pra/__init__.py +0 -0
  237. {bt_cli-0.4.56 → bt_cli-0.4.58}/tests/pra/test_client.py +0 -0
  238. {bt_cli-0.4.56 → bt_cli-0.4.58}/tests/pra/test_commands.py +0 -0
  239. {bt_cli-0.4.56 → bt_cli-0.4.58}/tests/pra-smoke-test.sh +0 -0
  240. {bt_cli-0.4.56 → bt_cli-0.4.58}/tests/pra-test-plan.md +0 -0
  241. {bt_cli-0.4.56 → bt_cli-0.4.58}/tests/pws/__init__.py +0 -0
  242. {bt_cli-0.4.56 → bt_cli-0.4.58}/tests/pws/test_client.py +0 -0
  243. {bt_cli-0.4.56 → bt_cli-0.4.58}/tests/pws/test_commands.py +0 -0
  244. {bt_cli-0.4.56 → bt_cli-0.4.58}/tests/pws-quick-test-plan.md +0 -0
  245. {bt_cli-0.4.56 → bt_cli-0.4.58}/tests/pws-smoke-test.sh +0 -0
  246. {bt_cli-0.4.56 → bt_cli-0.4.58}/tests/secrets/__init__.py +0 -0
  247. {bt_cli-0.4.56 → bt_cli-0.4.58}/tests/secrets/test_client.py +0 -0
  248. {bt_cli-0.4.56 → 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.56**
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,13 +39,13 @@ 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` |
46
46
  | EPM Linux | `bt epml` | `rbp` (cmdgrps/hostgrps/usergrps/tmdategrps/roles/policy/tests/tx), `settings`, `users`, `audit`, `siems`, `quick` |
47
47
  | Secrets API | `bt secrets` | `folders`, `static`, `dynamic` (incl. `generate`), `leases`, `integrations` |
48
- | Pathfinder | `bt pf` | `whoami`, `access`, `pats`, `mcp-tokens`, `machines` |
48
+ | Pathfinder | `bt pf` | `whoami`, `access`, `pats`, `mcp-tokens`, `machines`, `users` (invite/grant/revoke/site membership) |
49
49
 
50
50
  ## Common Patterns
51
51
 
@@ -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)
@@ -91,7 +92,7 @@ PASSWORD=$(bt pws quick checkout -s server -a admin --raw)
91
92
  - **EPML role banner format**: `bt epml rbp roles create --banner-text "Title"` builds a `############`-framed message using `%rbprole%`/`%event%` server substitutions, mirroring the appliance's existing roles. See the worked role example in the EPM-L SKILL.md.
92
93
  - **Secrets dynamic path is `/dynamic`** (NOT `/dynamic-secrets` as the doc claims); version header is `2026-04-28` (NOT 2026-01-02; server also rejects every earlier dated header — bumps are mandatory, not optional); `dynamic/{name}/generate` returns 201; `/leases/revoke` is scope-gated and 403 is the expected lab response — `bt secrets leases revoke` exits 3 with a clean warning, not generic failure. Documented in `src/bt_cli/data/skills/secrets/SKILL.md`.
93
94
  - **Secrets `generate` mints real AWS STS credentials and is audited** — don't loop it in dev. Use `dynamic list`/`get` + `leases list` for browsing.
94
- - **PF URL mapping**: Nomine swagger paths `/api/auth/X` → gateway `https://api.beyondtrust.io/site/<site-id>/platform/auth/X`. **PATs are site-bound** (minted on the active portal site; other sites → `401 Access denied for this site`). Org-level endpoints (`auth/Users`, `auth/Auditing`, `auth/SAMLProviders`) are 403 for PAT principals; user onboarding & token create/revoke need cookie auth on app.beyondtrust.io. Details in `src/bt_cli/data/skills/pf/SKILL.md`.
95
+ - **PF URL mapping**: Nomine swagger paths `/api/auth/X` → gateway `https://api.beyondtrust.io/site/<site-id>/platform/auth/X`. **PATs are site-bound** (minted on the active portal site; other sites → `401 Access denied for this site`). Org-level GETs (`auth/Users`, `auth/Auditing`, `auth/SAMLProviders`, `auth/*access/org`) are 403 for PAT principals; token create/revoke still needs cookie auth on app.beyondtrust.io. Onboarding/access writes (`UserInvitation*`, `Users/delete`, `productaccess`/`siteaccess` grant/revoke) are deployed on the PAT gateway (`bt pf users ...`) but PAT principals get the same org-level 403 on POST (live-verified 2026-06-11) — commands exit 3 with a cookie-auth hint until the gateway opens up. Details in `src/bt_cli/data/skills/pf/SKILL.md`.
95
96
 
96
97
  ## Functional vs Managed Accounts
97
98
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: bt-cli
3
- Version: 0.4.56
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.56"
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.56"
3
+ __version__ = "0.4.58"
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: pf
3
- description: Pathfinder platform commands — whoami, site/product access, PAT/MCP token listing, machine registry. Use when working with the Pathfinder portal API (Nomine), platform PATs, site IDs, or registered machine identities.
3
+ description: Pathfinder platform commands — whoami, site/product access, PAT/MCP token listing, machine registry, user invites and product/site access grants. Use when working with the Pathfinder portal API (Nomine), platform PATs, site IDs, user onboarding, or registered machine identities.
4
4
  ---
5
5
 
6
6
  # Pathfinder Platform (`bt pf`)
@@ -45,6 +45,37 @@ bt pf machines list # registered machine IDs
45
45
  bt pf machines get <uuid> # one machine's record (secret hash never shown)
46
46
  ```
47
47
 
48
+ ## User onboarding & access assignment (`bt pf users`)
49
+
50
+ ```bash
51
+ # Invite a user to the org (sends a real email; confirms unless --yes).
52
+ # Default site = BT_PF_SITE_ID; -p grants products on that site at invite time.
53
+ bt pf users invite -e new.user@corp.com --first-name New --last-name User \
54
+ -p pra -p epml
55
+
56
+ bt pf users cancel-invite new.user@corp.com # cancel a pending invite
57
+
58
+ # Per-user product access on a site (product slugs are lowercase:
59
+ # epmwm, pra, psc, rs, entitle, epml, insights, secrets)
60
+ bt pf users grant -u <user-uuid> -p epml # default site
61
+ bt pf users revoke -u <user-uuid> -p epml -s <site-uuid>
62
+
63
+ # Site membership (optionally granting products in the same call)
64
+ bt pf users add-to-site -u <user-uuid> -s <site-uuid> -p pra -p psc
65
+ bt pf users remove-from-site -u <user-uuid> -s <site-uuid>
66
+
67
+ bt pf users delete <user-uuid> [...] # bulk delete (org admin; confirms)
68
+ bt pf users list # org-level — 403 over PAT, exits 3 with hint
69
+ ```
70
+
71
+ **Write-path caveat (verified 2026-06-11):** all of these routes are
72
+ *deployed* on the PAT gateway (GET returns 405 = POST/DELETE exist), but PAT
73
+ principals are **not authorized** — a live productaccess grant POST returned
74
+ the same empty-body 403 as the org-level GETs. Every `bt pf users` write
75
+ currently exits 3 with the cookie-auth hint. The commands are correct and
76
+ will start working the moment the gateway grants PATs the org-level
77
+ permission (or a cookie-auth mode lands).
78
+
48
79
  ## What works over PAT vs what doesn't (verified 2026-06-11)
49
80
 
50
81
  | Endpoint | PAT result |
@@ -52,7 +83,10 @@ bt pf machines get <uuid> # one machine's record (secret hash never shown)
52
83
  | `auth/UserInfo`, `auth/User/Access`, `auth/User/Token`, `auth/User/McpToken` | 200 — any valid PAT |
53
84
  | `auth/Machine`, `auth/Machine/{id}` | 200 — only with a PAT from a product-bearing site (Administration-site PATs get 403) |
54
85
  | `auth/Users`, `auth/SAMLProviders`, `auth/Auditing` | 403 — org-level, blocked for PAT principals regardless of scopes seen so far |
55
- | `auth/UserInvitation` | 405 on GET (POST-only; reachable — onboarding over PAT untested) |
86
+ | `auth/productaccess/org`, `auth/access/org`, `auth/siteaccess/org` | 403 — same org-level block |
87
+ | `auth/productaccess` (collection GET) | 404 — not deployed on the gateway (also 404 on the app host) |
88
+ | `auth/UserInvitation`, `auth/UserInvitation/Product`, `auth/UserInvitation/Cancel`, `auth/Users/delete` | 405 on GET — POST routes deployed, but PAT writes hit the org-level 403 (see below) |
89
+ | `auth/productaccess/{p}/site/{s}/user/{u}`, `auth/siteaccess/{s}/user/{u}` | 405 on GET — routes deployed; live grant POST → empty-body 403 (org-level block applies to writes too) |
56
90
 
57
91
  PAT `access` scopes are the lowercase product IDs of the mint site (visible in
58
92
  `bt pf pats list`). The org-level 403s may require a scope no UI-minted PAT
@@ -70,7 +104,13 @@ but never activated.
70
104
 
71
105
  ## Not exposed via PAT (cookie-auth only, app.beyondtrust.io/api/auth/*)
72
106
 
73
- User onboarding (UserInvitation POST + Accept), PAT/MCP token creation and
74
- revocation, user listing, auditing, SAML providers. These need the `access`
75
- session cookie (browser JWT ~5min TTL, or the programmatic
76
- SignIn → /session/login flow — password-auth users only, not federated).
107
+ Confirmed PAT-blocked (403): user listing, auditing, SAML providers, the
108
+ org-default access-rule reads. Still cookie-only: PAT/MCP token creation and
109
+ revocation, invitation Accept. These need the `access` session cookie
110
+ (browser JWT ~5min TTL, or the programmatic SignIn → /session/login flow —
111
+ password-auth users only, not federated).
112
+
113
+ Invite body shape (spec `InviteOrganizationUserInput`): `{email, firstName,
114
+ lastName, siteMetaData: [{siteId, name, residency}], productMetaData:
115
+ [{name, siteId, siteName}]}` — `bt pf users invite` builds this, defaulting
116
+ the site to `BT_PF_SITE_ID` and omitting name/residency unless given.
@@ -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
@@ -0,0 +1,225 @@
1
+ """Pathfinder platform API client (PAT bearer auth, /site/<id>/platform/auth/... URLs)."""
2
+
3
+ import logging
4
+ from typing import Any, Dict, Optional
5
+ from urllib.parse import quote as _urlquote
6
+
7
+ import httpx
8
+
9
+ from bt_cli.core.client import _warn_ssl_disabled
10
+ from bt_cli.core.config import PFConfig, load_pf_config
11
+ from bt_cli.core.rest_debug import get_event_hooks
12
+
13
+ logger = logging.getLogger(__name__)
14
+
15
+
16
+ class PFClient:
17
+ """Client for the Pathfinder platform API (internal name: Nomine).
18
+
19
+ URL composition:
20
+
21
+ {api_url}/site/{site_id}/platform/auth/<path>
22
+
23
+ The Nomine swagger writes paths as `/api/auth/X`; on the gateway the
24
+ product segment is `platform` and the path keeps the `auth/` prefix —
25
+ so spec `/api/auth/UserInfo` is gateway `/site/<id>/platform/auth/UserInfo`.
26
+
27
+ Auth: `Authorization: Bearer <PAT>`. PATs are site-bound — minted while
28
+ a site is active in the portal, valid only for that site's paths
29
+ (`401 Access denied for this site` elsewhere). A just-minted PAT can
30
+ take ~1s to propagate to the edge validator (transient
31
+ `401 Personal access token not found`).
32
+
33
+ Gateway-blocked for PAT principals (verified 2026-06-11): the entire
34
+ org-admin surface — reads (`auth/Users`, `auth/SAMLProviders`,
35
+ `auth/Auditing`, `auth/*access/org`) AND writes (`auth/UserInvitation*`,
36
+ `auth/Users/delete`, the productaccess/siteaccess grant/revoke routes).
37
+ The block's signature is an empty-body 403; the routes themselves are
38
+ deployed (GET returns 405). `auth/Machine` works only with a PAT for a
39
+ product-bearing site (not the Administration site).
40
+ """
41
+
42
+ def __init__(self, config: PFConfig):
43
+ self.config = config
44
+ self.base_url = config.api_url.rstrip("/")
45
+ self.site_id = config.site_id
46
+ self._base = f"{self.base_url}/site/{self.site_id}/platform"
47
+
48
+ if not config.verify_ssl:
49
+ _warn_ssl_disabled()
50
+
51
+ self._client = httpx.Client(
52
+ verify=config.verify_ssl,
53
+ timeout=config.timeout,
54
+ event_hooks=get_event_hooks(),
55
+ )
56
+
57
+ # ---- lifecycle ------------------------------------------------------
58
+
59
+ def close(self) -> None:
60
+ self._client.close()
61
+
62
+ def __enter__(self) -> "PFClient":
63
+ return self
64
+
65
+ def __exit__(self, *args: Any) -> None:
66
+ self.close()
67
+
68
+ # ---- low-level ------------------------------------------------------
69
+
70
+ @staticmethod
71
+ def _q(value: Any) -> str:
72
+ """URL-quote a path segment to prevent traversal / query injection."""
73
+ return _urlquote(str(value), safe="")
74
+
75
+ def _url(self, path: str) -> str:
76
+ if not path.startswith("/"):
77
+ path = "/" + path
78
+ return f"{self._base}{path}"
79
+
80
+ def _headers(self) -> Dict[str, str]:
81
+ return {
82
+ "Authorization": f"Bearer {self.config.pat}",
83
+ "Accept": "application/json",
84
+ "Content-Type": "application/json",
85
+ }
86
+
87
+ def get(self, path: str, params: Optional[Dict[str, Any]] = None) -> Any:
88
+ """GET a platform path. Returns parsed JSON (None for empty bodies)."""
89
+ response = self._client.get(self._url(path), headers=self._headers(), params=params)
90
+ response.raise_for_status()
91
+ return self._parse(response)
92
+
93
+ def post(self, path: str, json: Any = None) -> Any:
94
+ """POST a platform path. Returns parsed JSON (None for empty bodies)."""
95
+ response = self._client.post(self._url(path), headers=self._headers(), json=json)
96
+ response.raise_for_status()
97
+ return self._parse(response)
98
+
99
+ def delete(self, path: str) -> Any:
100
+ """DELETE a platform path. Returns parsed JSON (None for empty bodies)."""
101
+ response = self._client.delete(self._url(path), headers=self._headers())
102
+ response.raise_for_status()
103
+ return self._parse(response)
104
+
105
+ @staticmethod
106
+ def _parse(response: httpx.Response) -> Any:
107
+ if not response.content:
108
+ return None
109
+ try:
110
+ return response.json()
111
+ except ValueError:
112
+ return response.text
113
+
114
+ # ---- verified endpoints ----------------------------------------------
115
+
116
+ def get_user_info(self) -> Dict[str, Any]:
117
+ """GET /auth/UserInfo — the PAT owner's profile.
118
+
119
+ Returns `{sub, given_name, family_name, email, role, groups,
120
+ isFederated, mfaEnabled}`.
121
+ """
122
+ return self.get("/auth/UserInfo")
123
+
124
+ def get_user_access(self) -> Dict[str, Any]:
125
+ """GET /auth/User/Access — `{siteAccessRules: [...], productAccessRules: [...]}`."""
126
+ return self.get("/auth/User/Access")
127
+
128
+ def list_pats(self) -> Dict[str, Any]:
129
+ """GET /auth/User/Token — `{personalAccessTokens: [...], success, ...}`."""
130
+ return self.get("/auth/User/Token")
131
+
132
+ def list_mcp_tokens(self) -> Dict[str, Any]:
133
+ """GET /auth/User/McpToken — `{mcpTokens: [...], success, ...}`."""
134
+ return self.get("/auth/User/McpToken")
135
+
136
+ def list_machines(self) -> Dict[str, Any]:
137
+ """GET /auth/Machine — `{machines: [uuid, ...], success, ...}`.
138
+
139
+ Only reachable with a PAT minted on a product-bearing site; an
140
+ Administration-site PAT gets 403.
141
+ """
142
+ return self.get("/auth/Machine")
143
+
144
+ def get_machine(self, machine_id: str) -> Dict[str, Any]:
145
+ """GET /auth/Machine/{id} — details for one registered machine."""
146
+ return self.get(f"/auth/Machine/{self._q(machine_id)}")
147
+
148
+ # ---- users & onboarding ----------------------------------------------
149
+ #
150
+ # Route status over PAT (probed 2026-06-11): the invitation POSTs,
151
+ # Users/delete POST, and the productaccess/siteaccess grant/revoke
152
+ # routes all exist (GET returns 405), but PAT principals are NOT
153
+ # authorized — a live productaccess grant POST returned the same
154
+ # empty-body 403 as the org-level GETs (`auth/Users`,
155
+ # `auth/productaccess/org`, `auth/access/org`, `auth/siteaccess/org`).
156
+ # The bare `auth/productaccess` collection GET is 404 (not deployed).
157
+ # These methods stay wired for cookie-auth support or a future gateway
158
+ # policy change; commands surface the 403 as exit 3 with a hint.
159
+
160
+ def list_users(self, params: Optional[Dict[str, Any]] = None) -> Any:
161
+ """GET /auth/Users — org user list (plain array).
162
+
163
+ Filters: email, firstName, lastName, active, siteId, userId,
164
+ mfaEnabled, page (1-based), pageSize. 403 for PAT principals as of
165
+ 2026-06-11 — works over cookie auth on app.beyondtrust.io.
166
+ """
167
+ return self.get("/auth/Users", params=params)
168
+
169
+ def invite_user(self, payload: Dict[str, Any]) -> Dict[str, Any]:
170
+ """POST /auth/UserInvitation — invite a user to the organization.
171
+
172
+ Payload: `{email, firstName, lastName, siteMetaData: [{siteId,
173
+ name, residency}], productMetaData: [{name, siteId, siteName}]}`.
174
+ Sends a real invitation email.
175
+ """
176
+ return self.post("/auth/UserInvitation", json=payload)
177
+
178
+ def cancel_invitation(self, email: str) -> Dict[str, Any]:
179
+ """POST /auth/UserInvitation/Cancel — cancel a pending invite."""
180
+ return self.post("/auth/UserInvitation/Cancel", json={"email": email})
181
+
182
+ def delete_users(self, user_ids: list) -> Dict[str, Any]:
183
+ """POST /auth/Users/delete — bulk-delete platform users (org admin)."""
184
+ return self.post("/auth/Users/delete", json={"userIds": list(user_ids)})
185
+
186
+ def grant_product_access(self, product_id: str, site_id: str, user_id: str) -> Dict[str, Any]:
187
+ """POST /auth/productaccess/{productId}/site/{siteId}/user/{userId}.
188
+
189
+ Product IDs are lowercase slugs (epmwm, pra, psc, rs, entitle,
190
+ epml, insights, secrets); normalized here so the contract holds
191
+ for every caller.
192
+ """
193
+ return self.post(
194
+ f"/auth/productaccess/{self._q(str(product_id).lower())}/site/{self._q(site_id)}"
195
+ f"/user/{self._q(user_id)}"
196
+ )
197
+
198
+ def revoke_product_access(self, product_id: str, site_id: str, user_id: str) -> Dict[str, Any]:
199
+ """DELETE /auth/productaccess/{productId}/site/{siteId}/user/{userId}."""
200
+ return self.delete(
201
+ f"/auth/productaccess/{self._q(str(product_id).lower())}/site/{self._q(site_id)}"
202
+ f"/user/{self._q(user_id)}"
203
+ )
204
+
205
+ def grant_site_access(self, site_id: str, user_id: str,
206
+ product_ids: Optional[list] = None) -> Dict[str, Any]:
207
+ """POST /auth/siteaccess/{siteId}/user/{userId} — add a user to a site.
208
+
209
+ Optional body: array of lowercase product slugs to grant on that
210
+ site too (the spec marks the body itself optional).
211
+ """
212
+ return self.post(
213
+ f"/auth/siteaccess/{self._q(site_id)}/user/{self._q(user_id)}",
214
+ json=[str(p).lower() for p in product_ids] if product_ids else None,
215
+ )
216
+
217
+ def revoke_site_access(self, site_id: str, user_id: str) -> Dict[str, Any]:
218
+ """DELETE /auth/siteaccess/{siteId}/user/{userId} — remove a user from a site."""
219
+ return self.delete(f"/auth/siteaccess/{self._q(site_id)}/user/{self._q(user_id)}")
220
+
221
+
222
+ def get_client() -> PFClient:
223
+ """Get a Pathfinder client with configuration from environment / config file."""
224
+ config = load_pf_config()
225
+ return PFClient(config)
@@ -7,6 +7,7 @@ from .machines import app as machines_app
7
7
  from .tokens import mcp_tokens_app, pats_app
8
8
  from .user import access as access_command
9
9
  from .user import whoami as whoami_command
10
+ from .users import users_app
10
11
 
11
12
  app = typer.Typer(
12
13
  name="pf",
@@ -18,6 +19,7 @@ app.add_typer(auth_app, name="auth")
18
19
  app.add_typer(pats_app, name="pats")
19
20
  app.add_typer(mcp_tokens_app, name="mcp-tokens")
20
21
  app.add_typer(machines_app, name="machines")
22
+ app.add_typer(users_app, name="users")
21
23
 
22
24
  app.command("whoami")(whoami_command)
23
25
  app.command("access")(access_command)