bt-cli 0.4.53__tar.gz → 0.4.55__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 (244) hide show
  1. {bt_cli-0.4.53 → bt_cli-0.4.55}/.github/workflows/ci.yml +7 -7
  2. {bt_cli-0.4.53 → bt_cli-0.4.55}/.github/workflows/release.yml +12 -12
  3. {bt_cli-0.4.53 → bt_cli-0.4.55}/CLAUDE.md +6 -2
  4. {bt_cli-0.4.53 → bt_cli-0.4.55}/PKG-INFO +1 -1
  5. {bt_cli-0.4.53 → bt_cli-0.4.55}/pyproject.toml +1 -1
  6. {bt_cli-0.4.53 → bt_cli-0.4.55}/src/bt_cli/__init__.py +1 -1
  7. {bt_cli-0.4.53 → bt_cli-0.4.55}/src/bt_cli/cli.py +51 -2
  8. {bt_cli-0.4.53 → bt_cli-0.4.55}/src/bt_cli/commands/configure.py +36 -8
  9. {bt_cli-0.4.53 → bt_cli-0.4.55}/src/bt_cli/core/config.py +64 -0
  10. {bt_cli-0.4.53 → bt_cli-0.4.55}/src/bt_cli/core/config_file.py +44 -8
  11. {bt_cli-0.4.53 → bt_cli-0.4.55}/src/bt_cli/core/output.py +6 -5
  12. {bt_cli-0.4.53 → bt_cli-0.4.55}/src/bt_cli/core/prompts.py +2 -1
  13. {bt_cli-0.4.53 → bt_cli-0.4.55}/src/bt_cli/core/rest_debug.py +19 -11
  14. {bt_cli-0.4.53 → bt_cli-0.4.55}/src/bt_cli/data/skills/epml/SKILL.md +12 -0
  15. bt_cli-0.4.55/src/bt_cli/data/skills/pf/SKILL.md +76 -0
  16. {bt_cli-0.4.53 → bt_cli-0.4.55}/src/bt_cli/epml/client/base.py +43 -0
  17. bt_cli-0.4.55/src/bt_cli/epml/commands/client_pkg.py +278 -0
  18. bt_cli-0.4.55/src/bt_cli/epml/commands/quick.py +139 -0
  19. bt_cli-0.4.55/src/bt_cli/pf/__init__.py +1 -0
  20. bt_cli-0.4.55/src/bt_cli/pf/client/__init__.py +5 -0
  21. bt_cli-0.4.55/src/bt_cli/pf/client/base.py +133 -0
  22. bt_cli-0.4.55/src/bt_cli/pf/commands/__init__.py +23 -0
  23. bt_cli-0.4.55/src/bt_cli/pf/commands/auth.py +35 -0
  24. bt_cli-0.4.55/src/bt_cli/pf/commands/machines.py +83 -0
  25. bt_cli-0.4.55/src/bt_cli/pf/commands/tokens.py +99 -0
  26. bt_cli-0.4.55/src/bt_cli/pf/commands/user.py +70 -0
  27. {bt_cli-0.4.53 → bt_cli-0.4.55}/tests/core/test_config.py +4 -2
  28. bt_cli-0.4.55/tests/core/test_config_file.py +67 -0
  29. bt_cli-0.4.55/tests/core/test_output.py +44 -0
  30. {bt_cli-0.4.53 → bt_cli-0.4.55}/tests/core/test_rest_debug.py +74 -0
  31. {bt_cli-0.4.53 → bt_cli-0.4.55}/tests/epml/test_client.py +44 -0
  32. {bt_cli-0.4.53 → bt_cli-0.4.55}/tests/epml/test_commands.py +87 -0
  33. bt_cli-0.4.55/tests/pf/test_client.py +79 -0
  34. bt_cli-0.4.55/tests/pf/test_commands.py +121 -0
  35. bt_cli-0.4.55/tests/pws/__init__.py +0 -0
  36. bt_cli-0.4.53/src/bt_cli/epml/commands/client_pkg.py +0 -99
  37. bt_cli-0.4.53/src/bt_cli/epml/commands/quick.py +0 -67
  38. {bt_cli-0.4.53 → bt_cli-0.4.55}/.claude/skills/bt/SKILL.md +0 -0
  39. {bt_cli-0.4.53 → bt_cli-0.4.55}/.claude/skills/entitle/SKILL.md +0 -0
  40. {bt_cli-0.4.53 → bt_cli-0.4.55}/.claude/skills/epml/SKILL.md +0 -0
  41. {bt_cli-0.4.53 → bt_cli-0.4.55}/.claude/skills/epmw/SKILL.md +0 -0
  42. {bt_cli-0.4.53 → bt_cli-0.4.55}/.claude/skills/pra/SKILL.md +0 -0
  43. {bt_cli-0.4.53 → bt_cli-0.4.55}/.claude/skills/pws/SKILL.md +0 -0
  44. {bt_cli-0.4.53 → bt_cli-0.4.55}/.env.example +0 -0
  45. {bt_cli-0.4.53 → bt_cli-0.4.55}/.gitignore +0 -0
  46. {bt_cli-0.4.53 → bt_cli-0.4.55}/README.md +0 -0
  47. {bt_cli-0.4.53 → bt_cli-0.4.55}/assets/cli-help.png +0 -0
  48. {bt_cli-0.4.53 → bt_cli-0.4.55}/assets/cli-output.png +0 -0
  49. {bt_cli-0.4.53 → bt_cli-0.4.55}/bt-cli.spec +0 -0
  50. {bt_cli-0.4.53 → bt_cli-0.4.55}/bt_entry.py +0 -0
  51. {bt_cli-0.4.53 → bt_cli-0.4.55}/epml-clients-server-side-filters-plan.md +0 -0
  52. {bt_cli-0.4.53 → bt_cli-0.4.55}/epml-implementation-plan.md +0 -0
  53. {bt_cli-0.4.53 → bt_cli-0.4.55}/pf-implementation-plan.md +0 -0
  54. {bt_cli-0.4.53 → bt_cli-0.4.55}/scripts/bt_entry.py +0 -0
  55. {bt_cli-0.4.53 → bt_cli-0.4.55}/scripts/pf_onboard.py +0 -0
  56. {bt_cli-0.4.53 → bt_cli-0.4.55}/scripts/sync-package-data.sh +0 -0
  57. {bt_cli-0.4.53 → bt_cli-0.4.55}/src/bt_cli/commands/__init__.py +0 -0
  58. {bt_cli-0.4.53 → bt_cli-0.4.55}/src/bt_cli/commands/learn.py +0 -0
  59. {bt_cli-0.4.53 → bt_cli-0.4.55}/src/bt_cli/commands/quick.py +0 -0
  60. {bt_cli-0.4.53 → bt_cli-0.4.55}/src/bt_cli/core/__init__.py +0 -0
  61. {bt_cli-0.4.53 → bt_cli-0.4.55}/src/bt_cli/core/auth.py +0 -0
  62. {bt_cli-0.4.53 → bt_cli-0.4.55}/src/bt_cli/core/client.py +0 -0
  63. {bt_cli-0.4.53 → bt_cli-0.4.55}/src/bt_cli/core/csv_utils.py +0 -0
  64. {bt_cli-0.4.53 → bt_cli-0.4.55}/src/bt_cli/core/errors.py +0 -0
  65. {bt_cli-0.4.53 → bt_cli-0.4.55}/src/bt_cli/data/CLAUDE.md +0 -0
  66. {bt_cli-0.4.53 → bt_cli-0.4.55}/src/bt_cli/data/__init__.py +0 -0
  67. {bt_cli-0.4.53 → bt_cli-0.4.55}/src/bt_cli/data/skills/bt/SKILL.md +0 -0
  68. {bt_cli-0.4.53 → bt_cli-0.4.55}/src/bt_cli/data/skills/entitle/SKILL.md +0 -0
  69. {bt_cli-0.4.53 → bt_cli-0.4.55}/src/bt_cli/data/skills/epmw/SKILL.md +0 -0
  70. {bt_cli-0.4.53 → bt_cli-0.4.55}/src/bt_cli/data/skills/pra/SKILL.md +0 -0
  71. {bt_cli-0.4.53 → bt_cli-0.4.55}/src/bt_cli/data/skills/pws/SKILL.md +0 -0
  72. {bt_cli-0.4.53 → bt_cli-0.4.55}/src/bt_cli/data/skills/secrets/SKILL.md +0 -0
  73. {bt_cli-0.4.53 → bt_cli-0.4.55}/src/bt_cli/entitle/__init__.py +0 -0
  74. {bt_cli-0.4.53 → bt_cli-0.4.55}/src/bt_cli/entitle/client/__init__.py +0 -0
  75. {bt_cli-0.4.53 → bt_cli-0.4.55}/src/bt_cli/entitle/client/base.py +0 -0
  76. {bt_cli-0.4.53 → bt_cli-0.4.55}/src/bt_cli/entitle/commands/__init__.py +0 -0
  77. {bt_cli-0.4.53 → bt_cli-0.4.55}/src/bt_cli/entitle/commands/accounts.py +0 -0
  78. {bt_cli-0.4.53 → bt_cli-0.4.55}/src/bt_cli/entitle/commands/applications.py +0 -0
  79. {bt_cli-0.4.53 → bt_cli-0.4.55}/src/bt_cli/entitle/commands/auth.py +0 -0
  80. {bt_cli-0.4.53 → bt_cli-0.4.55}/src/bt_cli/entitle/commands/bundles.py +0 -0
  81. {bt_cli-0.4.53 → bt_cli-0.4.55}/src/bt_cli/entitle/commands/integrations.py +0 -0
  82. {bt_cli-0.4.53 → bt_cli-0.4.55}/src/bt_cli/entitle/commands/permissions.py +0 -0
  83. {bt_cli-0.4.53 → bt_cli-0.4.55}/src/bt_cli/entitle/commands/policies.py +0 -0
  84. {bt_cli-0.4.53 → bt_cli-0.4.55}/src/bt_cli/entitle/commands/requests.py +0 -0
  85. {bt_cli-0.4.53 → bt_cli-0.4.55}/src/bt_cli/entitle/commands/resources.py +0 -0
  86. {bt_cli-0.4.53 → bt_cli-0.4.55}/src/bt_cli/entitle/commands/roles.py +0 -0
  87. {bt_cli-0.4.53 → bt_cli-0.4.55}/src/bt_cli/entitle/commands/users.py +0 -0
  88. {bt_cli-0.4.53 → bt_cli-0.4.55}/src/bt_cli/entitle/commands/workflows.py +0 -0
  89. {bt_cli-0.4.53 → bt_cli-0.4.55}/src/bt_cli/entitle/models/__init__.py +0 -0
  90. {bt_cli-0.4.53 → bt_cli-0.4.55}/src/bt_cli/entitle/models/bundle.py +0 -0
  91. {bt_cli-0.4.53 → bt_cli-0.4.55}/src/bt_cli/entitle/models/common.py +0 -0
  92. {bt_cli-0.4.53 → bt_cli-0.4.55}/src/bt_cli/entitle/models/integration.py +0 -0
  93. {bt_cli-0.4.53 → bt_cli-0.4.55}/src/bt_cli/entitle/models/permission.py +0 -0
  94. {bt_cli-0.4.53 → bt_cli-0.4.55}/src/bt_cli/entitle/models/policy.py +0 -0
  95. {bt_cli-0.4.53 → bt_cli-0.4.55}/src/bt_cli/entitle/models/resource.py +0 -0
  96. {bt_cli-0.4.53 → bt_cli-0.4.55}/src/bt_cli/entitle/models/role.py +0 -0
  97. {bt_cli-0.4.53 → bt_cli-0.4.55}/src/bt_cli/entitle/models/user.py +0 -0
  98. {bt_cli-0.4.53 → bt_cli-0.4.55}/src/bt_cli/entitle/models/workflow.py +0 -0
  99. {bt_cli-0.4.53 → bt_cli-0.4.55}/src/bt_cli/epml/__init__.py +0 -0
  100. {bt_cli-0.4.53 → bt_cli-0.4.55}/src/bt_cli/epml/client/__init__.py +0 -0
  101. {bt_cli-0.4.53 → bt_cli-0.4.55}/src/bt_cli/epml/commands/__init__.py +0 -0
  102. {bt_cli-0.4.53 → bt_cli-0.4.55}/src/bt_cli/epml/commands/audit.py +0 -0
  103. {bt_cli-0.4.53 → bt_cli-0.4.55}/src/bt_cli/epml/commands/auth.py +0 -0
  104. {bt_cli-0.4.53 → bt_cli-0.4.55}/src/bt_cli/epml/commands/clients.py +0 -0
  105. {bt_cli-0.4.53 → bt_cli-0.4.55}/src/bt_cli/epml/commands/external_apis.py +0 -0
  106. {bt_cli-0.4.53 → bt_cli-0.4.55}/src/bt_cli/epml/commands/hosts.py +0 -0
  107. {bt_cli-0.4.53 → bt_cli-0.4.55}/src/bt_cli/epml/commands/iolog.py +0 -0
  108. {bt_cli-0.4.53 → bt_cli-0.4.55}/src/bt_cli/epml/commands/license.py +0 -0
  109. {bt_cli-0.4.53 → bt_cli-0.4.55}/src/bt_cli/epml/commands/rbp_cmdgrps.py +0 -0
  110. {bt_cli-0.4.53 → bt_cli-0.4.55}/src/bt_cli/epml/commands/rbp_entitlement.py +0 -0
  111. {bt_cli-0.4.53 → bt_cli-0.4.55}/src/bt_cli/epml/commands/rbp_hostgrps.py +0 -0
  112. {bt_cli-0.4.53 → bt_cli-0.4.55}/src/bt_cli/epml/commands/rbp_policy.py +0 -0
  113. {bt_cli-0.4.53 → bt_cli-0.4.55}/src/bt_cli/epml/commands/rbp_roles.py +0 -0
  114. {bt_cli-0.4.53 → bt_cli-0.4.55}/src/bt_cli/epml/commands/rbp_tests.py +0 -0
  115. {bt_cli-0.4.53 → bt_cli-0.4.55}/src/bt_cli/epml/commands/rbp_tmdategrps.py +0 -0
  116. {bt_cli-0.4.53 → bt_cli-0.4.55}/src/bt_cli/epml/commands/rbp_tx.py +0 -0
  117. {bt_cli-0.4.53 → bt_cli-0.4.55}/src/bt_cli/epml/commands/rbp_usergrps.py +0 -0
  118. {bt_cli-0.4.53 → bt_cli-0.4.55}/src/bt_cli/epml/commands/settings.py +0 -0
  119. {bt_cli-0.4.53 → bt_cli-0.4.55}/src/bt_cli/epml/commands/siems.py +0 -0
  120. {bt_cli-0.4.53 → bt_cli-0.4.55}/src/bt_cli/epml/commands/users.py +0 -0
  121. {bt_cli-0.4.53 → bt_cli-0.4.55}/src/bt_cli/epml/models/__init__.py +0 -0
  122. {bt_cli-0.4.53 → bt_cli-0.4.55}/src/bt_cli/epmw/__init__.py +0 -0
  123. {bt_cli-0.4.53 → bt_cli-0.4.55}/src/bt_cli/epmw/client/__init__.py +0 -0
  124. {bt_cli-0.4.53 → bt_cli-0.4.55}/src/bt_cli/epmw/client/base.py +0 -0
  125. {bt_cli-0.4.53 → bt_cli-0.4.55}/src/bt_cli/epmw/commands/__init__.py +0 -0
  126. {bt_cli-0.4.53 → bt_cli-0.4.55}/src/bt_cli/epmw/commands/audits.py +0 -0
  127. {bt_cli-0.4.53 → bt_cli-0.4.55}/src/bt_cli/epmw/commands/auth.py +0 -0
  128. {bt_cli-0.4.53 → bt_cli-0.4.55}/src/bt_cli/epmw/commands/computers.py +0 -0
  129. {bt_cli-0.4.53 → bt_cli-0.4.55}/src/bt_cli/epmw/commands/events.py +0 -0
  130. {bt_cli-0.4.53 → bt_cli-0.4.55}/src/bt_cli/epmw/commands/groups.py +0 -0
  131. {bt_cli-0.4.53 → bt_cli-0.4.55}/src/bt_cli/epmw/commands/policies.py +0 -0
  132. {bt_cli-0.4.53 → bt_cli-0.4.55}/src/bt_cli/epmw/commands/quick.py +0 -0
  133. {bt_cli-0.4.53 → bt_cli-0.4.55}/src/bt_cli/epmw/commands/requests.py +0 -0
  134. {bt_cli-0.4.53 → bt_cli-0.4.55}/src/bt_cli/epmw/commands/roles.py +0 -0
  135. {bt_cli-0.4.53 → bt_cli-0.4.55}/src/bt_cli/epmw/commands/tasks.py +0 -0
  136. {bt_cli-0.4.53 → bt_cli-0.4.55}/src/bt_cli/epmw/commands/users.py +0 -0
  137. {bt_cli-0.4.53 → bt_cli-0.4.55}/src/bt_cli/epmw/models/__init__.py +0 -0
  138. {bt_cli-0.4.53 → bt_cli-0.4.55}/src/bt_cli/pra/__init__.py +0 -0
  139. {bt_cli-0.4.53 → bt_cli-0.4.55}/src/bt_cli/pra/client/__init__.py +0 -0
  140. {bt_cli-0.4.53 → bt_cli-0.4.55}/src/bt_cli/pra/client/base.py +0 -0
  141. {bt_cli-0.4.53 → bt_cli-0.4.55}/src/bt_cli/pra/commands/__init__.py +0 -0
  142. {bt_cli-0.4.53 → bt_cli-0.4.55}/src/bt_cli/pra/commands/auth.py +0 -0
  143. {bt_cli-0.4.53 → bt_cli-0.4.55}/src/bt_cli/pra/commands/group_policies.py +0 -0
  144. {bt_cli-0.4.53 → bt_cli-0.4.55}/src/bt_cli/pra/commands/import_export.py +0 -0
  145. {bt_cli-0.4.53 → bt_cli-0.4.55}/src/bt_cli/pra/commands/jump_clients.py +0 -0
  146. {bt_cli-0.4.53 → bt_cli-0.4.55}/src/bt_cli/pra/commands/jump_groups.py +0 -0
  147. {bt_cli-0.4.53 → bt_cli-0.4.55}/src/bt_cli/pra/commands/jump_items.py +0 -0
  148. {bt_cli-0.4.53 → bt_cli-0.4.55}/src/bt_cli/pra/commands/jumpoints.py +0 -0
  149. {bt_cli-0.4.53 → bt_cli-0.4.55}/src/bt_cli/pra/commands/policies.py +0 -0
  150. {bt_cli-0.4.53 → bt_cli-0.4.55}/src/bt_cli/pra/commands/quick.py +0 -0
  151. {bt_cli-0.4.53 → bt_cli-0.4.55}/src/bt_cli/pra/commands/teams.py +0 -0
  152. {bt_cli-0.4.53 → bt_cli-0.4.55}/src/bt_cli/pra/commands/users.py +0 -0
  153. {bt_cli-0.4.53 → bt_cli-0.4.55}/src/bt_cli/pra/commands/vault.py +0 -0
  154. {bt_cli-0.4.53 → bt_cli-0.4.55}/src/bt_cli/pra/models/__init__.py +0 -0
  155. {bt_cli-0.4.53 → bt_cli-0.4.55}/src/bt_cli/pra/models/common.py +0 -0
  156. {bt_cli-0.4.53 → bt_cli-0.4.55}/src/bt_cli/pra/models/group_policy.py +0 -0
  157. {bt_cli-0.4.53 → bt_cli-0.4.55}/src/bt_cli/pra/models/jump_client.py +0 -0
  158. {bt_cli-0.4.53 → bt_cli-0.4.55}/src/bt_cli/pra/models/jump_group.py +0 -0
  159. {bt_cli-0.4.53 → bt_cli-0.4.55}/src/bt_cli/pra/models/jump_item.py +0 -0
  160. {bt_cli-0.4.53 → bt_cli-0.4.55}/src/bt_cli/pra/models/jumpoint.py +0 -0
  161. {bt_cli-0.4.53 → bt_cli-0.4.55}/src/bt_cli/pra/models/team.py +0 -0
  162. {bt_cli-0.4.53 → bt_cli-0.4.55}/src/bt_cli/pra/models/user.py +0 -0
  163. {bt_cli-0.4.53 → bt_cli-0.4.55}/src/bt_cli/pra/models/vault.py +0 -0
  164. {bt_cli-0.4.53 → bt_cli-0.4.55}/src/bt_cli/pws/__init__.py +0 -0
  165. {bt_cli-0.4.53 → bt_cli-0.4.55}/src/bt_cli/pws/client/__init__.py +0 -0
  166. {bt_cli-0.4.53 → bt_cli-0.4.55}/src/bt_cli/pws/client/base.py +0 -0
  167. {bt_cli-0.4.53 → bt_cli-0.4.55}/src/bt_cli/pws/client/beyondinsight.py +0 -0
  168. {bt_cli-0.4.53 → bt_cli-0.4.55}/src/bt_cli/pws/client/passwordsafe.py +0 -0
  169. {bt_cli-0.4.53 → bt_cli-0.4.55}/src/bt_cli/pws/commands/__init__.py +0 -0
  170. {bt_cli-0.4.53 → bt_cli-0.4.55}/src/bt_cli/pws/commands/accounts.py +0 -0
  171. {bt_cli-0.4.53 → bt_cli-0.4.55}/src/bt_cli/pws/commands/assets.py +0 -0
  172. {bt_cli-0.4.53 → bt_cli-0.4.55}/src/bt_cli/pws/commands/attributes.py +0 -0
  173. {bt_cli-0.4.53 → bt_cli-0.4.55}/src/bt_cli/pws/commands/auth.py +0 -0
  174. {bt_cli-0.4.53 → bt_cli-0.4.55}/src/bt_cli/pws/commands/clouds.py +0 -0
  175. {bt_cli-0.4.53 → bt_cli-0.4.55}/src/bt_cli/pws/commands/config.py +0 -0
  176. {bt_cli-0.4.53 → bt_cli-0.4.55}/src/bt_cli/pws/commands/credentials.py +0 -0
  177. {bt_cli-0.4.53 → bt_cli-0.4.55}/src/bt_cli/pws/commands/databases.py +0 -0
  178. {bt_cli-0.4.53 → bt_cli-0.4.55}/src/bt_cli/pws/commands/directories.py +0 -0
  179. {bt_cli-0.4.53 → bt_cli-0.4.55}/src/bt_cli/pws/commands/functional.py +0 -0
  180. {bt_cli-0.4.53 → bt_cli-0.4.55}/src/bt_cli/pws/commands/import_export.py +0 -0
  181. {bt_cli-0.4.53 → bt_cli-0.4.55}/src/bt_cli/pws/commands/platforms.py +0 -0
  182. {bt_cli-0.4.53 → bt_cli-0.4.55}/src/bt_cli/pws/commands/quick.py +0 -0
  183. {bt_cli-0.4.53 → bt_cli-0.4.55}/src/bt_cli/pws/commands/search.py +0 -0
  184. {bt_cli-0.4.53 → bt_cli-0.4.55}/src/bt_cli/pws/commands/secrets.py +0 -0
  185. {bt_cli-0.4.53 → bt_cli-0.4.55}/src/bt_cli/pws/commands/systems.py +0 -0
  186. {bt_cli-0.4.53 → bt_cli-0.4.55}/src/bt_cli/pws/commands/users.py +0 -0
  187. {bt_cli-0.4.53 → bt_cli-0.4.55}/src/bt_cli/pws/commands/workgroups.py +0 -0
  188. {bt_cli-0.4.53 → bt_cli-0.4.55}/src/bt_cli/pws/config.py +0 -0
  189. {bt_cli-0.4.53 → bt_cli-0.4.55}/src/bt_cli/pws/models/__init__.py +0 -0
  190. {bt_cli-0.4.53 → bt_cli-0.4.55}/src/bt_cli/pws/models/account.py +0 -0
  191. {bt_cli-0.4.53 → bt_cli-0.4.55}/src/bt_cli/pws/models/asset.py +0 -0
  192. {bt_cli-0.4.53 → bt_cli-0.4.55}/src/bt_cli/pws/models/common.py +0 -0
  193. {bt_cli-0.4.53 → bt_cli-0.4.55}/src/bt_cli/pws/models/system.py +0 -0
  194. {bt_cli-0.4.53 → bt_cli-0.4.55}/src/bt_cli/secrets/__init__.py +0 -0
  195. {bt_cli-0.4.53 → bt_cli-0.4.55}/src/bt_cli/secrets/client/__init__.py +0 -0
  196. {bt_cli-0.4.53 → bt_cli-0.4.55}/src/bt_cli/secrets/client/base.py +0 -0
  197. {bt_cli-0.4.53 → bt_cli-0.4.55}/src/bt_cli/secrets/commands/__init__.py +0 -0
  198. {bt_cli-0.4.53 → bt_cli-0.4.55}/src/bt_cli/secrets/commands/_hints.py +0 -0
  199. {bt_cli-0.4.53 → bt_cli-0.4.55}/src/bt_cli/secrets/commands/auth.py +0 -0
  200. {bt_cli-0.4.53 → bt_cli-0.4.55}/src/bt_cli/secrets/commands/dynamic.py +0 -0
  201. {bt_cli-0.4.53 → bt_cli-0.4.55}/src/bt_cli/secrets/commands/folders.py +0 -0
  202. {bt_cli-0.4.53 → bt_cli-0.4.55}/src/bt_cli/secrets/commands/integrations.py +0 -0
  203. {bt_cli-0.4.53 → bt_cli-0.4.55}/src/bt_cli/secrets/commands/leases.py +0 -0
  204. {bt_cli-0.4.53 → bt_cli-0.4.55}/src/bt_cli/secrets/commands/static.py +0 -0
  205. {bt_cli-0.4.53 → bt_cli-0.4.55}/src/bt_cli/secrets/models/__init__.py +0 -0
  206. {bt_cli-0.4.53 → bt_cli-0.4.55}/tests/__init__.py +0 -0
  207. {bt_cli-0.4.53 → bt_cli-0.4.55}/tests/conftest.py +0 -0
  208. {bt_cli-0.4.53 → bt_cli-0.4.55}/tests/core/__init__.py +0 -0
  209. {bt_cli-0.4.53 → bt_cli-0.4.55}/tests/core/test_auth.py +0 -0
  210. {bt_cli-0.4.53 → bt_cli-0.4.55}/tests/core/test_errors.py +0 -0
  211. {bt_cli-0.4.53 → bt_cli-0.4.55}/tests/entitle/__init__.py +0 -0
  212. {bt_cli-0.4.53 → bt_cli-0.4.55}/tests/entitle/test_client.py +0 -0
  213. {bt_cli-0.4.53 → bt_cli-0.4.55}/tests/entitle/test_commands.py +0 -0
  214. {bt_cli-0.4.53 → bt_cli-0.4.55}/tests/entitle-smoke-test.sh +0 -0
  215. {bt_cli-0.4.53 → bt_cli-0.4.55}/tests/epml/__init__.py +0 -0
  216. {bt_cli-0.4.53 → bt_cli-0.4.55}/tests/epmw/__init__.py +0 -0
  217. {bt_cli-0.4.53 → bt_cli-0.4.55}/tests/epmw/test_client.py +0 -0
  218. {bt_cli-0.4.53 → bt_cli-0.4.55}/tests/epmw/test_commands.py +0 -0
  219. {bt_cli-0.4.53 → bt_cli-0.4.55}/tests/epmw-quick-test-plan.md +0 -0
  220. {bt_cli-0.4.53 → bt_cli-0.4.55}/tests/fixtures/__init__.py +0 -0
  221. {bt_cli-0.4.53 → bt_cli-0.4.55}/tests/fixtures/responses.py +0 -0
  222. {bt_cli-0.4.53 → bt_cli-0.4.55}/tests/integration/__init__.py +0 -0
  223. {bt_cli-0.4.53 → bt_cli-0.4.55}/tests/integration/conftest.py +0 -0
  224. {bt_cli-0.4.53 → bt_cli-0.4.55}/tests/integration/helpers.py +0 -0
  225. {bt_cli-0.4.53 → bt_cli-0.4.55}/tests/integration/test_entitle_integration.py +0 -0
  226. {bt_cli-0.4.53 → bt_cli-0.4.55}/tests/integration/test_epmw_integration.py +0 -0
  227. {bt_cli-0.4.53 → bt_cli-0.4.55}/tests/integration/test_epmw_lifecycle.py +0 -0
  228. {bt_cli-0.4.53 → bt_cli-0.4.55}/tests/integration/test_pra_integration.py +0 -0
  229. {bt_cli-0.4.53 → bt_cli-0.4.55}/tests/integration/test_pra_lifecycle.py +0 -0
  230. {bt_cli-0.4.53 → bt_cli-0.4.55}/tests/integration/test_pws_integration.py +0 -0
  231. {bt_cli-0.4.53 → bt_cli-0.4.55}/tests/integration/test_pws_lifecycle.py +0 -0
  232. {bt_cli-0.4.53/tests/pra → bt_cli-0.4.55/tests/pf}/__init__.py +0 -0
  233. {bt_cli-0.4.53/tests/pws → bt_cli-0.4.55/tests/pra}/__init__.py +0 -0
  234. {bt_cli-0.4.53 → bt_cli-0.4.55}/tests/pra/test_client.py +0 -0
  235. {bt_cli-0.4.53 → bt_cli-0.4.55}/tests/pra/test_commands.py +0 -0
  236. {bt_cli-0.4.53 → bt_cli-0.4.55}/tests/pra-smoke-test.sh +0 -0
  237. {bt_cli-0.4.53 → bt_cli-0.4.55}/tests/pra-test-plan.md +0 -0
  238. {bt_cli-0.4.53 → bt_cli-0.4.55}/tests/pws/test_client.py +0 -0
  239. {bt_cli-0.4.53 → bt_cli-0.4.55}/tests/pws/test_commands.py +0 -0
  240. {bt_cli-0.4.53 → bt_cli-0.4.55}/tests/pws-quick-test-plan.md +0 -0
  241. {bt_cli-0.4.53 → bt_cli-0.4.55}/tests/pws-smoke-test.sh +0 -0
  242. {bt_cli-0.4.53 → bt_cli-0.4.55}/tests/secrets/__init__.py +0 -0
  243. {bt_cli-0.4.53 → bt_cli-0.4.55}/tests/secrets/test_client.py +0 -0
  244. {bt_cli-0.4.53 → bt_cli-0.4.55}/tests/secrets/test_commands.py +0 -0
