bt-cli 0.4.30__tar.gz → 0.4.32__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (174) hide show
  1. {bt_cli-0.4.30 → bt_cli-0.4.32}/CLAUDE.md +1 -1
  2. {bt_cli-0.4.30 → bt_cli-0.4.32}/PKG-INFO +1 -1
  3. {bt_cli-0.4.30 → bt_cli-0.4.32}/pyproject.toml +1 -1
  4. {bt_cli-0.4.30 → bt_cli-0.4.32}/src/bt_cli/__init__.py +1 -1
  5. {bt_cli-0.4.30 → bt_cli-0.4.32}/src/bt_cli/pws/client/beyondinsight.py +36 -29
  6. {bt_cli-0.4.30 → bt_cli-0.4.32}/src/bt_cli/pws/client/passwordsafe.py +72 -24
  7. {bt_cli-0.4.30 → bt_cli-0.4.32}/src/bt_cli/pws/commands/search.py +40 -35
  8. {bt_cli-0.4.30 → bt_cli-0.4.32}/src/bt_cli/pws/commands/users.py +31 -11
  9. {bt_cli-0.4.30 → bt_cli-0.4.32}/.claude/skills/bt/SKILL.md +0 -0
  10. {bt_cli-0.4.30 → bt_cli-0.4.32}/.claude/skills/entitle/SKILL.md +0 -0
  11. {bt_cli-0.4.30 → bt_cli-0.4.32}/.claude/skills/epmw/SKILL.md +0 -0
  12. {bt_cli-0.4.30 → bt_cli-0.4.32}/.claude/skills/pra/SKILL.md +0 -0
  13. {bt_cli-0.4.30 → bt_cli-0.4.32}/.claude/skills/pws/SKILL.md +0 -0
  14. {bt_cli-0.4.30 → bt_cli-0.4.32}/.env.example +0 -0
  15. {bt_cli-0.4.30 → bt_cli-0.4.32}/.github/workflows/ci.yml +0 -0
  16. {bt_cli-0.4.30 → bt_cli-0.4.32}/.github/workflows/release.yml +0 -0
  17. {bt_cli-0.4.30 → bt_cli-0.4.32}/.gitignore +0 -0
  18. {bt_cli-0.4.30 → bt_cli-0.4.32}/README.md +0 -0
  19. {bt_cli-0.4.30 → bt_cli-0.4.32}/assets/cli-help.png +0 -0
  20. {bt_cli-0.4.30 → bt_cli-0.4.32}/assets/cli-output.png +0 -0
  21. {bt_cli-0.4.30 → bt_cli-0.4.32}/bt-cli.spec +0 -0
  22. {bt_cli-0.4.30 → bt_cli-0.4.32}/bt_entry.py +0 -0
  23. {bt_cli-0.4.30 → bt_cli-0.4.32}/scripts/bt_entry.py +0 -0
  24. {bt_cli-0.4.30 → bt_cli-0.4.32}/scripts/sync-package-data.sh +0 -0
  25. {bt_cli-0.4.30 → bt_cli-0.4.32}/src/bt_cli/cli.py +0 -0
  26. {bt_cli-0.4.30 → bt_cli-0.4.32}/src/bt_cli/commands/__init__.py +0 -0
  27. {bt_cli-0.4.30 → bt_cli-0.4.32}/src/bt_cli/commands/configure.py +0 -0
  28. {bt_cli-0.4.30 → bt_cli-0.4.32}/src/bt_cli/commands/learn.py +0 -0
  29. {bt_cli-0.4.30 → bt_cli-0.4.32}/src/bt_cli/commands/quick.py +0 -0
  30. {bt_cli-0.4.30 → bt_cli-0.4.32}/src/bt_cli/core/__init__.py +0 -0
  31. {bt_cli-0.4.30 → bt_cli-0.4.32}/src/bt_cli/core/auth.py +0 -0
  32. {bt_cli-0.4.30 → bt_cli-0.4.32}/src/bt_cli/core/client.py +0 -0
  33. {bt_cli-0.4.30 → bt_cli-0.4.32}/src/bt_cli/core/config.py +0 -0
  34. {bt_cli-0.4.30 → bt_cli-0.4.32}/src/bt_cli/core/config_file.py +0 -0
  35. {bt_cli-0.4.30 → bt_cli-0.4.32}/src/bt_cli/core/csv_utils.py +0 -0
  36. {bt_cli-0.4.30 → bt_cli-0.4.32}/src/bt_cli/core/errors.py +0 -0
  37. {bt_cli-0.4.30 → bt_cli-0.4.32}/src/bt_cli/core/output.py +0 -0
  38. {bt_cli-0.4.30 → bt_cli-0.4.32}/src/bt_cli/core/prompts.py +0 -0
  39. {bt_cli-0.4.30 → bt_cli-0.4.32}/src/bt_cli/core/rest_debug.py +0 -0
  40. {bt_cli-0.4.30 → bt_cli-0.4.32}/src/bt_cli/data/CLAUDE.md +0 -0
  41. {bt_cli-0.4.30 → bt_cli-0.4.32}/src/bt_cli/data/__init__.py +0 -0
  42. {bt_cli-0.4.30 → bt_cli-0.4.32}/src/bt_cli/data/skills/bt/SKILL.md +0 -0
  43. {bt_cli-0.4.30 → bt_cli-0.4.32}/src/bt_cli/data/skills/entitle/SKILL.md +0 -0
  44. {bt_cli-0.4.30 → bt_cli-0.4.32}/src/bt_cli/data/skills/epmw/SKILL.md +0 -0
  45. {bt_cli-0.4.30 → bt_cli-0.4.32}/src/bt_cli/data/skills/pra/SKILL.md +0 -0
  46. {bt_cli-0.4.30 → bt_cli-0.4.32}/src/bt_cli/data/skills/pws/SKILL.md +0 -0
  47. {bt_cli-0.4.30 → bt_cli-0.4.32}/src/bt_cli/entitle/__init__.py +0 -0
  48. {bt_cli-0.4.30 → bt_cli-0.4.32}/src/bt_cli/entitle/client/__init__.py +0 -0
  49. {bt_cli-0.4.30 → bt_cli-0.4.32}/src/bt_cli/entitle/client/base.py +0 -0
  50. {bt_cli-0.4.30 → bt_cli-0.4.32}/src/bt_cli/entitle/commands/__init__.py +0 -0
  51. {bt_cli-0.4.30 → bt_cli-0.4.32}/src/bt_cli/entitle/commands/accounts.py +0 -0
  52. {bt_cli-0.4.30 → bt_cli-0.4.32}/src/bt_cli/entitle/commands/applications.py +0 -0
  53. {bt_cli-0.4.30 → bt_cli-0.4.32}/src/bt_cli/entitle/commands/auth.py +0 -0
  54. {bt_cli-0.4.30 → bt_cli-0.4.32}/src/bt_cli/entitle/commands/bundles.py +0 -0
  55. {bt_cli-0.4.30 → bt_cli-0.4.32}/src/bt_cli/entitle/commands/integrations.py +0 -0
  56. {bt_cli-0.4.30 → bt_cli-0.4.32}/src/bt_cli/entitle/commands/permissions.py +0 -0
  57. {bt_cli-0.4.30 → bt_cli-0.4.32}/src/bt_cli/entitle/commands/policies.py +0 -0
  58. {bt_cli-0.4.30 → bt_cli-0.4.32}/src/bt_cli/entitle/commands/resources.py +0 -0
  59. {bt_cli-0.4.30 → bt_cli-0.4.32}/src/bt_cli/entitle/commands/roles.py +0 -0
  60. {bt_cli-0.4.30 → bt_cli-0.4.32}/src/bt_cli/entitle/commands/users.py +0 -0
  61. {bt_cli-0.4.30 → bt_cli-0.4.32}/src/bt_cli/entitle/commands/workflows.py +0 -0
  62. {bt_cli-0.4.30 → bt_cli-0.4.32}/src/bt_cli/entitle/models/__init__.py +0 -0
  63. {bt_cli-0.4.30 → bt_cli-0.4.32}/src/bt_cli/entitle/models/bundle.py +0 -0
  64. {bt_cli-0.4.30 → bt_cli-0.4.32}/src/bt_cli/entitle/models/common.py +0 -0
  65. {bt_cli-0.4.30 → bt_cli-0.4.32}/src/bt_cli/entitle/models/integration.py +0 -0
  66. {bt_cli-0.4.30 → bt_cli-0.4.32}/src/bt_cli/entitle/models/permission.py +0 -0
  67. {bt_cli-0.4.30 → bt_cli-0.4.32}/src/bt_cli/entitle/models/policy.py +0 -0
  68. {bt_cli-0.4.30 → bt_cli-0.4.32}/src/bt_cli/entitle/models/resource.py +0 -0
  69. {bt_cli-0.4.30 → bt_cli-0.4.32}/src/bt_cli/entitle/models/role.py +0 -0
  70. {bt_cli-0.4.30 → bt_cli-0.4.32}/src/bt_cli/entitle/models/user.py +0 -0
  71. {bt_cli-0.4.30 → bt_cli-0.4.32}/src/bt_cli/entitle/models/workflow.py +0 -0
  72. {bt_cli-0.4.30 → bt_cli-0.4.32}/src/bt_cli/epmw/__init__.py +0 -0
  73. {bt_cli-0.4.30 → bt_cli-0.4.32}/src/bt_cli/epmw/client/__init__.py +0 -0
  74. {bt_cli-0.4.30 → bt_cli-0.4.32}/src/bt_cli/epmw/client/base.py +0 -0
  75. {bt_cli-0.4.30 → bt_cli-0.4.32}/src/bt_cli/epmw/commands/__init__.py +0 -0
  76. {bt_cli-0.4.30 → bt_cli-0.4.32}/src/bt_cli/epmw/commands/audits.py +0 -0
  77. {bt_cli-0.4.30 → bt_cli-0.4.32}/src/bt_cli/epmw/commands/auth.py +0 -0
  78. {bt_cli-0.4.30 → bt_cli-0.4.32}/src/bt_cli/epmw/commands/computers.py +0 -0
  79. {bt_cli-0.4.30 → bt_cli-0.4.32}/src/bt_cli/epmw/commands/events.py +0 -0
  80. {bt_cli-0.4.30 → bt_cli-0.4.32}/src/bt_cli/epmw/commands/groups.py +0 -0
  81. {bt_cli-0.4.30 → bt_cli-0.4.32}/src/bt_cli/epmw/commands/policies.py +0 -0
  82. {bt_cli-0.4.30 → bt_cli-0.4.32}/src/bt_cli/epmw/commands/quick.py +0 -0
  83. {bt_cli-0.4.30 → bt_cli-0.4.32}/src/bt_cli/epmw/commands/requests.py +0 -0
  84. {bt_cli-0.4.30 → bt_cli-0.4.32}/src/bt_cli/epmw/commands/roles.py +0 -0
  85. {bt_cli-0.4.30 → bt_cli-0.4.32}/src/bt_cli/epmw/commands/tasks.py +0 -0
  86. {bt_cli-0.4.30 → bt_cli-0.4.32}/src/bt_cli/epmw/commands/users.py +0 -0
  87. {bt_cli-0.4.30 → bt_cli-0.4.32}/src/bt_cli/epmw/models/__init__.py +0 -0
  88. {bt_cli-0.4.30 → bt_cli-0.4.32}/src/bt_cli/pra/__init__.py +0 -0
  89. {bt_cli-0.4.30 → bt_cli-0.4.32}/src/bt_cli/pra/client/__init__.py +0 -0
  90. {bt_cli-0.4.30 → bt_cli-0.4.32}/src/bt_cli/pra/client/base.py +0 -0
  91. {bt_cli-0.4.30 → bt_cli-0.4.32}/src/bt_cli/pra/commands/__init__.py +0 -0
  92. {bt_cli-0.4.30 → bt_cli-0.4.32}/src/bt_cli/pra/commands/auth.py +0 -0
  93. {bt_cli-0.4.30 → bt_cli-0.4.32}/src/bt_cli/pra/commands/import_export.py +0 -0
  94. {bt_cli-0.4.30 → bt_cli-0.4.32}/src/bt_cli/pra/commands/jump_clients.py +0 -0
  95. {bt_cli-0.4.30 → bt_cli-0.4.32}/src/bt_cli/pra/commands/jump_groups.py +0 -0
  96. {bt_cli-0.4.30 → bt_cli-0.4.32}/src/bt_cli/pra/commands/jump_items.py +0 -0
  97. {bt_cli-0.4.30 → bt_cli-0.4.32}/src/bt_cli/pra/commands/jumpoints.py +0 -0
  98. {bt_cli-0.4.30 → bt_cli-0.4.32}/src/bt_cli/pra/commands/policies.py +0 -0
  99. {bt_cli-0.4.30 → bt_cli-0.4.32}/src/bt_cli/pra/commands/quick.py +0 -0
  100. {bt_cli-0.4.30 → bt_cli-0.4.32}/src/bt_cli/pra/commands/teams.py +0 -0
  101. {bt_cli-0.4.30 → bt_cli-0.4.32}/src/bt_cli/pra/commands/users.py +0 -0
  102. {bt_cli-0.4.30 → bt_cli-0.4.32}/src/bt_cli/pra/commands/vault.py +0 -0
  103. {bt_cli-0.4.30 → bt_cli-0.4.32}/src/bt_cli/pra/models/__init__.py +0 -0
  104. {bt_cli-0.4.30 → bt_cli-0.4.32}/src/bt_cli/pra/models/common.py +0 -0
  105. {bt_cli-0.4.30 → bt_cli-0.4.32}/src/bt_cli/pra/models/jump_client.py +0 -0
  106. {bt_cli-0.4.30 → bt_cli-0.4.32}/src/bt_cli/pra/models/jump_group.py +0 -0
  107. {bt_cli-0.4.30 → bt_cli-0.4.32}/src/bt_cli/pra/models/jump_item.py +0 -0
  108. {bt_cli-0.4.30 → bt_cli-0.4.32}/src/bt_cli/pra/models/jumpoint.py +0 -0
  109. {bt_cli-0.4.30 → bt_cli-0.4.32}/src/bt_cli/pra/models/team.py +0 -0
  110. {bt_cli-0.4.30 → bt_cli-0.4.32}/src/bt_cli/pra/models/user.py +0 -0
  111. {bt_cli-0.4.30 → bt_cli-0.4.32}/src/bt_cli/pra/models/vault.py +0 -0
  112. {bt_cli-0.4.30 → bt_cli-0.4.32}/src/bt_cli/pws/__init__.py +0 -0
  113. {bt_cli-0.4.30 → bt_cli-0.4.32}/src/bt_cli/pws/client/__init__.py +0 -0
  114. {bt_cli-0.4.30 → bt_cli-0.4.32}/src/bt_cli/pws/client/base.py +0 -0
  115. {bt_cli-0.4.30 → bt_cli-0.4.32}/src/bt_cli/pws/commands/__init__.py +0 -0
  116. {bt_cli-0.4.30 → bt_cli-0.4.32}/src/bt_cli/pws/commands/accounts.py +0 -0
  117. {bt_cli-0.4.30 → bt_cli-0.4.32}/src/bt_cli/pws/commands/assets.py +0 -0
  118. {bt_cli-0.4.30 → bt_cli-0.4.32}/src/bt_cli/pws/commands/attributes.py +0 -0
  119. {bt_cli-0.4.30 → bt_cli-0.4.32}/src/bt_cli/pws/commands/auth.py +0 -0
  120. {bt_cli-0.4.30 → bt_cli-0.4.32}/src/bt_cli/pws/commands/clouds.py +0 -0
  121. {bt_cli-0.4.30 → bt_cli-0.4.32}/src/bt_cli/pws/commands/config.py +0 -0
  122. {bt_cli-0.4.30 → bt_cli-0.4.32}/src/bt_cli/pws/commands/credentials.py +0 -0
  123. {bt_cli-0.4.30 → bt_cli-0.4.32}/src/bt_cli/pws/commands/databases.py +0 -0
  124. {bt_cli-0.4.30 → bt_cli-0.4.32}/src/bt_cli/pws/commands/directories.py +0 -0
  125. {bt_cli-0.4.30 → bt_cli-0.4.32}/src/bt_cli/pws/commands/functional.py +0 -0
  126. {bt_cli-0.4.30 → bt_cli-0.4.32}/src/bt_cli/pws/commands/import_export.py +0 -0
  127. {bt_cli-0.4.30 → bt_cli-0.4.32}/src/bt_cli/pws/commands/platforms.py +0 -0
  128. {bt_cli-0.4.30 → bt_cli-0.4.32}/src/bt_cli/pws/commands/quick.py +0 -0
  129. {bt_cli-0.4.30 → bt_cli-0.4.32}/src/bt_cli/pws/commands/secrets.py +0 -0
  130. {bt_cli-0.4.30 → bt_cli-0.4.32}/src/bt_cli/pws/commands/systems.py +0 -0
  131. {bt_cli-0.4.30 → bt_cli-0.4.32}/src/bt_cli/pws/commands/workgroups.py +0 -0
  132. {bt_cli-0.4.30 → bt_cli-0.4.32}/src/bt_cli/pws/config.py +0 -0
  133. {bt_cli-0.4.30 → bt_cli-0.4.32}/src/bt_cli/pws/models/__init__.py +0 -0
  134. {bt_cli-0.4.30 → bt_cli-0.4.32}/src/bt_cli/pws/models/account.py +0 -0
  135. {bt_cli-0.4.30 → bt_cli-0.4.32}/src/bt_cli/pws/models/asset.py +0 -0
  136. {bt_cli-0.4.30 → bt_cli-0.4.32}/src/bt_cli/pws/models/common.py +0 -0
  137. {bt_cli-0.4.30 → bt_cli-0.4.32}/src/bt_cli/pws/models/system.py +0 -0
  138. {bt_cli-0.4.30 → bt_cli-0.4.32}/tests/__init__.py +0 -0
  139. {bt_cli-0.4.30 → bt_cli-0.4.32}/tests/conftest.py +0 -0
  140. {bt_cli-0.4.30 → bt_cli-0.4.32}/tests/core/__init__.py +0 -0
  141. {bt_cli-0.4.30 → bt_cli-0.4.32}/tests/core/test_auth.py +0 -0
  142. {bt_cli-0.4.30 → bt_cli-0.4.32}/tests/core/test_config.py +0 -0
  143. {bt_cli-0.4.30 → bt_cli-0.4.32}/tests/core/test_errors.py +0 -0
  144. {bt_cli-0.4.30 → bt_cli-0.4.32}/tests/core/test_rest_debug.py +0 -0
  145. {bt_cli-0.4.30 → bt_cli-0.4.32}/tests/entitle/__init__.py +0 -0
  146. {bt_cli-0.4.30 → bt_cli-0.4.32}/tests/entitle/test_client.py +0 -0
  147. {bt_cli-0.4.30 → bt_cli-0.4.32}/tests/entitle/test_commands.py +0 -0
  148. {bt_cli-0.4.30 → bt_cli-0.4.32}/tests/entitle-smoke-test.sh +0 -0
  149. {bt_cli-0.4.30 → bt_cli-0.4.32}/tests/epmw/__init__.py +0 -0
  150. {bt_cli-0.4.30 → bt_cli-0.4.32}/tests/epmw/test_client.py +0 -0
  151. {bt_cli-0.4.30 → bt_cli-0.4.32}/tests/epmw/test_commands.py +0 -0
  152. {bt_cli-0.4.30 → bt_cli-0.4.32}/tests/epmw-quick-test-plan.md +0 -0
  153. {bt_cli-0.4.30 → bt_cli-0.4.32}/tests/fixtures/__init__.py +0 -0
  154. {bt_cli-0.4.30 → bt_cli-0.4.32}/tests/fixtures/responses.py +0 -0
  155. {bt_cli-0.4.30 → bt_cli-0.4.32}/tests/integration/__init__.py +0 -0
  156. {bt_cli-0.4.30 → bt_cli-0.4.32}/tests/integration/conftest.py +0 -0
  157. {bt_cli-0.4.30 → bt_cli-0.4.32}/tests/integration/helpers.py +0 -0
  158. {bt_cli-0.4.30 → bt_cli-0.4.32}/tests/integration/test_entitle_integration.py +0 -0
  159. {bt_cli-0.4.30 → bt_cli-0.4.32}/tests/integration/test_epmw_integration.py +0 -0
  160. {bt_cli-0.4.30 → bt_cli-0.4.32}/tests/integration/test_epmw_lifecycle.py +0 -0
  161. {bt_cli-0.4.30 → bt_cli-0.4.32}/tests/integration/test_pra_integration.py +0 -0
  162. {bt_cli-0.4.30 → bt_cli-0.4.32}/tests/integration/test_pra_lifecycle.py +0 -0
  163. {bt_cli-0.4.30 → bt_cli-0.4.32}/tests/integration/test_pws_integration.py +0 -0
  164. {bt_cli-0.4.30 → bt_cli-0.4.32}/tests/integration/test_pws_lifecycle.py +0 -0
  165. {bt_cli-0.4.30 → bt_cli-0.4.32}/tests/pra/__init__.py +0 -0
  166. {bt_cli-0.4.30 → bt_cli-0.4.32}/tests/pra/test_client.py +0 -0
  167. {bt_cli-0.4.30 → bt_cli-0.4.32}/tests/pra/test_commands.py +0 -0
  168. {bt_cli-0.4.30 → bt_cli-0.4.32}/tests/pra-smoke-test.sh +0 -0
  169. {bt_cli-0.4.30 → bt_cli-0.4.32}/tests/pra-test-plan.md +0 -0
  170. {bt_cli-0.4.30 → bt_cli-0.4.32}/tests/pws/__init__.py +0 -0
  171. {bt_cli-0.4.30 → bt_cli-0.4.32}/tests/pws/test_client.py +0 -0
  172. {bt_cli-0.4.30 → bt_cli-0.4.32}/tests/pws/test_commands.py +0 -0
  173. {bt_cli-0.4.30 → bt_cli-0.4.32}/tests/pws-quick-test-plan.md +0 -0
  174. {bt_cli-0.4.30 → bt_cli-0.4.32}/tests/pws-smoke-test.sh +0 -0
