bt-cli 0.4.57__tar.gz → 0.4.58__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.57 → bt_cli-0.4.58}/.github/workflows/release.yml +6 -2
- {bt_cli-0.4.57 → bt_cli-0.4.58}/CLAUDE.md +3 -2
- {bt_cli-0.4.57 → bt_cli-0.4.58}/PKG-INFO +1 -1
- {bt_cli-0.4.57 → bt_cli-0.4.58}/pyproject.toml +1 -1
- {bt_cli-0.4.57 → bt_cli-0.4.58}/src/bt_cli/__init__.py +1 -1
- {bt_cli-0.4.57 → bt_cli-0.4.58}/src/bt_cli/data/skills/pws/SKILL.md +55 -0
- {bt_cli-0.4.57 → bt_cli-0.4.58}/src/bt_cli/pws/client/passwordsafe.py +32 -2
- {bt_cli-0.4.57 → bt_cli-0.4.58}/src/bt_cli/pws/commands/__init__.py +3 -0
- {bt_cli-0.4.57 → bt_cli-0.4.58}/src/bt_cli/pws/commands/accounts.py +47 -0
- bt_cli-0.4.58/src/bt_cli/pws/commands/audit.py +117 -0
- {bt_cli-0.4.57 → bt_cli-0.4.58}/src/bt_cli/pws/commands/quick.py +92 -0
- bt_cli-0.4.58/src/bt_cli/pws/commands/sessions.py +243 -0
- {bt_cli-0.4.57 → bt_cli-0.4.58}/.claude/skills/bt/SKILL.md +0 -0
- {bt_cli-0.4.57 → bt_cli-0.4.58}/.claude/skills/entitle/SKILL.md +0 -0
- {bt_cli-0.4.57 → bt_cli-0.4.58}/.claude/skills/epml/SKILL.md +0 -0
- {bt_cli-0.4.57 → bt_cli-0.4.58}/.claude/skills/epmw/SKILL.md +0 -0
- {bt_cli-0.4.57 → bt_cli-0.4.58}/.claude/skills/pra/SKILL.md +0 -0
- {bt_cli-0.4.57 → bt_cli-0.4.58}/.claude/skills/pws/SKILL.md +0 -0
- {bt_cli-0.4.57 → bt_cli-0.4.58}/.env.example +0 -0
- {bt_cli-0.4.57 → bt_cli-0.4.58}/.github/workflows/ci.yml +0 -0
- {bt_cli-0.4.57 → bt_cli-0.4.58}/.gitignore +0 -0
- {bt_cli-0.4.57 → bt_cli-0.4.58}/README.md +0 -0
- {bt_cli-0.4.57 → bt_cli-0.4.58}/assets/cli-help.png +0 -0
- {bt_cli-0.4.57 → bt_cli-0.4.58}/assets/cli-output.png +0 -0
- {bt_cli-0.4.57 → bt_cli-0.4.58}/bt-cli.spec +0 -0
- {bt_cli-0.4.57 → bt_cli-0.4.58}/bt_entry.py +0 -0
- {bt_cli-0.4.57 → bt_cli-0.4.58}/epml-clients-server-side-filters-plan.md +0 -0
- {bt_cli-0.4.57 → bt_cli-0.4.58}/epml-implementation-plan.md +0 -0
- {bt_cli-0.4.57 → bt_cli-0.4.58}/pf-implementation-plan.md +0 -0
- {bt_cli-0.4.57 → bt_cli-0.4.58}/scripts/bt_entry.py +0 -0
- {bt_cli-0.4.57 → bt_cli-0.4.58}/scripts/pf_onboard.py +0 -0
- {bt_cli-0.4.57 → bt_cli-0.4.58}/scripts/sync-package-data.sh +0 -0
- {bt_cli-0.4.57 → bt_cli-0.4.58}/src/bt_cli/cli.py +0 -0
- {bt_cli-0.4.57 → bt_cli-0.4.58}/src/bt_cli/commands/__init__.py +0 -0
- {bt_cli-0.4.57 → bt_cli-0.4.58}/src/bt_cli/commands/configure.py +0 -0
- {bt_cli-0.4.57 → bt_cli-0.4.58}/src/bt_cli/commands/learn.py +0 -0
- {bt_cli-0.4.57 → bt_cli-0.4.58}/src/bt_cli/commands/quick.py +0 -0
- {bt_cli-0.4.57 → bt_cli-0.4.58}/src/bt_cli/core/__init__.py +0 -0
- {bt_cli-0.4.57 → bt_cli-0.4.58}/src/bt_cli/core/auth.py +0 -0
- {bt_cli-0.4.57 → bt_cli-0.4.58}/src/bt_cli/core/client.py +0 -0
- {bt_cli-0.4.57 → bt_cli-0.4.58}/src/bt_cli/core/config.py +0 -0
- {bt_cli-0.4.57 → bt_cli-0.4.58}/src/bt_cli/core/config_file.py +0 -0
- {bt_cli-0.4.57 → bt_cli-0.4.58}/src/bt_cli/core/csv_utils.py +0 -0
- {bt_cli-0.4.57 → bt_cli-0.4.58}/src/bt_cli/core/errors.py +0 -0
- {bt_cli-0.4.57 → bt_cli-0.4.58}/src/bt_cli/core/output.py +0 -0
- {bt_cli-0.4.57 → bt_cli-0.4.58}/src/bt_cli/core/prompts.py +0 -0
- {bt_cli-0.4.57 → bt_cli-0.4.58}/src/bt_cli/core/rest_debug.py +0 -0
- {bt_cli-0.4.57 → bt_cli-0.4.58}/src/bt_cli/data/CLAUDE.md +0 -0
- {bt_cli-0.4.57 → bt_cli-0.4.58}/src/bt_cli/data/__init__.py +0 -0
- {bt_cli-0.4.57 → bt_cli-0.4.58}/src/bt_cli/data/skills/bt/SKILL.md +0 -0
- {bt_cli-0.4.57 → bt_cli-0.4.58}/src/bt_cli/data/skills/entitle/SKILL.md +0 -0
- {bt_cli-0.4.57 → bt_cli-0.4.58}/src/bt_cli/data/skills/epml/SKILL.md +0 -0
- {bt_cli-0.4.57 → bt_cli-0.4.58}/src/bt_cli/data/skills/epmw/SKILL.md +0 -0
- {bt_cli-0.4.57 → bt_cli-0.4.58}/src/bt_cli/data/skills/pf/SKILL.md +0 -0
- {bt_cli-0.4.57 → bt_cli-0.4.58}/src/bt_cli/data/skills/pra/SKILL.md +0 -0
- {bt_cli-0.4.57 → bt_cli-0.4.58}/src/bt_cli/data/skills/secrets/SKILL.md +0 -0
- {bt_cli-0.4.57 → bt_cli-0.4.58}/src/bt_cli/entitle/__init__.py +0 -0
- {bt_cli-0.4.57 → bt_cli-0.4.58}/src/bt_cli/entitle/client/__init__.py +0 -0
- {bt_cli-0.4.57 → bt_cli-0.4.58}/src/bt_cli/entitle/client/base.py +0 -0
- {bt_cli-0.4.57 → bt_cli-0.4.58}/src/bt_cli/entitle/commands/__init__.py +0 -0
- {bt_cli-0.4.57 → bt_cli-0.4.58}/src/bt_cli/entitle/commands/accounts.py +0 -0
- {bt_cli-0.4.57 → bt_cli-0.4.58}/src/bt_cli/entitle/commands/applications.py +0 -0
- {bt_cli-0.4.57 → bt_cli-0.4.58}/src/bt_cli/entitle/commands/auth.py +0 -0
- {bt_cli-0.4.57 → bt_cli-0.4.58}/src/bt_cli/entitle/commands/bundles.py +0 -0
- {bt_cli-0.4.57 → bt_cli-0.4.58}/src/bt_cli/entitle/commands/integrations.py +0 -0
- {bt_cli-0.4.57 → bt_cli-0.4.58}/src/bt_cli/entitle/commands/permissions.py +0 -0
- {bt_cli-0.4.57 → bt_cli-0.4.58}/src/bt_cli/entitle/commands/policies.py +0 -0
- {bt_cli-0.4.57 → bt_cli-0.4.58}/src/bt_cli/entitle/commands/requests.py +0 -0
- {bt_cli-0.4.57 → bt_cli-0.4.58}/src/bt_cli/entitle/commands/resources.py +0 -0
- {bt_cli-0.4.57 → bt_cli-0.4.58}/src/bt_cli/entitle/commands/roles.py +0 -0
- {bt_cli-0.4.57 → bt_cli-0.4.58}/src/bt_cli/entitle/commands/users.py +0 -0
- {bt_cli-0.4.57 → bt_cli-0.4.58}/src/bt_cli/entitle/commands/workflows.py +0 -0
- {bt_cli-0.4.57 → bt_cli-0.4.58}/src/bt_cli/entitle/models/__init__.py +0 -0
- {bt_cli-0.4.57 → bt_cli-0.4.58}/src/bt_cli/entitle/models/bundle.py +0 -0
- {bt_cli-0.4.57 → bt_cli-0.4.58}/src/bt_cli/entitle/models/common.py +0 -0
- {bt_cli-0.4.57 → bt_cli-0.4.58}/src/bt_cli/entitle/models/integration.py +0 -0
- {bt_cli-0.4.57 → bt_cli-0.4.58}/src/bt_cli/entitle/models/permission.py +0 -0
- {bt_cli-0.4.57 → bt_cli-0.4.58}/src/bt_cli/entitle/models/policy.py +0 -0
- {bt_cli-0.4.57 → bt_cli-0.4.58}/src/bt_cli/entitle/models/resource.py +0 -0
- {bt_cli-0.4.57 → bt_cli-0.4.58}/src/bt_cli/entitle/models/role.py +0 -0
- {bt_cli-0.4.57 → bt_cli-0.4.58}/src/bt_cli/entitle/models/user.py +0 -0
- {bt_cli-0.4.57 → bt_cli-0.4.58}/src/bt_cli/entitle/models/workflow.py +0 -0
- {bt_cli-0.4.57 → bt_cli-0.4.58}/src/bt_cli/epml/__init__.py +0 -0
- {bt_cli-0.4.57 → bt_cli-0.4.58}/src/bt_cli/epml/client/__init__.py +0 -0
- {bt_cli-0.4.57 → bt_cli-0.4.58}/src/bt_cli/epml/client/base.py +0 -0
- {bt_cli-0.4.57 → bt_cli-0.4.58}/src/bt_cli/epml/commands/__init__.py +0 -0
- {bt_cli-0.4.57 → bt_cli-0.4.58}/src/bt_cli/epml/commands/audit.py +0 -0
- {bt_cli-0.4.57 → bt_cli-0.4.58}/src/bt_cli/epml/commands/auth.py +0 -0
- {bt_cli-0.4.57 → bt_cli-0.4.58}/src/bt_cli/epml/commands/client_pkg.py +0 -0
- {bt_cli-0.4.57 → bt_cli-0.4.58}/src/bt_cli/epml/commands/clients.py +0 -0
- {bt_cli-0.4.57 → bt_cli-0.4.58}/src/bt_cli/epml/commands/external_apis.py +0 -0
- {bt_cli-0.4.57 → bt_cli-0.4.58}/src/bt_cli/epml/commands/hosts.py +0 -0
- {bt_cli-0.4.57 → bt_cli-0.4.58}/src/bt_cli/epml/commands/iolog.py +0 -0
- {bt_cli-0.4.57 → bt_cli-0.4.58}/src/bt_cli/epml/commands/license.py +0 -0
- {bt_cli-0.4.57 → bt_cli-0.4.58}/src/bt_cli/epml/commands/quick.py +0 -0
- {bt_cli-0.4.57 → bt_cli-0.4.58}/src/bt_cli/epml/commands/rbp_cmdgrps.py +0 -0
- {bt_cli-0.4.57 → bt_cli-0.4.58}/src/bt_cli/epml/commands/rbp_entitlement.py +0 -0
- {bt_cli-0.4.57 → bt_cli-0.4.58}/src/bt_cli/epml/commands/rbp_hostgrps.py +0 -0
- {bt_cli-0.4.57 → bt_cli-0.4.58}/src/bt_cli/epml/commands/rbp_policy.py +0 -0
- {bt_cli-0.4.57 → bt_cli-0.4.58}/src/bt_cli/epml/commands/rbp_roles.py +0 -0
- {bt_cli-0.4.57 → bt_cli-0.4.58}/src/bt_cli/epml/commands/rbp_tests.py +0 -0
- {bt_cli-0.4.57 → bt_cli-0.4.58}/src/bt_cli/epml/commands/rbp_tmdategrps.py +0 -0
- {bt_cli-0.4.57 → bt_cli-0.4.58}/src/bt_cli/epml/commands/rbp_tx.py +0 -0
- {bt_cli-0.4.57 → bt_cli-0.4.58}/src/bt_cli/epml/commands/rbp_usergrps.py +0 -0
- {bt_cli-0.4.57 → bt_cli-0.4.58}/src/bt_cli/epml/commands/settings.py +0 -0
- {bt_cli-0.4.57 → bt_cli-0.4.58}/src/bt_cli/epml/commands/siems.py +0 -0
- {bt_cli-0.4.57 → bt_cli-0.4.58}/src/bt_cli/epml/commands/users.py +0 -0
- {bt_cli-0.4.57 → bt_cli-0.4.58}/src/bt_cli/epml/models/__init__.py +0 -0
- {bt_cli-0.4.57 → bt_cli-0.4.58}/src/bt_cli/epmw/__init__.py +0 -0
- {bt_cli-0.4.57 → bt_cli-0.4.58}/src/bt_cli/epmw/client/__init__.py +0 -0
- {bt_cli-0.4.57 → bt_cli-0.4.58}/src/bt_cli/epmw/client/base.py +0 -0
- {bt_cli-0.4.57 → bt_cli-0.4.58}/src/bt_cli/epmw/commands/__init__.py +0 -0
- {bt_cli-0.4.57 → bt_cli-0.4.58}/src/bt_cli/epmw/commands/audits.py +0 -0
- {bt_cli-0.4.57 → bt_cli-0.4.58}/src/bt_cli/epmw/commands/auth.py +0 -0
- {bt_cli-0.4.57 → bt_cli-0.4.58}/src/bt_cli/epmw/commands/computers.py +0 -0
- {bt_cli-0.4.57 → bt_cli-0.4.58}/src/bt_cli/epmw/commands/events.py +0 -0
- {bt_cli-0.4.57 → bt_cli-0.4.58}/src/bt_cli/epmw/commands/groups.py +0 -0
- {bt_cli-0.4.57 → bt_cli-0.4.58}/src/bt_cli/epmw/commands/policies.py +0 -0
- {bt_cli-0.4.57 → bt_cli-0.4.58}/src/bt_cli/epmw/commands/quick.py +0 -0
- {bt_cli-0.4.57 → bt_cli-0.4.58}/src/bt_cli/epmw/commands/requests.py +0 -0
- {bt_cli-0.4.57 → bt_cli-0.4.58}/src/bt_cli/epmw/commands/roles.py +0 -0
- {bt_cli-0.4.57 → bt_cli-0.4.58}/src/bt_cli/epmw/commands/tasks.py +0 -0
- {bt_cli-0.4.57 → bt_cli-0.4.58}/src/bt_cli/epmw/commands/users.py +0 -0
- {bt_cli-0.4.57 → bt_cli-0.4.58}/src/bt_cli/epmw/models/__init__.py +0 -0
- {bt_cli-0.4.57 → bt_cli-0.4.58}/src/bt_cli/pf/__init__.py +0 -0
- {bt_cli-0.4.57 → bt_cli-0.4.58}/src/bt_cli/pf/client/__init__.py +0 -0
- {bt_cli-0.4.57 → bt_cli-0.4.58}/src/bt_cli/pf/client/base.py +0 -0
- {bt_cli-0.4.57 → bt_cli-0.4.58}/src/bt_cli/pf/commands/__init__.py +0 -0
- {bt_cli-0.4.57 → bt_cli-0.4.58}/src/bt_cli/pf/commands/auth.py +0 -0
- {bt_cli-0.4.57 → bt_cli-0.4.58}/src/bt_cli/pf/commands/machines.py +0 -0
- {bt_cli-0.4.57 → bt_cli-0.4.58}/src/bt_cli/pf/commands/tokens.py +0 -0
- {bt_cli-0.4.57 → bt_cli-0.4.58}/src/bt_cli/pf/commands/user.py +0 -0
- {bt_cli-0.4.57 → bt_cli-0.4.58}/src/bt_cli/pf/commands/users.py +0 -0
- {bt_cli-0.4.57 → bt_cli-0.4.58}/src/bt_cli/pra/__init__.py +0 -0
- {bt_cli-0.4.57 → bt_cli-0.4.58}/src/bt_cli/pra/client/__init__.py +0 -0
- {bt_cli-0.4.57 → bt_cli-0.4.58}/src/bt_cli/pra/client/base.py +0 -0
- {bt_cli-0.4.57 → bt_cli-0.4.58}/src/bt_cli/pra/commands/__init__.py +0 -0
- {bt_cli-0.4.57 → bt_cli-0.4.58}/src/bt_cli/pra/commands/auth.py +0 -0
- {bt_cli-0.4.57 → bt_cli-0.4.58}/src/bt_cli/pra/commands/group_policies.py +0 -0
- {bt_cli-0.4.57 → bt_cli-0.4.58}/src/bt_cli/pra/commands/import_export.py +0 -0
- {bt_cli-0.4.57 → bt_cli-0.4.58}/src/bt_cli/pra/commands/jump_clients.py +0 -0
- {bt_cli-0.4.57 → bt_cli-0.4.58}/src/bt_cli/pra/commands/jump_groups.py +0 -0
- {bt_cli-0.4.57 → bt_cli-0.4.58}/src/bt_cli/pra/commands/jump_items.py +0 -0
- {bt_cli-0.4.57 → bt_cli-0.4.58}/src/bt_cli/pra/commands/jumpoints.py +0 -0
- {bt_cli-0.4.57 → bt_cli-0.4.58}/src/bt_cli/pra/commands/policies.py +0 -0
- {bt_cli-0.4.57 → bt_cli-0.4.58}/src/bt_cli/pra/commands/quick.py +0 -0
- {bt_cli-0.4.57 → bt_cli-0.4.58}/src/bt_cli/pra/commands/teams.py +0 -0
- {bt_cli-0.4.57 → bt_cli-0.4.58}/src/bt_cli/pra/commands/users.py +0 -0
- {bt_cli-0.4.57 → bt_cli-0.4.58}/src/bt_cli/pra/commands/vault.py +0 -0
- {bt_cli-0.4.57 → bt_cli-0.4.58}/src/bt_cli/pra/models/__init__.py +0 -0
- {bt_cli-0.4.57 → bt_cli-0.4.58}/src/bt_cli/pra/models/common.py +0 -0
- {bt_cli-0.4.57 → bt_cli-0.4.58}/src/bt_cli/pra/models/group_policy.py +0 -0
- {bt_cli-0.4.57 → bt_cli-0.4.58}/src/bt_cli/pra/models/jump_client.py +0 -0
- {bt_cli-0.4.57 → bt_cli-0.4.58}/src/bt_cli/pra/models/jump_group.py +0 -0
- {bt_cli-0.4.57 → bt_cli-0.4.58}/src/bt_cli/pra/models/jump_item.py +0 -0
- {bt_cli-0.4.57 → bt_cli-0.4.58}/src/bt_cli/pra/models/jumpoint.py +0 -0
- {bt_cli-0.4.57 → bt_cli-0.4.58}/src/bt_cli/pra/models/team.py +0 -0
- {bt_cli-0.4.57 → bt_cli-0.4.58}/src/bt_cli/pra/models/user.py +0 -0
- {bt_cli-0.4.57 → bt_cli-0.4.58}/src/bt_cli/pra/models/vault.py +0 -0
- {bt_cli-0.4.57 → bt_cli-0.4.58}/src/bt_cli/pws/__init__.py +0 -0
- {bt_cli-0.4.57 → bt_cli-0.4.58}/src/bt_cli/pws/client/__init__.py +0 -0
- {bt_cli-0.4.57 → bt_cli-0.4.58}/src/bt_cli/pws/client/base.py +0 -0
- {bt_cli-0.4.57 → bt_cli-0.4.58}/src/bt_cli/pws/client/beyondinsight.py +0 -0
- {bt_cli-0.4.57 → bt_cli-0.4.58}/src/bt_cli/pws/commands/assets.py +0 -0
- {bt_cli-0.4.57 → bt_cli-0.4.58}/src/bt_cli/pws/commands/attributes.py +0 -0
- {bt_cli-0.4.57 → bt_cli-0.4.58}/src/bt_cli/pws/commands/auth.py +0 -0
- {bt_cli-0.4.57 → bt_cli-0.4.58}/src/bt_cli/pws/commands/clouds.py +0 -0
- {bt_cli-0.4.57 → bt_cli-0.4.58}/src/bt_cli/pws/commands/config.py +0 -0
- {bt_cli-0.4.57 → bt_cli-0.4.58}/src/bt_cli/pws/commands/credentials.py +0 -0
- {bt_cli-0.4.57 → bt_cli-0.4.58}/src/bt_cli/pws/commands/databases.py +0 -0
- {bt_cli-0.4.57 → bt_cli-0.4.58}/src/bt_cli/pws/commands/directories.py +0 -0
- {bt_cli-0.4.57 → bt_cli-0.4.58}/src/bt_cli/pws/commands/functional.py +0 -0
- {bt_cli-0.4.57 → bt_cli-0.4.58}/src/bt_cli/pws/commands/import_export.py +0 -0
- {bt_cli-0.4.57 → bt_cli-0.4.58}/src/bt_cli/pws/commands/platforms.py +0 -0
- {bt_cli-0.4.57 → bt_cli-0.4.58}/src/bt_cli/pws/commands/search.py +0 -0
- {bt_cli-0.4.57 → bt_cli-0.4.58}/src/bt_cli/pws/commands/secrets.py +0 -0
- {bt_cli-0.4.57 → bt_cli-0.4.58}/src/bt_cli/pws/commands/systems.py +0 -0
- {bt_cli-0.4.57 → bt_cli-0.4.58}/src/bt_cli/pws/commands/users.py +0 -0
- {bt_cli-0.4.57 → bt_cli-0.4.58}/src/bt_cli/pws/commands/workgroups.py +0 -0
- {bt_cli-0.4.57 → bt_cli-0.4.58}/src/bt_cli/pws/config.py +0 -0
- {bt_cli-0.4.57 → bt_cli-0.4.58}/src/bt_cli/pws/models/__init__.py +0 -0
- {bt_cli-0.4.57 → bt_cli-0.4.58}/src/bt_cli/pws/models/account.py +0 -0
- {bt_cli-0.4.57 → bt_cli-0.4.58}/src/bt_cli/pws/models/asset.py +0 -0
- {bt_cli-0.4.57 → bt_cli-0.4.58}/src/bt_cli/pws/models/common.py +0 -0
- {bt_cli-0.4.57 → bt_cli-0.4.58}/src/bt_cli/pws/models/system.py +0 -0
- {bt_cli-0.4.57 → bt_cli-0.4.58}/src/bt_cli/secrets/__init__.py +0 -0
- {bt_cli-0.4.57 → bt_cli-0.4.58}/src/bt_cli/secrets/client/__init__.py +0 -0
- {bt_cli-0.4.57 → bt_cli-0.4.58}/src/bt_cli/secrets/client/base.py +0 -0
- {bt_cli-0.4.57 → bt_cli-0.4.58}/src/bt_cli/secrets/commands/__init__.py +0 -0
- {bt_cli-0.4.57 → bt_cli-0.4.58}/src/bt_cli/secrets/commands/_hints.py +0 -0
- {bt_cli-0.4.57 → bt_cli-0.4.58}/src/bt_cli/secrets/commands/auth.py +0 -0
- {bt_cli-0.4.57 → bt_cli-0.4.58}/src/bt_cli/secrets/commands/dynamic.py +0 -0
- {bt_cli-0.4.57 → bt_cli-0.4.58}/src/bt_cli/secrets/commands/folders.py +0 -0
- {bt_cli-0.4.57 → bt_cli-0.4.58}/src/bt_cli/secrets/commands/integrations.py +0 -0
- {bt_cli-0.4.57 → bt_cli-0.4.58}/src/bt_cli/secrets/commands/leases.py +0 -0
- {bt_cli-0.4.57 → bt_cli-0.4.58}/src/bt_cli/secrets/commands/static.py +0 -0
- {bt_cli-0.4.57 → bt_cli-0.4.58}/src/bt_cli/secrets/models/__init__.py +0 -0
- {bt_cli-0.4.57 → bt_cli-0.4.58}/tests/__init__.py +0 -0
- {bt_cli-0.4.57 → bt_cli-0.4.58}/tests/conftest.py +0 -0
- {bt_cli-0.4.57 → bt_cli-0.4.58}/tests/core/__init__.py +0 -0
- {bt_cli-0.4.57 → bt_cli-0.4.58}/tests/core/test_auth.py +0 -0
- {bt_cli-0.4.57 → bt_cli-0.4.58}/tests/core/test_config.py +0 -0
- {bt_cli-0.4.57 → bt_cli-0.4.58}/tests/core/test_config_file.py +0 -0
- {bt_cli-0.4.57 → bt_cli-0.4.58}/tests/core/test_errors.py +0 -0
- {bt_cli-0.4.57 → bt_cli-0.4.58}/tests/core/test_output.py +0 -0
- {bt_cli-0.4.57 → bt_cli-0.4.58}/tests/core/test_rest_debug.py +0 -0
- {bt_cli-0.4.57 → bt_cli-0.4.58}/tests/entitle/__init__.py +0 -0
- {bt_cli-0.4.57 → bt_cli-0.4.58}/tests/entitle/test_client.py +0 -0
- {bt_cli-0.4.57 → bt_cli-0.4.58}/tests/entitle/test_commands.py +0 -0
- {bt_cli-0.4.57 → bt_cli-0.4.58}/tests/entitle-smoke-test.sh +0 -0
- {bt_cli-0.4.57 → bt_cli-0.4.58}/tests/epml/__init__.py +0 -0
- {bt_cli-0.4.57 → bt_cli-0.4.58}/tests/epml/test_client.py +0 -0
- {bt_cli-0.4.57 → bt_cli-0.4.58}/tests/epml/test_commands.py +0 -0
- {bt_cli-0.4.57 → bt_cli-0.4.58}/tests/epmw/__init__.py +0 -0
- {bt_cli-0.4.57 → bt_cli-0.4.58}/tests/epmw/test_client.py +0 -0
- {bt_cli-0.4.57 → bt_cli-0.4.58}/tests/epmw/test_commands.py +0 -0
- {bt_cli-0.4.57 → bt_cli-0.4.58}/tests/epmw-quick-test-plan.md +0 -0
- {bt_cli-0.4.57 → bt_cli-0.4.58}/tests/fixtures/__init__.py +0 -0
- {bt_cli-0.4.57 → bt_cli-0.4.58}/tests/fixtures/responses.py +0 -0
- {bt_cli-0.4.57 → bt_cli-0.4.58}/tests/integration/__init__.py +0 -0
- {bt_cli-0.4.57 → bt_cli-0.4.58}/tests/integration/conftest.py +0 -0
- {bt_cli-0.4.57 → bt_cli-0.4.58}/tests/integration/helpers.py +0 -0
- {bt_cli-0.4.57 → bt_cli-0.4.58}/tests/integration/test_entitle_integration.py +0 -0
- {bt_cli-0.4.57 → bt_cli-0.4.58}/tests/integration/test_epmw_integration.py +0 -0
- {bt_cli-0.4.57 → bt_cli-0.4.58}/tests/integration/test_epmw_lifecycle.py +0 -0
- {bt_cli-0.4.57 → bt_cli-0.4.58}/tests/integration/test_pra_integration.py +0 -0
- {bt_cli-0.4.57 → bt_cli-0.4.58}/tests/integration/test_pra_lifecycle.py +0 -0
- {bt_cli-0.4.57 → bt_cli-0.4.58}/tests/integration/test_pws_integration.py +0 -0
- {bt_cli-0.4.57 → bt_cli-0.4.58}/tests/integration/test_pws_lifecycle.py +0 -0
- {bt_cli-0.4.57 → bt_cli-0.4.58}/tests/pf/__init__.py +0 -0
- {bt_cli-0.4.57 → bt_cli-0.4.58}/tests/pf/test_client.py +0 -0
- {bt_cli-0.4.57 → bt_cli-0.4.58}/tests/pf/test_commands.py +0 -0
- {bt_cli-0.4.57 → bt_cli-0.4.58}/tests/pra/__init__.py +0 -0
- {bt_cli-0.4.57 → bt_cli-0.4.58}/tests/pra/test_client.py +0 -0
- {bt_cli-0.4.57 → bt_cli-0.4.58}/tests/pra/test_commands.py +0 -0
- {bt_cli-0.4.57 → bt_cli-0.4.58}/tests/pra-smoke-test.sh +0 -0
- {bt_cli-0.4.57 → bt_cli-0.4.58}/tests/pra-test-plan.md +0 -0
- {bt_cli-0.4.57 → bt_cli-0.4.58}/tests/pws/__init__.py +0 -0
- {bt_cli-0.4.57 → bt_cli-0.4.58}/tests/pws/test_client.py +0 -0
- {bt_cli-0.4.57 → bt_cli-0.4.58}/tests/pws/test_commands.py +0 -0
- {bt_cli-0.4.57 → bt_cli-0.4.58}/tests/pws-quick-test-plan.md +0 -0
- {bt_cli-0.4.57 → bt_cli-0.4.58}/tests/pws-smoke-test.sh +0 -0
- {bt_cli-0.4.57 → bt_cli-0.4.58}/tests/secrets/__init__.py +0 -0
- {bt_cli-0.4.57 → bt_cli-0.4.58}/tests/secrets/test_client.py +0 -0
- {bt_cli-0.4.57 → bt_cli-0.4.58}/tests/secrets/test_commands.py +0 -0
|
@@ -48,6 +48,7 @@ jobs:
|
|
|
48
48
|
with:
|
|
49
49
|
name: bt-linux-amd64
|
|
50
50
|
path: dist/bt-linux-amd64
|
|
51
|
+
retention-days: 7
|
|
51
52
|
|
|
52
53
|
# Build macOS and Windows binaries
|
|
53
54
|
build-binaries:
|
|
@@ -96,6 +97,7 @@ jobs:
|
|
|
96
97
|
with:
|
|
97
98
|
name: ${{ matrix.asset_name }}
|
|
98
99
|
path: dist/${{ matrix.asset_name }}
|
|
100
|
+
retention-days: 7
|
|
99
101
|
|
|
100
102
|
# Build Python package
|
|
101
103
|
build-package:
|
|
@@ -125,6 +127,7 @@ jobs:
|
|
|
125
127
|
with:
|
|
126
128
|
name: python-package
|
|
127
129
|
path: dist/*
|
|
130
|
+
retention-days: 7
|
|
128
131
|
|
|
129
132
|
# Create GitHub release with all artifacts
|
|
130
133
|
release:
|
|
@@ -251,6 +254,7 @@ jobs:
|
|
|
251
254
|
path: dist
|
|
252
255
|
|
|
253
256
|
- name: Publish to PyPI
|
|
257
|
+
# No password — uses Trusted Publishing (OIDC) via the registered
|
|
258
|
+
# PyPI publisher for this repo/workflow/environment; also enables
|
|
259
|
+
# PEP 740 attestations, which an explicit password would disable.
|
|
254
260
|
uses: pypa/gh-action-pypi-publish@release/v1
|
|
255
|
-
with:
|
|
256
|
-
password: ${{ secrets.PYPI_API_TOKEN }}
|
|
@@ -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.58**
|
|
4
4
|
|
|
5
5
|
## Setup
|
|
6
6
|
|
|
@@ -39,7 +39,7 @@ Use these slash commands for detailed product guidance:
|
|
|
39
39
|
| Product | Command | Key Operations |
|
|
40
40
|
|---------|---------|----------------|
|
|
41
41
|
| Cross-product | `bt quick` | `pasm-onboard`, `pasm-offboard`, `pasm-search` |
|
|
42
|
-
| Password Safe | `bt pws` | `systems`, `accounts`, `credentials`, `secrets`, `quick` |
|
|
42
|
+
| Password Safe | `bt pws` | `systems`, `accounts`, `credentials`, `sessions`, `secrets`, `audit`, `quick` |
|
|
43
43
|
| PRA | `bt pra` | `jump-items`, `vault`, `jump-groups`, `quick` |
|
|
44
44
|
| Entitle | `bt entitle` | `integrations`, `resources`, `bundles`, `permissions` |
|
|
45
45
|
| EPM Windows | `bt epmw` | `computers`, `groups`, `policies`, `requests`, `quick` |
|
|
@@ -81,6 +81,7 @@ PASSWORD=$(bt pws quick checkout -s server -a admin --raw)
|
|
|
81
81
|
- PRA K8s tunnels: Require Linux jumpoint
|
|
82
82
|
- PWS assets: Must create via workgroup endpoint
|
|
83
83
|
- ECM integration: PWS system name must match PRA jump item name
|
|
84
|
+
- **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).
|
|
84
85
|
- Entitle permissions: Use server-side filters (`--resource`, `--user`, `--integration`) - dataset can be 20k+ records
|
|
85
86
|
- Entitle users: Use `--all` flag only when needed - default fetches first page only
|
|
86
87
|
- Windows builds: Rich must be pinned to `<14.0.0` (Rich 14 has PyInstaller unicode issues)
|
|
@@ -13,6 +13,7 @@ description: Password Safe commands for credentials, systems, accounts, secrets,
|
|
|
13
13
|
- `bt pws secrets safes delete` - Deletes entire safe
|
|
14
14
|
- `bt pws secrets folders delete` - Deletes folder and contents
|
|
15
15
|
- `bt pws quick offboard` - Removes system + accounts + asset
|
|
16
|
+
- `bt pws sessions terminate` - Ends an active session (cannot be resumed; use `suspend` to lock instead)
|
|
16
17
|
|
|
17
18
|
List affected resources first, then ask for explicit confirmation.
|
|
18
19
|
|
|
@@ -38,6 +39,11 @@ bt pws quick search root -o json
|
|
|
38
39
|
# Rotate password
|
|
39
40
|
bt pws quick rotate -s "axion-finapp-01" -a "root"
|
|
40
41
|
|
|
42
|
+
# Test stored credential (non-destructive - does NOT rotate)
|
|
43
|
+
# Exit 0 = valid, 2 = failed the test
|
|
44
|
+
bt pws quick test -s "axion-finapp-01" -a "root"
|
|
45
|
+
bt pws accounts test <account_id>
|
|
46
|
+
|
|
41
47
|
# Onboard system (asset + system + account)
|
|
42
48
|
# First find your workgroup and functional account IDs (see "Discover IDs" below)
|
|
43
49
|
bt pws quick onboard -n "my-server" -i "10.0.1.50" -w <workgroup_id>
|
|
@@ -144,6 +150,55 @@ bt pws credentials show <request_id>
|
|
|
144
150
|
bt pws credentials checkin <request_id>
|
|
145
151
|
```
|
|
146
152
|
|
|
153
|
+
## Sessions (view / suspend / terminate)
|
|
154
|
+
|
|
155
|
+
```bash
|
|
156
|
+
# List sessions - newest first, capped at 25 by default
|
|
157
|
+
bt pws sessions list
|
|
158
|
+
bt pws sessions list --active # only live sessions (started, not ended)
|
|
159
|
+
bt pws sessions list --all -o json # full history
|
|
160
|
+
bt pws sessions get <session_id>
|
|
161
|
+
|
|
162
|
+
# Suspend = lock (user disconnected, session can be resumed)
|
|
163
|
+
bt pws sessions suspend <session_id>
|
|
164
|
+
|
|
165
|
+
# Terminate = end permanently (cannot be resumed) - prompts unless --force
|
|
166
|
+
bt pws sessions terminate <session_id>
|
|
167
|
+
bt pws sessions terminate <session_id> --force
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
**Gotchas:**
|
|
171
|
+
- `/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
|
+
- `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.
|
|
173
|
+
- An **active session's `EndTime` is a placeholder equal to `StartTime`** (not null), so you can't identify live sessions by timestamps alone - use the Status code. `suspend` moves Status 1 → 5 (Locked); `terminate` moves it → 7 (Terminated).
|
|
174
|
+
- **`terminate` is `POST /Sessions/{id}/Terminate`** (symmetric with `/Lock`). `/Sessions/{id}` only allows GET/PUT - a DELETE returns 405.
|
|
175
|
+
|
|
176
|
+
## Permissions (Secrets Safe)
|
|
177
|
+
|
|
178
|
+
Safe permission grant/revoke is already supported (this is the "assign permissions via API" capability):
|
|
179
|
+
|
|
180
|
+
```bash
|
|
181
|
+
bt pws secrets safes permissions-options # list valid flags
|
|
182
|
+
bt pws secrets safes permissions <safe_id> # show current grants
|
|
183
|
+
bt pws secrets safes grant <safe_id> --user <uid> --permissions "Read,Edit"
|
|
184
|
+
bt pws secrets safes grant <safe_id> --group <gid> --permissions "Manage" --expires <iso8601>
|
|
185
|
+
bt pws secrets safes revoke <safe_id> --user <uid>
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
**Valid permission flags:** `None, Read, Create, Edit, Delete, Share, Manage` (`Manage` = full control / manage permissions).
|
|
189
|
+
|
|
190
|
+
## Audit Evidence Export
|
|
191
|
+
|
|
192
|
+
SIEM is the primary audit channel; these commands pull ad-hoc evidence (request + session history):
|
|
193
|
+
|
|
194
|
+
```bash
|
|
195
|
+
bt pws audit export --file evidence.json # both, JSON
|
|
196
|
+
bt pws audit export --requests --status approved --file reqs.csv --format csv
|
|
197
|
+
bt pws audit export --sessions -o json # stdout
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
**Note:** with `--format csv` and both sections, files are split (`evidence-requests.csv`, `evidence-sessions.csv`) since CSV is single-table.
|
|
201
|
+
|
|
147
202
|
## CSV Import/Export
|
|
148
203
|
|
|
149
204
|
```bash
|
|
@@ -288,6 +288,26 @@ class PasswordSafeMixin:
|
|
|
288
288
|
# Use POST to trigger immediate password change
|
|
289
289
|
return self.post(f"/ManagedAccounts/{account_id}/Credentials/Change")
|
|
290
290
|
|
|
291
|
+
def test_managed_account_credential(
|
|
292
|
+
self: "PasswordSafeClient",
|
|
293
|
+
account_id: int,
|
|
294
|
+
) -> dict[str, Any]:
|
|
295
|
+
"""Test the stored credential for a managed account.
|
|
296
|
+
|
|
297
|
+
Tests whether the credential currently stored in Password Safe still
|
|
298
|
+
authenticates against the target system, using the configured
|
|
299
|
+
functional account. This is non-destructive — it does not rotate or
|
|
300
|
+
change the password.
|
|
301
|
+
|
|
302
|
+
Args:
|
|
303
|
+
account_id: Managed account ID to test
|
|
304
|
+
|
|
305
|
+
Returns:
|
|
306
|
+
Response with a ``Success`` boolean indicating whether the
|
|
307
|
+
credential authenticated successfully.
|
|
308
|
+
"""
|
|
309
|
+
return self.post(f"/ManagedAccounts/{account_id}/Credentials/Test")
|
|
310
|
+
|
|
291
311
|
# =========================================================================
|
|
292
312
|
# Credential Requests (Checkout/Checkin)
|
|
293
313
|
# =========================================================================
|
|
@@ -525,7 +545,14 @@ class PasswordSafeMixin:
|
|
|
525
545
|
if status:
|
|
526
546
|
params["status"] = status
|
|
527
547
|
|
|
528
|
-
|
|
548
|
+
# NOTE: /Sessions ignores limit/offset and returns the full set on every
|
|
549
|
+
# call, so paginate() would loop forever. Use a single GET and normalize.
|
|
550
|
+
result = self.get("/Sessions", params=params)
|
|
551
|
+
if isinstance(result, list):
|
|
552
|
+
return result
|
|
553
|
+
if isinstance(result, dict) and "Data" in result:
|
|
554
|
+
return result["Data"]
|
|
555
|
+
return []
|
|
529
556
|
|
|
530
557
|
def get_session(self: "PasswordSafeClient", session_id: int) -> dict[str, Any]:
|
|
531
558
|
"""Get a session by ID.
|
|
@@ -552,13 +579,16 @@ class PasswordSafeMixin:
|
|
|
552
579
|
def terminate_session(self: "PasswordSafeClient", session_id: int) -> dict[str, Any]:
|
|
553
580
|
"""Terminate an active session.
|
|
554
581
|
|
|
582
|
+
Note: /Sessions/{id} only allows GET/PUT (DELETE returns 405). Termination
|
|
583
|
+
is a POST to the /Terminate sub-resource, symmetric with /Lock.
|
|
584
|
+
|
|
555
585
|
Args:
|
|
556
586
|
session_id: Session ID to terminate
|
|
557
587
|
|
|
558
588
|
Returns:
|
|
559
589
|
Response from termination
|
|
560
590
|
"""
|
|
561
|
-
return self.
|
|
591
|
+
return self.post(f"/Sessions/{session_id}/Terminate")
|
|
562
592
|
|
|
563
593
|
# =========================================================================
|
|
564
594
|
# Applications
|
|
@@ -11,6 +11,7 @@ app = typer.Typer(
|
|
|
11
11
|
# Import and register command groups
|
|
12
12
|
from . import auth, systems, accounts, assets, workgroups, platforms, users
|
|
13
13
|
from . import credentials, config, databases, directories, clouds, secrets, quick, functional, search, attributes
|
|
14
|
+
from . import sessions, audit
|
|
14
15
|
from .import_export import import_app, export_app
|
|
15
16
|
|
|
16
17
|
app.add_typer(auth.app, name="auth", help="Authentication commands")
|
|
@@ -21,6 +22,8 @@ app.add_typer(assets.app, name="assets", help="BeyondInsight assets")
|
|
|
21
22
|
app.add_typer(workgroups.app, name="workgroups", help="Manage workgroups")
|
|
22
23
|
app.add_typer(platforms.app, name="platforms", help="View available platforms")
|
|
23
24
|
app.add_typer(credentials.app, name="credentials", help="Credential checkout/checkin")
|
|
25
|
+
app.add_typer(sessions.app, name="sessions", help="View and control active sessions")
|
|
26
|
+
app.add_typer(audit.app, name="audit", help="Export audit evidence (request/session history)")
|
|
24
27
|
app.add_typer(config.app, name="config", help="View Password Safe configuration")
|
|
25
28
|
app.add_typer(databases.app, name="databases", help="Manage database instances")
|
|
26
29
|
app.add_typer(directories.app, name="directories", help="Manage directories (Entra ID, AD)")
|
|
@@ -372,6 +372,53 @@ def rotate_password(
|
|
|
372
372
|
raise typer.Exit(1)
|
|
373
373
|
|
|
374
374
|
|
|
375
|
+
@app.command("test")
|
|
376
|
+
def test_credential(
|
|
377
|
+
account_id: int = typer.Argument(..., help="Managed account ID to test"),
|
|
378
|
+
) -> None:
|
|
379
|
+
"""Test the stored credential for a managed account.
|
|
380
|
+
|
|
381
|
+
Checks whether the password Password Safe currently holds still
|
|
382
|
+
authenticates against the target system (using the functional account).
|
|
383
|
+
This is non-destructive — it does NOT rotate or change the password.
|
|
384
|
+
|
|
385
|
+
Exit code 0 = credential valid, 2 = credential failed the test.
|
|
386
|
+
"""
|
|
387
|
+
try:
|
|
388
|
+
with get_client() as client:
|
|
389
|
+
client.authenticate()
|
|
390
|
+
|
|
391
|
+
# Get account details for display
|
|
392
|
+
account = client.get_managed_account(account_id)
|
|
393
|
+
account_name = account.get("AccountName", "Unknown")
|
|
394
|
+
system_name = account.get("SystemName", str(account.get("ManagedSystemID", "?")))
|
|
395
|
+
|
|
396
|
+
console.print(f"Testing credential for [cyan]{account_name}[/cyan] on [yellow]{system_name}[/yellow]...")
|
|
397
|
+
|
|
398
|
+
result = client.test_managed_account_credential(account_id)
|
|
399
|
+
|
|
400
|
+
success = bool(result.get("Success")) if isinstance(result, dict) else False
|
|
401
|
+
|
|
402
|
+
if success:
|
|
403
|
+
console.print(f"[green]Credential test PASSED for {account_name}@{system_name}[/green]")
|
|
404
|
+
else:
|
|
405
|
+
console.print(f"[red]Credential test FAILED for {account_name}@{system_name}[/red]")
|
|
406
|
+
console.print("[dim]The stored password no longer authenticates. Consider rotating: bt pws accounts rotate {0}[/dim]".format(account_id))
|
|
407
|
+
raise typer.Exit(2)
|
|
408
|
+
|
|
409
|
+
except typer.Exit:
|
|
410
|
+
raise
|
|
411
|
+
except httpx.HTTPStatusError as e:
|
|
412
|
+
print_api_error(e, "test credential")
|
|
413
|
+
raise typer.Exit(1)
|
|
414
|
+
except httpx.RequestError as e:
|
|
415
|
+
print_api_error(e, "test credential")
|
|
416
|
+
raise typer.Exit(1)
|
|
417
|
+
except Exception as e:
|
|
418
|
+
print_api_error(e, "test credential")
|
|
419
|
+
raise typer.Exit(1)
|
|
420
|
+
|
|
421
|
+
|
|
375
422
|
# =============================================================================
|
|
376
423
|
# Synced Accounts
|
|
377
424
|
# =============================================================================
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
"""CLI commands for exporting Password Safe audit evidence.
|
|
2
|
+
|
|
3
|
+
These commands export request and session history as evidence artifacts. They
|
|
4
|
+
are NOT a replacement for the SIEM audit stream - the platform's SIEM output is
|
|
5
|
+
the primary, tamper-evident audit channel. Use this for ad-hoc evidence pulls
|
|
6
|
+
(e.g. attaching request/session history to a ticket or access review).
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
import csv
|
|
10
|
+
import json
|
|
11
|
+
|
|
12
|
+
import httpx
|
|
13
|
+
import typer
|
|
14
|
+
from rich.console import Console
|
|
15
|
+
|
|
16
|
+
from ...core.output import print_api_error
|
|
17
|
+
from ..client.base import get_client
|
|
18
|
+
|
|
19
|
+
app = typer.Typer(no_args_is_help=True, help="Export audit evidence (request/session history)")
|
|
20
|
+
console = Console()
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def _flatten(rows: list[dict]) -> list[dict]:
|
|
24
|
+
"""Stable union of keys across rows for CSV columns."""
|
|
25
|
+
cols: list[str] = []
|
|
26
|
+
for r in rows:
|
|
27
|
+
for k in r.keys():
|
|
28
|
+
if k not in cols:
|
|
29
|
+
cols.append(k)
|
|
30
|
+
return cols
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def _write_csv(path: str, rows: list[dict]) -> None:
|
|
34
|
+
cols = _flatten(rows)
|
|
35
|
+
with open(path, "w", newline="", encoding="utf-8") as f:
|
|
36
|
+
writer = csv.DictWriter(f, fieldnames=cols, extrasaction="ignore")
|
|
37
|
+
writer.writeheader()
|
|
38
|
+
for r in rows:
|
|
39
|
+
writer.writerow({k: ("" if r.get(k) is None else r.get(k)) for k in cols})
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
@app.command("export")
|
|
43
|
+
def export_audit(
|
|
44
|
+
requests: bool = typer.Option(False, "--requests", help="Include credential request history"),
|
|
45
|
+
sessions: bool = typer.Option(False, "--sessions", help="Include session history"),
|
|
46
|
+
status: str = typer.Option(None, "--status", "-s", help="Filter requests by status"),
|
|
47
|
+
file: str = typer.Option(None, "--file", help="Write evidence to this file (otherwise prints to stdout)"),
|
|
48
|
+
fmt: str = typer.Option("json", "--format", help="Output format when writing a file: json or csv"),
|
|
49
|
+
output: str = typer.Option("json", "--output", "-o", help="Stdout format when no --file: json or table"),
|
|
50
|
+
) -> None:
|
|
51
|
+
"""Export request and/or session history as audit evidence.
|
|
52
|
+
|
|
53
|
+
Defaults to BOTH requests and sessions when neither flag is given.
|
|
54
|
+
Note: SIEM is the primary audit channel - this is for ad-hoc evidence pulls.
|
|
55
|
+
|
|
56
|
+
Examples:
|
|
57
|
+
bt pws audit export --file evidence.json
|
|
58
|
+
bt pws audit export --requests --status approved --file requests.csv --format csv
|
|
59
|
+
bt pws audit export --sessions -o json
|
|
60
|
+
"""
|
|
61
|
+
# Default: include both when neither is specified
|
|
62
|
+
if not requests and not sessions:
|
|
63
|
+
requests = sessions = True
|
|
64
|
+
|
|
65
|
+
try:
|
|
66
|
+
with get_client() as client:
|
|
67
|
+
client.authenticate()
|
|
68
|
+
|
|
69
|
+
evidence: dict[str, list[dict]] = {}
|
|
70
|
+
if requests:
|
|
71
|
+
evidence["requests"] = client.list_requests(status=status, limit=None)
|
|
72
|
+
if sessions:
|
|
73
|
+
evidence["sessions"] = client.list_sessions()
|
|
74
|
+
|
|
75
|
+
# Write to file
|
|
76
|
+
if file:
|
|
77
|
+
if fmt == "csv":
|
|
78
|
+
# CSV is single-table; pick the one section requested, or split into two files.
|
|
79
|
+
sections = [k for k in ("requests", "sessions") if k in evidence]
|
|
80
|
+
if len(sections) == 1:
|
|
81
|
+
_write_csv(file, evidence[sections[0]])
|
|
82
|
+
written = [file]
|
|
83
|
+
else:
|
|
84
|
+
# Two sections: derive per-section filenames (foo.csv -> foo-requests.csv)
|
|
85
|
+
base = file[:-4] if file.lower().endswith(".csv") else file
|
|
86
|
+
written = []
|
|
87
|
+
for sec in sections:
|
|
88
|
+
path = f"{base}-{sec}.csv"
|
|
89
|
+
_write_csv(path, evidence[sec])
|
|
90
|
+
written.append(path)
|
|
91
|
+
else:
|
|
92
|
+
with open(file, "w", encoding="utf-8") as f:
|
|
93
|
+
json.dump(evidence, f, indent=2, default=str)
|
|
94
|
+
written = [file]
|
|
95
|
+
|
|
96
|
+
counts = ", ".join(f"{k}={len(v)}" for k, v in evidence.items())
|
|
97
|
+
console.print(f"[green]Audit evidence exported ({counts}).[/green]")
|
|
98
|
+
for p in written:
|
|
99
|
+
console.print(f" [cyan]{p}[/cyan]")
|
|
100
|
+
return
|
|
101
|
+
|
|
102
|
+
# No file: print to stdout
|
|
103
|
+
if output == "json":
|
|
104
|
+
console.print_json(json.dumps(evidence, default=str))
|
|
105
|
+
else:
|
|
106
|
+
for section, rows in evidence.items():
|
|
107
|
+
console.print(f"[bold]{section}[/bold]: {len(rows)} records")
|
|
108
|
+
|
|
109
|
+
except httpx.HTTPStatusError as e:
|
|
110
|
+
print_api_error(e, "export audit evidence")
|
|
111
|
+
raise typer.Exit(1)
|
|
112
|
+
except httpx.RequestError as e:
|
|
113
|
+
print_api_error(e, "export audit evidence")
|
|
114
|
+
raise typer.Exit(1)
|
|
115
|
+
except Exception as e:
|
|
116
|
+
print_api_error(e, "export audit evidence")
|
|
117
|
+
raise typer.Exit(1)
|
|
@@ -492,6 +492,98 @@ def quick_rotate(
|
|
|
492
492
|
raise typer.Exit(1)
|
|
493
493
|
|
|
494
494
|
|
|
495
|
+
@app.command("test")
|
|
496
|
+
def quick_test(
|
|
497
|
+
system: str = typer.Option(..., "--system", "-s", help="System name (partial match supported)"),
|
|
498
|
+
account: str = typer.Option(..., "--account", "-a", help="Account name"),
|
|
499
|
+
) -> None:
|
|
500
|
+
"""Find an account and test its stored credential.
|
|
501
|
+
|
|
502
|
+
Combines: find system -> find account -> test credential
|
|
503
|
+
|
|
504
|
+
Non-destructive — checks whether the password Password Safe holds still
|
|
505
|
+
authenticates against the system, without rotating it.
|
|
506
|
+
|
|
507
|
+
Exit code 0 = credential valid, 2 = credential failed the test.
|
|
508
|
+
|
|
509
|
+
Examples:
|
|
510
|
+
bt pws quick test -s "axion-finapp-01" -a "root"
|
|
511
|
+
bt pws quick test -s axion -a svc-backup
|
|
512
|
+
"""
|
|
513
|
+
try:
|
|
514
|
+
with get_client() as client:
|
|
515
|
+
client.authenticate()
|
|
516
|
+
|
|
517
|
+
# Step 1: Find the system
|
|
518
|
+
console.print(f"[dim]Finding system '{system}'...[/dim]")
|
|
519
|
+
systems = client.list_managed_systems(search=system)
|
|
520
|
+
|
|
521
|
+
if not systems:
|
|
522
|
+
print_error(f"No system found matching '{system}'")
|
|
523
|
+
raise typer.Exit(1)
|
|
524
|
+
|
|
525
|
+
# Try exact match first, then partial
|
|
526
|
+
matched_system = None
|
|
527
|
+
for s in systems:
|
|
528
|
+
if s.get("SystemName", "").lower() == system.lower():
|
|
529
|
+
matched_system = s
|
|
530
|
+
break
|
|
531
|
+
|
|
532
|
+
if not matched_system:
|
|
533
|
+
matched_system = systems[0]
|
|
534
|
+
if len(systems) > 1:
|
|
535
|
+
console.print(f"[yellow]Multiple matches found, using: {matched_system.get('SystemName')}[/yellow]")
|
|
536
|
+
|
|
537
|
+
system_id = matched_system.get("ManagedSystemID")
|
|
538
|
+
system_name = matched_system.get("SystemName")
|
|
539
|
+
|
|
540
|
+
# Step 2: Find the account
|
|
541
|
+
console.print(f"[dim]Finding account '{account}' on {system_name}...[/dim]")
|
|
542
|
+
accounts = client.list_managed_accounts(system_id=system_id)
|
|
543
|
+
|
|
544
|
+
matched_account = None
|
|
545
|
+
for acc in accounts:
|
|
546
|
+
if acc.get("AccountName", "").lower() == account.lower():
|
|
547
|
+
matched_account = acc
|
|
548
|
+
break
|
|
549
|
+
|
|
550
|
+
if not matched_account:
|
|
551
|
+
print_error(f"Account '{account}' not found on system '{system_name}'")
|
|
552
|
+
if accounts:
|
|
553
|
+
console.print("[dim]Available accounts:[/dim]")
|
|
554
|
+
for acc in accounts[:10]:
|
|
555
|
+
console.print(f" - {acc.get('AccountName')}")
|
|
556
|
+
raise typer.Exit(1)
|
|
557
|
+
|
|
558
|
+
account_id = matched_account.get("AccountId", matched_account.get("ManagedAccountID"))
|
|
559
|
+
account_name = matched_account.get("AccountName")
|
|
560
|
+
|
|
561
|
+
# Step 3: Test the credential
|
|
562
|
+
console.print(f"[dim]Testing credential for {account_name}@{system_name}...[/dim]")
|
|
563
|
+
result = client.test_managed_account_credential(account_id)
|
|
564
|
+
|
|
565
|
+
success = bool(result.get("Success")) if isinstance(result, dict) else False
|
|
566
|
+
|
|
567
|
+
if success:
|
|
568
|
+
print_success(f"Credential test PASSED for {account_name}@{system_name}")
|
|
569
|
+
else:
|
|
570
|
+
print_error(f"Credential test FAILED for {account_name}@{system_name}")
|
|
571
|
+
console.print("[dim]The stored password no longer authenticates. Consider: bt pws quick rotate -s ... -a ...[/dim]")
|
|
572
|
+
raise typer.Exit(2)
|
|
573
|
+
|
|
574
|
+
except typer.Exit:
|
|
575
|
+
raise
|
|
576
|
+
except httpx.HTTPStatusError as e:
|
|
577
|
+
print_api_error(e, "quick test")
|
|
578
|
+
raise typer.Exit(1)
|
|
579
|
+
except httpx.RequestError as e:
|
|
580
|
+
print_api_error(e, "quick test")
|
|
581
|
+
raise typer.Exit(1)
|
|
582
|
+
except Exception as e:
|
|
583
|
+
print_api_error(e, "quick test")
|
|
584
|
+
raise typer.Exit(1)
|
|
585
|
+
|
|
586
|
+
|
|
495
587
|
@app.command("find-secret")
|
|
496
588
|
def quick_find_secret(
|
|
497
589
|
query: str = typer.Argument(..., help="Search term (searches folders and secrets)"),
|