@@ -16,10 +16,10 @@ jobs:
16
16
  python-version: ['3.10', '3.11', '3.12']
17
17
 
18
18
  steps:
19
- - uses: actions/checkout@v4
19
+ - uses: actions/checkout@v5
20
20
 
21
21
  - name: Set up Python ${{ matrix.python-version }}
22
- uses: actions/setup-python@v5
22
+ uses: actions/setup-python@v6
23
23
  with:
24
24
  python-version: ${{ matrix.python-version }}
25
25
 
@@ -48,7 +48,7 @@ jobs:
48
48
  pytest tests/ --ignore=tests/integration -v --cov=bt_cli --cov-report=term-missing --cov-report=xml
49
49
 
50
50
  - name: Upload coverage to Codecov
51
- uses: codecov/codecov-action@v4
51
+ uses: codecov/codecov-action@v5
52
52
  if: matrix.python-version == '3.12'
53
53
  with:
54
54
  files: ./coverage.xml
@@ -58,10 +58,10 @@ jobs:
58
58
  runs-on: ubuntu-latest
59
59
 
60
60
  steps:
61
- - uses: actions/checkout@v4
61
+ - uses: actions/checkout@v5
62
62
 
63
63
  - name: Set up Python
64
- uses: actions/setup-python@v5
64
+ uses: actions/setup-python@v6
65
65
  with:
