bt-cli 0.4.60__tar.gz → 0.4.62__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.60 → bt_cli-0.4.62}/CLAUDE.md +2 -1
- {bt_cli-0.4.60 → bt_cli-0.4.62}/PKG-INFO +1 -1
- {bt_cli-0.4.60 → bt_cli-0.4.62}/pyproject.toml +1 -1
- {bt_cli-0.4.60 → bt_cli-0.4.62}/src/bt_cli/__init__.py +1 -1
- {bt_cli-0.4.60 → bt_cli-0.4.62}/src/bt_cli/data/skills/pws/SKILL.md +39 -0
- {bt_cli-0.4.60 → bt_cli-0.4.62}/src/bt_cli/pws/client/passwordsafe.py +29 -0
- {bt_cli-0.4.60 → bt_cli-0.4.62}/src/bt_cli/pws/commands/quick.py +340 -72
- {bt_cli-0.4.60 → bt_cli-0.4.62}/src/bt_cli/pws/commands/sessions.py +60 -0
- {bt_cli-0.4.60 → bt_cli-0.4.62}/tests/pws/test_client.py +24 -0
- {bt_cli-0.4.60 → bt_cli-0.4.62}/tests/pws/test_commands.py +70 -1
- bt_cli-0.4.62/tests/pws/test_quick_sessions.py +40 -0
- {bt_cli-0.4.60 → bt_cli-0.4.62}/.claude/skills/bt/SKILL.md +0 -0
- {bt_cli-0.4.60 → bt_cli-0.4.62}/.claude/skills/entitle/SKILL.md +0 -0
- {bt_cli-0.4.60 → bt_cli-0.4.62}/.claude/skills/epml/SKILL.md +0 -0
- {bt_cli-0.4.60 → bt_cli-0.4.62}/.claude/skills/epmw/SKILL.md +0 -0
- {bt_cli-0.4.60 → bt_cli-0.4.62}/.claude/skills/pra/SKILL.md +0 -0
- {bt_cli-0.4.60 → bt_cli-0.4.62}/.claude/skills/pws/SKILL.md +0 -0
- {bt_cli-0.4.60 → bt_cli-0.4.62}/.env.example +0 -0
- {bt_cli-0.4.60 → bt_cli-0.4.62}/.github/workflows/ci.yml +0 -0
- {bt_cli-0.4.60 → bt_cli-0.4.62}/.github/workflows/release.yml +0 -0
- {bt_cli-0.4.60 → bt_cli-0.4.62}/.gitignore +0 -0
- {bt_cli-0.4.60 → bt_cli-0.4.62}/README.md +0 -0
- {bt_cli-0.4.60 → bt_cli-0.4.62}/assets/cli-help.png +0 -0
- {bt_cli-0.4.60 → bt_cli-0.4.62}/assets/cli-output.png +0 -0
- {bt_cli-0.4.60 → bt_cli-0.4.62}/bt-cli.spec +0 -0
- {bt_cli-0.4.60 → bt_cli-0.4.62}/bt_entry.py +0 -0
- {bt_cli-0.4.60 → bt_cli-0.4.62}/epml-clients-server-side-filters-plan.md +0 -0
- {bt_cli-0.4.60 → bt_cli-0.4.62}/epml-implementation-plan.md +0 -0
- {bt_cli-0.4.60 → bt_cli-0.4.62}/pf-implementation-plan.md +0 -0
- {bt_cli-0.4.60 → bt_cli-0.4.62}/scripts/bt_entry.py +0 -0
- {bt_cli-0.4.60 → bt_cli-0.4.62}/scripts/pf_onboard.py +0 -0
- {bt_cli-0.4.60 → bt_cli-0.4.62}/scripts/sync-package-data.sh +0 -0
- {bt_cli-0.4.60 → bt_cli-0.4.62}/src/bt_cli/cli.py +0 -0
- {bt_cli-0.4.60 → bt_cli-0.4.62}/src/bt_cli/commands/__init__.py +0 -0
- {bt_cli-0.4.60 → bt_cli-0.4.62}/src/bt_cli/commands/configure.py +0 -0
- {bt_cli-0.4.60 → bt_cli-0.4.62}/src/bt_cli/commands/learn.py +0 -0
- {bt_cli-0.4.60 → bt_cli-0.4.62}/src/bt_cli/commands/quick.py +0 -0
- {bt_cli-0.4.60 → bt_cli-0.4.62}/src/bt_cli/core/__init__.py +0 -0
- {bt_cli-0.4.60 → bt_cli-0.4.62}/src/bt_cli/core/auth.py +0 -0
- {bt_cli-0.4.60 → bt_cli-0.4.62}/src/bt_cli/core/client.py +0 -0
- {bt_cli-0.4.60 → bt_cli-0.4.62}/src/bt_cli/core/config.py +0 -0
- {bt_cli-0.4.60 → bt_cli-0.4.62}/src/bt_cli/core/config_file.py +0 -0
- {bt_cli-0.4.60 → bt_cli-0.4.62}/src/bt_cli/core/csv_utils.py +0 -0
- {bt_cli-0.4.60 → bt_cli-0.4.62}/src/bt_cli/core/errors.py +0 -0
- {bt_cli-0.4.60 → bt_cli-0.4.62}/src/bt_cli/core/output.py +0 -0
- {bt_cli-0.4.60 → bt_cli-0.4.62}/src/bt_cli/core/prompts.py +0 -0
- {bt_cli-0.4.60 → bt_cli-0.4.62}/src/bt_cli/core/rest_debug.py +0 -0
- {bt_cli-0.4.60 → bt_cli-0.4.62}/src/bt_cli/data/CLAUDE.md +0 -0
- {bt_cli-0.4.60 → bt_cli-0.4.62}/src/bt_cli/data/__init__.py +0 -0
- {bt_cli-0.4.60 → bt_cli-0.4.62}/src/bt_cli/data/skills/bt/SKILL.md +0 -0
- {bt_cli-0.4.60 → bt_cli-0.4.62}/src/bt_cli/data/skills/entitle/SKILL.md +0 -0
- {bt_cli-0.4.60 → bt_cli-0.4.62}/src/bt_cli/data/skills/epml/SKILL.md +0 -0
- {bt_cli-0.4.60 → bt_cli-0.4.62}/src/bt_cli/data/skills/epmw/SKILL.md +0 -0
- {bt_cli-0.4.60 → bt_cli-0.4.62}/src/bt_cli/data/skills/pf/SKILL.md +0 -0
- {bt_cli-0.4.60 → bt_cli-0.4.62}/src/bt_cli/data/skills/pra/SKILL.md +0 -0
- {bt_cli-0.4.60 → bt_cli-0.4.62}/src/bt_cli/data/skills/secrets/SKILL.md +0 -0
- {bt_cli-0.4.60 → bt_cli-0.4.62}/src/bt_cli/entitle/__init__.py +0 -0
- {bt_cli-0.4.60 → bt_cli-0.4.62}/src/bt_cli/entitle/client/__init__.py +0 -0
- {bt_cli-0.4.60 → bt_cli-0.4.62}/src/bt_cli/entitle/client/base.py +0 -0
- {bt_cli-0.4.60 → bt_cli-0.4.62}/src/bt_cli/entitle/commands/__init__.py +0 -0
- {bt_cli-0.4.60 → bt_cli-0.4.62}/src/bt_cli/entitle/commands/accounts.py +0 -0
- {bt_cli-0.4.60 → bt_cli-0.4.62}/src/bt_cli/entitle/commands/applications.py +0 -0
- {bt_cli-0.4.60 → bt_cli-0.4.62}/src/bt_cli/entitle/commands/auth.py +0 -0
- {bt_cli-0.4.60 → bt_cli-0.4.62}/src/bt_cli/entitle/commands/bundles.py +0 -0
- {bt_cli-0.4.60 → bt_cli-0.4.62}/src/bt_cli/entitle/commands/integrations.py +0 -0
- {bt_cli-0.4.60 → bt_cli-0.4.62}/src/bt_cli/entitle/commands/permissions.py +0 -0
- {bt_cli-0.4.60 → bt_cli-0.4.62}/src/bt_cli/entitle/commands/policies.py +0 -0
- {bt_cli-0.4.60 → bt_cli-0.4.62}/src/bt_cli/entitle/commands/requests.py +0 -0
- {bt_cli-0.4.60 → bt_cli-0.4.62}/src/bt_cli/entitle/commands/resources.py +0 -0
- {bt_cli-0.4.60 → bt_cli-0.4.62}/src/bt_cli/entitle/commands/roles.py +0 -0
- {bt_cli-0.4.60 → bt_cli-0.4.62}/src/bt_cli/entitle/commands/users.py +0 -0
- {bt_cli-0.4.60 → bt_cli-0.4.62}/src/bt_cli/entitle/commands/workflows.py +0 -0
- {bt_cli-0.4.60 → bt_cli-0.4.62}/src/bt_cli/entitle/models/__init__.py +0 -0
- {bt_cli-0.4.60 → bt_cli-0.4.62}/src/bt_cli/entitle/models/bundle.py +0 -0
- {bt_cli-0.4.60 → bt_cli-0.4.62}/src/bt_cli/entitle/models/common.py +0 -0
- {bt_cli-0.4.60 → bt_cli-0.4.62}/src/bt_cli/entitle/models/integration.py +0 -0
- {bt_cli-0.4.60 → bt_cli-0.4.62}/src/bt_cli/entitle/models/permission.py +0 -0
- {bt_cli-0.4.60 → bt_cli-0.4.62}/src/bt_cli/entitle/models/policy.py +0 -0
- {bt_cli-0.4.60 → bt_cli-0.4.62}/src/bt_cli/entitle/models/resource.py +0 -0
- {bt_cli-0.4.60 → bt_cli-0.4.62}/src/bt_cli/entitle/models/role.py +0 -0
- {bt_cli-0.4.60 → bt_cli-0.4.62}/src/bt_cli/entitle/models/user.py +0 -0
- {bt_cli-0.4.60 → bt_cli-0.4.62}/src/bt_cli/entitle/models/workflow.py +0 -0
- {bt_cli-0.4.60 → bt_cli-0.4.62}/src/bt_cli/epml/__init__.py +0 -0
- {bt_cli-0.4.60 → bt_cli-0.4.62}/src/bt_cli/epml/client/__init__.py +0 -0
- {bt_cli-0.4.60 → bt_cli-0.4.62}/src/bt_cli/epml/client/base.py +0 -0
- {bt_cli-0.4.60 → bt_cli-0.4.62}/src/bt_cli/epml/commands/__init__.py +0 -0
- {bt_cli-0.4.60 → bt_cli-0.4.62}/src/bt_cli/epml/commands/audit.py +0 -0
- {bt_cli-0.4.60 → bt_cli-0.4.62}/src/bt_cli/epml/commands/auth.py +0 -0
- {bt_cli-0.4.60 → bt_cli-0.4.62}/src/bt_cli/epml/commands/client_pkg.py +0 -0
- {bt_cli-0.4.60 → bt_cli-0.4.62}/src/bt_cli/epml/commands/clients.py +0 -0
- {bt_cli-0.4.60 → bt_cli-0.4.62}/src/bt_cli/epml/commands/external_apis.py +0 -0
- {bt_cli-0.4.60 → bt_cli-0.4.62}/src/bt_cli/epml/commands/hosts.py +0 -0
- {bt_cli-0.4.60 → bt_cli-0.4.62}/src/bt_cli/epml/commands/iolog.py +0 -0
- {bt_cli-0.4.60 → bt_cli-0.4.62}/src/bt_cli/epml/commands/license.py +0 -0
- {bt_cli-0.4.60 → bt_cli-0.4.62}/src/bt_cli/epml/commands/quick.py +0 -0
- {bt_cli-0.4.60 → bt_cli-0.4.62}/src/bt_cli/epml/commands/rbp_cmdgrps.py +0 -0
- {bt_cli-0.4.60 → bt_cli-0.4.62}/src/bt_cli/epml/commands/rbp_entitlement.py +0 -0
- {bt_cli-0.4.60 → bt_cli-0.4.62}/src/bt_cli/epml/commands/rbp_hostgrps.py +0 -0
- {bt_cli-0.4.60 → bt_cli-0.4.62}/src/bt_cli/epml/commands/rbp_policy.py +0 -0
- {bt_cli-0.4.60 → bt_cli-0.4.62}/src/bt_cli/epml/commands/rbp_roles.py +0 -0
- {bt_cli-0.4.60 → bt_cli-0.4.62}/src/bt_cli/epml/commands/rbp_tests.py +0 -0
- {bt_cli-0.4.60 → bt_cli-0.4.62}/src/bt_cli/epml/commands/rbp_tmdategrps.py +0 -0
- {bt_cli-0.4.60 → bt_cli-0.4.62}/src/bt_cli/epml/commands/rbp_tx.py +0 -0
- {bt_cli-0.4.60 → bt_cli-0.4.62}/src/bt_cli/epml/commands/rbp_usergrps.py +0 -0
- {bt_cli-0.4.60 → bt_cli-0.4.62}/src/bt_cli/epml/commands/settings.py +0 -0
- {bt_cli-0.4.60 → bt_cli-0.4.62}/src/bt_cli/epml/commands/siems.py +0 -0
- {bt_cli-0.4.60 → bt_cli-0.4.62}/src/bt_cli/epml/commands/users.py +0 -0
- {bt_cli-0.4.60 → bt_cli-0.4.62}/src/bt_cli/epml/models/__init__.py +0 -0
- {bt_cli-0.4.60 → bt_cli-0.4.62}/src/bt_cli/epmw/__init__.py +0 -0
- {bt_cli-0.4.60 → bt_cli-0.4.62}/src/bt_cli/epmw/client/__init__.py +0 -0
- {bt_cli-0.4.60 → bt_cli-0.4.62}/src/bt_cli/epmw/client/base.py +0 -0
- {bt_cli-0.4.60 → bt_cli-0.4.62}/src/bt_cli/epmw/commands/__init__.py +0 -0
- {bt_cli-0.4.60 → bt_cli-0.4.62}/src/bt_cli/epmw/commands/audits.py +0 -0
- {bt_cli-0.4.60 → bt_cli-0.4.62}/src/bt_cli/epmw/commands/auth.py +0 -0
- {bt_cli-0.4.60 → bt_cli-0.4.62}/src/bt_cli/epmw/commands/computers.py +0 -0
- {bt_cli-0.4.60 → bt_cli-0.4.62}/src/bt_cli/epmw/commands/events.py +0 -0
- {bt_cli-0.4.60 → bt_cli-0.4.62}/src/bt_cli/epmw/commands/groups.py +0 -0
- {bt_cli-0.4.60 → bt_cli-0.4.62}/src/bt_cli/epmw/commands/policies.py +0 -0
- {bt_cli-0.4.60 → bt_cli-0.4.62}/src/bt_cli/epmw/commands/quick.py +0 -0
- {bt_cli-0.4.60 → bt_cli-0.4.62}/src/bt_cli/epmw/commands/requests.py +0 -0
- {bt_cli-0.4.60 → bt_cli-0.4.62}/src/bt_cli/epmw/commands/roles.py +0 -0
- {bt_cli-0.4.60 → bt_cli-0.4.62}/src/bt_cli/epmw/commands/tasks.py +0 -0
- {bt_cli-0.4.60 → bt_cli-0.4.62}/src/bt_cli/epmw/commands/users.py +0 -0
- {bt_cli-0.4.60 → bt_cli-0.4.62}/src/bt_cli/epmw/models/__init__.py +0 -0
- {bt_cli-0.4.60 → bt_cli-0.4.62}/src/bt_cli/pf/__init__.py +0 -0
- {bt_cli-0.4.60 → bt_cli-0.4.62}/src/bt_cli/pf/client/__init__.py +0 -0
- {bt_cli-0.4.60 → bt_cli-0.4.62}/src/bt_cli/pf/client/base.py +0 -0
- {bt_cli-0.4.60 → bt_cli-0.4.62}/src/bt_cli/pf/commands/__init__.py +0 -0
- {bt_cli-0.4.60 → bt_cli-0.4.62}/src/bt_cli/pf/commands/auth.py +0 -0
- {bt_cli-0.4.60 → bt_cli-0.4.62}/src/bt_cli/pf/commands/machines.py +0 -0
- {bt_cli-0.4.60 → bt_cli-0.4.62}/src/bt_cli/pf/commands/tokens.py +0 -0
- {bt_cli-0.4.60 → bt_cli-0.4.62}/src/bt_cli/pf/commands/user.py +0 -0
- {bt_cli-0.4.60 → bt_cli-0.4.62}/src/bt_cli/pf/commands/users.py +0 -0
- {bt_cli-0.4.60 → bt_cli-0.4.62}/src/bt_cli/pra/__init__.py +0 -0
- {bt_cli-0.4.60 → bt_cli-0.4.62}/src/bt_cli/pra/client/__init__.py +0 -0
- {bt_cli-0.4.60 → bt_cli-0.4.62}/src/bt_cli/pra/client/base.py +0 -0
- {bt_cli-0.4.60 → bt_cli-0.4.62}/src/bt_cli/pra/commands/__init__.py +0 -0
- {bt_cli-0.4.60 → bt_cli-0.4.62}/src/bt_cli/pra/commands/auth.py +0 -0
- {bt_cli-0.4.60 → bt_cli-0.4.62}/src/bt_cli/pra/commands/group_policies.py +0 -0
- {bt_cli-0.4.60 → bt_cli-0.4.62}/src/bt_cli/pra/commands/import_export.py +0 -0
- {bt_cli-0.4.60 → bt_cli-0.4.62}/src/bt_cli/pra/commands/jump_clients.py +0 -0
- {bt_cli-0.4.60 → bt_cli-0.4.62}/src/bt_cli/pra/commands/jump_groups.py +0 -0
- {bt_cli-0.4.60 → bt_cli-0.4.62}/src/bt_cli/pra/commands/jump_items.py +0 -0
- {bt_cli-0.4.60 → bt_cli-0.4.62}/src/bt_cli/pra/commands/jumpoints.py +0 -0
- {bt_cli-0.4.60 → bt_cli-0.4.62}/src/bt_cli/pra/commands/policies.py +0 -0
- {bt_cli-0.4.60 → bt_cli-0.4.62}/src/bt_cli/pra/commands/quick.py +0 -0
- {bt_cli-0.4.60 → bt_cli-0.4.62}/src/bt_cli/pra/commands/teams.py +0 -0
- {bt_cli-0.4.60 → bt_cli-0.4.62}/src/bt_cli/pra/commands/users.py +0 -0
- {bt_cli-0.4.60 → bt_cli-0.4.62}/src/bt_cli/pra/commands/vault.py +0 -0
- {bt_cli-0.4.60 → bt_cli-0.4.62}/src/bt_cli/pra/models/__init__.py +0 -0
- {bt_cli-0.4.60 → bt_cli-0.4.62}/src/bt_cli/pra/models/common.py +0 -0
- {bt_cli-0.4.60 → bt_cli-0.4.62}/src/bt_cli/pra/models/group_policy.py +0 -0
- {bt_cli-0.4.60 → bt_cli-0.4.62}/src/bt_cli/pra/models/jump_client.py +0 -0
- {bt_cli-0.4.60 → bt_cli-0.4.62}/src/bt_cli/pra/models/jump_group.py +0 -0
- {bt_cli-0.4.60 → bt_cli-0.4.62}/src/bt_cli/pra/models/jump_item.py +0 -0
- {bt_cli-0.4.60 → bt_cli-0.4.62}/src/bt_cli/pra/models/jumpoint.py +0 -0
- {bt_cli-0.4.60 → bt_cli-0.4.62}/src/bt_cli/pra/models/team.py +0 -0
- {bt_cli-0.4.60 → bt_cli-0.4.62}/src/bt_cli/pra/models/user.py +0 -0
- {bt_cli-0.4.60 → bt_cli-0.4.62}/src/bt_cli/pra/models/vault.py +0 -0
- {bt_cli-0.4.60 → bt_cli-0.4.62}/src/bt_cli/pws/__init__.py +0 -0
- {bt_cli-0.4.60 → bt_cli-0.4.62}/src/bt_cli/pws/client/__init__.py +0 -0
- {bt_cli-0.4.60 → bt_cli-0.4.62}/src/bt_cli/pws/client/base.py +0 -0
- {bt_cli-0.4.60 → bt_cli-0.4.62}/src/bt_cli/pws/client/beyondinsight.py +0 -0
- {bt_cli-0.4.60 → bt_cli-0.4.62}/src/bt_cli/pws/commands/__init__.py +0 -0
- {bt_cli-0.4.60 → bt_cli-0.4.62}/src/bt_cli/pws/commands/accounts.py +0 -0
- {bt_cli-0.4.60 → bt_cli-0.4.62}/src/bt_cli/pws/commands/assets.py +0 -0
- {bt_cli-0.4.60 → bt_cli-0.4.62}/src/bt_cli/pws/commands/attributes.py +0 -0
- {bt_cli-0.4.60 → bt_cli-0.4.62}/src/bt_cli/pws/commands/audit.py +0 -0
- {bt_cli-0.4.60 → bt_cli-0.4.62}/src/bt_cli/pws/commands/auth.py +0 -0
- {bt_cli-0.4.60 → bt_cli-0.4.62}/src/bt_cli/pws/commands/clouds.py +0 -0
- {bt_cli-0.4.60 → bt_cli-0.4.62}/src/bt_cli/pws/commands/config.py +0 -0
- {bt_cli-0.4.60 → bt_cli-0.4.62}/src/bt_cli/pws/commands/credentials.py +0 -0
- {bt_cli-0.4.60 → bt_cli-0.4.62}/src/bt_cli/pws/commands/databases.py +0 -0
- {bt_cli-0.4.60 → bt_cli-0.4.62}/src/bt_cli/pws/commands/directories.py +0 -0
- {bt_cli-0.4.60 → bt_cli-0.4.62}/src/bt_cli/pws/commands/functional.py +0 -0
- {bt_cli-0.4.60 → bt_cli-0.4.62}/src/bt_cli/pws/commands/import_export.py +0 -0
- {bt_cli-0.4.60 → bt_cli-0.4.62}/src/bt_cli/pws/commands/platforms.py +0 -0
- {bt_cli-0.4.60 → bt_cli-0.4.62}/src/bt_cli/pws/commands/search.py +0 -0
- {bt_cli-0.4.60 → bt_cli-0.4.62}/src/bt_cli/pws/commands/secrets.py +0 -0
- {bt_cli-0.4.60 → bt_cli-0.4.62}/src/bt_cli/pws/commands/systems.py +0 -0
- {bt_cli-0.4.60 → bt_cli-0.4.62}/src/bt_cli/pws/commands/users.py +0 -0
- {bt_cli-0.4.60 → bt_cli-0.4.62}/src/bt_cli/pws/commands/workgroups.py +0 -0
- {bt_cli-0.4.60 → bt_cli-0.4.62}/src/bt_cli/pws/config.py +0 -0
- {bt_cli-0.4.60 → bt_cli-0.4.62}/src/bt_cli/pws/models/__init__.py +0 -0
- {bt_cli-0.4.60 → bt_cli-0.4.62}/src/bt_cli/pws/models/account.py +0 -0
- {bt_cli-0.4.60 → bt_cli-0.4.62}/src/bt_cli/pws/models/asset.py +0 -0
- {bt_cli-0.4.60 → bt_cli-0.4.62}/src/bt_cli/pws/models/common.py +0 -0
- {bt_cli-0.4.60 → bt_cli-0.4.62}/src/bt_cli/pws/models/system.py +0 -0
- {bt_cli-0.4.60 → bt_cli-0.4.62}/src/bt_cli/secrets/__init__.py +0 -0
- {bt_cli-0.4.60 → bt_cli-0.4.62}/src/bt_cli/secrets/client/__init__.py +0 -0
- {bt_cli-0.4.60 → bt_cli-0.4.62}/src/bt_cli/secrets/client/base.py +0 -0
- {bt_cli-0.4.60 → bt_cli-0.4.62}/src/bt_cli/secrets/commands/__init__.py +0 -0
- {bt_cli-0.4.60 → bt_cli-0.4.62}/src/bt_cli/secrets/commands/_hints.py +0 -0
- {bt_cli-0.4.60 → bt_cli-0.4.62}/src/bt_cli/secrets/commands/auth.py +0 -0
- {bt_cli-0.4.60 → bt_cli-0.4.62}/src/bt_cli/secrets/commands/dynamic.py +0 -0
- {bt_cli-0.4.60 → bt_cli-0.4.62}/src/bt_cli/secrets/commands/folders.py +0 -0
- {bt_cli-0.4.60 → bt_cli-0.4.62}/src/bt_cli/secrets/commands/integrations.py +0 -0
- {bt_cli-0.4.60 → bt_cli-0.4.62}/src/bt_cli/secrets/commands/leases.py +0 -0
- {bt_cli-0.4.60 → bt_cli-0.4.62}/src/bt_cli/secrets/commands/static.py +0 -0
- {bt_cli-0.4.60 → bt_cli-0.4.62}/src/bt_cli/secrets/models/__init__.py +0 -0
- {bt_cli-0.4.60 → bt_cli-0.4.62}/tests/__init__.py +0 -0
- {bt_cli-0.4.60 → bt_cli-0.4.62}/tests/conftest.py +0 -0
- {bt_cli-0.4.60 → bt_cli-0.4.62}/tests/core/__init__.py +0 -0
- {bt_cli-0.4.60 → bt_cli-0.4.62}/tests/core/test_auth.py +0 -0
- {bt_cli-0.4.60 → bt_cli-0.4.62}/tests/core/test_config.py +0 -0
- {bt_cli-0.4.60 → bt_cli-0.4.62}/tests/core/test_config_file.py +0 -0
- {bt_cli-0.4.60 → bt_cli-0.4.62}/tests/core/test_errors.py +0 -0
- {bt_cli-0.4.60 → bt_cli-0.4.62}/tests/core/test_output.py +0 -0
- {bt_cli-0.4.60 → bt_cli-0.4.62}/tests/core/test_rest_debug.py +0 -0
- {bt_cli-0.4.60 → bt_cli-0.4.62}/tests/entitle/__init__.py +0 -0
- {bt_cli-0.4.60 → bt_cli-0.4.62}/tests/entitle/test_client.py +0 -0
- {bt_cli-0.4.60 → bt_cli-0.4.62}/tests/entitle/test_commands.py +0 -0
- {bt_cli-0.4.60 → bt_cli-0.4.62}/tests/entitle-smoke-test.sh +0 -0
- {bt_cli-0.4.60 → bt_cli-0.4.62}/tests/epml/__init__.py +0 -0
- {bt_cli-0.4.60 → bt_cli-0.4.62}/tests/epml/test_client.py +0 -0
- {bt_cli-0.4.60 → bt_cli-0.4.62}/tests/epml/test_commands.py +0 -0
- {bt_cli-0.4.60 → bt_cli-0.4.62}/tests/epmw/__init__.py +0 -0
- {bt_cli-0.4.60 → bt_cli-0.4.62}/tests/epmw/test_client.py +0 -0
- {bt_cli-0.4.60 → bt_cli-0.4.62}/tests/epmw/test_commands.py +0 -0
- {bt_cli-0.4.60 → bt_cli-0.4.62}/tests/epmw-quick-test-plan.md +0 -0
- {bt_cli-0.4.60 → bt_cli-0.4.62}/tests/fixtures/__init__.py +0 -0
- {bt_cli-0.4.60 → bt_cli-0.4.62}/tests/fixtures/responses.py +0 -0
- {bt_cli-0.4.60 → bt_cli-0.4.62}/tests/integration/__init__.py +0 -0
- {bt_cli-0.4.60 → bt_cli-0.4.62}/tests/integration/conftest.py +0 -0
- {bt_cli-0.4.60 → bt_cli-0.4.62}/tests/integration/helpers.py +0 -0
- {bt_cli-0.4.60 → bt_cli-0.4.62}/tests/integration/test_entitle_integration.py +0 -0
- {bt_cli-0.4.60 → bt_cli-0.4.62}/tests/integration/test_epmw_integration.py +0 -0
- {bt_cli-0.4.60 → bt_cli-0.4.62}/tests/integration/test_epmw_lifecycle.py +0 -0
- {bt_cli-0.4.60 → bt_cli-0.4.62}/tests/integration/test_pra_integration.py +0 -0
- {bt_cli-0.4.60 → bt_cli-0.4.62}/tests/integration/test_pra_lifecycle.py +0 -0
- {bt_cli-0.4.60 → bt_cli-0.4.62}/tests/integration/test_pws_integration.py +0 -0
- {bt_cli-0.4.60 → bt_cli-0.4.62}/tests/integration/test_pws_lifecycle.py +0 -0
- {bt_cli-0.4.60 → bt_cli-0.4.62}/tests/pf/__init__.py +0 -0
- {bt_cli-0.4.60 → bt_cli-0.4.62}/tests/pf/test_client.py +0 -0
- {bt_cli-0.4.60 → bt_cli-0.4.62}/tests/pf/test_commands.py +0 -0
- {bt_cli-0.4.60 → bt_cli-0.4.62}/tests/pra/__init__.py +0 -0
- {bt_cli-0.4.60 → bt_cli-0.4.62}/tests/pra/test_client.py +0 -0
- {bt_cli-0.4.60 → bt_cli-0.4.62}/tests/pra/test_commands.py +0 -0
- {bt_cli-0.4.60 → bt_cli-0.4.62}/tests/pra/test_jump_item_fixes.py +0 -0
- {bt_cli-0.4.60 → bt_cli-0.4.62}/tests/pra-smoke-test.sh +0 -0
- {bt_cli-0.4.60 → bt_cli-0.4.62}/tests/pra-test-plan.md +0 -0
- {bt_cli-0.4.60 → bt_cli-0.4.62}/tests/pws/__init__.py +0 -0
- {bt_cli-0.4.60 → bt_cli-0.4.62}/tests/pws-quick-test-plan.md +0 -0
- {bt_cli-0.4.60 → bt_cli-0.4.62}/tests/pws-smoke-test.sh +0 -0
- {bt_cli-0.4.60 → bt_cli-0.4.62}/tests/secrets/__init__.py +0 -0
- {bt_cli-0.4.60 → bt_cli-0.4.62}/tests/secrets/test_client.py +0 -0
- {bt_cli-0.4.60 → bt_cli-0.4.62}/tests/secrets/test_commands.py +0 -0
|
@@ -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.
|
|
3
|
+
BeyondTrust Platform CLI for Password Safe, Entitle, PRA, EPM Windows, EPM Linux, and the BeyondTrust Secrets API. **Version: 0.4.62**
|
|
4
4
|
|
|
5
5
|
## Setup
|
|
6
6
|
|
|
@@ -86,6 +86,7 @@ PASSWORD=$(bt pws quick checkout -s server -a admin --raw)
|
|
|
86
86
|
- **PRA `export jump-items` defaults to a REAL export** (all types: shell/rdp/vnc/web/tunnel, full type-specific columns) that round-trips through `import jump-items`; `--sample` gives the blank template. `bt pra jump-items web create` exists.
|
|
87
87
|
- **PRA vault `associate -t protocol_tunnel` is MSSQL-only** (live-verified): server rejects psql/mysql/tcp/k8s with "Only SQL Server Tunnels are allowed" — use tag/criteria association for those. **TCP `--ports` needs listen;destination *pairs*** (even count; odd → 400 "number of parameters is invalid"). Jumpoint listing is `bt pra jumpoints list` (plural).
|
|
88
88
|
- **PWS `/Sessions` ignores `limit`/`offset`** and returns the full session *history* every call — never `paginate()` it (infinite loop); `list_sessions` does a single GET and `bt pws sessions list` caps display client-side. `Status` codes (live-confirmed): 0=Not started, 1=Active, 2=Completed, 5=Locked, 7=Terminated; `--active`=Status 1. Active sessions have `EndTime`==`StartTime` (placeholder, not null) — identify live sessions by Status, not timestamps. **`terminate` = `POST /Sessions/{id}/Terminate`** (symmetric w/ `/Lock`); `/Sessions/{id}` is GET/PUT only (DELETE → 405).
|
|
89
|
+
- **PWS SSH/RDP session establishment = `POST /Requests/{id}/Sessions`** (`{"SessionType":"SSH"|"RDP"}`), NOT `POST /Sessions` (collection is GET-only → 405). Create the request with matching `AccessType` first. **SSH** returns a ready `Command` (`ssh -p 4422 <ticket>@<proxyhost>`) + `Link`; **RDP** returns `Host`/`Port`/`Ticket` only — the `.rdp` is built client-side (`full address` = proxy `Host:Port`, `username` = `Ticket`); there's no server-side launch file (`/Sessions/RDPFile` is audit-playback → 403). Ticket is the proxy username; password is proxy-injected. `bt pws quick checkout --access-type ssh|rdp` and `bt pws quick ssh`/`quick rdp` (with `--launch`); session commands do **not** auto-checkin.
|
|
89
90
|
- Entitle permissions: Use server-side filters (`--resource`, `--user`, `--integration`) - dataset can be 20k+ records
|
|
90
91
|
- Entitle users: Use `--all` flag only when needed - default fetches first page only
|
|
91
92
|
- Windows builds: Rich must be pinned to `<14.0.0` (Rich 14 has PyInstaller unicode issues)
|
|
@@ -150,6 +150,36 @@ bt pws credentials show <request_id>
|
|
|
150
150
|
bt pws credentials checkin <request_id>
|
|
151
151
|
```
|
|
152
152
|
|
|
153
|
+
## SSH / RDP session checkout
|
|
154
|
+
|
|
155
|
+
`bt pws quick checkout` handles three access types. Interactive runs prompt for
|
|
156
|
+
the access type; scriptable via `--access-type`. There are also dedicated
|
|
157
|
+
`quick ssh` / `quick rdp` wrappers.
|
|
158
|
+
|
|
159
|
+
```bash
|
|
160
|
+
# Credential (default) - password / dsskey (SSH key) / passphrase
|
|
161
|
+
bt pws quick checkout -s server -a admin
|
|
162
|
+
bt pws quick checkout -s server -a admin --credential-type dsskey --raw
|
|
163
|
+
|
|
164
|
+
# SSH session - prints a ready-to-run ssh command (proxy injects the password)
|
|
165
|
+
bt pws quick ssh -s linhost2 -a app_account02
|
|
166
|
+
bt pws quick checkout -s linhost2 -a app_account02 --access-type ssh
|
|
167
|
+
bt pws quick ssh -s linhost2 -a app --launch # exec ssh in this terminal
|
|
168
|
+
eval "$(bt pws quick ssh -s linhost2 -a app --raw)" # raw = just the ssh command
|
|
169
|
+
|
|
170
|
+
# RDP session - writes a launchable .rdp file (proxy target + one-time ticket)
|
|
171
|
+
bt pws quick rdp -s WIN03-APP -a Administrator
|
|
172
|
+
bt pws quick rdp -s WIN03-APP -a Administrator --out ~/win03.rdp
|
|
173
|
+
bt pws quick rdp -s WIN03-APP -a Administrator --launch # mstsc / xfreerdp / open
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
**How it works (live-verified):**
|
|
177
|
+
- Session establishment is **`POST /Requests/{id}/Sessions`** (`{"SessionType":"SSH"|"RDP"}`), created after a request with the matching `AccessType`. The `/Sessions` collection is GET-only (POST → 405).
|
|
178
|
+
- **SSH** response carries a ready `Command` (`ssh -p 4422 <ticket>@<proxyhost>`) and `Link` — just run it. The `<ticket>` is the proxy username; the password is injected by the proxy, so you never handle a credential.
|
|
179
|
+
- **RDP** response gives `Host`/`Port`/`Ticket` only. There is **no server-side `.rdp`** (`/Sessions/RDPFile` is audit-playback → 403), so the CLI builds the file client-side (`full address:s:<Host>:<Port>` + `username:s:<Ticket>`).
|
|
180
|
+
- `Host` is the **session proxy node** (same for every target), not the managed system's own host.
|
|
181
|
+
- Session checkout does **not** auto-checkin — the session needs the request open. Check in when done: `bt pws credentials checkin <request_id>`.
|
|
182
|
+
|
|
153
183
|
## Sessions (view / suspend / terminate)
|
|
154
184
|
|
|
155
185
|
```bash
|
|
@@ -165,8 +195,17 @@ bt pws sessions suspend <session_id>
|
|
|
165
195
|
# Terminate = end permanently (cannot be resumed) - prompts unless --force
|
|
166
196
|
bt pws sessions terminate <session_id>
|
|
167
197
|
bt pws sessions terminate <session_id> --force
|
|
198
|
+
|
|
199
|
+
# Export session history as evidence (same filters as list, no display cap)
|
|
200
|
+
bt pws sessions export --file sessions.json
|
|
201
|
+
bt pws sessions export --active --file active-sessions.csv --format csv
|
|
202
|
+
bt pws sessions export --system 13 -o json
|
|
168
203
|
```
|
|
169
204
|
|
|
205
|
+
`sessions export` is a sessions-only shortcut; use `bt pws audit export` to pull
|
|
206
|
+
requests + sessions together. Because `/Sessions` ignores server-side filters,
|
|
207
|
+
`--system` and `--active` are applied **client-side** after the full fetch.
|
|
208
|
+
|
|
170
209
|
**Gotchas:**
|
|
171
210
|
- `/Sessions` returns session **history**, not just active sessions, and **ignores `limit`/`offset`** (returns the full set every call - the client does a single GET, the CLI caps display client-side). Use `--active` for the live ones.
|
|
172
211
|
- `Status` codes confirmed live: **0 = Not started, 1 = Active, 2 = Completed, 5 = Locked, 7 = Terminated** (3/9 are other terminal states). `--active` filters on Status == 1.
|
|
@@ -429,6 +429,35 @@ class PasswordSafeMixin:
|
|
|
429
429
|
access_type=access_type,
|
|
430
430
|
)
|
|
431
431
|
|
|
432
|
+
def create_session(
|
|
433
|
+
self: "PasswordSafeClient",
|
|
434
|
+
request_id: int,
|
|
435
|
+
session_type: str = "SSH",
|
|
436
|
+
node_id: Optional[str] = None,
|
|
437
|
+
) -> dict[str, Any]:
|
|
438
|
+
"""Establish an SSH/RDP session for an approved request.
|
|
439
|
+
|
|
440
|
+
Live-verified: session creation is POST /Requests/{id}/Sessions (the
|
|
441
|
+
/Sessions collection is GET-only - a POST there returns 405). Create the
|
|
442
|
+
request first with a matching AccessType (SSH/RDP), then call this.
|
|
443
|
+
|
|
444
|
+
Args:
|
|
445
|
+
request_id: Request ID (must have AccessType SSH or RDP)
|
|
446
|
+
session_type: "SSH", "RDP", or "App"
|
|
447
|
+
node_id: Optional session node (proxy) ID
|
|
448
|
+
|
|
449
|
+
Returns:
|
|
450
|
+
Session dict. Common keys: Host, Port, Ticket, SessionID, NodeID.
|
|
451
|
+
SSH also returns a ready-to-use Command ("ssh -p <port> <ticket>@<host>")
|
|
452
|
+
and Link ("ssh://..."). RDP returns Host/Port/Ticket only - the .rdp
|
|
453
|
+
file is built client-side (there is no server-side launch file; the
|
|
454
|
+
password is injected by the proxy, with Ticket as the proxy username).
|
|
455
|
+
"""
|
|
456
|
+
data: dict[str, Any] = {"SessionType": session_type}
|
|
457
|
+
if node_id:
|
|
458
|
+
data["NodeID"] = node_id
|
|
459
|
+
return self.post(f"/Requests/{request_id}/Sessions", json=data)
|
|
460
|
+
|
|
432
461
|
def get_credential(
|
|
433
462
|
self: "PasswordSafeClient",
|
|
434
463
|
request_id: int,
|
|
@@ -2,6 +2,11 @@
|
|
|
2
2
|
|
|
3
3
|
from typing import Optional
|
|
4
4
|
import json
|
|
5
|
+
import os
|
|
6
|
+
import shutil
|
|
7
|
+
import subprocess
|
|
8
|
+
import sys
|
|
9
|
+
from pathlib import Path
|
|
5
10
|
|
|
6
11
|
import httpx
|
|
7
12
|
import typer
|
|
@@ -18,6 +23,129 @@ from ..models.common import CredentialType, CREDENTIAL_TYPE_META
|
|
|
18
23
|
app = typer.Typer(no_args_is_help=True, help="Quick commands - common multi-step operations in one command")
|
|
19
24
|
console = Console()
|
|
20
25
|
|
|
26
|
+
# Access types accepted by POST /Requests (AccessType). "view" returns a
|
|
27
|
+
# credential; "rdp"/"ssh" establish a proxied session.
|
|
28
|
+
ACCESS_TYPES = {"view": "View", "rdp": "RDP", "ssh": "SSH"}
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def _resolve_target(client, system: Optional[str], account: Optional[str], quiet: bool) -> dict:
|
|
32
|
+
"""Find a managed system + account by name, prompting when interactive.
|
|
33
|
+
|
|
34
|
+
Shared by checkout / ssh / rdp. Returns a dict with system_id, system_name,
|
|
35
|
+
account_id, account_name. Exits (typer.Exit) with a friendly message when a
|
|
36
|
+
system or account can't be resolved. `quiet` suppresses progress lines (set
|
|
37
|
+
for --raw and -o json so stdout stays clean for scripts/jq).
|
|
38
|
+
"""
|
|
39
|
+
if not system:
|
|
40
|
+
system = prompt_if_missing(system, "System name (or partial match)")
|
|
41
|
+
|
|
42
|
+
console.print(f"[dim]Finding system '{system}'...[/dim]") if not quiet else None
|
|
43
|
+
systems = client.list_managed_systems(search=system)
|
|
44
|
+
if not systems:
|
|
45
|
+
print_error(f"No system found matching '{system}'")
|
|
46
|
+
raise typer.Exit(1)
|
|
47
|
+
|
|
48
|
+
matched_system = None
|
|
49
|
+
for s in systems:
|
|
50
|
+
if s.get("SystemName", "").lower() == system.lower():
|
|
51
|
+
matched_system = s
|
|
52
|
+
break
|
|
53
|
+
if not matched_system:
|
|
54
|
+
matched_system = systems[0]
|
|
55
|
+
if len(systems) > 1 and not quiet:
|
|
56
|
+
console.print(f"[yellow]Multiple matches found, using: {matched_system.get('SystemName')}[/yellow]")
|
|
57
|
+
|
|
58
|
+
system_id = matched_system.get("ManagedSystemID")
|
|
59
|
+
system_name = matched_system.get("SystemName")
|
|
60
|
+
|
|
61
|
+
accounts = client.list_managed_accounts(system_id=system_id)
|
|
62
|
+
if not account:
|
|
63
|
+
if not accounts:
|
|
64
|
+
print_error(f"No accounts found on system '{system_name}'")
|
|
65
|
+
raise typer.Exit(1)
|
|
66
|
+
account = prompt_from_list(
|
|
67
|
+
accounts, "Account name", "AccountName", "AccountName",
|
|
68
|
+
f"Accounts on {system_name}", str
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
console.print(f"[dim]Finding account '{account}' on {system_name}...[/dim]") if not quiet else None
|
|
72
|
+
matched_account = None
|
|
73
|
+
for acc in accounts:
|
|
74
|
+
if acc.get("AccountName", "").lower() == account.lower():
|
|
75
|
+
matched_account = acc
|
|
76
|
+
break
|
|
77
|
+
if not matched_account:
|
|
78
|
+
print_error(f"Account '{account}' not found on system '{system_name}'")
|
|
79
|
+
if accounts and not quiet:
|
|
80
|
+
console.print("[dim]Available accounts:[/dim]")
|
|
81
|
+
for acc in accounts[:10]:
|
|
82
|
+
console.print(f" - {acc.get('AccountName')}")
|
|
83
|
+
raise typer.Exit(1)
|
|
84
|
+
|
|
85
|
+
return {
|
|
86
|
+
"system_id": system_id,
|
|
87
|
+
"system_name": system_name,
|
|
88
|
+
"account_id": matched_account.get("AccountId", matched_account.get("ManagedAccountID")),
|
|
89
|
+
"account_name": matched_account.get("AccountName"),
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def _build_rdp_file(session: dict) -> str:
|
|
94
|
+
"""Build .rdp file content from a PWS RDP session response.
|
|
95
|
+
|
|
96
|
+
The proxy Host:Port is the connection target and Ticket is the proxy
|
|
97
|
+
username; the password is injected by the proxy. There is no server-side
|
|
98
|
+
.rdp file (verified), so we assemble the minimal working file client-side.
|
|
99
|
+
"""
|
|
100
|
+
host = session.get("Host", "")
|
|
101
|
+
port = session.get("Port", "")
|
|
102
|
+
ticket = session.get("Ticket") or session.get("ID", "")
|
|
103
|
+
return (
|
|
104
|
+
f"full address:s:{host}:{port}\n"
|
|
105
|
+
f"username:s:{ticket}\n"
|
|
106
|
+
"prompt for credentials:i:0\n"
|
|
107
|
+
"administrative session:i:0\n"
|
|
108
|
+
)
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def _ssh_command(session: dict) -> str:
|
|
112
|
+
"""Ready-to-run ssh command for a PWS SSH session (server usually provides it)."""
|
|
113
|
+
cmd = session.get("Command")
|
|
114
|
+
if cmd:
|
|
115
|
+
return cmd
|
|
116
|
+
return f"ssh -p {session.get('Port')} {session.get('Ticket') or session.get('ID')}@{session.get('Host')}"
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def _launch_ssh(command: str) -> None:
|
|
120
|
+
"""Exec an ssh session in the current terminal (replaces the process)."""
|
|
121
|
+
parts = command.split()
|
|
122
|
+
exe = shutil.which(parts[0])
|
|
123
|
+
if not exe:
|
|
124
|
+
print_warning(f"'{parts[0]}' not found on PATH - run the command above manually.")
|
|
125
|
+
return
|
|
126
|
+
console.print(f"[dim]Launching: {command}[/dim]")
|
|
127
|
+
os.execvp(exe, parts) # replaces this process with the ssh client
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def _launch_rdp(path: Path) -> None:
|
|
131
|
+
"""Open a .rdp file with the platform RDP client."""
|
|
132
|
+
if sys.platform.startswith("win"):
|
|
133
|
+
candidates = [["mstsc", str(path)]]
|
|
134
|
+
elif sys.platform == "darwin":
|
|
135
|
+
candidates = [["open", str(path)]]
|
|
136
|
+
else:
|
|
137
|
+
candidates = [["xfreerdp", str(path)], ["xdg-open", str(path)]]
|
|
138
|
+
|
|
139
|
+
for cmd in candidates:
|
|
140
|
+
if shutil.which(cmd[0]):
|
|
141
|
+
console.print(f"[dim]Launching: {' '.join(cmd)}[/dim]")
|
|
142
|
+
subprocess.Popen(cmd, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
|
|
143
|
+
return
|
|
144
|
+
print_warning(
|
|
145
|
+
f"No RDP client found ({', '.join(c[0] for c in candidates)}). "
|
|
146
|
+
f"Open the file manually: {path}"
|
|
147
|
+
)
|
|
148
|
+
|
|
21
149
|
|
|
22
150
|
@app.command("checkout")
|
|
23
151
|
def quick_checkout(
|
|
@@ -25,90 +153,62 @@ def quick_checkout(
|
|
|
25
153
|
account: Optional[str] = typer.Option(None, "--account", "-a", help="Account name"),
|
|
26
154
|
duration: int = typer.Option(60, "--duration", "-d", help="Duration in minutes"),
|
|
27
155
|
reason: Optional[str] = typer.Option(None, "--reason", "-r", help="Reason for checkout"),
|
|
28
|
-
|
|
29
|
-
|
|
156
|
+
access_type: Optional[str] = typer.Option(None, "--access-type", help="Access type: view (credential), rdp, or ssh session. Prompts interactively; defaults to view."),
|
|
157
|
+
credential_type: str = typer.Option("password", "--credential-type", help="For view access: password, dsskey, passphrase"),
|
|
158
|
+
launch: bool = typer.Option(False, "--launch", help="For rdp/ssh: launch the local client after establishing the session"),
|
|
159
|
+
out: Optional[str] = typer.Option(None, "--out", help="For rdp: path to write the .rdp file (default ./<account>@<system>.rdp)"),
|
|
160
|
+
raw: bool = typer.Option(False, "--raw", help="Output only the value (credential / ssh command / rdp path)"),
|
|
30
161
|
output: str = typer.Option("table", "--output", "-o", help="Output format: table or json"),
|
|
31
162
|
) -> None:
|
|
32
|
-
"""Checkout
|
|
163
|
+
"""Checkout a credential, or establish an RDP/SSH session, in one step.
|
|
33
164
|
|
|
34
|
-
Combines: find system -> find account ->
|
|
165
|
+
Combines: find system -> find account -> request -> deliver.
|
|
35
166
|
|
|
36
|
-
|
|
167
|
+
Access types:
|
|
168
|
+
view Return the credential (password / dsskey SSH key / passphrase).
|
|
169
|
+
ssh Establish an SSH proxy session and print the ready ssh command.
|
|
170
|
+
rdp Establish an RDP proxy session and write a launchable .rdp file.
|
|
171
|
+
|
|
172
|
+
If system, account, or access type aren't provided, prompts interactively.
|
|
37
173
|
|
|
38
174
|
Examples:
|
|
39
|
-
bt pws quick checkout #
|
|
40
|
-
bt pws quick checkout -s
|
|
41
|
-
bt pws quick checkout -s axion -a root --duration 30
|
|
175
|
+
bt pws quick checkout # interactive (prompts for access type)
|
|
176
|
+
bt pws quick checkout -s axion -a root
|
|
42
177
|
PASSWORD=$(bt pws quick checkout -s server -a admin --raw)
|
|
43
178
|
KEY=$(bt pws quick checkout -s server -a admin --credential-type dsskey --raw)
|
|
179
|
+
bt pws quick checkout -s linhost -a app --access-type ssh --launch
|
|
180
|
+
bt pws quick checkout -s win03 -a Administrator --access-type rdp --out win03.rdp
|
|
44
181
|
"""
|
|
182
|
+
# Resolve access type (prompt when interactive, default to view)
|
|
183
|
+
if access_type is None:
|
|
184
|
+
access_type = "view" if raw else prompt_choice(
|
|
185
|
+
"Access type",
|
|
186
|
+
[("view", "credential"), ("ssh", "SSH session"), ("rdp", "RDP session")],
|
|
187
|
+
default="view",
|
|
188
|
+
)
|
|
189
|
+
access_type = access_type.lower()
|
|
190
|
+
if access_type not in ACCESS_TYPES:
|
|
191
|
+
print_error(f"Invalid access type: {access_type}. Must be: view, rdp, ssh")
|
|
192
|
+
raise typer.Exit(1)
|
|
193
|
+
|
|
194
|
+
if access_type in ("ssh", "rdp"):
|
|
195
|
+
_do_session_checkout(
|
|
196
|
+
system=system, account=account, session_type=access_type,
|
|
197
|
+
duration=duration, reason=reason, launch=launch, out=out,
|
|
198
|
+
raw=raw, output=output,
|
|
199
|
+
)
|
|
200
|
+
return
|
|
201
|
+
|
|
202
|
+
quiet = raw or output == "json"
|
|
45
203
|
try:
|
|
46
204
|
with get_client() as client:
|
|
47
205
|
client.authenticate()
|
|
206
|
+
target = _resolve_target(client, system, account, quiet)
|
|
207
|
+
system_id, system_name = target["system_id"], target["system_name"]
|
|
208
|
+
account_id, account_name = target["account_id"], target["account_name"]
|
|
48
209
|
|
|
49
|
-
#
|
|
50
|
-
if not
|
|
51
|
-
system = prompt_if_missing(system, "System name (or partial match)")
|
|
52
|
-
|
|
53
|
-
# Step 1: Find the system
|
|
54
|
-
console.print(f"[dim]Finding system '{system}'...[/dim]") if not raw else None
|
|
55
|
-
systems = client.list_managed_systems(search=system)
|
|
56
|
-
|
|
57
|
-
if not systems:
|
|
58
|
-
print_error(f"No system found matching '{system}'")
|
|
59
|
-
raise typer.Exit(1)
|
|
60
|
-
|
|
61
|
-
# Try exact match first, then partial
|
|
62
|
-
matched_system = None
|
|
63
|
-
for s in systems:
|
|
64
|
-
if s.get("SystemName", "").lower() == system.lower():
|
|
65
|
-
matched_system = s
|
|
66
|
-
break
|
|
67
|
-
|
|
68
|
-
if not matched_system:
|
|
69
|
-
# Use first partial match
|
|
70
|
-
matched_system = systems[0]
|
|
71
|
-
if len(systems) > 1 and not raw:
|
|
72
|
-
console.print(f"[yellow]Multiple matches found, using: {matched_system.get('SystemName')}[/yellow]")
|
|
73
|
-
|
|
74
|
-
system_id = matched_system.get("ManagedSystemID")
|
|
75
|
-
system_name = matched_system.get("SystemName")
|
|
76
|
-
|
|
77
|
-
# Step 2: Find the account
|
|
78
|
-
accounts = client.list_managed_accounts(system_id=system_id)
|
|
79
|
-
|
|
80
|
-
# Interactive prompt for account if not provided
|
|
81
|
-
if not account:
|
|
82
|
-
if not accounts:
|
|
83
|
-
print_error(f"No accounts found on system '{system_name}'")
|
|
84
|
-
raise typer.Exit(1)
|
|
85
|
-
account = prompt_from_list(
|
|
86
|
-
accounts, "Account name", "AccountName", "AccountName",
|
|
87
|
-
f"Accounts on {system_name}", str
|
|
88
|
-
)
|
|
89
|
-
|
|
90
|
-
console.print(f"[dim]Finding account '{account}' on {system_name}...[/dim]") if not raw else None
|
|
91
|
-
|
|
92
|
-
matched_account = None
|
|
93
|
-
for acc in accounts:
|
|
94
|
-
if acc.get("AccountName", "").lower() == account.lower():
|
|
95
|
-
matched_account = acc
|
|
96
|
-
break
|
|
97
|
-
|
|
98
|
-
if not matched_account:
|
|
99
|
-
print_error(f"Account '{account}' not found on system '{system_name}'")
|
|
100
|
-
# Show available accounts
|
|
101
|
-
if accounts and not raw:
|
|
102
|
-
console.print("[dim]Available accounts:[/dim]")
|
|
103
|
-
for acc in accounts[:10]:
|
|
104
|
-
console.print(f" - {acc.get('AccountName')}")
|
|
105
|
-
raise typer.Exit(1)
|
|
106
|
-
|
|
107
|
-
account_id = matched_account.get("AccountId", matched_account.get("ManagedAccountID"))
|
|
108
|
-
account_name = matched_account.get("AccountName")
|
|
109
|
-
|
|
110
|
-
# Step 3: Checkout
|
|
111
|
-
console.print(f"[dim]Checking out {account_name}@{system_name}...[/dim]") if not raw else None
|
|
210
|
+
# Checkout
|
|
211
|
+
console.print(f"[dim]Checking out {account_name}@{system_name}...[/dim]") if not quiet else None
|
|
112
212
|
request = client.create_request(
|
|
113
213
|
account_id=account_id,
|
|
114
214
|
system_id=system_id,
|
|
@@ -118,7 +218,7 @@ def quick_checkout(
|
|
|
118
218
|
)
|
|
119
219
|
request_id = request.get("RequestID")
|
|
120
220
|
|
|
121
|
-
#
|
|
221
|
+
# Get the credential
|
|
122
222
|
try:
|
|
123
223
|
ctype = CredentialType(credential_type)
|
|
124
224
|
except ValueError:
|
|
@@ -169,6 +269,174 @@ def quick_checkout(
|
|
|
169
269
|
raise typer.Exit(1)
|
|
170
270
|
|
|
171
271
|
|
|
272
|
+
def _do_session_checkout(
|
|
273
|
+
system: Optional[str],
|
|
274
|
+
account: Optional[str],
|
|
275
|
+
session_type: str,
|
|
276
|
+
duration: int,
|
|
277
|
+
reason: Optional[str],
|
|
278
|
+
launch: bool,
|
|
279
|
+
out: Optional[str],
|
|
280
|
+
raw: bool,
|
|
281
|
+
output: str,
|
|
282
|
+
) -> None:
|
|
283
|
+
"""Shared RDP/SSH session establishment for checkout / ssh / rdp commands.
|
|
284
|
+
|
|
285
|
+
Creates a request with the matching AccessType, establishes the proxy
|
|
286
|
+
session, then delivers the artifact: an ssh command (SSH) or a .rdp file
|
|
287
|
+
(RDP). Does NOT check the request in - the session needs it to stay open.
|
|
288
|
+
"""
|
|
289
|
+
st = session_type.lower()
|
|
290
|
+
access = ACCESS_TYPES[st]
|
|
291
|
+
quiet = raw or output == "json"
|
|
292
|
+
try:
|
|
293
|
+
with get_client() as client:
|
|
294
|
+
client.authenticate()
|
|
295
|
+
target = _resolve_target(client, system, account, quiet)
|
|
296
|
+
system_id, system_name = target["system_id"], target["system_name"]
|
|
297
|
+
account_id, account_name = target["account_id"], target["account_name"]
|
|
298
|
+
|
|
299
|
+
console.print(f"[dim]Establishing {access} session to {account_name}@{system_name}...[/dim]") if not quiet else None
|
|
300
|
+
request = client.create_request(
|
|
301
|
+
account_id=account_id,
|
|
302
|
+
system_id=system_id,
|
|
303
|
+
duration_minutes=duration,
|
|
304
|
+
reason=reason,
|
|
305
|
+
access_type=access,
|
|
306
|
+
)
|
|
307
|
+
request_id = request.get("RequestID")
|
|
308
|
+
session = client.create_session(request_id, session_type=access)
|
|
309
|
+
|
|
310
|
+
session_id = session.get("SessionID")
|
|
311
|
+
|
|
312
|
+
if st == "ssh":
|
|
313
|
+
command = _ssh_command(session)
|
|
314
|
+
if raw:
|
|
315
|
+
print(command, end="")
|
|
316
|
+
elif output == "json":
|
|
317
|
+
console.print_json(json.dumps({
|
|
318
|
+
"request_id": request_id,
|
|
319
|
+
"session_id": session_id,
|
|
320
|
+
"system": system_name,
|
|
321
|
+
"account": account_name,
|
|
322
|
+
"command": command,
|
|
323
|
+
"link": session.get("Link"),
|
|
324
|
+
"host": session.get("Host"),
|
|
325
|
+
"port": session.get("Port"),
|
|
326
|
+
}, default=str))
|
|
327
|
+
else:
|
|
328
|
+
console.print(Panel(
|
|
329
|
+
f"[green]SSH session established![/green]\n\n"
|
|
330
|
+
f"System: [cyan]{system_name}[/cyan] Account: [cyan]{account_name}[/cyan]\n"
|
|
331
|
+
f"Request ID: [bold yellow]{request_id}[/bold yellow] Session ID: {session_id}\n\n"
|
|
332
|
+
f"Connect:\n [bold green]{rich_escape(command)}[/bold green]\n\n"
|
|
333
|
+
f"[dim]The proxy injects the password - no credential needed.\n"
|
|
334
|
+
f"Checkin: bt pws credentials checkin {request_id}[/dim]",
|
|
335
|
+
title="Quick SSH",
|
|
336
|
+
))
|
|
337
|
+
if launch:
|
|
338
|
+
_launch_ssh(command)
|
|
339
|
+
return
|
|
340
|
+
|
|
341
|
+
# RDP
|
|
342
|
+
content = _build_rdp_file(session)
|
|
343
|
+
path = Path(out) if out else Path(f"{account_name}@{system_name}.rdp")
|
|
344
|
+
path.write_text(content)
|
|
345
|
+
abspath = str(path.resolve())
|
|
346
|
+
|
|
347
|
+
if raw:
|
|
348
|
+
print(abspath, end="")
|
|
349
|
+
elif output == "json":
|
|
350
|
+
console.print_json(json.dumps({
|
|
351
|
+
"request_id": request_id,
|
|
352
|
+
"session_id": session_id,
|
|
353
|
+
"system": system_name,
|
|
354
|
+
"account": account_name,
|
|
355
|
+
"rdp_file": abspath,
|
|
356
|
+
"host": session.get("Host"),
|
|
357
|
+
"port": session.get("Port"),
|
|
358
|
+
}, default=str))
|
|
359
|
+
else:
|
|
360
|
+
console.print(Panel(
|
|
361
|
+
f"[green]RDP session established![/green]\n\n"
|
|
362
|
+
f"System: [cyan]{system_name}[/cyan] Account: [cyan]{account_name}[/cyan]\n"
|
|
363
|
+
f"Request ID: [bold yellow]{request_id}[/bold yellow] Session ID: {session_id}\n\n"
|
|
364
|
+
f"RDP file: [bold green]{rich_escape(abspath)}[/bold green]\n"
|
|
365
|
+
f"Proxy: {session.get('Host')}:{session.get('Port')}\n\n"
|
|
366
|
+
f"[dim]Open the file with your RDP client (or pass --launch).\n"
|
|
367
|
+
f"Checkin: bt pws credentials checkin {request_id}[/dim]",
|
|
368
|
+
title="Quick RDP",
|
|
369
|
+
))
|
|
370
|
+
if launch:
|
|
371
|
+
_launch_rdp(path)
|
|
372
|
+
|
|
373
|
+
except httpx.HTTPStatusError as e:
|
|
374
|
+
print_api_error(e, f"quick {st}")
|
|
375
|
+
raise typer.Exit(1)
|
|
376
|
+
except httpx.RequestError as e:
|
|
377
|
+
print_api_error(e, f"quick {st}")
|
|
378
|
+
raise typer.Exit(1)
|
|
379
|
+
except typer.Exit:
|
|
380
|
+
raise
|
|
381
|
+
except Exception as e:
|
|
382
|
+
print_api_error(e, f"quick {st}")
|
|
383
|
+
raise typer.Exit(1)
|
|
384
|
+
|
|
385
|
+
|
|
386
|
+
@app.command("ssh")
|
|
387
|
+
def quick_ssh(
|
|
388
|
+
system: Optional[str] = typer.Option(None, "--system", "-s", help="System name (partial match supported)"),
|
|
389
|
+
account: Optional[str] = typer.Option(None, "--account", "-a", help="Account name"),
|
|
390
|
+
duration: int = typer.Option(60, "--duration", "-d", help="Duration in minutes"),
|
|
391
|
+
reason: Optional[str] = typer.Option(None, "--reason", "-r", help="Reason for the session"),
|
|
392
|
+
launch: bool = typer.Option(False, "--launch", help="Launch the ssh client after establishing the session"),
|
|
393
|
+
raw: bool = typer.Option(False, "--raw", help="Output only the ssh command (for scripts)"),
|
|
394
|
+
output: str = typer.Option("table", "--output", "-o", help="Output format: table or json"),
|
|
395
|
+
) -> None:
|
|
396
|
+
"""Establish an SSH proxy session and print a ready-to-run ssh command.
|
|
397
|
+
|
|
398
|
+
The Password Safe proxy injects the credential - you never see the password.
|
|
399
|
+
|
|
400
|
+
Examples:
|
|
401
|
+
bt pws quick ssh -s linhost -a app_account02
|
|
402
|
+
bt pws quick ssh -s linhost -a app --launch
|
|
403
|
+
eval "$(bt pws quick ssh -s linhost -a app --raw)"
|
|
404
|
+
"""
|
|
405
|
+
_do_session_checkout(
|
|
406
|
+
system=system, account=account, session_type="ssh",
|
|
407
|
+
duration=duration, reason=reason, launch=launch, out=None,
|
|
408
|
+
raw=raw, output=output,
|
|
409
|
+
)
|
|
410
|
+
|
|
411
|
+
|
|
412
|
+
@app.command("rdp")
|
|
413
|
+
def quick_rdp(
|
|
414
|
+
system: Optional[str] = typer.Option(None, "--system", "-s", help="System name (partial match supported)"),
|
|
415
|
+
account: Optional[str] = typer.Option(None, "--account", "-a", help="Account name"),
|
|
416
|
+
duration: int = typer.Option(60, "--duration", "-d", help="Duration in minutes"),
|
|
417
|
+
reason: Optional[str] = typer.Option(None, "--reason", "-r", help="Reason for the session"),
|
|
418
|
+
launch: bool = typer.Option(False, "--launch", help="Launch the RDP client on the generated file"),
|
|
419
|
+
out: Optional[str] = typer.Option(None, "--out", help="Path to write the .rdp file (default ./<account>@<system>.rdp)"),
|
|
420
|
+
raw: bool = typer.Option(False, "--raw", help="Output only the .rdp file path (for scripts)"),
|
|
421
|
+
output: str = typer.Option("table", "--output", "-o", help="Output format: table or json"),
|
|
422
|
+
) -> None:
|
|
423
|
+
"""Establish an RDP proxy session and write a launchable .rdp file.
|
|
424
|
+
|
|
425
|
+
The Password Safe proxy injects the credential; the .rdp file targets the
|
|
426
|
+
proxy with the one-time ticket as the username.
|
|
427
|
+
|
|
428
|
+
Examples:
|
|
429
|
+
bt pws quick rdp -s win03 -a Administrator
|
|
430
|
+
bt pws quick rdp -s win03 -a Administrator --launch
|
|
431
|
+
bt pws quick rdp -s win03 -a Administrator --out ~/sessions/win03.rdp
|
|
432
|
+
"""
|
|
433
|
+
_do_session_checkout(
|
|
434
|
+
system=system, account=account, session_type="rdp",
|
|
435
|
+
duration=duration, reason=reason, launch=launch, out=out,
|
|
436
|
+
raw=raw, output=output,
|
|
437
|
+
)
|
|
438
|
+
|
|
439
|
+
|
|
172
440
|
@app.command("checkin")
|
|
173
441
|
def quick_checkin(
|
|
174
442
|
request_id: int = typer.Argument(..., help="Request ID to check in"),
|
|
@@ -9,6 +9,7 @@ from rich.table import Table
|
|
|
9
9
|
|
|
10
10
|
from ...core.output import print_api_error
|
|
11
11
|
from ..client.base import get_client
|
|
12
|
+
from .audit import _write_csv
|
|
12
13
|
|
|
13
14
|
app = typer.Typer(no_args_is_help=True, help="View and control active Password Safe sessions")
|
|
14
15
|
console = Console()
|
|
@@ -167,6 +168,65 @@ def get_session(
|
|
|
167
168
|
raise typer.Exit(1)
|
|
168
169
|
|
|
169
170
|
|
|
171
|
+
@app.command("export")
|
|
172
|
+
def export_sessions(
|
|
173
|
+
system: int = typer.Option(None, "--system", help="Filter by managed system ID"),
|
|
174
|
+
active: bool = typer.Option(False, "--active", help="Export only active (Status 1) sessions"),
|
|
175
|
+
status: str = typer.Option(None, "--status", "-s", help="Filter by raw numeric status code"),
|
|
176
|
+
file: str = typer.Option(None, "--file", help="Write to this file (otherwise prints to stdout)"),
|
|
177
|
+
fmt: str = typer.Option("json", "--format", help="File format: json or csv"),
|
|
178
|
+
output: str = typer.Option("json", "--output", "-o", help="Stdout format when no --file: json or table"),
|
|
179
|
+
) -> None:
|
|
180
|
+
"""Export session history as an evidence artifact.
|
|
181
|
+
|
|
182
|
+
Same filters as `sessions list`, but exports ALL matching rows (no display
|
|
183
|
+
cap). SIEM is the primary audit channel - this is for ad-hoc evidence pulls.
|
|
184
|
+
To export requests and sessions together, see `bt pws audit export`.
|
|
185
|
+
|
|
186
|
+
Examples:
|
|
187
|
+
bt pws sessions export --file sessions.json
|
|
188
|
+
bt pws sessions export --active --file active-sessions.csv --format csv
|
|
189
|
+
bt pws sessions export --system 13 -o json
|
|
190
|
+
"""
|
|
191
|
+
try:
|
|
192
|
+
with get_client() as client:
|
|
193
|
+
client.authenticate()
|
|
194
|
+
sessions = client.list_sessions(system_id=system, status=status)
|
|
195
|
+
|
|
196
|
+
# /Sessions ignores server-side filters (returns full history every call),
|
|
197
|
+
# so filter client-side to make --system/--active actually apply.
|
|
198
|
+
if system is not None:
|
|
199
|
+
sessions = [s for s in sessions if s.get("ManagedSystemID") == system]
|
|
200
|
+
if active:
|
|
201
|
+
sessions = [s for s in sessions if is_active(s)]
|
|
202
|
+
|
|
203
|
+
if file:
|
|
204
|
+
if fmt == "csv":
|
|
205
|
+
_write_csv(file, sessions)
|
|
206
|
+
else:
|
|
207
|
+
with open(file, "w", encoding="utf-8") as f:
|
|
208
|
+
json.dump(sessions, f, indent=2, default=str)
|
|
209
|
+
console.print(f"[green]Exported {len(sessions)} session(s).[/green]")
|
|
210
|
+
console.print(f" [cyan]{file}[/cyan]")
|
|
211
|
+
return
|
|
212
|
+
|
|
213
|
+
# No file: print to stdout
|
|
214
|
+
if output == "json":
|
|
215
|
+
console.print_json(json.dumps(sessions, default=str))
|
|
216
|
+
else:
|
|
217
|
+
print_sessions_table(sessions, title=f"Sessions ({len(sessions)})")
|
|
218
|
+
|
|
219
|
+
except httpx.HTTPStatusError as e:
|
|
220
|
+
print_api_error(e, "export sessions")
|
|
221
|
+
raise typer.Exit(1)
|
|
222
|
+
except httpx.RequestError as e:
|
|
223
|
+
print_api_error(e, "export sessions")
|
|
224
|
+
raise typer.Exit(1)
|
|
225
|
+
except Exception as e:
|
|
226
|
+
print_api_error(e, "export sessions")
|
|
227
|
+
raise typer.Exit(1)
|
|
228
|
+
|
|
229
|
+
|
|
170
230
|
@app.command("suspend")
|
|
171
231
|
def suspend_session(
|
|
172
232
|
session_id: int = typer.Argument(..., help="Session ID to lock/suspend"),
|