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.
- {bt_cli-0.4.30 → bt_cli-0.4.32}/CLAUDE.md +1 -1
- {bt_cli-0.4.30 → bt_cli-0.4.32}/PKG-INFO +1 -1
- {bt_cli-0.4.30 → bt_cli-0.4.32}/pyproject.toml +1 -1
- {bt_cli-0.4.30 → bt_cli-0.4.32}/src/bt_cli/__init__.py +1 -1
- {bt_cli-0.4.30 → bt_cli-0.4.32}/src/bt_cli/pws/client/beyondinsight.py +36 -29
- {bt_cli-0.4.30 → bt_cli-0.4.32}/src/bt_cli/pws/client/passwordsafe.py +72 -24
- {bt_cli-0.4.30 → bt_cli-0.4.32}/src/bt_cli/pws/commands/search.py +40 -35
- {bt_cli-0.4.30 → bt_cli-0.4.32}/src/bt_cli/pws/commands/users.py +31 -11
- {bt_cli-0.4.30 → bt_cli-0.4.32}/.claude/skills/bt/SKILL.md +0 -0
- {bt_cli-0.4.30 → bt_cli-0.4.32}/.claude/skills/entitle/SKILL.md +0 -0
- {bt_cli-0.4.30 → bt_cli-0.4.32}/.claude/skills/epmw/SKILL.md +0 -0
- {bt_cli-0.4.30 → bt_cli-0.4.32}/.claude/skills/pra/SKILL.md +0 -0
- {bt_cli-0.4.30 → bt_cli-0.4.32}/.claude/skills/pws/SKILL.md +0 -0
- {bt_cli-0.4.30 → bt_cli-0.4.32}/.env.example +0 -0
- {bt_cli-0.4.30 → bt_cli-0.4.32}/.github/workflows/ci.yml +0 -0
- {bt_cli-0.4.30 → bt_cli-0.4.32}/.github/workflows/release.yml +0 -0
- {bt_cli-0.4.30 → bt_cli-0.4.32}/.gitignore +0 -0
- {bt_cli-0.4.30 → bt_cli-0.4.32}/README.md +0 -0
- {bt_cli-0.4.30 → bt_cli-0.4.32}/assets/cli-help.png +0 -0
- {bt_cli-0.4.30 → bt_cli-0.4.32}/assets/cli-output.png +0 -0
- {bt_cli-0.4.30 → bt_cli-0.4.32}/bt-cli.spec +0 -0
- {bt_cli-0.4.30 → bt_cli-0.4.32}/bt_entry.py +0 -0
- {bt_cli-0.4.30 → bt_cli-0.4.32}/scripts/bt_entry.py +0 -0
- {bt_cli-0.4.30 → bt_cli-0.4.32}/scripts/sync-package-data.sh +0 -0
- {bt_cli-0.4.30 → bt_cli-0.4.32}/src/bt_cli/cli.py +0 -0
- {bt_cli-0.4.30 → bt_cli-0.4.32}/src/bt_cli/commands/__init__.py +0 -0
- {bt_cli-0.4.30 → bt_cli-0.4.32}/src/bt_cli/commands/configure.py +0 -0
- {bt_cli-0.4.30 → bt_cli-0.4.32}/src/bt_cli/commands/learn.py +0 -0
- {bt_cli-0.4.30 → bt_cli-0.4.32}/src/bt_cli/commands/quick.py +0 -0
- {bt_cli-0.4.30 → bt_cli-0.4.32}/src/bt_cli/core/__init__.py +0 -0
- {bt_cli-0.4.30 → bt_cli-0.4.32}/src/bt_cli/core/auth.py +0 -0
- {bt_cli-0.4.30 → bt_cli-0.4.32}/src/bt_cli/core/client.py +0 -0
- {bt_cli-0.4.30 → bt_cli-0.4.32}/src/bt_cli/core/config.py +0 -0
- {bt_cli-0.4.30 → bt_cli-0.4.32}/src/bt_cli/core/config_file.py +0 -0
- {bt_cli-0.4.30 → bt_cli-0.4.32}/src/bt_cli/core/csv_utils.py +0 -0
- {bt_cli-0.4.30 → bt_cli-0.4.32}/src/bt_cli/core/errors.py +0 -0
- {bt_cli-0.4.30 → bt_cli-0.4.32}/src/bt_cli/core/output.py +0 -0
- {bt_cli-0.4.30 → bt_cli-0.4.32}/src/bt_cli/core/prompts.py +0 -0
- {bt_cli-0.4.30 → bt_cli-0.4.32}/src/bt_cli/core/rest_debug.py +0 -0
- {bt_cli-0.4.30 → bt_cli-0.4.32}/src/bt_cli/data/CLAUDE.md +0 -0
- {bt_cli-0.4.30 → bt_cli-0.4.32}/src/bt_cli/data/__init__.py +0 -0
- {bt_cli-0.4.30 → bt_cli-0.4.32}/src/bt_cli/data/skills/bt/SKILL.md +0 -0
- {bt_cli-0.4.30 → bt_cli-0.4.32}/src/bt_cli/data/skills/entitle/SKILL.md +0 -0
- {bt_cli-0.4.30 → bt_cli-0.4.32}/src/bt_cli/data/skills/epmw/SKILL.md +0 -0
- {bt_cli-0.4.30 → bt_cli-0.4.32}/src/bt_cli/data/skills/pra/SKILL.md +0 -0
- {bt_cli-0.4.30 → bt_cli-0.4.32}/src/bt_cli/data/skills/pws/SKILL.md +0 -0
- {bt_cli-0.4.30 → bt_cli-0.4.32}/src/bt_cli/entitle/__init__.py +0 -0
- {bt_cli-0.4.30 → bt_cli-0.4.32}/src/bt_cli/entitle/client/__init__.py +0 -0
- {bt_cli-0.4.30 → bt_cli-0.4.32}/src/bt_cli/entitle/client/base.py +0 -0
- {bt_cli-0.4.30 → bt_cli-0.4.32}/src/bt_cli/entitle/commands/__init__.py +0 -0
- {bt_cli-0.4.30 → bt_cli-0.4.32}/src/bt_cli/entitle/commands/accounts.py +0 -0
- {bt_cli-0.4.30 → bt_cli-0.4.32}/src/bt_cli/entitle/commands/applications.py +0 -0
- {bt_cli-0.4.30 → bt_cli-0.4.32}/src/bt_cli/entitle/commands/auth.py +0 -0
- {bt_cli-0.4.30 → bt_cli-0.4.32}/src/bt_cli/entitle/commands/bundles.py +0 -0
- {bt_cli-0.4.30 → bt_cli-0.4.32}/src/bt_cli/entitle/commands/integrations.py +0 -0
- {bt_cli-0.4.30 → bt_cli-0.4.32}/src/bt_cli/entitle/commands/permissions.py +0 -0
- {bt_cli-0.4.30 → bt_cli-0.4.32}/src/bt_cli/entitle/commands/policies.py +0 -0
- {bt_cli-0.4.30 → bt_cli-0.4.32}/src/bt_cli/entitle/commands/resources.py +0 -0
- {bt_cli-0.4.30 → bt_cli-0.4.32}/src/bt_cli/entitle/commands/roles.py +0 -0
- {bt_cli-0.4.30 → bt_cli-0.4.32}/src/bt_cli/entitle/commands/users.py +0 -0
- {bt_cli-0.4.30 → bt_cli-0.4.32}/src/bt_cli/entitle/commands/workflows.py +0 -0
- {bt_cli-0.4.30 → bt_cli-0.4.32}/src/bt_cli/entitle/models/__init__.py +0 -0
- {bt_cli-0.4.30 → bt_cli-0.4.32}/src/bt_cli/entitle/models/bundle.py +0 -0
- {bt_cli-0.4.30 → bt_cli-0.4.32}/src/bt_cli/entitle/models/common.py +0 -0
- {bt_cli-0.4.30 → bt_cli-0.4.32}/src/bt_cli/entitle/models/integration.py +0 -0
- {bt_cli-0.4.30 → bt_cli-0.4.32}/src/bt_cli/entitle/models/permission.py +0 -0
- {bt_cli-0.4.30 → bt_cli-0.4.32}/src/bt_cli/entitle/models/policy.py +0 -0
- {bt_cli-0.4.30 → bt_cli-0.4.32}/src/bt_cli/entitle/models/resource.py +0 -0
- {bt_cli-0.4.30 → bt_cli-0.4.32}/src/bt_cli/entitle/models/role.py +0 -0
- {bt_cli-0.4.30 → bt_cli-0.4.32}/src/bt_cli/entitle/models/user.py +0 -0
- {bt_cli-0.4.30 → bt_cli-0.4.32}/src/bt_cli/entitle/models/workflow.py +0 -0
- {bt_cli-0.4.30 → bt_cli-0.4.32}/src/bt_cli/epmw/__init__.py +0 -0
- {bt_cli-0.4.30 → bt_cli-0.4.32}/src/bt_cli/epmw/client/__init__.py +0 -0
- {bt_cli-0.4.30 → bt_cli-0.4.32}/src/bt_cli/epmw/client/base.py +0 -0
- {bt_cli-0.4.30 → bt_cli-0.4.32}/src/bt_cli/epmw/commands/__init__.py +0 -0
- {bt_cli-0.4.30 → bt_cli-0.4.32}/src/bt_cli/epmw/commands/audits.py +0 -0
- {bt_cli-0.4.30 → bt_cli-0.4.32}/src/bt_cli/epmw/commands/auth.py +0 -0
- {bt_cli-0.4.30 → bt_cli-0.4.32}/src/bt_cli/epmw/commands/computers.py +0 -0
- {bt_cli-0.4.30 → bt_cli-0.4.32}/src/bt_cli/epmw/commands/events.py +0 -0
- {bt_cli-0.4.30 → bt_cli-0.4.32}/src/bt_cli/epmw/commands/groups.py +0 -0
- {bt_cli-0.4.30 → bt_cli-0.4.32}/src/bt_cli/epmw/commands/policies.py +0 -0
- {bt_cli-0.4.30 → bt_cli-0.4.32}/src/bt_cli/epmw/commands/quick.py +0 -0
- {bt_cli-0.4.30 → bt_cli-0.4.32}/src/bt_cli/epmw/commands/requests.py +0 -0
- {bt_cli-0.4.30 → bt_cli-0.4.32}/src/bt_cli/epmw/commands/roles.py +0 -0
- {bt_cli-0.4.30 → bt_cli-0.4.32}/src/bt_cli/epmw/commands/tasks.py +0 -0
- {bt_cli-0.4.30 → bt_cli-0.4.32}/src/bt_cli/epmw/commands/users.py +0 -0
- {bt_cli-0.4.30 → bt_cli-0.4.32}/src/bt_cli/epmw/models/__init__.py +0 -0
- {bt_cli-0.4.30 → bt_cli-0.4.32}/src/bt_cli/pra/__init__.py +0 -0
- {bt_cli-0.4.30 → bt_cli-0.4.32}/src/bt_cli/pra/client/__init__.py +0 -0
- {bt_cli-0.4.30 → bt_cli-0.4.32}/src/bt_cli/pra/client/base.py +0 -0
- {bt_cli-0.4.30 → bt_cli-0.4.32}/src/bt_cli/pra/commands/__init__.py +0 -0
- {bt_cli-0.4.30 → bt_cli-0.4.32}/src/bt_cli/pra/commands/auth.py +0 -0
- {bt_cli-0.4.30 → bt_cli-0.4.32}/src/bt_cli/pra/commands/import_export.py +0 -0
- {bt_cli-0.4.30 → bt_cli-0.4.32}/src/bt_cli/pra/commands/jump_clients.py +0 -0
- {bt_cli-0.4.30 → bt_cli-0.4.32}/src/bt_cli/pra/commands/jump_groups.py +0 -0
- {bt_cli-0.4.30 → bt_cli-0.4.32}/src/bt_cli/pra/commands/jump_items.py +0 -0
- {bt_cli-0.4.30 → bt_cli-0.4.32}/src/bt_cli/pra/commands/jumpoints.py +0 -0
- {bt_cli-0.4.30 → bt_cli-0.4.32}/src/bt_cli/pra/commands/policies.py +0 -0
- {bt_cli-0.4.30 → bt_cli-0.4.32}/src/bt_cli/pra/commands/quick.py +0 -0
- {bt_cli-0.4.30 → bt_cli-0.4.32}/src/bt_cli/pra/commands/teams.py +0 -0
- {bt_cli-0.4.30 → bt_cli-0.4.32}/src/bt_cli/pra/commands/users.py +0 -0
- {bt_cli-0.4.30 → bt_cli-0.4.32}/src/bt_cli/pra/commands/vault.py +0 -0
- {bt_cli-0.4.30 → bt_cli-0.4.32}/src/bt_cli/pra/models/__init__.py +0 -0
- {bt_cli-0.4.30 → bt_cli-0.4.32}/src/bt_cli/pra/models/common.py +0 -0
- {bt_cli-0.4.30 → bt_cli-0.4.32}/src/bt_cli/pra/models/jump_client.py +0 -0
- {bt_cli-0.4.30 → bt_cli-0.4.32}/src/bt_cli/pra/models/jump_group.py +0 -0
- {bt_cli-0.4.30 → bt_cli-0.4.32}/src/bt_cli/pra/models/jump_item.py +0 -0
- {bt_cli-0.4.30 → bt_cli-0.4.32}/src/bt_cli/pra/models/jumpoint.py +0 -0
- {bt_cli-0.4.30 → bt_cli-0.4.32}/src/bt_cli/pra/models/team.py +0 -0
- {bt_cli-0.4.30 → bt_cli-0.4.32}/src/bt_cli/pra/models/user.py +0 -0
- {bt_cli-0.4.30 → bt_cli-0.4.32}/src/bt_cli/pra/models/vault.py +0 -0
- {bt_cli-0.4.30 → bt_cli-0.4.32}/src/bt_cli/pws/__init__.py +0 -0
- {bt_cli-0.4.30 → bt_cli-0.4.32}/src/bt_cli/pws/client/__init__.py +0 -0
- {bt_cli-0.4.30 → bt_cli-0.4.32}/src/bt_cli/pws/client/base.py +0 -0
- {bt_cli-0.4.30 → bt_cli-0.4.32}/src/bt_cli/pws/commands/__init__.py +0 -0
- {bt_cli-0.4.30 → bt_cli-0.4.32}/src/bt_cli/pws/commands/accounts.py +0 -0
- {bt_cli-0.4.30 → bt_cli-0.4.32}/src/bt_cli/pws/commands/assets.py +0 -0
- {bt_cli-0.4.30 → bt_cli-0.4.32}/src/bt_cli/pws/commands/attributes.py +0 -0
- {bt_cli-0.4.30 → bt_cli-0.4.32}/src/bt_cli/pws/commands/auth.py +0 -0
- {bt_cli-0.4.30 → bt_cli-0.4.32}/src/bt_cli/pws/commands/clouds.py +0 -0
- {bt_cli-0.4.30 → bt_cli-0.4.32}/src/bt_cli/pws/commands/config.py +0 -0
- {bt_cli-0.4.30 → bt_cli-0.4.32}/src/bt_cli/pws/commands/credentials.py +0 -0
- {bt_cli-0.4.30 → bt_cli-0.4.32}/src/bt_cli/pws/commands/databases.py +0 -0
- {bt_cli-0.4.30 → bt_cli-0.4.32}/src/bt_cli/pws/commands/directories.py +0 -0
- {bt_cli-0.4.30 → bt_cli-0.4.32}/src/bt_cli/pws/commands/functional.py +0 -0
- {bt_cli-0.4.30 → bt_cli-0.4.32}/src/bt_cli/pws/commands/import_export.py +0 -0
- {bt_cli-0.4.30 → bt_cli-0.4.32}/src/bt_cli/pws/commands/platforms.py +0 -0
- {bt_cli-0.4.30 → bt_cli-0.4.32}/src/bt_cli/pws/commands/quick.py +0 -0
- {bt_cli-0.4.30 → bt_cli-0.4.32}/src/bt_cli/pws/commands/secrets.py +0 -0
- {bt_cli-0.4.30 → bt_cli-0.4.32}/src/bt_cli/pws/commands/systems.py +0 -0
- {bt_cli-0.4.30 → bt_cli-0.4.32}/src/bt_cli/pws/commands/workgroups.py +0 -0
- {bt_cli-0.4.30 → bt_cli-0.4.32}/src/bt_cli/pws/config.py +0 -0
- {bt_cli-0.4.30 → bt_cli-0.4.32}/src/bt_cli/pws/models/__init__.py +0 -0
- {bt_cli-0.4.30 → bt_cli-0.4.32}/src/bt_cli/pws/models/account.py +0 -0
- {bt_cli-0.4.30 → bt_cli-0.4.32}/src/bt_cli/pws/models/asset.py +0 -0
- {bt_cli-0.4.30 → bt_cli-0.4.32}/src/bt_cli/pws/models/common.py +0 -0
- {bt_cli-0.4.30 → bt_cli-0.4.32}/src/bt_cli/pws/models/system.py +0 -0
- {bt_cli-0.4.30 → bt_cli-0.4.32}/tests/__init__.py +0 -0
- {bt_cli-0.4.30 → bt_cli-0.4.32}/tests/conftest.py +0 -0
- {bt_cli-0.4.30 → bt_cli-0.4.32}/tests/core/__init__.py +0 -0
- {bt_cli-0.4.30 → bt_cli-0.4.32}/tests/core/test_auth.py +0 -0
- {bt_cli-0.4.30 → bt_cli-0.4.32}/tests/core/test_config.py +0 -0
- {bt_cli-0.4.30 → bt_cli-0.4.32}/tests/core/test_errors.py +0 -0
- {bt_cli-0.4.30 → bt_cli-0.4.32}/tests/core/test_rest_debug.py +0 -0
- {bt_cli-0.4.30 → bt_cli-0.4.32}/tests/entitle/__init__.py +0 -0
- {bt_cli-0.4.30 → bt_cli-0.4.32}/tests/entitle/test_client.py +0 -0
- {bt_cli-0.4.30 → bt_cli-0.4.32}/tests/entitle/test_commands.py +0 -0
- {bt_cli-0.4.30 → bt_cli-0.4.32}/tests/entitle-smoke-test.sh +0 -0
- {bt_cli-0.4.30 → bt_cli-0.4.32}/tests/epmw/__init__.py +0 -0
- {bt_cli-0.4.30 → bt_cli-0.4.32}/tests/epmw/test_client.py +0 -0
- {bt_cli-0.4.30 → bt_cli-0.4.32}/tests/epmw/test_commands.py +0 -0
- {bt_cli-0.4.30 → bt_cli-0.4.32}/tests/epmw-quick-test-plan.md +0 -0
- {bt_cli-0.4.30 → bt_cli-0.4.32}/tests/fixtures/__init__.py +0 -0
- {bt_cli-0.4.30 → bt_cli-0.4.32}/tests/fixtures/responses.py +0 -0
- {bt_cli-0.4.30 → bt_cli-0.4.32}/tests/integration/__init__.py +0 -0
- {bt_cli-0.4.30 → bt_cli-0.4.32}/tests/integration/conftest.py +0 -0
- {bt_cli-0.4.30 → bt_cli-0.4.32}/tests/integration/helpers.py +0 -0
- {bt_cli-0.4.30 → bt_cli-0.4.32}/tests/integration/test_entitle_integration.py +0 -0
- {bt_cli-0.4.30 → bt_cli-0.4.32}/tests/integration/test_epmw_integration.py +0 -0
- {bt_cli-0.4.30 → bt_cli-0.4.32}/tests/integration/test_epmw_lifecycle.py +0 -0
- {bt_cli-0.4.30 → bt_cli-0.4.32}/tests/integration/test_pra_integration.py +0 -0
- {bt_cli-0.4.30 → bt_cli-0.4.32}/tests/integration/test_pra_lifecycle.py +0 -0
- {bt_cli-0.4.30 → bt_cli-0.4.32}/tests/integration/test_pws_integration.py +0 -0
- {bt_cli-0.4.30 → bt_cli-0.4.32}/tests/integration/test_pws_lifecycle.py +0 -0
- {bt_cli-0.4.30 → bt_cli-0.4.32}/tests/pra/__init__.py +0 -0
- {bt_cli-0.4.30 → bt_cli-0.4.32}/tests/pra/test_client.py +0 -0
- {bt_cli-0.4.30 → bt_cli-0.4.32}/tests/pra/test_commands.py +0 -0
- {bt_cli-0.4.30 → bt_cli-0.4.32}/tests/pra-smoke-test.sh +0 -0
- {bt_cli-0.4.30 → bt_cli-0.4.32}/tests/pra-test-plan.md +0 -0
- {bt_cli-0.4.30 → bt_cli-0.4.32}/tests/pws/__init__.py +0 -0
- {bt_cli-0.4.30 → bt_cli-0.4.32}/tests/pws/test_client.py +0 -0
- {bt_cli-0.4.30 → bt_cli-0.4.32}/tests/pws/test_commands.py +0 -0
- {bt_cli-0.4.30 → bt_cli-0.4.32}/tests/pws-quick-test-plan.md +0 -0
- {bt_cli-0.4.30 → bt_cli-0.4.32}/tests/pws-smoke-test.sh +0 -0
|
@@ -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
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
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
|
-
|
|
194
|
-
|
|
195
|
-
|
|
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
|
-
|
|
203
|
+
systems = self.paginate(
|
|
209
204
|
f"/Workgroups/{workgroup_id}/ManagedSystems", params=params
|
|
210
205
|
)
|
|
211
|
-
|
|
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
|
-
|
|
825
|
-
|
|
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
|
-
|
|
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
|
-
|
|
833
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
871
|
-
|
|
872
|
-
|
|
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
|
-
|
|
878
|
-
|
|
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
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
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
|
-
|
|
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=
|
|
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
|
|
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(
|
|
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
|
-
|
|
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 #
|
|
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
|
|
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(
|
|
258
|
+
console.print_json(json.dumps(user_obj, default=str))
|
|
241
259
|
else:
|
|
242
|
-
print_user_detail(
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|