@@ -1,6 +1,6 @@
1
1
  # BT-CLI
2
2
 
3
- BeyondTrust Platform CLI for Password Safe, Entitle, PRA, and EPM Windows. **Version: 0.4.29**
3
+ BeyondTrust Platform CLI for Password Safe, Entitle, PRA, and EPM Windows. **Version: 0.4.32**
4
4
 
5
5
  ## Setup
6
6
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: bt-cli
3
- Version: 0.4.30
3
+ Version: 0.4.32
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.30"
7
+ version = "0.4.32"
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.30"
3
+ __version__ = "0.4.32"
@@ -160,23 +160,24 @@ class BeyondInsightMixin:
160
160
  search_term: str,
161
161
  limit: Optional[int] = None,
162
162
  ) -> list[dict[str, Any]]:
163
- """Search for assets.
164
-
165
- Args:
166
- search_term: Search term
167
- limit: Maximum number of results
168
-
169
- Returns:
170
- List of matching asset objects
171
- """
172
- data = {"SearchTerm": search_term}
173
- if limit:
174
- data["Limit"] = limit
175
-
176
- response = self.post("/Assets/Search", json=data)
177
- if isinstance(response, list):
178
- return response
179
- return response.get("Data", response.get("data", []))
163
+ """Search for assets by substring across name, DNS, IP, domain.
164
+
165
+ POST /Assets/Search only supports exact-match on specific fields
166
+ (AssetName, DnsName, IPAddress, etc.), so we fetch and filter
167
+ client-side for a fuzzy search experience.
168
+ """
169
+ assets = self.list_assets()
170
+ term = search_term.lower()
171
+ matched = [
172
+ a for a in assets
173
+ if term in (a.get("AssetName", "") or "").lower()
174
+ or term in (a.get("DnsName", "") or "").lower()
175
+ or term in (a.get("IPAddress", "") or "").lower()
176
+ or term in (a.get("DomainName", "") or "").lower()
177
+ ]
178
+ if limit is not None:
179
+ matched = matched[:limit]
180
+ return matched
180
181
 
