bt-cli 0.4.53__tar.gz → 0.4.55__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.53 → bt_cli-0.4.55}/.github/workflows/ci.yml +7 -7
- {bt_cli-0.4.53 → bt_cli-0.4.55}/.github/workflows/release.yml +12 -12
- {bt_cli-0.4.53 → bt_cli-0.4.55}/CLAUDE.md +6 -2
- {bt_cli-0.4.53 → bt_cli-0.4.55}/PKG-INFO +1 -1
- {bt_cli-0.4.53 → bt_cli-0.4.55}/pyproject.toml +1 -1
- {bt_cli-0.4.53 → bt_cli-0.4.55}/src/bt_cli/__init__.py +1 -1
- {bt_cli-0.4.53 → bt_cli-0.4.55}/src/bt_cli/cli.py +51 -2
- {bt_cli-0.4.53 → bt_cli-0.4.55}/src/bt_cli/commands/configure.py +36 -8
- {bt_cli-0.4.53 → bt_cli-0.4.55}/src/bt_cli/core/config.py +64 -0
- {bt_cli-0.4.53 → bt_cli-0.4.55}/src/bt_cli/core/config_file.py +44 -8
- {bt_cli-0.4.53 → bt_cli-0.4.55}/src/bt_cli/core/output.py +6 -5
- {bt_cli-0.4.53 → bt_cli-0.4.55}/src/bt_cli/core/prompts.py +2 -1
- {bt_cli-0.4.53 → bt_cli-0.4.55}/src/bt_cli/core/rest_debug.py +19 -11
- {bt_cli-0.4.53 → bt_cli-0.4.55}/src/bt_cli/data/skills/epml/SKILL.md +12 -0
- bt_cli-0.4.55/src/bt_cli/data/skills/pf/SKILL.md +76 -0
- {bt_cli-0.4.53 → bt_cli-0.4.55}/src/bt_cli/epml/client/base.py +43 -0
- bt_cli-0.4.55/src/bt_cli/epml/commands/client_pkg.py +278 -0
- bt_cli-0.4.55/src/bt_cli/epml/commands/quick.py +139 -0
- bt_cli-0.4.55/src/bt_cli/pf/__init__.py +1 -0
- bt_cli-0.4.55/src/bt_cli/pf/client/__init__.py +5 -0
- bt_cli-0.4.55/src/bt_cli/pf/client/base.py +133 -0
- bt_cli-0.4.55/src/bt_cli/pf/commands/__init__.py +23 -0
- bt_cli-0.4.55/src/bt_cli/pf/commands/auth.py +35 -0
- bt_cli-0.4.55/src/bt_cli/pf/commands/machines.py +83 -0
- bt_cli-0.4.55/src/bt_cli/pf/commands/tokens.py +99 -0
- bt_cli-0.4.55/src/bt_cli/pf/commands/user.py +70 -0
- {bt_cli-0.4.53 → bt_cli-0.4.55}/tests/core/test_config.py +4 -2
- bt_cli-0.4.55/tests/core/test_config_file.py +67 -0
- bt_cli-0.4.55/tests/core/test_output.py +44 -0
- {bt_cli-0.4.53 → bt_cli-0.4.55}/tests/core/test_rest_debug.py +74 -0
- {bt_cli-0.4.53 → bt_cli-0.4.55}/tests/epml/test_client.py +44 -0
- {bt_cli-0.4.53 → bt_cli-0.4.55}/tests/epml/test_commands.py +87 -0
- bt_cli-0.4.55/tests/pf/test_client.py +79 -0
- bt_cli-0.4.55/tests/pf/test_commands.py +121 -0
- bt_cli-0.4.55/tests/pws/__init__.py +0 -0
- bt_cli-0.4.53/src/bt_cli/epml/commands/client_pkg.py +0 -99
- bt_cli-0.4.53/src/bt_cli/epml/commands/quick.py +0 -67
- {bt_cli-0.4.53 → bt_cli-0.4.55}/.claude/skills/bt/SKILL.md +0 -0
- {bt_cli-0.4.53 → bt_cli-0.4.55}/.claude/skills/entitle/SKILL.md +0 -0
- {bt_cli-0.4.53 → bt_cli-0.4.55}/.claude/skills/epml/SKILL.md +0 -0
- {bt_cli-0.4.53 → bt_cli-0.4.55}/.claude/skills/epmw/SKILL.md +0 -0
- {bt_cli-0.4.53 → bt_cli-0.4.55}/.claude/skills/pra/SKILL.md +0 -0
- {bt_cli-0.4.53 → bt_cli-0.4.55}/.claude/skills/pws/SKILL.md +0 -0
- {bt_cli-0.4.53 → bt_cli-0.4.55}/.env.example +0 -0
- {bt_cli-0.4.53 → bt_cli-0.4.55}/.gitignore +0 -0
- {bt_cli-0.4.53 → bt_cli-0.4.55}/README.md +0 -0
- {bt_cli-0.4.53 → bt_cli-0.4.55}/assets/cli-help.png +0 -0
- {bt_cli-0.4.53 → bt_cli-0.4.55}/assets/cli-output.png +0 -0
- {bt_cli-0.4.53 → bt_cli-0.4.55}/bt-cli.spec +0 -0
- {bt_cli-0.4.53 → bt_cli-0.4.55}/bt_entry.py +0 -0
- {bt_cli-0.4.53 → bt_cli-0.4.55}/epml-clients-server-side-filters-plan.md +0 -0
- {bt_cli-0.4.53 → bt_cli-0.4.55}/epml-implementation-plan.md +0 -0
- {bt_cli-0.4.53 → bt_cli-0.4.55}/pf-implementation-plan.md +0 -0
- {bt_cli-0.4.53 → bt_cli-0.4.55}/scripts/bt_entry.py +0 -0
- {bt_cli-0.4.53 → bt_cli-0.4.55}/scripts/pf_onboard.py +0 -0
- {bt_cli-0.4.53 → bt_cli-0.4.55}/scripts/sync-package-data.sh +0 -0
- {bt_cli-0.4.53 → bt_cli-0.4.55}/src/bt_cli/commands/__init__.py +0 -0
- {bt_cli-0.4.53 → bt_cli-0.4.55}/src/bt_cli/commands/learn.py +0 -0
- {bt_cli-0.4.53 → bt_cli-0.4.55}/src/bt_cli/commands/quick.py +0 -0
- {bt_cli-0.4.53 → bt_cli-0.4.55}/src/bt_cli/core/__init__.py +0 -0
- {bt_cli-0.4.53 → bt_cli-0.4.55}/src/bt_cli/core/auth.py +0 -0
- {bt_cli-0.4.53 → bt_cli-0.4.55}/src/bt_cli/core/client.py +0 -0
- {bt_cli-0.4.53 → bt_cli-0.4.55}/src/bt_cli/core/csv_utils.py +0 -0
- {bt_cli-0.4.53 → bt_cli-0.4.55}/src/bt_cli/core/errors.py +0 -0
- {bt_cli-0.4.53 → bt_cli-0.4.55}/src/bt_cli/data/CLAUDE.md +0 -0
- {bt_cli-0.4.53 → bt_cli-0.4.55}/src/bt_cli/data/__init__.py +0 -0
- {bt_cli-0.4.53 → bt_cli-0.4.55}/src/bt_cli/data/skills/bt/SKILL.md +0 -0
- {bt_cli-0.4.53 → bt_cli-0.4.55}/src/bt_cli/data/skills/entitle/SKILL.md +0 -0
- {bt_cli-0.4.53 → bt_cli-0.4.55}/src/bt_cli/data/skills/epmw/SKILL.md +0 -0
- {bt_cli-0.4.53 → bt_cli-0.4.55}/src/bt_cli/data/skills/pra/SKILL.md +0 -0
- {bt_cli-0.4.53 → bt_cli-0.4.55}/src/bt_cli/data/skills/pws/SKILL.md +0 -0
- {bt_cli-0.4.53 → bt_cli-0.4.55}/src/bt_cli/data/skills/secrets/SKILL.md +0 -0
- {bt_cli-0.4.53 → bt_cli-0.4.55}/src/bt_cli/entitle/__init__.py +0 -0
- {bt_cli-0.4.53 → bt_cli-0.4.55}/src/bt_cli/entitle/client/__init__.py +0 -0
- {bt_cli-0.4.53 → bt_cli-0.4.55}/src/bt_cli/entitle/client/base.py +0 -0
- {bt_cli-0.4.53 → bt_cli-0.4.55}/src/bt_cli/entitle/commands/__init__.py +0 -0
- {bt_cli-0.4.53 → bt_cli-0.4.55}/src/bt_cli/entitle/commands/accounts.py +0 -0
- {bt_cli-0.4.53 → bt_cli-0.4.55}/src/bt_cli/entitle/commands/applications.py +0 -0
- {bt_cli-0.4.53 → bt_cli-0.4.55}/src/bt_cli/entitle/commands/auth.py +0 -0
- {bt_cli-0.4.53 → bt_cli-0.4.55}/src/bt_cli/entitle/commands/bundles.py +0 -0
- {bt_cli-0.4.53 → bt_cli-0.4.55}/src/bt_cli/entitle/commands/integrations.py +0 -0
- {bt_cli-0.4.53 → bt_cli-0.4.55}/src/bt_cli/entitle/commands/permissions.py +0 -0
- {bt_cli-0.4.53 → bt_cli-0.4.55}/src/bt_cli/entitle/commands/policies.py +0 -0
- {bt_cli-0.4.53 → bt_cli-0.4.55}/src/bt_cli/entitle/commands/requests.py +0 -0
- {bt_cli-0.4.53 → bt_cli-0.4.55}/src/bt_cli/entitle/commands/resources.py +0 -0
- {bt_cli-0.4.53 → bt_cli-0.4.55}/src/bt_cli/entitle/commands/roles.py +0 -0
- {bt_cli-0.4.53 → bt_cli-0.4.55}/src/bt_cli/entitle/commands/users.py +0 -0
- {bt_cli-0.4.53 → bt_cli-0.4.55}/src/bt_cli/entitle/commands/workflows.py +0 -0
- {bt_cli-0.4.53 → bt_cli-0.4.55}/src/bt_cli/entitle/models/__init__.py +0 -0
- {bt_cli-0.4.53 → bt_cli-0.4.55}/src/bt_cli/entitle/models/bundle.py +0 -0
- {bt_cli-0.4.53 → bt_cli-0.4.55}/src/bt_cli/entitle/models/common.py +0 -0
- {bt_cli-0.4.53 → bt_cli-0.4.55}/src/bt_cli/entitle/models/integration.py +0 -0
- {bt_cli-0.4.53 → bt_cli-0.4.55}/src/bt_cli/entitle/models/permission.py +0 -0
- {bt_cli-0.4.53 → bt_cli-0.4.55}/src/bt_cli/entitle/models/policy.py +0 -0
- {bt_cli-0.4.53 → bt_cli-0.4.55}/src/bt_cli/entitle/models/resource.py +0 -0
- {bt_cli-0.4.53 → bt_cli-0.4.55}/src/bt_cli/entitle/models/role.py +0 -0
- {bt_cli-0.4.53 → bt_cli-0.4.55}/src/bt_cli/entitle/models/user.py +0 -0
- {bt_cli-0.4.53 → bt_cli-0.4.55}/src/bt_cli/entitle/models/workflow.py +0 -0
- {bt_cli-0.4.53 → bt_cli-0.4.55}/src/bt_cli/epml/__init__.py +0 -0
- {bt_cli-0.4.53 → bt_cli-0.4.55}/src/bt_cli/epml/client/__init__.py +0 -0
- {bt_cli-0.4.53 → bt_cli-0.4.55}/src/bt_cli/epml/commands/__init__.py +0 -0
- {bt_cli-0.4.53 → bt_cli-0.4.55}/src/bt_cli/epml/commands/audit.py +0 -0
- {bt_cli-0.4.53 → bt_cli-0.4.55}/src/bt_cli/epml/commands/auth.py +0 -0
- {bt_cli-0.4.53 → bt_cli-0.4.55}/src/bt_cli/epml/commands/clients.py +0 -0
- {bt_cli-0.4.53 → bt_cli-0.4.55}/src/bt_cli/epml/commands/external_apis.py +0 -0
- {bt_cli-0.4.53 → bt_cli-0.4.55}/src/bt_cli/epml/commands/hosts.py +0 -0
- {bt_cli-0.4.53 → bt_cli-0.4.55}/src/bt_cli/epml/commands/iolog.py +0 -0
- {bt_cli-0.4.53 → bt_cli-0.4.55}/src/bt_cli/epml/commands/license.py +0 -0
- {bt_cli-0.4.53 → bt_cli-0.4.55}/src/bt_cli/epml/commands/rbp_cmdgrps.py +0 -0
- {bt_cli-0.4.53 → bt_cli-0.4.55}/src/bt_cli/epml/commands/rbp_entitlement.py +0 -0
- {bt_cli-0.4.53 → bt_cli-0.4.55}/src/bt_cli/epml/commands/rbp_hostgrps.py +0 -0
- {bt_cli-0.4.53 → bt_cli-0.4.55}/src/bt_cli/epml/commands/rbp_policy.py +0 -0
- {bt_cli-0.4.53 → bt_cli-0.4.55}/src/bt_cli/epml/commands/rbp_roles.py +0 -0
- {bt_cli-0.4.53 → bt_cli-0.4.55}/src/bt_cli/epml/commands/rbp_tests.py +0 -0
- {bt_cli-0.4.53 → bt_cli-0.4.55}/src/bt_cli/epml/commands/rbp_tmdategrps.py +0 -0
- {bt_cli-0.4.53 → bt_cli-0.4.55}/src/bt_cli/epml/commands/rbp_tx.py +0 -0
- {bt_cli-0.4.53 → bt_cli-0.4.55}/src/bt_cli/epml/commands/rbp_usergrps.py +0 -0
- {bt_cli-0.4.53 → bt_cli-0.4.55}/src/bt_cli/epml/commands/settings.py +0 -0
- {bt_cli-0.4.53 → bt_cli-0.4.55}/src/bt_cli/epml/commands/siems.py +0 -0
- {bt_cli-0.4.53 → bt_cli-0.4.55}/src/bt_cli/epml/commands/users.py +0 -0
- {bt_cli-0.4.53 → bt_cli-0.4.55}/src/bt_cli/epml/models/__init__.py +0 -0
- {bt_cli-0.4.53 → bt_cli-0.4.55}/src/bt_cli/epmw/__init__.py +0 -0
- {bt_cli-0.4.53 → bt_cli-0.4.55}/src/bt_cli/epmw/client/__init__.py +0 -0
- {bt_cli-0.4.53 → bt_cli-0.4.55}/src/bt_cli/epmw/client/base.py +0 -0
- {bt_cli-0.4.53 → bt_cli-0.4.55}/src/bt_cli/epmw/commands/__init__.py +0 -0
- {bt_cli-0.4.53 → bt_cli-0.4.55}/src/bt_cli/epmw/commands/audits.py +0 -0
- {bt_cli-0.4.53 → bt_cli-0.4.55}/src/bt_cli/epmw/commands/auth.py +0 -0
- {bt_cli-0.4.53 → bt_cli-0.4.55}/src/bt_cli/epmw/commands/computers.py +0 -0
- {bt_cli-0.4.53 → bt_cli-0.4.55}/src/bt_cli/epmw/commands/events.py +0 -0
- {bt_cli-0.4.53 → bt_cli-0.4.55}/src/bt_cli/epmw/commands/groups.py +0 -0
- {bt_cli-0.4.53 → bt_cli-0.4.55}/src/bt_cli/epmw/commands/policies.py +0 -0
- {bt_cli-0.4.53 → bt_cli-0.4.55}/src/bt_cli/epmw/commands/quick.py +0 -0
- {bt_cli-0.4.53 → bt_cli-0.4.55}/src/bt_cli/epmw/commands/requests.py +0 -0
- {bt_cli-0.4.53 → bt_cli-0.4.55}/src/bt_cli/epmw/commands/roles.py +0 -0
- {bt_cli-0.4.53 → bt_cli-0.4.55}/src/bt_cli/epmw/commands/tasks.py +0 -0
- {bt_cli-0.4.53 → bt_cli-0.4.55}/src/bt_cli/epmw/commands/users.py +0 -0
- {bt_cli-0.4.53 → bt_cli-0.4.55}/src/bt_cli/epmw/models/__init__.py +0 -0
- {bt_cli-0.4.53 → bt_cli-0.4.55}/src/bt_cli/pra/__init__.py +0 -0
- {bt_cli-0.4.53 → bt_cli-0.4.55}/src/bt_cli/pra/client/__init__.py +0 -0
- {bt_cli-0.4.53 → bt_cli-0.4.55}/src/bt_cli/pra/client/base.py +0 -0
- {bt_cli-0.4.53 → bt_cli-0.4.55}/src/bt_cli/pra/commands/__init__.py +0 -0
- {bt_cli-0.4.53 → bt_cli-0.4.55}/src/bt_cli/pra/commands/auth.py +0 -0
- {bt_cli-0.4.53 → bt_cli-0.4.55}/src/bt_cli/pra/commands/group_policies.py +0 -0
- {bt_cli-0.4.53 → bt_cli-0.4.55}/src/bt_cli/pra/commands/import_export.py +0 -0
- {bt_cli-0.4.53 → bt_cli-0.4.55}/src/bt_cli/pra/commands/jump_clients.py +0 -0
- {bt_cli-0.4.53 → bt_cli-0.4.55}/src/bt_cli/pra/commands/jump_groups.py +0 -0
- {bt_cli-0.4.53 → bt_cli-0.4.55}/src/bt_cli/pra/commands/jump_items.py +0 -0
- {bt_cli-0.4.53 → bt_cli-0.4.55}/src/bt_cli/pra/commands/jumpoints.py +0 -0
- {bt_cli-0.4.53 → bt_cli-0.4.55}/src/bt_cli/pra/commands/policies.py +0 -0
- {bt_cli-0.4.53 → bt_cli-0.4.55}/src/bt_cli/pra/commands/quick.py +0 -0
- {bt_cli-0.4.53 → bt_cli-0.4.55}/src/bt_cli/pra/commands/teams.py +0 -0
- {bt_cli-0.4.53 → bt_cli-0.4.55}/src/bt_cli/pra/commands/users.py +0 -0
- {bt_cli-0.4.53 → bt_cli-0.4.55}/src/bt_cli/pra/commands/vault.py +0 -0
- {bt_cli-0.4.53 → bt_cli-0.4.55}/src/bt_cli/pra/models/__init__.py +0 -0
- {bt_cli-0.4.53 → bt_cli-0.4.55}/src/bt_cli/pra/models/common.py +0 -0
- {bt_cli-0.4.53 → bt_cli-0.4.55}/src/bt_cli/pra/models/group_policy.py +0 -0
- {bt_cli-0.4.53 → bt_cli-0.4.55}/src/bt_cli/pra/models/jump_client.py +0 -0
- {bt_cli-0.4.53 → bt_cli-0.4.55}/src/bt_cli/pra/models/jump_group.py +0 -0
- {bt_cli-0.4.53 → bt_cli-0.4.55}/src/bt_cli/pra/models/jump_item.py +0 -0
- {bt_cli-0.4.53 → bt_cli-0.4.55}/src/bt_cli/pra/models/jumpoint.py +0 -0
- {bt_cli-0.4.53 → bt_cli-0.4.55}/src/bt_cli/pra/models/team.py +0 -0
- {bt_cli-0.4.53 → bt_cli-0.4.55}/src/bt_cli/pra/models/user.py +0 -0
- {bt_cli-0.4.53 → bt_cli-0.4.55}/src/bt_cli/pra/models/vault.py +0 -0
- {bt_cli-0.4.53 → bt_cli-0.4.55}/src/bt_cli/pws/__init__.py +0 -0
- {bt_cli-0.4.53 → bt_cli-0.4.55}/src/bt_cli/pws/client/__init__.py +0 -0
- {bt_cli-0.4.53 → bt_cli-0.4.55}/src/bt_cli/pws/client/base.py +0 -0
- {bt_cli-0.4.53 → bt_cli-0.4.55}/src/bt_cli/pws/client/beyondinsight.py +0 -0
- {bt_cli-0.4.53 → bt_cli-0.4.55}/src/bt_cli/pws/client/passwordsafe.py +0 -0
- {bt_cli-0.4.53 → bt_cli-0.4.55}/src/bt_cli/pws/commands/__init__.py +0 -0
- {bt_cli-0.4.53 → bt_cli-0.4.55}/src/bt_cli/pws/commands/accounts.py +0 -0
- {bt_cli-0.4.53 → bt_cli-0.4.55}/src/bt_cli/pws/commands/assets.py +0 -0
- {bt_cli-0.4.53 → bt_cli-0.4.55}/src/bt_cli/pws/commands/attributes.py +0 -0
- {bt_cli-0.4.53 → bt_cli-0.4.55}/src/bt_cli/pws/commands/auth.py +0 -0
- {bt_cli-0.4.53 → bt_cli-0.4.55}/src/bt_cli/pws/commands/clouds.py +0 -0
- {bt_cli-0.4.53 → bt_cli-0.4.55}/src/bt_cli/pws/commands/config.py +0 -0
- {bt_cli-0.4.53 → bt_cli-0.4.55}/src/bt_cli/pws/commands/credentials.py +0 -0
- {bt_cli-0.4.53 → bt_cli-0.4.55}/src/bt_cli/pws/commands/databases.py +0 -0
- {bt_cli-0.4.53 → bt_cli-0.4.55}/src/bt_cli/pws/commands/directories.py +0 -0
- {bt_cli-0.4.53 → bt_cli-0.4.55}/src/bt_cli/pws/commands/functional.py +0 -0
- {bt_cli-0.4.53 → bt_cli-0.4.55}/src/bt_cli/pws/commands/import_export.py +0 -0
- {bt_cli-0.4.53 → bt_cli-0.4.55}/src/bt_cli/pws/commands/platforms.py +0 -0
- {bt_cli-0.4.53 → bt_cli-0.4.55}/src/bt_cli/pws/commands/quick.py +0 -0
- {bt_cli-0.4.53 → bt_cli-0.4.55}/src/bt_cli/pws/commands/search.py +0 -0
- {bt_cli-0.4.53 → bt_cli-0.4.55}/src/bt_cli/pws/commands/secrets.py +0 -0
- {bt_cli-0.4.53 → bt_cli-0.4.55}/src/bt_cli/pws/commands/systems.py +0 -0
- {bt_cli-0.4.53 → bt_cli-0.4.55}/src/bt_cli/pws/commands/users.py +0 -0
- {bt_cli-0.4.53 → bt_cli-0.4.55}/src/bt_cli/pws/commands/workgroups.py +0 -0
- {bt_cli-0.4.53 → bt_cli-0.4.55}/src/bt_cli/pws/config.py +0 -0
- {bt_cli-0.4.53 → bt_cli-0.4.55}/src/bt_cli/pws/models/__init__.py +0 -0
- {bt_cli-0.4.53 → bt_cli-0.4.55}/src/bt_cli/pws/models/account.py +0 -0
- {bt_cli-0.4.53 → bt_cli-0.4.55}/src/bt_cli/pws/models/asset.py +0 -0
- {bt_cli-0.4.53 → bt_cli-0.4.55}/src/bt_cli/pws/models/common.py +0 -0
- {bt_cli-0.4.53 → bt_cli-0.4.55}/src/bt_cli/pws/models/system.py +0 -0
- {bt_cli-0.4.53 → bt_cli-0.4.55}/src/bt_cli/secrets/__init__.py +0 -0
- {bt_cli-0.4.53 → bt_cli-0.4.55}/src/bt_cli/secrets/client/__init__.py +0 -0
- {bt_cli-0.4.53 → bt_cli-0.4.55}/src/bt_cli/secrets/client/base.py +0 -0
- {bt_cli-0.4.53 → bt_cli-0.4.55}/src/bt_cli/secrets/commands/__init__.py +0 -0
- {bt_cli-0.4.53 → bt_cli-0.4.55}/src/bt_cli/secrets/commands/_hints.py +0 -0
- {bt_cli-0.4.53 → bt_cli-0.4.55}/src/bt_cli/secrets/commands/auth.py +0 -0
- {bt_cli-0.4.53 → bt_cli-0.4.55}/src/bt_cli/secrets/commands/dynamic.py +0 -0
- {bt_cli-0.4.53 → bt_cli-0.4.55}/src/bt_cli/secrets/commands/folders.py +0 -0
- {bt_cli-0.4.53 → bt_cli-0.4.55}/src/bt_cli/secrets/commands/integrations.py +0 -0
- {bt_cli-0.4.53 → bt_cli-0.4.55}/src/bt_cli/secrets/commands/leases.py +0 -0
- {bt_cli-0.4.53 → bt_cli-0.4.55}/src/bt_cli/secrets/commands/static.py +0 -0
- {bt_cli-0.4.53 → bt_cli-0.4.55}/src/bt_cli/secrets/models/__init__.py +0 -0
- {bt_cli-0.4.53 → bt_cli-0.4.55}/tests/__init__.py +0 -0
- {bt_cli-0.4.53 → bt_cli-0.4.55}/tests/conftest.py +0 -0
- {bt_cli-0.4.53 → bt_cli-0.4.55}/tests/core/__init__.py +0 -0
- {bt_cli-0.4.53 → bt_cli-0.4.55}/tests/core/test_auth.py +0 -0
- {bt_cli-0.4.53 → bt_cli-0.4.55}/tests/core/test_errors.py +0 -0
- {bt_cli-0.4.53 → bt_cli-0.4.55}/tests/entitle/__init__.py +0 -0
- {bt_cli-0.4.53 → bt_cli-0.4.55}/tests/entitle/test_client.py +0 -0
- {bt_cli-0.4.53 → bt_cli-0.4.55}/tests/entitle/test_commands.py +0 -0
- {bt_cli-0.4.53 → bt_cli-0.4.55}/tests/entitle-smoke-test.sh +0 -0
- {bt_cli-0.4.53 → bt_cli-0.4.55}/tests/epml/__init__.py +0 -0
- {bt_cli-0.4.53 → bt_cli-0.4.55}/tests/epmw/__init__.py +0 -0
- {bt_cli-0.4.53 → bt_cli-0.4.55}/tests/epmw/test_client.py +0 -0
- {bt_cli-0.4.53 → bt_cli-0.4.55}/tests/epmw/test_commands.py +0 -0
- {bt_cli-0.4.53 → bt_cli-0.4.55}/tests/epmw-quick-test-plan.md +0 -0
- {bt_cli-0.4.53 → bt_cli-0.4.55}/tests/fixtures/__init__.py +0 -0
- {bt_cli-0.4.53 → bt_cli-0.4.55}/tests/fixtures/responses.py +0 -0
- {bt_cli-0.4.53 → bt_cli-0.4.55}/tests/integration/__init__.py +0 -0
- {bt_cli-0.4.53 → bt_cli-0.4.55}/tests/integration/conftest.py +0 -0
- {bt_cli-0.4.53 → bt_cli-0.4.55}/tests/integration/helpers.py +0 -0
- {bt_cli-0.4.53 → bt_cli-0.4.55}/tests/integration/test_entitle_integration.py +0 -0
- {bt_cli-0.4.53 → bt_cli-0.4.55}/tests/integration/test_epmw_integration.py +0 -0
- {bt_cli-0.4.53 → bt_cli-0.4.55}/tests/integration/test_epmw_lifecycle.py +0 -0
- {bt_cli-0.4.53 → bt_cli-0.4.55}/tests/integration/test_pra_integration.py +0 -0
- {bt_cli-0.4.53 → bt_cli-0.4.55}/tests/integration/test_pra_lifecycle.py +0 -0
- {bt_cli-0.4.53 → bt_cli-0.4.55}/tests/integration/test_pws_integration.py +0 -0
- {bt_cli-0.4.53 → bt_cli-0.4.55}/tests/integration/test_pws_lifecycle.py +0 -0
- {bt_cli-0.4.53/tests/pra → bt_cli-0.4.55/tests/pf}/__init__.py +0 -0
- {bt_cli-0.4.53/tests/pws → bt_cli-0.4.55/tests/pra}/__init__.py +0 -0
- {bt_cli-0.4.53 → bt_cli-0.4.55}/tests/pra/test_client.py +0 -0
- {bt_cli-0.4.53 → bt_cli-0.4.55}/tests/pra/test_commands.py +0 -0
- {bt_cli-0.4.53 → bt_cli-0.4.55}/tests/pra-smoke-test.sh +0 -0
- {bt_cli-0.4.53 → bt_cli-0.4.55}/tests/pra-test-plan.md +0 -0
- {bt_cli-0.4.53 → bt_cli-0.4.55}/tests/pws/test_client.py +0 -0
- {bt_cli-0.4.53 → bt_cli-0.4.55}/tests/pws/test_commands.py +0 -0
- {bt_cli-0.4.53 → bt_cli-0.4.55}/tests/pws-quick-test-plan.md +0 -0
- {bt_cli-0.4.53 → bt_cli-0.4.55}/tests/pws-smoke-test.sh +0 -0
- {bt_cli-0.4.53 → bt_cli-0.4.55}/tests/secrets/__init__.py +0 -0
- {bt_cli-0.4.53 → bt_cli-0.4.55}/tests/secrets/test_client.py +0 -0
- {bt_cli-0.4.53 → bt_cli-0.4.55}/tests/secrets/test_commands.py +0 -0
|
@@ -16,10 +16,10 @@ jobs:
|
|
|
16
16
|
python-version: ['3.10', '3.11', '3.12']
|
|
17
17
|
|
|
18
18
|
steps:
|
|
19
|
-
- uses: actions/checkout@
|
|
19
|
+
- uses: actions/checkout@v5
|
|
20
20
|
|
|
21
21
|
- name: Set up Python ${{ matrix.python-version }}
|
|
22
|
-
uses: actions/setup-python@
|
|
22
|
+
uses: actions/setup-python@v6
|
|
23
23
|
with:
|
|
24
24
|
python-version: ${{ matrix.python-version }}
|
|
25
25
|
|
|
@@ -48,7 +48,7 @@ jobs:
|
|
|
48
48
|
pytest tests/ --ignore=tests/integration -v --cov=bt_cli --cov-report=term-missing --cov-report=xml
|
|
49
49
|
|
|
50
50
|
- name: Upload coverage to Codecov
|
|
51
|
-
uses: codecov/codecov-action@
|
|
51
|
+
uses: codecov/codecov-action@v5
|
|
52
52
|
if: matrix.python-version == '3.12'
|
|
53
53
|
with:
|
|
54
54
|
files: ./coverage.xml
|
|
@@ -58,10 +58,10 @@ jobs:
|
|
|
58
58
|
runs-on: ubuntu-latest
|
|
59
59
|
|
|
60
60
|
steps:
|
|
61
|
-
- uses: actions/checkout@
|
|
61
|
+
- uses: actions/checkout@v5
|
|
62
62
|
|
|
63
63
|
- name: Set up Python
|
|
64
|
-
uses: actions/setup-python@
|
|
64
|
+
uses: actions/setup-python@v6
|
|
65
65
|
with:
|
|
66
66
|
python-version: '3.11'
|
|
67
67
|
|
|
@@ -87,10 +87,10 @@ jobs:
|
|
|
87
87
|
if: github.event_name == 'workflow_dispatch' || github.event_name == 'schedule'
|
|
88
88
|
|
|
89
89
|
steps:
|
|
90
|
-
- uses: actions/checkout@
|
|
90
|
+
- uses: actions/checkout@v5
|
|
91
91
|
|
|
92
92
|
- name: Set up Python
|
|
93
|
-
uses: actions/setup-python@
|
|
93
|
+
uses: actions/setup-python@v6
|
|
94
94
|
with:
|
|
95
95
|
python-version: '3.12'
|
|
96
96
|
|
|
@@ -28,7 +28,7 @@ jobs:
|
|
|
28
28
|
image: python:3.11-slim-bullseye
|
|
29
29
|
|
|
30
30
|
steps:
|
|
31
|
-
- uses: actions/checkout@
|
|
31
|
+
- uses: actions/checkout@v5
|
|
32
32
|
|
|
33
33
|
- name: Install build dependencies
|
|
34
34
|
run: |
|
|
@@ -44,7 +44,7 @@ jobs:
|
|
|
44
44
|
run: mv dist/bt dist/bt-linux-amd64
|
|
45
45
|
|
|
46
46
|
- name: Upload artifact
|
|
47
|
-
uses: actions/upload-artifact@
|
|
47
|
+
uses: actions/upload-artifact@v7
|
|
48
48
|
with:
|
|
49
49
|
name: bt-linux-amd64
|
|
50
50
|
path: dist/bt-linux-amd64
|
|
@@ -67,10 +67,10 @@ jobs:
|
|
|
67
67
|
runs-on: ${{ matrix.os }}
|
|
68
68
|
|
|
69
69
|
steps:
|
|
70
|
-
- uses: actions/checkout@
|
|
70
|
+
- uses: actions/checkout@v5
|
|
71
71
|
|
|
72
72
|
- name: Set up Python
|
|
73
|
-
uses: actions/setup-python@
|
|
73
|
+
uses: actions/setup-python@v6
|
|
74
74
|
with:
|
|
75
75
|
python-version: '3.11'
|
|
76
76
|
|
|
@@ -92,7 +92,7 @@ jobs:
|
|
|
92
92
|
run: move dist\bt.exe dist\${{ matrix.asset_name }}
|
|
93
93
|
|
|
94
94
|
- name: Upload artifact
|
|
95
|
-
uses: actions/upload-artifact@
|
|
95
|
+
uses: actions/upload-artifact@v7
|
|
96
96
|
with:
|
|
97
97
|
name: ${{ matrix.asset_name }}
|
|
98
98
|
path: dist/${{ matrix.asset_name }}
|
|
@@ -102,10 +102,10 @@ jobs:
|
|
|
102
102
|
runs-on: ubuntu-latest
|
|
103
103
|
|
|
104
104
|
steps:
|
|
105
|
-
- uses: actions/checkout@
|
|
105
|
+
- uses: actions/checkout@v5
|
|
106
106
|
|
|
107
107
|
- name: Set up Python
|
|
108
|
-
uses: actions/setup-python@
|
|
108
|
+
uses: actions/setup-python@v6
|
|
109
109
|
with:
|
|
110
110
|
python-version: '3.11'
|
|
111
111
|
|
|
@@ -121,7 +121,7 @@ jobs:
|
|
|
121
121
|
run: twine check dist/*
|
|
122
122
|
|
|
123
123
|
- name: Upload package artifact
|
|
124
|
-
uses: actions/upload-artifact@
|
|
124
|
+
uses: actions/upload-artifact@v7
|
|
125
125
|
with:
|
|
126
126
|
name: python-package
|
|
127
127
|
path: dist/*
|
|
@@ -132,10 +132,10 @@ jobs:
|
|
|
132
132
|
runs-on: ubuntu-latest
|
|
133
133
|
|
|
134
134
|
steps:
|
|
135
|
-
- uses: actions/checkout@
|
|
135
|
+
- uses: actions/checkout@v5
|
|
136
136
|
|
|
137
137
|
- name: Download all artifacts
|
|
138
|
-
uses: actions/download-artifact@
|
|
138
|
+
uses: actions/download-artifact@v8
|
|
139
139
|
with:
|
|
140
140
|
path: artifacts
|
|
141
141
|
|
|
@@ -149,7 +149,7 @@ jobs:
|
|
|
149
149
|
fi
|
|
150
150
|
|
|
151
151
|
- name: Create Release
|
|
152
|
-
uses: softprops/action-gh-release@
|
|
152
|
+
uses: softprops/action-gh-release@v3
|
|
153
153
|
with:
|
|
154
154
|
name: bt-cli v${{ steps.version.outputs.version }}
|
|
155
155
|
tag_name: v${{ steps.version.outputs.version }}
|
|
@@ -245,7 +245,7 @@ jobs:
|
|
|
245
245
|
|
|
246
246
|
steps:
|
|
247
247
|
- name: Download package artifact
|
|
248
|
-
uses: actions/download-artifact@
|
|
248
|
+
uses: actions/download-artifact@v8
|
|
249
249
|
with:
|
|
250
250
|
name: python-package
|
|
251
251
|
path: dist
|
|
@@ -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.55**
|
|
4
4
|
|
|
5
5
|
## Setup
|
|
6
6
|
|
|
@@ -32,6 +32,7 @@ Use these slash commands for detailed product guidance:
|
|
|
32
32
|
| `/epmw` | EPM Windows - computers, policies, requests |
|
|
33
33
|
| `/epml` | EPM Linux - RBP roles/cmdgrps/usergrps, policy, test suites, transactions |
|
|
34
34
|
| `/secrets` | Secrets API - folders, static, dynamic AWS credentials, leases, integrations |
|
|
35
|
+
| `/pf` | Pathfinder platform - whoami, site/product access, PAT/MCP tokens, machines |
|
|
35
36
|
|
|
36
37
|
## Command Structure
|
|
37
38
|
|
|
@@ -44,6 +45,7 @@ Use these slash commands for detailed product guidance:
|
|
|
44
45
|
| EPM Windows | `bt epmw` | `computers`, `groups`, `policies`, `requests`, `quick` |
|
|
45
46
|
| EPM Linux | `bt epml` | `rbp` (cmdgrps/hostgrps/usergrps/tmdategrps/roles/policy/tests/tx), `settings`, `users`, `audit`, `siems`, `quick` |
|
|
46
47
|
| Secrets API | `bt secrets` | `folders`, `static`, `dynamic` (incl. `generate`), `leases`, `integrations` |
|
|
48
|
+
| Pathfinder | `bt pf` | `whoami`, `access`, `pats`, `mcp-tokens`, `machines` |
|
|
47
49
|
|
|
48
50
|
## Common Patterns
|
|
49
51
|
|
|
@@ -89,6 +91,7 @@ PASSWORD=$(bt pws quick checkout -s server -a admin --raw)
|
|
|
89
91
|
- **EPML role banner format**: `bt epml rbp roles create --banner-text "Title"` builds a `############`-framed message using `%rbprole%`/`%event%` server substitutions, mirroring the appliance's existing roles. See the worked role example in the EPM-L SKILL.md.
|
|
90
92
|
- **Secrets dynamic path is `/dynamic`** (NOT `/dynamic-secrets` as the doc claims); version header is `2026-04-28` (NOT 2026-01-02; server also rejects every earlier dated header — bumps are mandatory, not optional); `dynamic/{name}/generate` returns 201; `/leases/revoke` is scope-gated and 403 is the expected lab response — `bt secrets leases revoke` exits 3 with a clean warning, not generic failure. Documented in `src/bt_cli/data/skills/secrets/SKILL.md`.
|
|
91
93
|
- **Secrets `generate` mints real AWS STS credentials and is audited** — don't loop it in dev. Use `dynamic list`/`get` + `leases list` for browsing.
|
|
94
|
+
- **PF URL mapping**: Nomine swagger paths `/api/auth/X` → gateway `https://api.beyondtrust.io/site/<site-id>/platform/auth/X`. **PATs are site-bound** (minted on the active portal site; other sites → `401 Access denied for this site`). Org-level endpoints (`auth/Users`, `auth/Auditing`, `auth/SAMLProviders`) are 403 for PAT principals; user onboarding & token create/revoke need cookie auth on app.beyondtrust.io. Details in `src/bt_cli/data/skills/pf/SKILL.md`.
|
|
92
95
|
|
|
93
96
|
## Functional vs Managed Accounts
|
|
94
97
|
|
|
@@ -118,7 +121,8 @@ src/bt_cli/
|
|
|
118
121
|
├── entitle/ # Entitle
|
|
119
122
|
├── epmw/ # EPM Windows
|
|
120
123
|
├── epml/ # EPM Linux (PAT auth, /site/<id>/epm/linux/... URLs)
|
|
121
|
-
|
|
124
|
+
├── secrets/ # BeyondTrust Secrets API (PAT auth, /site/<id>/secrets/... URLs)
|
|
125
|
+
└── pf/ # Pathfinder platform (PAT auth, /site/<id>/platform/auth/... URLs)
|
|
122
126
|
```
|
|
123
127
|
|
|
124
128
|
Each product follows: `client/` (API), `commands/` (CLI), `models/` (Pydantic)
|
|
@@ -95,6 +95,12 @@ def _get_secrets_app() -> typer.Typer:
|
|
|
95
95
|
return secrets_app
|
|
96
96
|
|
|
97
97
|
|
|
98
|
+
def _get_pf_app() -> typer.Typer:
|
|
99
|
+
"""Lazy load Pathfinder platform commands."""
|
|
100
|
+
from .pf.commands import app as pf_app
|
|
101
|
+
return pf_app
|
|
102
|
+
|
|
103
|
+
|
|
98
104
|
def _get_configure_app() -> typer.Typer:
|
|
99
105
|
"""Lazy load configure commands."""
|
|
100
106
|
from .commands.configure import app as configure_app
|
|
@@ -145,6 +151,11 @@ try:
|
|
|
145
151
|
except Exception:
|
|
146
152
|
pass # Secrets module not ready yet
|
|
147
153
|
|
|
154
|
+
try:
|
|
155
|
+
app.add_typer(_get_pf_app(), name="pf", help="Pathfinder platform commands")
|
|
156
|
+
except Exception:
|
|
157
|
+
pass # PF module not ready yet
|
|
158
|
+
|
|
148
159
|
try:
|
|
149
160
|
app.add_typer(_get_configure_app(), name="configure", help="Configure bt-cli settings")
|
|
150
161
|
except Exception:
|
|
@@ -299,9 +310,9 @@ def tree_command(
|
|
|
299
310
|
|
|
300
311
|
if product:
|
|
301
312
|
product = product.lower()
|
|
302
|
-
if product not in ["pws", "pra", "entitle", "epmw", "epml", "secrets", "quick", "configure"]:
|
|
313
|
+
if product not in ["pws", "pra", "entitle", "epmw", "epml", "secrets", "pf", "quick", "configure"]:
|
|
303
314
|
console.print(f"[red]Unknown product: {product}[/red]")
|
|
304
|
-
console.print("Available: pws, pra, entitle, epmw, epml, secrets, quick, configure")
|
|
315
|
+
console.print("Available: pws, pra, entitle, epmw, epml, secrets, pf, quick, configure")
|
|
305
316
|
raise typer.Exit(1)
|
|
306
317
|
|
|
307
318
|
tree = Tree("[bold cyan]bt[/bold cyan]")
|
|
@@ -868,6 +879,11 @@ def whoami(
|
|
|
868
879
|
if secrets_result:
|
|
869
880
|
results.append(secrets_result)
|
|
870
881
|
|
|
882
|
+
# Test Pathfinder platform
|
|
883
|
+
pf_result = _test_pf_connection()
|
|
884
|
+
if pf_result:
|
|
885
|
+
results.append(pf_result)
|
|
886
|
+
|
|
871
887
|
if not results:
|
|
872
888
|
console.print("[yellow]No products configured.[/yellow]")
|
|
873
889
|
console.print("\nTo configure products, set environment variables or run:")
|
|
@@ -1108,6 +1124,39 @@ def _test_secrets_connection() -> Optional[dict]:
|
|
|
1108
1124
|
}
|
|
1109
1125
|
|
|
1110
1126
|
|
|
1127
|
+
def _test_pf_connection() -> Optional[dict]:
|
|
1128
|
+
"""Test Pathfinder platform connection and return status."""
|
|
1129
|
+
try:
|
|
1130
|
+
from .core.config import load_pf_config
|
|
1131
|
+
from .pf.client import get_client
|
|
1132
|
+
|
|
1133
|
+
config = load_pf_config()
|
|
1134
|
+
masked_pat = config.pat[:12] + "..." if len(config.pat) > 12 else "***"
|
|
1135
|
+
result = {
|
|
1136
|
+
"product": "Pathfinder",
|
|
1137
|
+
"url": f"{config.api_url}/site/{config.site_id}/platform",
|
|
1138
|
+
"auth_method": f"PAT ({masked_pat})",
|
|
1139
|
+
"connected": False,
|
|
1140
|
+
}
|
|
1141
|
+
|
|
1142
|
+
with get_client() as client:
|
|
1143
|
+
info = client.get_user_info() or {}
|
|
1144
|
+
result["connected"] = True
|
|
1145
|
+
result["user_info"] = f"{info.get('email', '?')} ({info.get('role', '?')})"
|
|
1146
|
+
|
|
1147
|
+
return result
|
|
1148
|
+
except ValueError:
|
|
1149
|
+
return None
|
|
1150
|
+
except Exception as e:
|
|
1151
|
+
return {
|
|
1152
|
+
"product": "Pathfinder",
|
|
1153
|
+
"url": "",
|
|
1154
|
+
"auth_method": "-",
|
|
1155
|
+
"connected": False,
|
|
1156
|
+
"error": str(e)[:50],
|
|
1157
|
+
}
|
|
1158
|
+
|
|
1159
|
+
|
|
1111
1160
|
def run() -> None:
|
|
1112
1161
|
"""Run the CLI application."""
|
|
1113
1162
|
app()
|
|
@@ -41,12 +41,26 @@ def configure_callback(
|
|
|
41
41
|
),
|
|
42
42
|
api_url: Optional[str] = typer.Option(None, "--api-url", help="API URL"),
|
|
43
43
|
client_id: Optional[str] = typer.Option(None, "--client-id", help="OAuth Client ID"),
|
|
44
|
-
client_secret: Optional[str] = typer.Option(
|
|
45
|
-
|
|
44
|
+
client_secret: Optional[str] = typer.Option(
|
|
45
|
+
None,
|
|
46
|
+
"--client-secret",
|
|
47
|
+
help="OAuth Client Secret (visible in shell history/process list — prefer interactive `bt configure`)",
|
|
48
|
+
),
|
|
49
|
+
api_key: Optional[str] = typer.Option(
|
|
50
|
+
None,
|
|
51
|
+
"--api-key",
|
|
52
|
+
help="API Key (visible in shell history/process list — prefer interactive `bt configure`)",
|
|
53
|
+
),
|
|
46
54
|
user_api_key: Optional[str] = typer.Option(
|
|
47
55
|
None,
|
|
48
56
|
"--user-api-key",
|
|
49
|
-
help="Entitle user-context API key
|
|
57
|
+
help="Entitle user-context API key, only required for `bt entitle requests create` "
|
|
58
|
+
"(visible in shell history/process list — prefer interactive `bt configure`)",
|
|
59
|
+
),
|
|
60
|
+
show_input: bool = typer.Option(
|
|
61
|
+
False,
|
|
62
|
+
"--show-input",
|
|
63
|
+
help="Show secret values while typing/pasting in interactive mode (default: hidden)",
|
|
50
64
|
),
|
|
51
65
|
) -> None:
|
|
52
66
|
"""Configure bt-cli interactively or via flags.
|
|
@@ -79,10 +93,14 @@ def configure_callback(
|
|
|
79
93
|
)
|
|
80
94
|
else:
|
|
81
95
|
# Interactive mode
|
|
82
|
-
_configure_interactive(product, profile)
|
|
96
|
+
_configure_interactive(product, profile, show_input=show_input)
|
|
83
97
|
|
|
84
98
|
|
|
85
|
-
def _configure_interactive(
|
|
99
|
+
def _configure_interactive(
|
|
100
|
+
product: Optional[str] = None,
|
|
101
|
+
profile: Optional[str] = None,
|
|
102
|
+
show_input: bool = False,
|
|
103
|
+
) -> None:
|
|
86
104
|
"""Run interactive configuration wizard."""
|
|
87
105
|
console.print()
|
|
88
106
|
console.print(Panel.fit(
|
|
@@ -154,6 +172,10 @@ def _configure_interactive(product: Optional[str] = None, profile: Optional[str]
|
|
|
154
172
|
if "example" in field_info:
|
|
155
173
|
console.print(f" [dim]Example: {field_info['example']}[/dim]")
|
|
156
174
|
|
|
175
|
+
# Show how-to-find-it hint if available
|
|
176
|
+
if "hint" in field_info:
|
|
177
|
+
console.print(f" [dim]{field_info['hint']}[/dim]")
|
|
178
|
+
|
|
157
179
|
# Handle boolean fields
|
|
158
180
|
if isinstance(default, bool):
|
|
159
181
|
value = Confirm.ask(prompt_text, default=default)
|
|
@@ -164,17 +186,19 @@ def _configure_interactive(product: Optional[str] = None, profile: Optional[str]
|
|
|
164
186
|
choices=field_info["choices"],
|
|
165
187
|
default=str(default) if default else None
|
|
166
188
|
)
|
|
167
|
-
# Handle secret fields -
|
|
189
|
+
# Handle secret fields - input hidden by default (--show-input to reveal for
|
|
190
|
+
# paste verification); never echo more than the last 4 chars of an existing value
|
|
168
191
|
elif field_info.get("secret"):
|
|
169
192
|
if existing.get(field_name):
|
|
170
193
|
existing_val = str(existing[field_name])
|
|
171
194
|
if existing_val.startswith("keyring://"):
|
|
172
195
|
console.print(f" [dim](current: stored in keyring)[/dim]")
|
|
173
196
|
else:
|
|
174
|
-
|
|
175
|
-
|
|
197
|
+
hint = "****" + existing_val[-4:] if len(existing_val) > 4 else "****"
|
|
198
|
+
console.print(f" [dim](current: {hint}, press Enter to keep)[/dim]")
|
|
176
199
|
value = Prompt.ask(
|
|
177
200
|
prompt_text,
|
|
201
|
+
password=not show_input,
|
|
178
202
|
default="" if not default else None
|
|
179
203
|
)
|
|
180
204
|
if not value and default:
|
|
@@ -198,6 +222,10 @@ def _configure_interactive(product: Optional[str] = None, profile: Optional[str]
|
|
|
198
222
|
console.print(f" [dim]Stored in keyring[/dim]")
|
|
199
223
|
else:
|
|
200
224
|
new_config[field_name] = value
|
|
225
|
+
print_warning(
|
|
226
|
+
f"Keyring storage failed for '{field_name}' — "
|
|
227
|
+
"value saved to config file instead (file mode 0600)"
|
|
228
|
+
)
|
|
201
229
|
else:
|
|
202
230
|
new_config[field_name] = value
|
|
203
231
|
|
|
@@ -146,6 +146,33 @@ class SecretsConfig(ProductConfig):
|
|
|
146
146
|
raise ValueError("BT_SECRETS_PAT is required")
|
|
147
147
|
|
|
148
148
|
|
|
149
|
+
@dataclass
|
|
150
|
+
class PFConfig(ProductConfig):
|
|
151
|
+
"""Pathfinder platform (Nomine auth service) configuration.
|
|
152
|
+
|
|
153
|
+
Uses Personal Access Token (PAT) bearer authentication against the
|
|
154
|
+
BeyondTrust public API gateway. URLs are composed as:
|
|
155
|
+
|
|
156
|
+
{api_url}/site/{site_id}/platform/auth/<path>
|
|
157
|
+
|
|
158
|
+
PATs are bound to the site that was active in the Pathfinder portal
|
|
159
|
+
when they were minted — a PAT for one site gets
|
|
160
|
+
`401 Access denied for this site` on every other site's paths.
|
|
161
|
+
"""
|
|
162
|
+
|
|
163
|
+
site_id: str = ""
|
|
164
|
+
pat: str = ""
|
|
165
|
+
|
|
166
|
+
def validate(self) -> None:
|
|
167
|
+
"""Validate configuration."""
|
|
168
|
+
if not self.api_url:
|
|
169
|
+
raise ValueError("BT_PF_API_URL is required")
|
|
170
|
+
if not self.site_id:
|
|
171
|
+
raise ValueError("BT_PF_SITE_ID is required")
|
|
172
|
+
if not self.pat:
|
|
173
|
+
raise ValueError("BT_PF_PAT is required")
|
|
174
|
+
|
|
175
|
+
|
|
149
176
|
@dataclass
|
|
150
177
|
class EPMLConfig(ProductConfig):
|
|
151
178
|
"""EPM Linux configuration.
|
|
@@ -545,6 +572,42 @@ def load_secrets_config(env_file: Optional[str] = None, profile: Optional[str] =
|
|
|
545
572
|
return config
|
|
546
573
|
|
|
547
574
|
|
|
575
|
+
def load_pf_config(env_file: Optional[str] = None, profile: Optional[str] = None) -> PFConfig:
|
|
576
|
+
"""Load Pathfinder platform configuration.
|
|
577
|
+
|
|
578
|
+
Configuration sources (in order of precedence):
|
|
579
|
+
1. Environment variables
|
|
580
|
+
2. Config file (~/.bt-cli/config.yaml)
|
|
581
|
+
|
|
582
|
+
Environment variables:
|
|
583
|
+
BT_PF_API_URL - Gateway URL (default: https://api.beyondtrust.io)
|
|
584
|
+
BT_PF_SITE_ID - Site UUID the PAT was minted on (required)
|
|
585
|
+
BT_PF_PAT - Personal Access Token (required)
|
|
586
|
+
BT_PF_VERIFY_SSL - SSL verification (default: true)
|
|
587
|
+
BT_PF_TIMEOUT - Request timeout in seconds (default: 30)
|
|
588
|
+
"""
|
|
589
|
+
if env_file:
|
|
590
|
+
load_dotenv(env_file)
|
|
591
|
+
else:
|
|
592
|
+
load_dotenv()
|
|
593
|
+
|
|
594
|
+
profile = profile or _get_profile()
|
|
595
|
+
layered = get_layered_config("pf", profile)
|
|
596
|
+
|
|
597
|
+
if "pat" in layered:
|
|
598
|
+
layered["pat"] = _resolve_value(layered["pat"])
|
|
599
|
+
|
|
600
|
+
config = PFConfig(
|
|
601
|
+
api_url=_strip_controls(layered.get("api_url") or os.getenv("BT_PF_API_URL", "https://api.beyondtrust.io")),
|
|
602
|
+
site_id=_strip_controls(layered.get("site_id") or os.getenv("BT_PF_SITE_ID", "")),
|
|
603
|
+
pat=_strip_controls(layered.get("pat") or os.getenv("BT_PF_PAT", "")),
|
|
604
|
+
verify_ssl=_to_bool(layered.get("verify_ssl")) if "verify_ssl" in layered else _get_bool(os.getenv("BT_PF_VERIFY_SSL")),
|
|
605
|
+
timeout=_to_float(layered.get("timeout")) if "timeout" in layered else _get_float(os.getenv("BT_PF_TIMEOUT"), 30.0),
|
|
606
|
+
)
|
|
607
|
+
config.validate()
|
|
608
|
+
return config
|
|
609
|
+
|
|
610
|
+
|
|
548
611
|
def load_config(product: str, env_file: Optional[str] = None) -> ProductConfig:
|
|
549
612
|
"""Load configuration for a specific product.
|
|
550
613
|
|
|
@@ -565,6 +628,7 @@ def load_config(product: str, env_file: Optional[str] = None) -> ProductConfig:
|
|
|
565
628
|
"epmw": load_epmw_config,
|
|
566
629
|
"epml": load_epml_config,
|
|
567
630
|
"secrets": load_secrets_config,
|
|
631
|
+
"pf": load_pf_config,
|
|
568
632
|
}
|
|
569
633
|
|
|
570
634
|
loader = loaders.get(product.lower())
|
|
@@ -9,6 +9,8 @@ Supports:
|
|
|
9
9
|
|
|
10
10
|
import logging
|
|
11
11
|
import os
|
|
12
|
+
import sys
|
|
13
|
+
import tempfile
|
|
12
14
|
from dataclasses import dataclass, field
|
|
13
15
|
from pathlib import Path
|
|
14
16
|
from typing import Any, Optional
|
|
@@ -110,6 +112,7 @@ PRODUCTS = {
|
|
|
110
112
|
"required": True,
|
|
111
113
|
"secret": False,
|
|
112
114
|
"example": "7f735e1b-5ecb-49fa-87e8-eddf54a9c745",
|
|
115
|
+
"hint": "Find yours: log into app.beyondtrust.io, choose your site, then browse to https://app.beyondtrust.io/api/platform/currentSite",
|
|
113
116
|
},
|
|
114
117
|
"pat": {"prompt": "Personal Access Token", "required": True, "secret": True},
|
|
115
118
|
"api_version": {
|
|
@@ -122,6 +125,28 @@ PRODUCTS = {
|
|
|
122
125
|
"timeout": {"prompt": "Timeout (seconds)", "required": False, "secret": False, "default": 30},
|
|
123
126
|
},
|
|
124
127
|
},
|
|
128
|
+
"pf": {
|
|
129
|
+
"name": "Pathfinder Platform",
|
|
130
|
+
"fields": {
|
|
131
|
+
"api_url": {
|
|
132
|
+
"prompt": "Gateway URL",
|
|
133
|
+
"required": True,
|
|
134
|
+
"secret": False,
|
|
135
|
+
"default": "https://api.beyondtrust.io",
|
|
136
|
+
"example": "https://api.beyondtrust.io",
|
|
137
|
+
},
|
|
138
|
+
"site_id": {
|
|
139
|
+
"prompt": "Site ID (UUID — the site the PAT was minted on)",
|
|
140
|
+
"required": True,
|
|
141
|
+
"secret": False,
|
|
142
|
+
"example": "6a7546a7-e111-4fe3-bc14-d6de9c2177b9",
|
|
143
|
+
"hint": "Find yours: log into app.beyondtrust.io, choose your site, then browse to https://app.beyondtrust.io/api/platform/currentSite",
|
|
144
|
+
},
|
|
145
|
+
"pat": {"prompt": "Personal Access Token", "required": True, "secret": True},
|
|
146
|
+
"verify_ssl": {"prompt": "Verify SSL", "required": False, "secret": False, "default": True},
|
|
147
|
+
"timeout": {"prompt": "Timeout (seconds)", "required": False, "secret": False, "default": 30},
|
|
148
|
+
},
|
|
149
|
+
},
|
|
125
150
|
"epml": {
|
|
126
151
|
"name": "EPM Linux",
|
|
127
152
|
"fields": {
|
|
@@ -137,6 +162,7 @@ PRODUCTS = {
|
|
|
137
162
|
"required": True,
|
|
138
163
|
"secret": False,
|
|
139
164
|
"example": "c882e07c-753b-4fcd-a1e6-7d39defec5ae",
|
|
165
|
+
"hint": "Find yours: log into app.beyondtrust.io, choose your site, then browse to https://app.beyondtrust.io/api/platform/currentSite",
|
|
140
166
|
},
|
|
141
167
|
"pat": {"prompt": "Personal Access Token", "required": True, "secret": True},
|
|
142
168
|
"default_host_id": {
|
|
@@ -242,7 +268,14 @@ def load_config_file(path: Optional[Path] = None) -> ConfigFile:
|
|
|
242
268
|
profiles=data.get("profiles", {}),
|
|
243
269
|
)
|
|
244
270
|
except (yaml.YAMLError, OSError) as e:
|
|
245
|
-
#
|
|
271
|
+
# Fall back to empty config, but tell the user — a corrupt/unreadable
|
|
272
|
+
# file silently ignored looks like "my profiles disappeared"
|
|
273
|
+
logger.warning(f"Failed to load config file {path}: {e}")
|
|
274
|
+
print(
|
|
275
|
+
f"\033[93mWarning: could not read config file {path} ({type(e).__name__}) - "
|
|
276
|
+
"ignoring it. Fix or delete the file to silence this warning.\033[0m",
|
|
277
|
+
file=sys.stderr,
|
|
278
|
+
)
|
|
246
279
|
return ConfigFile()
|
|
247
280
|
|
|
248
281
|
|
|
@@ -267,9 +300,6 @@ def save_config_file(config: ConfigFile, path: Optional[Path] = None) -> None:
|
|
|
267
300
|
# Security: Create file atomically with secure permissions (0o600)
|
|
268
301
|
# This prevents TOCTOU race where file could be readable between
|
|
269
302
|
# creation and chmod.
|
|
270
|
-
import os
|
|
271
|
-
import sys
|
|
272
|
-
import tempfile
|
|
273
303
|
|
|
274
304
|
# Write to temp file in same directory, then atomic rename
|
|
275
305
|
dir_path = path.parent
|
|
@@ -283,10 +313,9 @@ def save_config_file(config: ConfigFile, path: Optional[Path] = None) -> None:
|
|
|
283
313
|
os.fchmod(fd, 0o600)
|
|
284
314
|
with os.fdopen(fd, "w") as f:
|
|
285
315
|
yaml.dump(data, f, default_flow_style=False, sort_keys=False)
|
|
286
|
-
#
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
os.rename(tmp_path, path)
|
|
316
|
+
# os.replace overwrites atomically on POSIX and Windows alike —
|
|
317
|
+
# no delete-then-rename window where the config doesn't exist
|
|
318
|
+
os.replace(tmp_path, path)
|
|
290
319
|
# On Windows, set permissions after the fact using chmod
|
|
291
320
|
if sys.platform == "win32":
|
|
292
321
|
try:
|
|
@@ -356,6 +385,7 @@ def _get_env_prefix(product: str) -> str:
|
|
|
356
385
|
"epmw": "BT_EPM",
|
|
357
386
|
"epml": "BT_EPML",
|
|
358
387
|
"secrets": "BT_SECRETS",
|
|
388
|
+
"pf": "BT_PF",
|
|
359
389
|
}
|
|
360
390
|
return prefixes.get(product, f"BT_{product.upper()}")
|
|
361
391
|
|
|
@@ -394,6 +424,12 @@ def _get_env_mappings(product: str) -> dict[str, str]:
|
|
|
394
424
|
# Secrets API doesn't use api_key/client_id/client_secret — drop the inherited mappings
|
|
395
425
|
for unused in ("api_key", "client_id", "client_secret"):
|
|
396
426
|
mappings.pop(unused, None)
|
|
427
|
+
elif product == "pf":
|
|
428
|
+
mappings["site_id"] = f"{prefix}_SITE_ID"
|
|
429
|
+
mappings["pat"] = f"{prefix}_PAT"
|
|
430
|
+
# Pathfinder uses PAT auth only — drop the inherited mappings
|
|
431
|
+
for unused in ("api_key", "client_id", "client_secret"):
|
|
432
|
+
mappings.pop(unused, None)
|
|
397
433
|
|
|
398
434
|
return mappings
|
|
399
435
|
|
|
@@ -5,6 +5,7 @@ from enum import Enum
|
|
|
5
5
|
from typing import Any, Optional
|
|
6
6
|
|
|
7
7
|
from rich.console import Console
|
|
8
|
+
from rich.markup import escape
|
|
8
9
|
from rich.panel import Panel
|
|
9
10
|
from rich.table import Table
|
|
10
11
|
|
|
@@ -141,7 +142,7 @@ def print_success(message: str) -> None:
|
|
|
141
142
|
Args:
|
|
142
143
|
message: Message to display
|
|
143
144
|
"""
|
|
144
|
-
console.print(f"[green]{message}[/green]")
|
|
145
|
+
console.print(f"[green]{escape(message)}[/green]")
|
|
145
146
|
|
|
146
147
|
|
|
147
148
|
def print_error(message: str) -> None:
|
|
@@ -150,7 +151,7 @@ def print_error(message: str) -> None:
|
|
|
150
151
|
Args:
|
|
151
152
|
message: Error message to display
|
|
152
153
|
"""
|
|
153
|
-
console.print(f"[red]Error:[/red] {message}")
|
|
154
|
+
console.print(f"[red]Error:[/red] {escape(message)}")
|
|
154
155
|
|
|
155
156
|
|
|
156
157
|
def print_warning(message: str) -> None:
|
|
@@ -159,7 +160,7 @@ def print_warning(message: str) -> None:
|
|
|
159
160
|
Args:
|
|
160
161
|
message: Warning message to display
|
|
161
162
|
"""
|
|
162
|
-
console.print(f"[yellow]Warning:[/yellow] {message}")
|
|
163
|
+
console.print(f"[yellow]Warning:[/yellow] {escape(message)}")
|
|
163
164
|
|
|
164
165
|
|
|
165
166
|
def print_info(message: str) -> None:
|
|
@@ -168,7 +169,7 @@ def print_info(message: str) -> None:
|
|
|
168
169
|
Args:
|
|
169
170
|
message: Info message to display
|
|
170
171
|
"""
|
|
171
|
-
console.print(f"[blue]{message}[/blue]")
|
|
172
|
+
console.print(f"[blue]{escape(message)}[/blue]")
|
|
172
173
|
|
|
173
174
|
|
|
174
175
|
def confirm_action(message: str, default: bool = False) -> bool:
|
|
@@ -202,4 +203,4 @@ def print_api_error(error: Exception, operation: str) -> None:
|
|
|
202
203
|
operation: Description of the operation that failed (e.g., "list systems")
|
|
203
204
|
"""
|
|
204
205
|
message = handle_api_error(error, operation)
|
|
205
|
-
console.print(f"[red]Error:[/red] {message}")
|
|
206
|
+
console.print(f"[red]Error:[/red] {escape(message)}")
|
|
@@ -64,10 +64,11 @@ def prompt_from_list(
|
|
|
64
64
|
The selected ID
|
|
65
65
|
"""
|
|
66
66
|
console.print(f"\n[bold]{title}:[/bold]")
|
|
67
|
+
id_width = max((len(str(item.get(id_key, ""))) for item in items), default=1)
|
|
67
68
|
for item in items:
|
|
68
69
|
item_id = item.get(id_key, "")
|
|
69
70
|
item_name = item.get(name_key, "Unknown")
|
|
70
|
-
console.print(f" {item_id}: {item_name}")
|
|
71
|
+
console.print(f" {str(item_id):>{id_width}}: {item_name}")
|
|
71
72
|
raw = typer.prompt(prompt_text, type=value_type)
|
|
72
73
|
if value_type is str:
|
|
73
74
|
return _clean_str(raw) # type: ignore[return-value]
|