bt-cli 0.4.31__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.31 → bt_cli-0.4.32}/CLAUDE.md +1 -1
  2. {bt_cli-0.4.31 → bt_cli-0.4.32}/PKG-INFO +1 -1
  3. {bt_cli-0.4.31 → bt_cli-0.4.32}/pyproject.toml +1 -1
  4. {bt_cli-0.4.31 → bt_cli-0.4.32}/src/bt_cli/__init__.py +1 -1
  5. {bt_cli-0.4.31 → bt_cli-0.4.32}/src/bt_cli/pws/client/beyondinsight.py +36 -29
  6. {bt_cli-0.4.31 → bt_cli-0.4.32}/src/bt_cli/pws/client/passwordsafe.py +24 -0
  7. {bt_cli-0.4.31 → bt_cli-0.4.32}/src/bt_cli/pws/commands/search.py +40 -35
  8. {bt_cli-0.4.31 → bt_cli-0.4.32}/src/bt_cli/pws/commands/users.py +20 -6
  9. {bt_cli-0.4.31 → bt_cli-0.4.32}/.claude/skills/bt/SKILL.md +0 -0
  10. {bt_cli-0.4.31 → bt_cli-0.4.32}/.claude/skills/entitle/SKILL.md +0 -0
  11. {bt_cli-0.4.31 → bt_cli-0.4.32}/.claude/skills/epmw/SKILL.md +0 -0
  12. {bt_cli-0.4.31 → bt_cli-0.4.32}/.claude/skills/pra/SKILL.md +0 -0
  13. {bt_cli-0.4.31 → bt_cli-0.4.32}/.claude/skills/pws/SKILL.md +0 -0
  14. {bt_cli-0.4.31 → bt_cli-0.4.32}/.env.example +0 -0
  15. {bt_cli-0.4.31 → bt_cli-0.4.32}/.github/workflows/ci.yml +0 -0
  16. {bt_cli-0.4.31 → bt_cli-0.4.32}/.github/workflows/release.yml +0 -0
  17. {bt_cli-0.4.31 → bt_cli-0.4.32}/.gitignore +0 -0
  18. {bt_cli-0.4.31 → bt_cli-0.4.32}/README.md +0 -0
  19. {bt_cli-0.4.31 → bt_cli-0.4.32}/assets/cli-help.png +0 -0
  20. {bt_cli-0.4.31 → bt_cli-0.4.32}/assets/cli-output.png +0 -0
  21. {bt_cli-0.4.31 → bt_cli-0.4.32}/bt-cli.spec +0 -0
  22. {bt_cli-0.4.31 → bt_cli-0.4.32}/bt_entry.py +0 -0
  23. {bt_cli-0.4.31 → bt_cli-0.4.32}/scripts/bt_entry.py +0 -0
  24. {bt_cli-0.4.31 → bt_cli-0.4.32}/scripts/sync-package-data.sh +0 -0
  25. {bt_cli-0.4.31 → bt_cli-0.4.32}/src/bt_cli/cli.py +0 -0
  26. {bt_cli-0.4.31 → bt_cli-0.4.32}/src/bt_cli/commands/__init__.py +0 -0
  27. {bt_cli-0.4.31 → bt_cli-0.4.32}/src/bt_cli/commands/configure.py +0 -0
  28. {bt_cli-0.4.31 → bt_cli-0.4.32}/src/bt_cli/commands/learn.py +0 -0
  29. {bt_cli-0.4.31 → bt_cli-0.4.32}/src/bt_cli/commands/quick.py +0 -0
  30. {bt_cli-0.4.31 → bt_cli-0.4.32}/src/bt_cli/core/__init__.py +0 -0
  31. {bt_cli-0.4.31 → bt_cli-0.4.32}/src/bt_cli/core/auth.py +0 -0
  32. {bt_cli-0.4.31 → bt_cli-0.4.32}/src/bt_cli/core/client.py +0 -0
  33. {bt_cli-0.4.31 → bt_cli-0.4.32}/src/bt_cli/core/config.py +0 -0
  34. {bt_cli-0.4.31 → bt_cli-0.4.32}/src/bt_cli/core/config_file.py +0 -0
  35. {bt_cli-0.4.31 → bt_cli-0.4.32}/src/bt_cli/core/csv_utils.py +0 -0
  36. {bt_cli-0.4.31 → bt_cli-0.4.32}/src/bt_cli/core/errors.py +0 -0
  37. {bt_cli-0.4.31 → bt_cli-0.4.32}/src/bt_cli/core/output.py +0 -0
  38. {bt_cli-0.4.31 → bt_cli-0.4.32}/src/bt_cli/core/prompts.py +0 -0
  39. {bt_cli-0.4.31 → bt_cli-0.4.32}/src/bt_cli/core/rest_debug.py +0 -0
  40. {bt_cli-0.4.31 → bt_cli-0.4.32}/src/bt_cli/data/CLAUDE.md +0 -0
  41. {bt_cli-0.4.31 → bt_cli-0.4.32}/src/bt_cli/data/__init__.py +0 -0
  42. {bt_cli-0.4.31 → bt_cli-0.4.32}/src/bt_cli/data/skills/bt/SKILL.md +0 -0
  43. {bt_cli-0.4.31 → bt_cli-0.4.32}/src/bt_cli/data/skills/entitle/SKILL.md +0 -0
  44. {bt_cli-0.4.31 → bt_cli-0.4.32}/src/bt_cli/data/skills/epmw/SKILL.md +0 -0
  45. {bt_cli-0.4.31 → bt_cli-0.4.32}/src/bt_cli/data/skills/pra/SKILL.md +0 -0
  46. {bt_cli-0.4.31 → bt_cli-0.4.32}/src/bt_cli/data/skills/pws/SKILL.md +0 -0
  47. {bt_cli-0.4.31 → bt_cli-0.4.32}/src/bt_cli/entitle/__init__.py +0 -0
  48. {bt_cli-0.4.31 → bt_cli-0.4.32}/src/bt_cli/entitle/client/__init__.py +0 -0
  49. {bt_cli-0.4.31 → bt_cli-0.4.32}/src/bt_cli/entitle/client/base.py +0 -0
  50. {bt_cli-0.4.31 → bt_cli-0.4.32}/src/bt_cli/entitle/commands/__init__.py +0 -0
  51. {bt_cli-0.4.31 → bt_cli-0.4.32}/src/bt_cli/entitle/commands/accounts.py +0 -0
  52. {bt_cli-0.4.31 → bt_cli-0.4.32}/src/bt_cli/entitle/commands/applications.py +0 -0
  53. {bt_cli-0.4.31 → bt_cli-0.4.32}/src/bt_cli/entitle/commands/auth.py +0 -0
  54. {bt_cli-0.4.31 → bt_cli-0.4.32}/src/bt_cli/entitle/commands/bundles.py +0 -0
  55. {bt_cli-0.4.31 → bt_cli-0.4.32}/src/bt_cli/entitle/commands/integrations.py +0 -0
  56. {bt_cli-0.4.31 → bt_cli-0.4.32}/src/bt_cli/entitle/commands/permissions.py +0 -0
  57. {bt_cli-0.4.31 → bt_cli-0.4.32}/src/bt_cli/entitle/commands/policies.py +0 -0
  58. {bt_cli-0.4.31 → bt_cli-0.4.32}/src/bt_cli/entitle/commands/resources.py +0 -0
  59. {bt_cli-0.4.31 → bt_cli-0.4.32}/src/bt_cli/entitle/commands/roles.py +0 -0
  60. {bt_cli-0.4.31 → bt_cli-0.4.32}/src/bt_cli/entitle/commands/users.py +0 -0
  61. {bt_cli-0.4.31 → bt_cli-0.4.32}/src/bt_cli/entitle/commands/workflows.py +0 -0
  62. {bt_cli-0.4.31 → bt_cli-0.4.32}/src/bt_cli/entitle/models/__init__.py +0 -0
  63. {bt_cli-0.4.31 → bt_cli-0.4.32}/src/bt_cli/entitle/models/bundle.py +0 -0
  64. {bt_cli-0.4.31 → bt_cli-0.4.32}/src/bt_cli/entitle/models/common.py +0 -0
  65. {bt_cli-0.4.31 → bt_cli-0.4.32}/src/bt_cli/entitle/models/integration.py +0 -0
  66. {bt_cli-0.4.31 → bt_cli-0.4.32}/src/bt_cli/entitle/models/permission.py +0 -0
  67. {bt_cli-0.4.31 → bt_cli-0.4.32}/src/bt_cli/entitle/models/policy.py +0 -0
  68. {bt_cli-0.4.31 → bt_cli-0.4.32}/src/bt_cli/entitle/models/resource.py +0 -0
  69. {bt_cli-0.4.31 → bt_cli-0.4.32}/src/bt_cli/entitle/models/role.py +0 -0
  70. {bt_cli-0.4.31 → bt_cli-0.4.32}/src/bt_cli/entitle/models/user.py +0 -0
  71. {bt_cli-0.4.31 → bt_cli-0.4.32}/src/bt_cli/entitle/models/workflow.py +0 -0
  72. {bt_cli-0.4.31 → bt_cli-0.4.32}/src/bt_cli/epmw/__init__.py +0 -0
  73. {bt_cli-0.4.31 → bt_cli-0.4.32}/src/bt_cli/epmw/client/__init__.py +0 -0
  74. {bt_cli-0.4.31 → bt_cli-0.4.32}/src/bt_cli/epmw/client/base.py +0 -0
  75. {bt_cli-0.4.31 → bt_cli-0.4.32}/src/bt_cli/epmw/commands/__init__.py +0 -0
  76. {bt_cli-0.4.31 → bt_cli-0.4.32}/src/bt_cli/epmw/commands/audits.py +0 -0
  77. {bt_cli-0.4.31 → bt_cli-0.4.32}/src/bt_cli/epmw/commands/auth.py +0 -0
  78. {bt_cli-0.4.31 → bt_cli-0.4.32}/src/bt_cli/epmw/commands/computers.py +0 -0
  79. {bt_cli-0.4.31 → bt_cli-0.4.32}/src/bt_cli/epmw/commands/events.py +0 -0
  80. {bt_cli-0.4.31 → bt_cli-0.4.32}/src/bt_cli/epmw/commands/groups.py +0 -0
  81. {bt_cli-0.4.31 → bt_cli-0.4.32}/src/bt_cli/epmw/commands/policies.py +0 -0
  82. {bt_cli-0.4.31 → bt_cli-0.4.32}/src/bt_cli/epmw/commands/quick.py +0 -0
  83. {bt_cli-0.4.31 → bt_cli-0.4.32}/src/bt_cli/epmw/commands/requests.py +0 -0
  84. {bt_cli-0.4.31 → bt_cli-0.4.32}/src/bt_cli/epmw/commands/roles.py +0 -0
  85. {bt_cli-0.4.31 → bt_cli-0.4.32}/src/bt_cli/epmw/commands/tasks.py +0 -0
  86. {bt_cli-0.4.31 → bt_cli-0.4.32}/src/bt_cli/epmw/commands/users.py +0 -0
  87. {bt_cli-0.4.31 → bt_cli-0.4.32}/src/bt_cli/epmw/models/__init__.py +0 -0
  88. {bt_cli-0.4.31 → bt_cli-0.4.32}/src/bt_cli/pra/__init__.py +0 -0
  89. {bt_cli-0.4.31 → bt_cli-0.4.32}/src/bt_cli/pra/client/__init__.py +0 -0
  90. {bt_cli-0.4.31 → bt_cli-0.4.32}/src/bt_cli/pra/client/base.py +0 -0
  91. {bt_cli-0.4.31 → bt_cli-0.4.32}/src/bt_cli/pra/commands/__init__.py +0 -0
  92. {bt_cli-0.4.31 → bt_cli-0.4.32}/src/bt_cli/pra/commands/auth.py +0 -0
  93. {bt_cli-0.4.31 → bt_cli-0.4.32}/src/bt_cli/pra/commands/import_export.py +0 -0
  94. {bt_cli-0.4.31 → bt_cli-0.4.32}/src/bt_cli/pra/commands/jump_clients.py +0 -0
  95. {bt_cli-0.4.31 → bt_cli-0.4.32}/src/bt_cli/pra/commands/jump_groups.py +0 -0
  96. {bt_cli-0.4.31 → bt_cli-0.4.32}/src/bt_cli/pra/commands/jump_items.py +0 -0
  97. {bt_cli-0.4.31 → bt_cli-0.4.32}/src/bt_cli/pra/commands/jumpoints.py +0 -0
  98. {bt_cli-0.4.31 → bt_cli-0.4.32}/src/bt_cli/pra/commands/policies.py +0 -0
  99. {bt_cli-0.4.31 → bt_cli-0.4.32}/src/bt_cli/pra/commands/quick.py +0 -0
  100. {bt_cli-0.4.31 → bt_cli-0.4.32}/src/bt_cli/pra/commands/teams.py +0 -0
  101. {bt_cli-0.4.31 → bt_cli-0.4.32}/src/bt_cli/pra/commands/users.py +0 -0
  102. {bt_cli-0.4.31 → bt_cli-0.4.32}/src/bt_cli/pra/commands/vault.py +0 -0
  103. {bt_cli-0.4.31 → bt_cli-0.4.32}/src/bt_cli/pra/models/__init__.py +0 -0
  104. {bt_cli-0.4.31 → bt_cli-0.4.32}/src/bt_cli/pra/models/common.py +0 -0
  105. {bt_cli-0.4.31 → bt_cli-0.4.32}/src/bt_cli/pra/models/jump_client.py +0 -0
  106. {bt_cli-0.4.31 → bt_cli-0.4.32}/src/bt_cli/pra/models/jump_group.py +0 -0
  107. {bt_cli-0.4.31 → bt_cli-0.4.32}/src/bt_cli/pra/models/jump_item.py +0 -0
  108. {bt_cli-0.4.31 → bt_cli-0.4.32}/src/bt_cli/pra/models/jumpoint.py +0 -0
  109. {bt_cli-0.4.31 → bt_cli-0.4.32}/src/bt_cli/pra/models/team.py +0 -0
  110. {bt_cli-0.4.31 → bt_cli-0.4.32}/src/bt_cli/pra/models/user.py +0 -0
  111. {bt_cli-0.4.31 → bt_cli-0.4.32}/src/bt_cli/pra/models/vault.py +0 -0
  112. {bt_cli-0.4.31 → bt_cli-0.4.32}/src/bt_cli/pws/__init__.py +0 -0
  113. {bt_cli-0.4.31 → bt_cli-0.4.32}/src/bt_cli/pws/client/__init__.py +0 -0
  114. {bt_cli-0.4.31 → bt_cli-0.4.32}/src/bt_cli/pws/client/base.py +0 -0
  115. {bt_cli-0.4.31 → bt_cli-0.4.32}/src/bt_cli/pws/commands/__init__.py +0 -0
  116. {bt_cli-0.4.31 → bt_cli-0.4.32}/src/bt_cli/pws/commands/accounts.py +0 -0
  117. {bt_cli-0.4.31 → bt_cli-0.4.32}/src/bt_cli/pws/commands/assets.py +0 -0
  118. {bt_cli-0.4.31 → bt_cli-0.4.32}/src/bt_cli/pws/commands/attributes.py +0 -0
  119. {bt_cli-0.4.31 → bt_cli-0.4.32}/src/bt_cli/pws/commands/auth.py +0 -0
  120. {bt_cli-0.4.31 → bt_cli-0.4.32}/src/bt_cli/pws/commands/clouds.py +0 -0
  121. {bt_cli-0.4.31 → bt_cli-0.4.32}/src/bt_cli/pws/commands/config.py +0 -0
  122. {bt_cli-0.4.31 → bt_cli-0.4.32}/src/bt_cli/pws/commands/credentials.py +0 -0
  123. {bt_cli-0.4.31 → bt_cli-0.4.32}/src/bt_cli/pws/commands/databases.py +0 -0
  124. {bt_cli-0.4.31 → bt_cli-0.4.32}/src/bt_cli/pws/commands/directories.py +0 -0
  125. {bt_cli-0.4.31 → bt_cli-0.4.32}/src/bt_cli/pws/commands/functional.py +0 -0
  126. {bt_cli-0.4.31 → bt_cli-0.4.32}/src/bt_cli/pws/commands/import_export.py +0 -0
  127. {bt_cli-0.4.31 → bt_cli-0.4.32}/src/bt_cli/pws/commands/platforms.py +0 -0
  128. {bt_cli-0.4.31 → bt_cli-0.4.32}/src/bt_cli/pws/commands/quick.py +0 -0
  129. {bt_cli-0.4.31 → bt_cli-0.4.32}/src/bt_cli/pws/commands/secrets.py +0 -0
  130. {bt_cli-0.4.31 → bt_cli-0.4.32}/src/bt_cli/pws/commands/systems.py +0 -0
  131. {bt_cli-0.4.31 → bt_cli-0.4.32}/src/bt_cli/pws/commands/workgroups.py +0 -0
  132. {bt_cli-0.4.31 → bt_cli-0.4.32}/src/bt_cli/pws/config.py +0 -0
  133. {bt_cli-0.4.31 → bt_cli-0.4.32}/src/bt_cli/pws/models/__init__.py +0 -0
  134. {bt_cli-0.4.31 → bt_cli-0.4.32}/src/bt_cli/pws/models/account.py +0 -0
  135. {bt_cli-0.4.31 → bt_cli-0.4.32}/src/bt_cli/pws/models/asset.py +0 -0
  136. {bt_cli-0.4.31 → bt_cli-0.4.32}/src/bt_cli/pws/models/common.py +0 -0
  137. {bt_cli-0.4.31 → bt_cli-0.4.32}/src/bt_cli/pws/models/system.py +0 -0
  138. {bt_cli-0.4.31 → bt_cli-0.4.32}/tests/__init__.py +0 -0
  139. {bt_cli-0.4.31 → bt_cli-0.4.32}/tests/conftest.py +0 -0
  140. {bt_cli-0.4.31 → bt_cli-0.4.32}/tests/core/__init__.py +0 -0
  141. {bt_cli-0.4.31 → bt_cli-0.4.32}/tests/core/test_auth.py +0 -0
  142. {bt_cli-0.4.31 → bt_cli-0.4.32}/tests/core/test_config.py +0 -0
  143. {bt_cli-0.4.31 → bt_cli-0.4.32}/tests/core/test_errors.py +0 -0
  144. {bt_cli-0.4.31 → bt_cli-0.4.32}/tests/core/test_rest_debug.py +0 -0
  145. {bt_cli-0.4.31 → bt_cli-0.4.32}/tests/entitle/__init__.py +0 -0
  146. {bt_cli-0.4.31 → bt_cli-0.4.32}/tests/entitle/test_client.py +0 -0
  147. {bt_cli-0.4.31 → bt_cli-0.4.32}/tests/entitle/test_commands.py +0 -0
  148. {bt_cli-0.4.31 → bt_cli-0.4.32}/tests/entitle-smoke-test.sh +0 -0
  149. {bt_cli-0.4.31 → bt_cli-0.4.32}/tests/epmw/__init__.py +0 -0
  150. {bt_cli-0.4.31 → bt_cli-0.4.32}/tests/epmw/test_client.py +0 -0
  151. {bt_cli-0.4.31 → bt_cli-0.4.32}/tests/epmw/test_commands.py +0 -0
  152. {bt_cli-0.4.31 → bt_cli-0.4.32}/tests/epmw-quick-test-plan.md +0 -0
  153. {bt_cli-0.4.31 → bt_cli-0.4.32}/tests/fixtures/__init__.py +0 -0
  154. {bt_cli-0.4.31 → bt_cli-0.4.32}/tests/fixtures/responses.py +0 -0
  155. {bt_cli-0.4.31 → bt_cli-0.4.32}/tests/integration/__init__.py +0 -0
  156. {bt_cli-0.4.31 → bt_cli-0.4.32}/tests/integration/conftest.py +0 -0
  157. {bt_cli-0.4.31 → bt_cli-0.4.32}/tests/integration/helpers.py +0 -0
  158. {bt_cli-0.4.31 → bt_cli-0.4.32}/tests/integration/test_entitle_integration.py +0 -0
  159. {bt_cli-0.4.31 → bt_cli-0.4.32}/tests/integration/test_epmw_integration.py +0 -0
  160. {bt_cli-0.4.31 → bt_cli-0.4.32}/tests/integration/test_epmw_lifecycle.py +0 -0
  161. {bt_cli-0.4.31 → bt_cli-0.4.32}/tests/integration/test_pra_integration.py +0 -0
  162. {bt_cli-0.4.31 → bt_cli-0.4.32}/tests/integration/test_pra_lifecycle.py +0 -0
  163. {bt_cli-0.4.31 → bt_cli-0.4.32}/tests/integration/test_pws_integration.py +0 -0
  164. {bt_cli-0.4.31 → bt_cli-0.4.32}/tests/integration/test_pws_lifecycle.py +0 -0
  165. {bt_cli-0.4.31 → bt_cli-0.4.32}/tests/pra/__init__.py +0 -0
  166. {bt_cli-0.4.31 → bt_cli-0.4.32}/tests/pra/test_client.py +0 -0
  167. {bt_cli-0.4.31 → bt_cli-0.4.32}/tests/pra/test_commands.py +0 -0
  168. {bt_cli-0.4.31 → bt_cli-0.4.32}/tests/pra-smoke-test.sh +0 -0
  169. {bt_cli-0.4.31 → bt_cli-0.4.32}/tests/pra-test-plan.md +0 -0
  170. {bt_cli-0.4.31 → bt_cli-0.4.32}/tests/pws/__init__.py +0 -0
  171. {bt_cli-0.4.31 → bt_cli-0.4.32}/tests/pws/test_client.py +0 -0
  172. {bt_cli-0.4.31 → bt_cli-0.4.32}/tests/pws/test_commands.py +0 -0
  173. {bt_cli-0.4.31 → bt_cli-0.4.32}/tests/pws-quick-test-plan.md +0 -0
  174. {bt_cli-0.4.31 → 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.31**
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.31
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.31"
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.31"
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
@@ -929,6 +929,30 @@ class PasswordSafeMixin:
929
929
  """
930
930
  return self.get(f"/Users/{user_id}")
931
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
+
932
956
  # =========================================================================
933
957
  # Roles
934
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"])
@@ -228,25 +228,39 @@ def list_users(
228
228
 
229
229
  @app.command("get")
230
230
  def get_user(
231
- user_id: int = typer.Argument(..., help="User ID"),
231
+ user: str = typer.Argument(..., help="User ID (int) or exact username"),
232
232
  output: str = typer.Option("table", "--output", "-o", help="Output format: table or json"),
233
233
  ) -> None:
234
- """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.
235
239
 
236
240
  Examples:
237
- 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
238
244
  bt pws users get 4 -o json # JSON output
239
245
  """
240
246
  try:
241
247
  with get_client() as client:
242
248
  client.authenticate()
243
- 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)
244
256
 
245
257
  if output == "json":
246
- console.print_json(json.dumps(user, default=str))
258
+ console.print_json(json.dumps(user_obj, default=str))
247
259
  else:
248
- print_user_detail(user)
260
+ print_user_detail(user_obj)
249
261
 
262
+ except typer.Exit:
263
+ raise
250
264
  except httpx.HTTPStatusError as e:
251
265
  print_api_error(e, "get user")
252
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