66
66
  python-version: '3.11'
67
67
 
@@ -87,10 +87,10 @@ jobs:
87
87
  if: github.event_name == 'workflow_dispatch' || github.event_name == 'schedule'
88
88
 
89
89
  steps:
90
- - uses: actions/checkout@v4
90
+ - uses: actions/checkout@v5
91
91
 
92
92
  - name: Set up Python
93
- uses: actions/setup-python@v5
93
+ uses: actions/setup-python@v6
94
94
  with:
95
95
  python-version: '3.12'
96
96
 
@@ -28,7 +28,7 @@ jobs:
28
28
  image: python:3.11-slim-bullseye
29
29
 
30
30
  steps:
31
- - uses: actions/checkout@v4
31
+ - uses: actions/checkout@v5
32
32
 
33
33
  - name: Install build dependencies
34
34
  run: |
@@ -44,7 +44,7 @@ jobs:
44
44
  run: mv dist/bt dist/bt-linux-amd64
45
45
 
46
46
  - name: Upload artifact
47
- uses: actions/upload-artifact@v4
47
+ uses: actions/upload-artifact@v7
48
48
  with:
49
49
  name: bt-linux-amd64
50
50
  path: dist/bt-linux-amd64
@@ -67,10 +67,10 @@ jobs:
67
67
  runs-on: ${{ matrix.os }}
68
68
 
69
69
  steps:
70
- - uses: actions/checkout@v4
70
+ - uses: actions/checkout@v5
71
71
 
72
72
  - name: Set up Python
73
- uses: actions/setup-python@v5
73
+ uses: actions/setup-python@v6
74
74
  with:
75
75
  python-version: '3.11'
76
76
 
@@ -92,7 +92,7 @@ jobs:
92
92
  run: move dist\bt.exe dist\${{ matrix.asset_name }}
93
93
 
94
94
  - name: Upload artifact
95
- uses: actions/upload-artifact@v4
95
+ uses: actions/upload-artifact@v7
96
96
  with:
97
97
  name: ${{ matrix.asset_name }}
98
98
  path: dist/${{ matrix.asset_name }}
@@ -102,10 +102,10 @@ jobs:
102
102
  runs-on: ubuntu-latest
103
103
 
104
104
  steps:
105
- - uses: actions/checkout@v4
105
+ - uses: actions/checkout@v5
106
106
 
107
107
  - name: Set up Python
108
- uses: actions/setup-python@v5
108
+ uses: actions/setup-python@v6
109
109
  with:
110
110
  python-version: '3.11'
111
111
 
@@ -121,7 +121,7 @@ jobs:
121
121
  run: twine check dist/*
122
122
 
123
123
  - name: Upload package artifact
124
- uses: actions/upload-artifact@v4
124
+ uses: actions/upload-artifact@v7
125
125
  with:
126
126
  name: python-package
127
127
  path: dist/*
@@ -132,10 +132,10 @@ jobs:
132
132
  runs-on: ubuntu-latest
133
133
 
134
134
  steps:
135
- - uses: actions/checkout@v4
135
+ - uses: actions/checkout@v5
136
136
 
137
137
  - name: Download all artifacts
138
- uses: actions/download-artifact@v4
138
+ uses: actions/download-artifact@v8
139
139
  with:
140
140
  path: artifacts
141
141
 
@@ -149,7 +149,7 @@ jobs:
149
149
  fi
150
150
 
151
151
  - name: Create Release
152
- uses: softprops/action-gh-release@v1
152
+ uses: softprops/action-gh-release@v3
153
153
  with:
154
154
  name: bt-cli v${{ steps.version.outputs.version }}
155
155
  tag_name: v${{ steps.version.outputs.version }}
@@ -245,7 +245,7 @@ jobs:
245
245
 
246
246
  steps:
247
247
  - name: Download package artifact
248
- uses: actions/download-artifact@v4
248
+ uses: actions/download-artifact@v8
249
249
  with:
250
250
  name: python-package
251
251
  path: dist
@@ -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.53**
3
+ BeyondTrust Platform CLI for Password Safe, Entitle, PRA, EPM Windows, EPM Linux, and the BeyondTrust Secrets API. **Version: 0.4.55**
4
4
 
5
5
  ## Setup
6
6
 
@@ -32,6 +32,7 @@ Use these slash commands for detailed product guidance:
32
32
  | `/epmw` | EPM Windows - computers, policies, requests |
33
33
  | `/epml` | EPM Linux - RBP roles/cmdgrps/usergrps, policy, test suites, transactions |
34
34
  | `/secrets` | Secrets API - folders, static, dynamic AWS credentials, leases, integrations |
35
+ | `/pf` | Pathfinder platform - whoami, site/product access, PAT/MCP tokens, machines |
35
36
 
36
37
  ## Command Structure
37
38
 
@@ -44,6 +45,7 @@ Use these slash commands for detailed product guidance:
44
45
  | EPM Windows | `bt epmw` | `computers`, `groups`, `policies`, `requests`, `quick` |
45
46
  | EPM Linux | `bt epml` | `rbp` (cmdgrps/hostgrps/usergrps/tmdategrps/roles/policy/tests/tx), `settings`, `users`, `audit`, `siems`, `quick` |
46
47
  | Secrets API | `bt secrets` | `folders`, `static`, `dynamic` (incl. `generate`), `leases`, `integrations` |
48
+ | Pathfinder | `bt pf` | `whoami`, `access`, `pats`, `mcp-tokens`, `machines` |
47
49
 
48
50
  ## Common Patterns
49
51
 
@@ -89,6 +91,7 @@ PASSWORD=$(bt pws quick checkout -s server -a admin --raw)
89
91
  - **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.
90
92
  - **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`.
91
93
  - **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`.
92
95
 
93
96
  ## Functional vs Managed Accounts
94
97
 
@@ -118,7 +121,8 @@ src/bt_cli/
118
121
  ├── entitle/ # Entitle
119
122
  ├── epmw/ # EPM Windows
120
123
  ├── epml/ # EPM Linux (PAT auth, /site/<id>/epm/linux/... URLs)
121
- └── secrets/ # BeyondTrust Secrets API (PAT auth, /site/<id>/secrets/... URLs)
124
+ ├── secrets/ # BeyondTrust Secrets API (PAT auth, /site/<id>/secrets/... URLs)
125
+ └── pf/ # Pathfinder platform (PAT auth, /site/<id>/platform/auth/... URLs)
122
126
  ```
123
127
 
124
128
  Each product follows: `client/` (API), `commands/` (CLI), `models/` (Pydantic)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: bt-cli
3
- Version: 0.4.53
3
+ Version: 0.4.55
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.53"
7
+ version = "0.4.55"
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.53"
3
+ __version__ = "0.4.55"
@@ -95,6 +95,12 @@ def _get_secrets_app() -> typer.Typer:
95
95
  return secrets_app
96
96
 
97
97
 
98
+ def _get_pf_app() -> typer.Typer:
99
+ """Lazy load Pathfinder platform commands."""
100
+ from .pf.commands import app as pf_app
101
+ return pf_app
102
+
103
+
98
104
  def _get_configure_app() -> typer.Typer:
99
105
  """Lazy load configure commands."""
100
106
  from .commands.configure import app as configure_app
@@ -145,6 +151,11 @@ try:
145
151
  except Exception:
146
152
  pass # Secrets module not ready yet
147
153
 
154
+ try:
155
+ app.add_typer(_get_pf_app(), name="pf", help="Pathfinder platform commands")
156
+ except Exception:
157
+ pass # PF module not ready yet
158
+
148
159
  try:
149
160
  app.add_typer(_get_configure_app(), name="configure", help="Configure bt-cli settings")
150
161
  except Exception:
@@ -299,9 +310,9 @@ def tree_command(
299
310
 
300
311
  if product:
301
312
  product = product.lower()
302
- if product not in ["pws", "pra", "entitle", "epmw", "epml", "secrets", "quick", "configure"]:
313
+ if product not in ["pws", "pra", "entitle", "epmw", "epml", "secrets", "pf", "quick", "configure"]:
303
314
  console.print(f"[red]Unknown product: {product}[/red]")
304
- console.print("Available: pws, pra, entitle, epmw, epml, secrets, quick, configure")
315
+ console.print("Available: pws, pra, entitle, epmw, epml, secrets, pf, quick, configure")
305
316
  raise typer.Exit(1)
306
317
 
307
318
  tree = Tree("[bold cyan]bt[/bold cyan]")
@@ -868,6 +879,11 @@ def whoami(
868
879
  if secrets_result:
869
880
  results.append(secrets_result)
870
881
 
882
+ # Test Pathfinder platform
883
+ pf_result = _test_pf_connection()
884
+ if pf_result:
885
+ results.append(pf_result)
886
+
871
887
  if not results:
872
888
  console.print("[yellow]No products configured.[/yellow]")
873
889
  console.print("\nTo configure products, set environment variables or run:")
@@ -1108,6 +1124,39 @@ def _test_secrets_connection() -> Optional[dict]:
1108
1124
  }
1109
1125
 
1110
1126
 
1127
+ def _test_pf_connection() -> Optional[dict]:
1128
+ """Test Pathfinder platform connection and return status."""
1129
+ try:
1130
+ from .core.config import load_pf_config
1131
+ from .pf.client import get_client
1132
+
1133
+ config = load_pf_config()
1134
+ masked_pat = config.pat[:12] + "..." if len(config.pat) > 12 else "***"
1135
+ result = {
1136
+ "product": "Pathfinder",
1137
+ "url": f"{config.api_url}/site/{config.site_id}/platform",
1138
+ "auth_method": f"PAT ({masked_pat})",
1139
+ "connected": False,
1140
+ }
1141
+
1142
+ with get_client() as client:
1143
+ info = client.get_user_info() or {}
1144
+ result["connected"] = True
1145
+ result["user_info"] = f"{info.get('email', '?')} ({info.get('role', '?')})"
1146
+
1147
+ return result
1148
+ except ValueError:
1149
+ return None
1150
+ except Exception as e:
1151
+ return {
1152
+ "product": "Pathfinder",
1153
+ "url": "",
1154
+ "auth_method": "-",
1155
+ "connected": False,
1156
+ "error": str(e)[:50],
1157
+ }
1158
+
1159
+
1111
1160
  def run() -> None:
1112
1161
  """Run the CLI application."""
1113
1162
  app()
@@ -41,12 +41,26 @@ def configure_callback(
41
41
  ),
42
42
  api_url: Optional[str] = typer.Option(None, "--api-url", help="API URL"),
43
43
  client_id: Optional[str] = typer.Option(None, "--client-id", help="OAuth Client ID"),
44
- client_secret: Optional[str] = typer.Option(None, "--client-secret", help="OAuth Client Secret"),
45
- api_key: Optional[str] = typer.Option(None, "--api-key", help="API Key"),
44
+ client_secret: Optional[str] = typer.Option(
45
+ None,
46
+ "--client-secret",
47
+ help="OAuth Client Secret (visible in shell history/process list — prefer interactive `bt configure`)",
48
+ ),
49
+ api_key: Optional[str] = typer.Option(
50
+ None,
51
+ "--api-key",
52
+ help="API Key (visible in shell history/process list — prefer interactive `bt configure`)",
53
+ ),
46
54
  user_api_key: Optional[str] = typer.Option(
47
55
  None,
48
56
  "--user-api-key",
49
- help="Entitle user-context API key (only required for `bt entitle requests create`)",
57
+ help="Entitle user-context API key, only required for `bt entitle requests create` "
58
+ "(visible in shell history/process list — prefer interactive `bt configure`)",
59
+ ),
60
+ show_input: bool = typer.Option(
61
+ False,
62
+ "--show-input",
63
+ help="Show secret values while typing/pasting in interactive mode (default: hidden)",
50
64
  ),
51
65
  ) -> None:
52
66
  """Configure bt-cli interactively or via flags.
@@ -79,10 +93,14 @@ def configure_callback(
79
93
  )
80
94
  else:
81
95
  # Interactive mode
82
- _configure_interactive(product, profile)
96
+ _configure_interactive(product, profile, show_input=show_input)
83
97
 
84
98
 
85
- def _configure_interactive(product: Optional[str] = None, profile: Optional[str] = None) -> None:
99
+ def _configure_interactive(
100
+ product: Optional[str] = None,
101
+ profile: Optional[str] = None,
102
+ show_input: bool = False,
103
+ ) -> None:
86
104
  """Run interactive configuration wizard."""
87
105
  console.print()
88
106
  console.print(Panel.fit(
@@ -154,6 +172,10 @@ def _configure_interactive(product: Optional[str] = None, profile: Optional[str]
154
172
  if "example" in field_info:
155
173
  console.print(f" [dim]Example: {field_info['example']}[/dim]")
156
174
 
175
+ # Show how-to-find-it hint if available
176
+ if "hint" in field_info:
177
+ console.print(f" [dim]{field_info['hint']}[/dim]")
178
+
157
179
  # Handle boolean fields
158
180
  if isinstance(default, bool):
159
181
  value = Confirm.ask(prompt_text, default=default)
@@ -164,17 +186,19 @@ def _configure_interactive(product: Optional[str] = None, profile: Optional[str]
164
186
  choices=field_info["choices"],
165
187
  default=str(default) if default else None
166
188
  )
167
- # Handle secret fields - show the value (not hidden) for easier pasting verification
189
+ # Handle secret fields - input hidden by default (--show-input to reveal for
190
+ # paste verification); never echo more than the last 4 chars of an existing value
168
191
  elif field_info.get("secret"):
169
192
  if existing.get(field_name):
170
193
  existing_val = str(existing[field_name])
171
194
  if existing_val.startswith("keyring://"):
172
195
  console.print(f" [dim](current: stored in keyring)[/dim]")
173
196
  else:
174
- console.print(f" [dim](current: {existing_val[:20]}...)[/dim]" if len(existing_val) > 20 else f" [dim](current: {existing_val})[/dim]")
175
- # Don't use password=True so users can see what they paste
197
+ hint = "****" + existing_val[-4:] if len(existing_val) > 4 else "****"
198
+ console.print(f" [dim](current: {hint}, press Enter to keep)[/dim]")
176
199
  value = Prompt.ask(
177
200
  prompt_text,
201
+ password=not show_input,
178
202
  default="" if not default else None
179
203
  )
180
204
  if not value and default:
@@ -198,6 +222,10 @@ def _configure_interactive(product: Optional[str] = None, profile: Optional[str]
198
222
  console.print(f" [dim]Stored in keyring[/dim]")
199
223
  else:
200
224
  new_config[field_name] = value
225
+ print_warning(
226
+ f"Keyring storage failed for '{field_name}' — "
227
+ "value saved to config file instead (file mode 0600)"
228
+ )
201
229
  else:
202
230
  new_config[field_name] = value
203
231
 
@@ -146,6 +146,33 @@ class SecretsConfig(ProductConfig):
146
146
  raise ValueError("BT_SECRETS_PAT is required")
147
147
 
148
148
 
149
+ @dataclass
150
+ class PFConfig(ProductConfig):
151
+ """Pathfinder platform (Nomine auth service) configuration.
152
+
153
+ Uses Personal Access Token (PAT) bearer authentication against the
154
+ BeyondTrust public API gateway. URLs are composed as:
155
+
156
+ {api_url}/site/{site_id}/platform/auth/<path>
157
+
158
+ PATs are bound to the site that was active in the Pathfinder portal
159
+ when they were minted — a PAT for one site gets
160
+ `401 Access denied for this site` on every other site's paths.
161
+ """
162
+
163
+ site_id: str = ""
164
+ pat: str = ""
165
+
166
+ def validate(self) -> None:
167
+ """Validate configuration."""
168
+ if not self.api_url:
169
+ raise ValueError("BT_PF_API_URL is required")
170
+ if not self.site_id:
171
+ raise ValueError("BT_PF_SITE_ID is required")
172
+ if not self.pat:
173
+ raise ValueError("BT_PF_PAT is required")
174
+
175
+
149
176
  @dataclass
150
177
  class EPMLConfig(ProductConfig):
151
178
  """EPM Linux configuration.
@@ -545,6 +572,42 @@ def load_secrets_config(env_file: Optional[str] = None, profile: Optional[str] =
545
572
  return config
546
573
 
547
574
 
575
+ def load_pf_config(env_file: Optional[str] = None, profile: Optional[str] = None) -> PFConfig:
576
+ """Load Pathfinder platform configuration.
577
+
578
+ Configuration sources (in order of precedence):
579
+ 1. Environment variables
580
+ 2. Config file (~/.bt-cli/config.yaml)
581
+
582
+ Environment variables:
583
+ BT_PF_API_URL - Gateway URL (default: https://api.beyondtrust.io)
584
+ BT_PF_SITE_ID - Site UUID the PAT was minted on (required)
585
+ BT_PF_PAT - Personal Access Token (required)
586
+ BT_PF_VERIFY_SSL - SSL verification (default: true)
587
+ BT_PF_TIMEOUT - Request timeout in seconds (default: 30)
588
+ """
589
+ if env_file:
590
+ load_dotenv(env_file)
591
+ else:
592
+ load_dotenv()
593
+
594
+ profile = profile or _get_profile()
595
+ layered = get_layered_config("pf", profile)
596
+
597
+ if "pat" in layered:
598
+ layered["pat"] = _resolve_value(layered["pat"])
599
+
600
+ config = PFConfig(
601
+ api_url=_strip_controls(layered.get("api_url") or os.getenv("BT_PF_API_URL", "https://api.beyondtrust.io")),
602
+ site_id=_strip_controls(layered.get("site_id") or os.getenv("BT_PF_SITE_ID", "")),
603
+ pat=_strip_controls(layered.get("pat") or os.getenv("BT_PF_PAT", "")),
604
+ verify_ssl=_to_bool(layered.get("verify_ssl")) if "verify_ssl" in layered else _get_bool(os.getenv("BT_PF_VERIFY_SSL")),
605
+ timeout=_to_float(layered.get("timeout")) if "timeout" in layered else _get_float(os.getenv("BT_PF_TIMEOUT"), 30.0),
606
+ )
607
+ config.validate()
608
+ return config
609
+
610
+
548
611
  def load_config(product: str, env_file: Optional[str] = None) -> ProductConfig:
549
612
  """Load configuration for a specific product.
550
613
 
@@ -565,6 +628,7 @@ def load_config(product: str, env_file: Optional[str] = None) -> ProductConfig:
565
628
  "epmw": load_epmw_config,
566
629
  "epml": load_epml_config,
567
630
  "secrets": load_secrets_config,
631
+ "pf": load_pf_config,
568
632
  }
569
633
 
570
634
  loader = loaders.get(product.lower())
@@ -9,6 +9,8 @@ Supports:
9
9
 
10
10
  import logging
11
11
  import os
12
+ import sys
13
+ import tempfile
12
14
  from dataclasses import dataclass, field
13
15
  from pathlib import Path
14
16
  from typing import Any, Optional
@@ -110,6 +112,7 @@ PRODUCTS = {
110
112
  "required": True,
111
113
  "secret": False,
112
114
  "example": "7f735e1b-5ecb-49fa-87e8-eddf54a9c745",
115
+ "hint": "Find yours: log into app.beyondtrust.io, choose your site, then browse to https://app.beyondtrust.io/api/platform/currentSite",
113
116
  },
114
117
  "pat": {"prompt": "Personal Access Token", "required": True, "secret": True},
115
118
  "api_version": {
@@ -122,6 +125,28 @@ PRODUCTS = {
122
125
  "timeout": {"prompt": "Timeout (seconds)", "required": False, "secret": False, "default": 30},
123
126
  },
124
127
  },
128
+ "pf": {
129
+ "name": "Pathfinder Platform",
130
+ "fields": {
131
+ "api_url": {
132
+ "prompt": "Gateway URL",
133
+ "required": True,
134
+ "secret": False,
135
+ "default": "https://api.beyondtrust.io",
136
+ "example": "https://api.beyondtrust.io",
137
+ },
138
+ "site_id": {
139
+ "prompt": "Site ID (UUID — the site the PAT was minted on)",
140
+ "required": True,
141
+ "secret": False,
142
+ "example": "6a7546a7-e111-4fe3-bc14-d6de9c2177b9",
143
+ "hint": "Find yours: log into app.beyondtrust.io, choose your site, then browse to https://app.beyondtrust.io/api/platform/currentSite",
144
+ },
145
+ "pat": {"prompt": "Personal Access Token", "required": True, "secret": True},
146
+ "verify_ssl": {"prompt": "Verify SSL", "required": False, "secret": False, "default": True},
147
+ "timeout": {"prompt": "Timeout (seconds)", "required": False, "secret": False, "default": 30},
148
+ },
149
+ },
125
150
  "epml": {
126
151
  "name": "EPM Linux",
127
152
  "fields": {
@@ -137,6 +162,7 @@ PRODUCTS = {
137
162
  "required": True,
138
163
  "secret": False,
139
164
  "example": "c882e07c-753b-4fcd-a1e6-7d39defec5ae",
165
+ "hint": "Find yours: log into app.beyondtrust.io, choose your site, then browse to https://app.beyondtrust.io/api/platform/currentSite",
140
166
  },
141
167
  "pat": {"prompt": "Personal Access Token", "required": True, "secret": True},
142
168
  "default_host_id": {
@@ -242,7 +268,14 @@ def load_config_file(path: Optional[Path] = None) -> ConfigFile:
242
268
  profiles=data.get("profiles", {}),
243
269
  )
244
270
  except (yaml.YAMLError, OSError) as e:
245
- # Return empty config on error, caller can handle
271
+ # Fall back to empty config, but tell the user — a corrupt/unreadable
272
+ # file silently ignored looks like "my profiles disappeared"
273
+ logger.warning(f"Failed to load config file {path}: {e}")
274
+ print(
275
+ f"\033[93mWarning: could not read config file {path} ({type(e).__name__}) - "
276
+ "ignoring it. Fix or delete the file to silence this warning.\033[0m",
277
+ file=sys.stderr,
278
+ )
246
279
  return ConfigFile()
247
280
 
248
281
 
@@ -267,9 +300,6 @@ def save_config_file(config: ConfigFile, path: Optional[Path] = None) -> None:
267
300
  # Security: Create file atomically with secure permissions (0o600)
268
301
  # This prevents TOCTOU race where file could be readable between
269
302
  # creation and chmod.
270
- import os
271
- import sys
272
- import tempfile
273
303
 
274
304
  # Write to temp file in same directory, then atomic rename
275
305
  dir_path = path.parent
@@ -283,10 +313,9 @@ def save_config_file(config: ConfigFile, path: Optional[Path] = None) -> None:
283
313
  os.fchmod(fd, 0o600)
284
314
  with os.fdopen(fd, "w") as f:
285
315
  yaml.dump(data, f, default_flow_style=False, sort_keys=False)
286
- # Atomic rename (on Windows, need to remove target first if exists)
287
- if sys.platform == "win32" and path.exists():
288
- os.unlink(path)
289
- os.rename(tmp_path, path)
316
+ # os.replace overwrites atomically on POSIX and Windows alike
317
+ # no delete-then-rename window where the config doesn't exist
318
+ os.replace(tmp_path, path)
290
319
  # On Windows, set permissions after the fact using chmod
291
320
  if sys.platform == "win32":
292
321
  try:
@@ -356,6 +385,7 @@ def _get_env_prefix(product: str) -> str:
356
385
  "epmw": "BT_EPM",
357
386
  "epml": "BT_EPML",
358
387
  "secrets": "BT_SECRETS",
388
+ "pf": "BT_PF",
359
389
  }
360
390
  return prefixes.get(product, f"BT_{product.upper()}")
361
391
 
@@ -394,6 +424,12 @@ def _get_env_mappings(product: str) -> dict[str, str]:
394
424
  # Secrets API doesn't use api_key/client_id/client_secret — drop the inherited mappings
395
425
  for unused in ("api_key", "client_id", "client_secret"):
396
426
  mappings.pop(unused, None)
427
+ elif product == "pf":
428
+ mappings["site_id"] = f"{prefix}_SITE_ID"
429
+ mappings["pat"] = f"{prefix}_PAT"
430
+ # Pathfinder uses PAT auth only — drop the inherited mappings
431
+ for unused in ("api_key", "client_id", "client_secret"):
432
+ mappings.pop(unused, None)
397
433
 
398
434
  return mappings
399
435
 
@@ -5,6 +5,7 @@ from enum import Enum
5
5
  from typing import Any, Optional
6
6
 
7
7
  from rich.console import Console
8
+ from rich.markup import escape
8
9
  from rich.panel import Panel
9
10
  from rich.table import Table
10
11
 
@@ -141,7 +142,7 @@ def print_success(message: str) -> None:
141
142
  Args:
142
143
  message: Message to display
143
144
  """
144
- console.print(f"[green]{message}[/green]")
145
+ console.print(f"[green]{escape(message)}[/green]")
145
146
 
146
147
 
147
148
  def print_error(message: str) -> None:
@@ -150,7 +151,7 @@ def print_error(message: str) -> None:
150
151
  Args:
151
152
  message: Error message to display
152
153
  """
153
- console.print(f"[red]Error:[/red] {message}")
154
+ console.print(f"[red]Error:[/red] {escape(message)}")
154
155
 
155
156
 
156
157
  def print_warning(message: str) -> None:
@@ -159,7 +160,7 @@ def print_warning(message: str) -> None:
159
160
  Args:
160
161
  message: Warning message to display
161
162
  """
162
- console.print(f"[yellow]Warning:[/yellow] {message}")
163
+ console.print(f"[yellow]Warning:[/yellow] {escape(message)}")
163
164
 
164
165
 
165
166
  def print_info(message: str) -> None:
@@ -168,7 +169,7 @@ def print_info(message: str) -> None:
168
169
  Args:
169
170
  message: Info message to display
170
171
  """
171
- console.print(f"[blue]{message}[/blue]")
172
+ console.print(f"[blue]{escape(message)}[/blue]")
172
173
 
173
174
 
174
175
  def confirm_action(message: str, default: bool = False) -> bool:
@@ -202,4 +203,4 @@ def print_api_error(error: Exception, operation: str) -> None:
202
203
  operation: Description of the operation that failed (e.g., "list systems")
203
204
  """
204
205
  message = handle_api_error(error, operation)
205
- console.print(f"[red]Error:[/red] {message}")
206
+ console.print(f"[red]Error:[/red] {escape(message)}")
@@ -64,10 +64,11 @@ def prompt_from_list(
64
64
  The selected ID
65
65
  """
66
66
  console.print(f"\n[bold]{title}:[/bold]")
67
+ id_width = max((len(str(item.get(id_key, ""))) for item in items), default=1)
67
68
  for item in items:
68
69
  item_id = item.get(id_key, "")
69
70
  item_name = item.get(name_key, "Unknown")
70
- console.print(f" {item_id}: {item_name}")
71
+ console.print(f" {str(item_id):>{id_width}}: {item_name}")
71
72
  raw = typer.prompt(prompt_text, type=value_type)
72
73
  if value_type is str:
73
74
  return _clean_str(raw) # type: ignore[return-value]