181
182
  # =========================================================================
182
183
  # Managed Systems
@@ -190,25 +191,31 @@ class BeyondInsightMixin:
190
191
  ) -> list[dict[str, Any]]:
191
192
  """List managed systems.
192
193
 
193
- Args:
194
- workgroup_id: Optional workgroup filter
195
- search: Optional search filter
196
- limit: Maximum number of results
197
-
198
- Returns:
199
- List of managed system objects
194
+ /ManagedSystems only supports an exact-match ``name`` query param,
195
+ so ``search`` is applied client-side as a substring match across
196
+ SystemName / IPAddress / Description for parity with other commands.
200
197
  """
201
- params = {}
202
- if search:
203
- params["search"] = search
198
+ params: dict[str, Any] = {}
204
199
  if limit:
205
200
  params["limit"] = limit
206
201
 
207
202
  if workgroup_id:
208
- return self.paginate(
203
+ systems = self.paginate(
209
204
  f"/Workgroups/{workgroup_id}/ManagedSystems", params=params
210
205
  )
211
- return self.paginate("/ManagedSystems", params=params)
206
+ else:
207
+ systems = self.paginate("/ManagedSystems", params=params)
208
+
209
+ if search:
210
+ term = search.lower()
211
+ systems = [
212
+ s for s in systems
213
+ if term in (s.get("SystemName", "") or "").lower()
214
+ or term in (s.get("IPAddress", "") or "").lower()
215
+ or term in (s.get("Description", "") or "").lower()
216
+ ]
217
+
218
+ return systems
212
219
 
213
220
  def get_managed_system(
214
221
  self: "PasswordSafeClient", system_id: int
@@ -821,16 +821,29 @@ class PasswordSafeMixin:
821
821
  ) -> list[dict[str, Any]]:
822
822
  """List user groups.
823
823
 
824
- Args:
825
- search: Optional name filter
826
-
827
- Returns:
828
- List of user group objects
824
+ /UserGroups does not support pagination. The ``name`` server-side
825
+ param is an exact match, so filtering is handled client-side.
829
826
  """
830
- params = {}
827
+ result = self.get("/UserGroups")
828
+ if isinstance(result, list):
829
+ groups = result
830
+ elif isinstance(result, dict):
831
+ if "GroupID" in result:
832
+ groups = [result]
833
+ else:
834
+ groups = result.get("Data", result.get("results", []))
835
+ else:
836
+ groups = []
837
+
831
838
  if search:
832
- params["name"] = search
833
- return self.paginate("/UserGroups", params=params)
839
+ search_lower = search.lower()
840
+ groups = [
841
+ g for g in groups
842
+ if search_lower in (g.get("Name", "") or "").lower()
843
+ or search_lower in (g.get("Description", "") or "").lower()
844
+ ]
845
+
846
+ return groups
834
847
 
835
848
  def get_user_group(self: "PasswordSafeClient", group_id: int) -> dict[str, Any]:
836
849
  """Get a user group by ID.
@@ -848,13 +861,16 @@ class PasswordSafeMixin:
848
861
  ) -> list[dict[str, Any]]:
849
862
  """Get users in a user group.
850
863
 
851
- Args:
852
- group_id: User group ID
853
-
854
- Returns:
855
- List of user objects (includes ClientID, AccessPolicyID for API users)
864
+ /UserGroups/{id}/Users does not support pagination.
856
865
  """
857
- return self.paginate(f"/UserGroups/{group_id}/Users")
866
+ result = self.get(f"/UserGroups/{group_id}/Users")
867
+ if isinstance(result, list):
868
+ return result
869
+ if isinstance(result, dict):
870
+ if "UserID" in result:
871
+ return [result]
872
+ return result.get("Data", result.get("results", []))
873
+ return []
858
874
 
859
875
  # =========================================================================
860
876
  # Users
@@ -864,20 +880,29 @@ class PasswordSafeMixin:
864
880
  self: "PasswordSafeClient",
865
881
  search: Optional[str] = None,
866
882
  limit: Optional[int] = None,
883
+ include_inactive: bool = False,
867
884
  ) -> list[dict[str, Any]]:
868
885
  """List users.
869
886
 
870
- Args:
871
- search: Optional username search filter (client-side filtering)
872
- limit: Maximum number of results (None for all)
873
-
874
- Returns:
875
- List of user objects
887
+ /Users does not support pagination. The ``username`` server-side
888
+ param is an exact match (returns a single user dict or 404), so
889
+ searching is handled client-side across username/first/last/email.
876
890
  """
877
- # API doesn't support server-side search, so we fetch all and filter client-side
878
- users = self.paginate("/Users")
891
+ params: dict[str, Any] = {}
892
+ if include_inactive:
893
+ params["includeInactive"] = "true"
894
+
895
+ result = self.get("/Users", params=params)
896
+ if isinstance(result, list):
897
+ users = result
898
+ elif isinstance(result, dict):
899
+ if "UserID" in result:
900
+ users = [result]
901
+ else:
902
+ users = result.get("Data", result.get("results", []))
903
+ else:
904
+ users = []
879
905
 
880
- # Apply client-side search filter
881
906
  if search:
882
907
  search_lower = search.lower()
883
908
  users = [
@@ -888,7 +913,6 @@ class PasswordSafeMixin:
888
913
  or search_lower in (u.get("EmailAddress", "") or "").lower()
889
914
  ]
890
915
 
891
- # Apply limit
892
916
  if limit is not None:
893
917
  users = users[:limit]
894
918
 
@@ -905,6 +929,30 @@ class PasswordSafeMixin:
905
929
  """
906
930
  return self.get(f"/Users/{user_id}")
907
931
 
932
+ def get_user_by_username(
933
+ self: "PasswordSafeClient", username: str
934
+ ) -> Optional[dict[str, Any]]:
935
+ """Look up a single user by exact username (case-insensitive).
936
+
937
+ Uses the /Users?username= server-side lookup (single round trip,
938
+ no full-list fetch). Returns None on 404. Username match is
939
+ case-insensitive but exact — no substring matching.
940
+ """
941
+ import httpx as _httpx
942
+
943
+ try:
944
+ result = self.get("/Users", params={"username": username})
945
+ except _httpx.HTTPStatusError as e:
946
+ if e.response.status_code == 404:
947
+ return None
948
+ raise
949
+
950
+ if isinstance(result, dict) and "UserID" in result:
951
+ return result
952
+ if isinstance(result, list):
953
+ return result[0] if result else None
954
+ return None
955
+
908
956
  # =========================================================================
909
957
  # Roles
910
958
  # =========================================================================
@@ -42,6 +42,7 @@ def search(
42
42
  "managed_accounts": [],
43
43
  "managed_systems": [],
44
44
  "assets": [],
45
+ "users": [],
45
46
  "secrets": [],
46
47
  }
47
48
 
@@ -51,7 +52,6 @@ def search(
51
52
 
52
53
  console.print(f"[dim]Searching PWS for '{query}'...[/dim]\n")
53
54
 
54
- # Search functional accounts
55
55
  try:
56
56
  functional = client.list_functional_accounts()
57
57
  results["functional_accounts"] = [
@@ -63,52 +63,35 @@ def search(
63
63
  except Exception as e:
64
64
  console.print(f"[dim]Functional accounts: {e}[/dim]")
65
65
 
66
- # Search managed accounts
67
66
  try:
68
- # Use API search if available
69
- accounts = client.list_managed_accounts(account_name=query, limit=50)
70
- if not accounts:
71
- # Fall back to listing and filtering
72
- all_accounts = client.list_managed_accounts(limit=200)
73
- accounts = [
74
- a for a in all_accounts
75
- if query_lower in str(a.get("AccountName", "")).lower()
76
- or query_lower in str(a.get("SystemName", "")).lower()
77
- ]
78
- results["managed_accounts"] = accounts[:10]
67
+ all_accounts = client.list_managed_accounts(limit=500)
68
+ results["managed_accounts"] = [
69
+ a for a in all_accounts
70
+ if query_lower in str(a.get("AccountName", "")).lower()
71
+ or query_lower in str(a.get("SystemName", "")).lower()
72
+ ][:10]
79
73
  except Exception as e:
80
74
  console.print(f"[dim]Managed accounts: {e}[/dim]")
81
75
 
82
- # Search managed systems
83
76
  try:
84
- systems = client.list_managed_systems(search=query, limit=50)
85
- if not systems:
86
- all_systems = client.list_managed_systems(limit=200)
87
- systems = [
88
- s for s in all_systems
89
- if query_lower in str(s.get("SystemName", "")).lower()
90
- or query_lower in str(s.get("IPAddress", "")).lower()
91
- or query_lower in str(s.get("Description", "")).lower()
92
- ]
93
- results["managed_systems"] = systems[:10]
77
+ results["managed_systems"] = client.list_managed_systems(search=query)[:10]
94
78
  except Exception as e:
95
79
  console.print(f"[dim]Managed systems: {e}[/dim]")
96
80
 
97
- # Search assets
98
81
  try:
99
- assets = client.search_assets(query, limit=50)
100
- if not assets:
101
- all_assets = client.list_assets(limit=200)
102
- assets = [
103
- a for a in all_assets
104
- if query_lower in str(a.get("AssetName", "")).lower()
105
- or query_lower in str(a.get("IPAddress", "")).lower()
106
- or query_lower in str(a.get("DnsName", "")).lower()
107
- ]
108
- results["assets"] = assets[:10]
82
+ results["assets"] = client.search_assets(query, limit=10)
109
83
  except Exception as e:
110
84
  console.print(f"[dim]Assets: {e}[/dim]")
111
85
 
86
+ try:
87
+ exact_user = client.get_user_by_username(query)
88
+ if exact_user:
89
+ results["users"] = [exact_user]
90
+ else:
91
+ results["users"] = client.list_users(search=query, limit=10)
92
+ except Exception as e:
93
+ console.print(f"[dim]Users: {e}[/dim]")
94
+
112
95
  # Search secrets (if accessible)
113
96
  try:
114
97
  # Get all safes and search folders/secrets
@@ -222,6 +205,28 @@ def search(
222
205
  console.print(table)
223
206
  console.print()
224
207
 
208
+ # Users
209
+ if results["users"]:
210
+ total_found += len(results["users"])
211
+ table = Table(title="Users")
212
+ table.add_column("ID", style="cyan")
213
+ table.add_column("Username", style="green")
214
+ table.add_column("Name", style="yellow")
215
+ table.add_column("Email", style="blue")
216
+
217
+ for u in results["users"]:
218
+ first = u.get("FirstName") or ""
219
+ last = u.get("LastName") or ""
220
+ display_name = f"{first} {last}".strip() or "-"
221
+ table.add_row(
222
+ str(u.get("UserID", "")),
223
+ u.get("UserName", ""),
224
+ display_name,
225
+ u.get("EmailAddress") or "-",
226
+ )
227
+ console.print(table)
228
+ console.print()
229
+
225
230
  # Secrets
226
231
  if results["secrets"]:
227
232
  total_found += len(results["secrets"])
@@ -179,23 +179,29 @@ def print_roles_table(roles: list[dict], title: str = "Roles") -> None:
179
179
 
180
180
  @app.command("list")
181
181
  def list_users(
182
- search: Optional[str] = typer.Option(None, "--search", "-s", help="Search by username"),
182
+ search: Optional[str] = typer.Option(None, "--search", "-s", help="Search by username (also matches first/last/email)"),
183
183
  limit: int = typer.Option(50, "--limit", "-l", help="Maximum results (default: 50)"),
184
- fetch_all: bool = typer.Option(False, "--all", help="Fetch all results (may be slow)"),
184
+ fetch_all: bool = typer.Option(False, "--all", help="Fetch all results"),
185
+ include_inactive: bool = typer.Option(False, "--include-inactive", help="Include inactive users"),
185
186
  output: str = typer.Option("table", "--output", "-o", help="Output format: table or json"),
186
187
  ) -> None:
187
188
  """List all users.
188
189
 
189
190
  Examples:
190
- bt pws users list # First 50 users
191
- bt pws users list --all # All users
191
+ bt pws users list # First 50 active users
192
+ bt pws users list --all # All active users
193
+ bt pws users list --include-inactive # Include inactive users
192
194
  bt pws users list -s "admin" # Search by username
193
195
  bt pws users list -o json # JSON output
194
196
  """
195
197
  try:
196
198
  with get_client() as client:
197
199
  client.authenticate()
198
- users = client.list_users(search=search, limit=None if fetch_all else limit)
200
+ users = client.list_users(
201
+ search=search,
202
+ limit=None if fetch_all else limit,
203
+ include_inactive=include_inactive,
204
+ )
199
205
 
200
206
  if output == "json":
201
207
  console.print_json(json.dumps(users, default=str))
@@ -222,25 +228,39 @@ def list_users(
222
228
 
223
229
  @app.command("get")
224
230
  def get_user(
225
- user_id: int = typer.Argument(..., help="User ID"),
231
+ user: str = typer.Argument(..., help="User ID (int) or exact username"),
226
232
  output: str = typer.Option("table", "--output", "-o", help="Output format: table or json"),
227
233
  ) -> None:
228
- """Get a user by ID.
234
+ """Get a user by ID or exact username (case-insensitive).
235
+
236
+ Username lookup uses /Users?username= — one round trip, no full list
237
+ fetch. Username match is exact (no substring); use ``list -s`` for
238
+ fuzzy search.
229
239
 
230
240
  Examples:
231
- bt pws users get 1 # Get user ID 1
241
+ bt pws users get 1 # Lookup by ID
242
+ bt pws users get Administrator # Lookup by exact username
243
+ bt pws users get dave@example.com # If username is an email
232
244
  bt pws users get 4 -o json # JSON output
233
245
  """
234
246
  try:
235
247
  with get_client() as client:
236
248
  client.authenticate()
237
- user = client.get_user(user_id)
249
+ if user.isdigit():
250
+ user_obj = client.get_user(int(user))
251
+ else:
252
+ user_obj = client.get_user_by_username(user)
253
+ if user_obj is None:
254
+ console.print(f"[yellow]No user found with username '{user}'.[/yellow]")
255
+ raise typer.Exit(1)
238
256
 
239
257
  if output == "json":
240
- console.print_json(json.dumps(user, default=str))
258
+ console.print_json(json.dumps(user_obj, default=str))
241
259
  else:
242
- print_user_detail(user)
260
+ print_user_detail(user_obj)
243
261
 
262
+ except typer.Exit:
263
+ raise
244
264
  except httpx.HTTPStatusError as e:
245
265
  print_api_error(e, "get user")
246
266
  raise typer.Exit(1)
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes