bt-cli 0.4.56__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.56 → bt_cli-0.4.58}/.github/workflows/release.yml +6 -2
- {bt_cli-0.4.56 → bt_cli-0.4.58}/CLAUDE.md +5 -4
- {bt_cli-0.4.56 → bt_cli-0.4.58}/PKG-INFO +1 -1
- {bt_cli-0.4.56 → bt_cli-0.4.58}/pyproject.toml +1 -1
- {bt_cli-0.4.56 → bt_cli-0.4.58}/src/bt_cli/__init__.py +1 -1
- {bt_cli-0.4.56 → bt_cli-0.4.58}/src/bt_cli/data/skills/pf/SKILL.md +46 -6
- {bt_cli-0.4.56 → bt_cli-0.4.58}/src/bt_cli/data/skills/pws/SKILL.md +55 -0
- bt_cli-0.4.58/src/bt_cli/pf/client/base.py +225 -0
- {bt_cli-0.4.56 → bt_cli-0.4.58}/src/bt_cli/pf/commands/__init__.py +2 -0
- bt_cli-0.4.58/src/bt_cli/pf/commands/users.py +298 -0
- {bt_cli-0.4.56 → bt_cli-0.4.58}/src/bt_cli/pws/client/passwordsafe.py +32 -2
- {bt_cli-0.4.56 → bt_cli-0.4.58}/src/bt_cli/pws/commands/__init__.py +3 -0
- {bt_cli-0.4.56 → 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.56 → 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.58/tests/pf/test_client.py +159 -0
- bt_cli-0.4.58/tests/pf/test_commands.py +266 -0
- bt_cli-0.4.56/src/bt_cli/pf/client/base.py +0 -133
- bt_cli-0.4.56/tests/pf/test_client.py +0 -79
- bt_cli-0.4.56/tests/pf/test_commands.py +0 -121
- {bt_cli-0.4.56 → bt_cli-0.4.58}/.claude/skills/bt/SKILL.md +0 -0
- {bt_cli-0.4.56 → bt_cli-0.4.58}/.claude/skills/entitle/SKILL.md +0 -0
- {bt_cli-0.4.56 → bt_cli-0.4.58}/.claude/skills/epml/SKILL.md +0 -0
- {bt_cli-0.4.56 → bt_cli-0.4.58}/.claude/skills/epmw/SKILL.md +0 -0
- {bt_cli-0.4.56 → bt_cli-0.4.58}/.claude/skills/pra/SKILL.md +0 -0
- {bt_cli-0.4.56 → bt_cli-0.4.58}/.claude/skills/pws/SKILL.md +0 -0
- {bt_cli-0.4.56 → bt_cli-0.4.58}/.env.example +0 -0
- {bt_cli-0.4.56 → bt_cli-0.4.58}/.github/workflows/ci.yml +0 -0
- {bt_cli-0.4.56 → bt_cli-0.4.58}/.gitignore +0 -0
- {bt_cli-0.4.56 → bt_cli-0.4.58}/README.md +0 -0
- {bt_cli-0.4.56 → bt_cli-0.4.58}/assets/cli-help.png +0 -0
- {bt_cli-0.4.56 → bt_cli-0.4.58}/assets/cli-output.png +0 -0
- {bt_cli-0.4.56 → bt_cli-0.4.58}/bt-cli.spec +0 -0
- {bt_cli-0.4.56 → bt_cli-0.4.58}/bt_entry.py +0 -0
- {bt_cli-0.4.56 → bt_cli-0.4.58}/epml-clients-server-side-filters-plan.md +0 -0
- {bt_cli-0.4.56 → bt_cli-0.4.58}/epml-implementation-plan.md +0 -0
- {bt_cli-0.4.56 → bt_cli-0.4.58}/pf-implementation-plan.md +0 -0
- {bt_cli-0.4.56 → bt_cli-0.4.58}/scripts/bt_entry.py +0 -0
- {bt_cli-0.4.56 → bt_cli-0.4.58}/scripts/pf_onboard.py +0 -0
- {bt_cli-0.4.56 → bt_cli-0.4.58}/scripts/sync-package-data.sh +0 -0
- {bt_cli-0.4.56 → bt_cli-0.4.58}/src/bt_cli/cli.py +0 -0
- {bt_cli-0.4.56 → bt_cli-0.4.58}/src/bt_cli/commands/__init__.py +0 -0
- {bt_cli-0.4.56 → bt_cli-0.4.58}/src/bt_cli/commands/configure.py +0 -0
- {bt_cli-0.4.56 → bt_cli-0.4.58}/src/bt_cli/commands/learn.py +0 -0
- {bt_cli-0.4.56 → bt_cli-0.4.58}/src/bt_cli/commands/quick.py +0 -0
- {bt_cli-0.4.56 → bt_cli-0.4.58}/src/bt_cli/core/__init__.py +0 -0
- {bt_cli-0.4.56 → bt_cli-0.4.58}/src/bt_cli/core/auth.py +0 -0
- {bt_cli-0.4.56 → bt_cli-0.4.58}/src/bt_cli/core/client.py +0 -0
- {bt_cli-0.4.56 → bt_cli-0.4.58}/src/bt_cli/core/config.py +0 -0
- {bt_cli-0.4.56 → bt_cli-0.4.58}/src/bt_cli/core/config_file.py +0 -0
- {bt_cli-0.4.56 → bt_cli-0.4.58}/src/bt_cli/core/csv_utils.py +0 -0
- {bt_cli-0.4.56 → bt_cli-0.4.58}/src/bt_cli/core/errors.py +0 -0
- {bt_cli-0.4.56 → bt_cli-0.4.58}/src/bt_cli/core/output.py +0 -0
- {bt_cli-0.4.56 → bt_cli-0.4.58}/src/bt_cli/core/prompts.py +0 -0
- {bt_cli-0.4.56 → bt_cli-0.4.58}/src/bt_cli/core/rest_debug.py +0 -0
- {bt_cli-0.4.56 → bt_cli-0.4.58}/src/bt_cli/data/CLAUDE.md +0 -0
- {bt_cli-0.4.56 → bt_cli-0.4.58}/src/bt_cli/data/__init__.py +0 -0
- {bt_cli-0.4.56 → bt_cli-0.4.58}/src/bt_cli/data/skills/bt/SKILL.md +0 -0
- {bt_cli-0.4.56 → bt_cli-0.4.58}/src/bt_cli/data/skills/entitle/SKILL.md +0 -0
- {bt_cli-0.4.56 → bt_cli-0.4.58}/src/bt_cli/data/skills/epml/SKILL.md +0 -0
- {bt_cli-0.4.56 → bt_cli-0.4.58}/src/bt_cli/data/skills/epmw/SKILL.md +0 -0
- {bt_cli-0.4.56 → bt_cli-0.4.58}/src/bt_cli/data/skills/pra/SKILL.md +0 -0
- {bt_cli-0.4.56 → bt_cli-0.4.58}/src/bt_cli/data/skills/secrets/SKILL.md +0 -0
- {bt_cli-0.4.56 → bt_cli-0.4.58}/src/bt_cli/entitle/__init__.py +0 -0
- {bt_cli-0.4.56 → bt_cli-0.4.58}/src/bt_cli/entitle/client/__init__.py +0 -0
- {bt_cli-0.4.56 → bt_cli-0.4.58}/src/bt_cli/entitle/client/base.py +0 -0
- {bt_cli-0.4.56 → bt_cli-0.4.58}/src/bt_cli/entitle/commands/__init__.py +0 -0
- {bt_cli-0.4.56 → bt_cli-0.4.58}/src/bt_cli/entitle/commands/accounts.py +0 -0
- {bt_cli-0.4.56 → bt_cli-0.4.58}/src/bt_cli/entitle/commands/applications.py +0 -0
- {bt_cli-0.4.56 → bt_cli-0.4.58}/src/bt_cli/entitle/commands/auth.py +0 -0
- {bt_cli-0.4.56 → bt_cli-0.4.58}/src/bt_cli/entitle/commands/bundles.py +0 -0
- {bt_cli-0.4.56 → bt_cli-0.4.58}/src/bt_cli/entitle/commands/integrations.py +0 -0
- {bt_cli-0.4.56 → bt_cli-0.4.58}/src/bt_cli/entitle/commands/permissions.py +0 -0
- {bt_cli-0.4.56 → bt_cli-0.4.58}/src/bt_cli/entitle/commands/policies.py +0 -0
- {bt_cli-0.4.56 → bt_cli-0.4.58}/src/bt_cli/entitle/commands/requests.py +0 -0
- {bt_cli-0.4.56 → bt_cli-0.4.58}/src/bt_cli/entitle/commands/resources.py +0 -0
- {bt_cli-0.4.56 → bt_cli-0.4.58}/src/bt_cli/entitle/commands/roles.py +0 -0
- {bt_cli-0.4.56 → bt_cli-0.4.58}/src/bt_cli/entitle/commands/users.py +0 -0
- {bt_cli-0.4.56 → bt_cli-0.4.58}/src/bt_cli/entitle/commands/workflows.py +0 -0
- {bt_cli-0.4.56 → bt_cli-0.4.58}/src/bt_cli/entitle/models/__init__.py +0 -0
- {bt_cli-0.4.56 → bt_cli-0.4.58}/src/bt_cli/entitle/models/bundle.py +0 -0
- {bt_cli-0.4.56 → bt_cli-0.4.58}/src/bt_cli/entitle/models/common.py +0 -0
- {bt_cli-0.4.56 → bt_cli-0.4.58}/src/bt_cli/entitle/models/integration.py +0 -0
- {bt_cli-0.4.56 → bt_cli-0.4.58}/src/bt_cli/entitle/models/permission.py +0 -0
- {bt_cli-0.4.56 → bt_cli-0.4.58}/src/bt_cli/entitle/models/policy.py +0 -0
- {bt_cli-0.4.56 → bt_cli-0.4.58}/src/bt_cli/entitle/models/resource.py +0 -0
- {bt_cli-0.4.56 → bt_cli-0.4.58}/src/bt_cli/entitle/models/role.py +0 -0
- {bt_cli-0.4.56 → bt_cli-0.4.58}/src/bt_cli/entitle/models/user.py +0 -0
- {bt_cli-0.4.56 → bt_cli-0.4.58}/src/bt_cli/entitle/models/workflow.py +0 -0
- {bt_cli-0.4.56 → bt_cli-0.4.58}/src/bt_cli/epml/__init__.py +0 -0
- {bt_cli-0.4.56 → bt_cli-0.4.58}/src/bt_cli/epml/client/__init__.py +0 -0
- {bt_cli-0.4.56 → bt_cli-0.4.58}/src/bt_cli/epml/client/base.py +0 -0
- {bt_cli-0.4.56 → bt_cli-0.4.58}/src/bt_cli/epml/commands/__init__.py +0 -0
- {bt_cli-0.4.56 → bt_cli-0.4.58}/src/bt_cli/epml/commands/audit.py +0 -0
- {bt_cli-0.4.56 → bt_cli-0.4.58}/src/bt_cli/epml/commands/auth.py +0 -0
- {bt_cli-0.4.56 → bt_cli-0.4.58}/src/bt_cli/epml/commands/client_pkg.py +0 -0
- {bt_cli-0.4.56 → bt_cli-0.4.58}/src/bt_cli/epml/commands/clients.py +0 -0
- {bt_cli-0.4.56 → bt_cli-0.4.58}/src/bt_cli/epml/commands/external_apis.py +0 -0
- {bt_cli-0.4.56 → bt_cli-0.4.58}/src/bt_cli/epml/commands/hosts.py +0 -0
- {bt_cli-0.4.56 → bt_cli-0.4.58}/src/bt_cli/epml/commands/iolog.py +0 -0
- {bt_cli-0.4.56 → bt_cli-0.4.58}/src/bt_cli/epml/commands/license.py +0 -0
- {bt_cli-0.4.56 → bt_cli-0.4.58}/src/bt_cli/epml/commands/quick.py +0 -0
- {bt_cli-0.4.56 → bt_cli-0.4.58}/src/bt_cli/epml/commands/rbp_cmdgrps.py +0 -0
- {bt_cli-0.4.56 → bt_cli-0.4.58}/src/bt_cli/epml/commands/rbp_entitlement.py +0 -0
- {bt_cli-0.4.56 → bt_cli-0.4.58}/src/bt_cli/epml/commands/rbp_hostgrps.py +0 -0
- {bt_cli-0.4.56 → bt_cli-0.4.58}/src/bt_cli/epml/commands/rbp_policy.py +0 -0
- {bt_cli-0.4.56 → bt_cli-0.4.58}/src/bt_cli/epml/commands/rbp_roles.py +0 -0
- {bt_cli-0.4.56 → bt_cli-0.4.58}/src/bt_cli/epml/commands/rbp_tests.py +0 -0
- {bt_cli-0.4.56 → bt_cli-0.4.58}/src/bt_cli/epml/commands/rbp_tmdategrps.py +0 -0
- {bt_cli-0.4.56 → bt_cli-0.4.58}/src/bt_cli/epml/commands/rbp_tx.py +0 -0
- {bt_cli-0.4.56 → bt_cli-0.4.58}/src/bt_cli/epml/commands/rbp_usergrps.py +0 -0
- {bt_cli-0.4.56 → bt_cli-0.4.58}/src/bt_cli/epml/commands/settings.py +0 -0
- {bt_cli-0.4.56 → bt_cli-0.4.58}/src/bt_cli/epml/commands/siems.py +0 -0
- {bt_cli-0.4.56 → bt_cli-0.4.58}/src/bt_cli/epml/commands/users.py +0 -0
- {bt_cli-0.4.56 → bt_cli-0.4.58}/src/bt_cli/epml/models/__init__.py +0 -0
- {bt_cli-0.4.56 → bt_cli-0.4.58}/src/bt_cli/epmw/__init__.py +0 -0
- {bt_cli-0.4.56 → bt_cli-0.4.58}/src/bt_cli/epmw/client/__init__.py +0 -0
- {bt_cli-0.4.56 → bt_cli-0.4.58}/src/bt_cli/epmw/client/base.py +0 -0
- {bt_cli-0.4.56 → bt_cli-0.4.58}/src/bt_cli/epmw/commands/__init__.py +0 -0
- {bt_cli-0.4.56 → bt_cli-0.4.58}/src/bt_cli/epmw/commands/audits.py +0 -0
- {bt_cli-0.4.56 → bt_cli-0.4.58}/src/bt_cli/epmw/commands/auth.py +0 -0
- {bt_cli-0.4.56 → bt_cli-0.4.58}/src/bt_cli/epmw/commands/computers.py +0 -0
- {bt_cli-0.4.56 → bt_cli-0.4.58}/src/bt_cli/epmw/commands/events.py +0 -0
- {bt_cli-0.4.56 → bt_cli-0.4.58}/src/bt_cli/epmw/commands/groups.py +0 -0
- {bt_cli-0.4.56 → bt_cli-0.4.58}/src/bt_cli/epmw/commands/policies.py +0 -0
- {bt_cli-0.4.56 → bt_cli-0.4.58}/src/bt_cli/epmw/commands/quick.py +0 -0
- {bt_cli-0.4.56 → bt_cli-0.4.58}/src/bt_cli/epmw/commands/requests.py +0 -0
- {bt_cli-0.4.56 → bt_cli-0.4.58}/src/bt_cli/epmw/commands/roles.py +0 -0
- {bt_cli-0.4.56 → bt_cli-0.4.58}/src/bt_cli/epmw/commands/tasks.py +0 -0
- {bt_cli-0.4.56 → bt_cli-0.4.58}/src/bt_cli/epmw/commands/users.py +0 -0
- {bt_cli-0.4.56 → bt_cli-0.4.58}/src/bt_cli/epmw/models/__init__.py +0 -0
- {bt_cli-0.4.56 → bt_cli-0.4.58}/src/bt_cli/pf/__init__.py +0 -0
- {bt_cli-0.4.56 → bt_cli-0.4.58}/src/bt_cli/pf/client/__init__.py +0 -0
- {bt_cli-0.4.56 → bt_cli-0.4.58}/src/bt_cli/pf/commands/auth.py +0 -0
- {bt_cli-0.4.56 → bt_cli-0.4.58}/src/bt_cli/pf/commands/machines.py +0 -0
- {bt_cli-0.4.56 → bt_cli-0.4.58}/src/bt_cli/pf/commands/tokens.py +0 -0
- {bt_cli-0.4.56 → bt_cli-0.4.58}/src/bt_cli/pf/commands/user.py +0 -0
- {bt_cli-0.4.56 → bt_cli-0.4.58}/src/bt_cli/pra/__init__.py +0 -0
- {bt_cli-0.4.56 → bt_cli-0.4.58}/src/bt_cli/pra/client/__init__.py +0 -0
- {bt_cli-0.4.56 → bt_cli-0.4.58}/src/bt_cli/pra/client/base.py +0 -0
- {bt_cli-0.4.56 → bt_cli-0.4.58}/src/bt_cli/pra/commands/__init__.py +0 -0
- {bt_cli-0.4.56 → bt_cli-0.4.58}/src/bt_cli/pra/commands/auth.py +0 -0
- {bt_cli-0.4.56 → bt_cli-0.4.58}/src/bt_cli/pra/commands/group_policies.py +0 -0
- {bt_cli-0.4.56 → bt_cli-0.4.58}/src/bt_cli/pra/commands/import_export.py +0 -0
- {bt_cli-0.4.56 → bt_cli-0.4.58}/src/bt_cli/pra/commands/jump_clients.py +0 -0
- {bt_cli-0.4.56 → bt_cli-0.4.58}/src/bt_cli/pra/commands/jump_groups.py +0 -0
- {bt_cli-0.4.56 → bt_cli-0.4.58}/src/bt_cli/pra/commands/jump_items.py +0 -0
- {bt_cli-0.4.56 → bt_cli-0.4.58}/src/bt_cli/pra/commands/jumpoints.py +0 -0
- {bt_cli-0.4.56 → bt_cli-0.4.58}/src/bt_cli/pra/commands/policies.py +0 -0
- {bt_cli-0.4.56 → bt_cli-0.4.58}/src/bt_cli/pra/commands/quick.py +0 -0
- {bt_cli-0.4.56 → bt_cli-0.4.58}/src/bt_cli/pra/commands/teams.py +0 -0
- {bt_cli-0.4.56 → bt_cli-0.4.58}/src/bt_cli/pra/commands/users.py +0 -0
- {bt_cli-0.4.56 → bt_cli-0.4.58}/src/bt_cli/pra/commands/vault.py +0 -0
- {bt_cli-0.4.56 → bt_cli-0.4.58}/src/bt_cli/pra/models/__init__.py +0 -0
- {bt_cli-0.4.56 → bt_cli-0.4.58}/src/bt_cli/pra/models/common.py +0 -0
- {bt_cli-0.4.56 → bt_cli-0.4.58}/src/bt_cli/pra/models/group_policy.py +0 -0
- {bt_cli-0.4.56 → bt_cli-0.4.58}/src/bt_cli/pra/models/jump_client.py +0 -0
- {bt_cli-0.4.56 → bt_cli-0.4.58}/src/bt_cli/pra/models/jump_group.py +0 -0
- {bt_cli-0.4.56 → bt_cli-0.4.58}/src/bt_cli/pra/models/jump_item.py +0 -0
- {bt_cli-0.4.56 → bt_cli-0.4.58}/src/bt_cli/pra/models/jumpoint.py +0 -0
- {bt_cli-0.4.56 → bt_cli-0.4.58}/src/bt_cli/pra/models/team.py +0 -0
- {bt_cli-0.4.56 → bt_cli-0.4.58}/src/bt_cli/pra/models/user.py +0 -0
- {bt_cli-0.4.56 → bt_cli-0.4.58}/src/bt_cli/pra/models/vault.py +0 -0
- {bt_cli-0.4.56 → bt_cli-0.4.58}/src/bt_cli/pws/__init__.py +0 -0
- {bt_cli-0.4.56 → bt_cli-0.4.58}/src/bt_cli/pws/client/__init__.py +0 -0
- {bt_cli-0.4.56 → bt_cli-0.4.58}/src/bt_cli/pws/client/base.py +0 -0
- {bt_cli-0.4.56 → bt_cli-0.4.58}/src/bt_cli/pws/client/beyondinsight.py +0 -0
- {bt_cli-0.4.56 → bt_cli-0.4.58}/src/bt_cli/pws/commands/assets.py +0 -0
- {bt_cli-0.4.56 → bt_cli-0.4.58}/src/bt_cli/pws/commands/attributes.py +0 -0
- {bt_cli-0.4.56 → bt_cli-0.4.58}/src/bt_cli/pws/commands/auth.py +0 -0
- {bt_cli-0.4.56 → bt_cli-0.4.58}/src/bt_cli/pws/commands/clouds.py +0 -0
- {bt_cli-0.4.56 → bt_cli-0.4.58}/src/bt_cli/pws/commands/config.py +0 -0
- {bt_cli-0.4.56 → bt_cli-0.4.58}/src/bt_cli/pws/commands/credentials.py +0 -0
- {bt_cli-0.4.56 → bt_cli-0.4.58}/src/bt_cli/pws/commands/databases.py +0 -0
- {bt_cli-0.4.56 → bt_cli-0.4.58}/src/bt_cli/pws/commands/directories.py +0 -0
- {bt_cli-0.4.56 → bt_cli-0.4.58}/src/bt_cli/pws/commands/functional.py +0 -0
- {bt_cli-0.4.56 → bt_cli-0.4.58}/src/bt_cli/pws/commands/import_export.py +0 -0
- {bt_cli-0.4.56 → bt_cli-0.4.58}/src/bt_cli/pws/commands/platforms.py +0 -0
- {bt_cli-0.4.56 → bt_cli-0.4.58}/src/bt_cli/pws/commands/search.py +0 -0
- {bt_cli-0.4.56 → bt_cli-0.4.58}/src/bt_cli/pws/commands/secrets.py +0 -0
- {bt_cli-0.4.56 → bt_cli-0.4.58}/src/bt_cli/pws/commands/systems.py +0 -0
- {bt_cli-0.4.56 → bt_cli-0.4.58}/src/bt_cli/pws/commands/users.py +0 -0
- {bt_cli-0.4.56 → bt_cli-0.4.58}/src/bt_cli/pws/commands/workgroups.py +0 -0
- {bt_cli-0.4.56 → bt_cli-0.4.58}/src/bt_cli/pws/config.py +0 -0
- {bt_cli-0.4.56 → bt_cli-0.4.58}/src/bt_cli/pws/models/__init__.py +0 -0
- {bt_cli-0.4.56 → bt_cli-0.4.58}/src/bt_cli/pws/models/account.py +0 -0
- {bt_cli-0.4.56 → bt_cli-0.4.58}/src/bt_cli/pws/models/asset.py +0 -0
- {bt_cli-0.4.56 → bt_cli-0.4.58}/src/bt_cli/pws/models/common.py +0 -0
- {bt_cli-0.4.56 → bt_cli-0.4.58}/src/bt_cli/pws/models/system.py +0 -0
- {bt_cli-0.4.56 → bt_cli-0.4.58}/src/bt_cli/secrets/__init__.py +0 -0
- {bt_cli-0.4.56 → bt_cli-0.4.58}/src/bt_cli/secrets/client/__init__.py +0 -0
- {bt_cli-0.4.56 → bt_cli-0.4.58}/src/bt_cli/secrets/client/base.py +0 -0
- {bt_cli-0.4.56 → bt_cli-0.4.58}/src/bt_cli/secrets/commands/__init__.py +0 -0
- {bt_cli-0.4.56 → bt_cli-0.4.58}/src/bt_cli/secrets/commands/_hints.py +0 -0
- {bt_cli-0.4.56 → bt_cli-0.4.58}/src/bt_cli/secrets/commands/auth.py +0 -0
- {bt_cli-0.4.56 → bt_cli-0.4.58}/src/bt_cli/secrets/commands/dynamic.py +0 -0
- {bt_cli-0.4.56 → bt_cli-0.4.58}/src/bt_cli/secrets/commands/folders.py +0 -0
- {bt_cli-0.4.56 → bt_cli-0.4.58}/src/bt_cli/secrets/commands/integrations.py +0 -0
- {bt_cli-0.4.56 → bt_cli-0.4.58}/src/bt_cli/secrets/commands/leases.py +0 -0
- {bt_cli-0.4.56 → bt_cli-0.4.58}/src/bt_cli/secrets/commands/static.py +0 -0
- {bt_cli-0.4.56 → bt_cli-0.4.58}/src/bt_cli/secrets/models/__init__.py +0 -0
- {bt_cli-0.4.56 → bt_cli-0.4.58}/tests/__init__.py +0 -0
- {bt_cli-0.4.56 → bt_cli-0.4.58}/tests/conftest.py +0 -0
- {bt_cli-0.4.56 → bt_cli-0.4.58}/tests/core/__init__.py +0 -0
- {bt_cli-0.4.56 → bt_cli-0.4.58}/tests/core/test_auth.py +0 -0
- {bt_cli-0.4.56 → bt_cli-0.4.58}/tests/core/test_config.py +0 -0
- {bt_cli-0.4.56 → bt_cli-0.4.58}/tests/core/test_config_file.py +0 -0
- {bt_cli-0.4.56 → bt_cli-0.4.58}/tests/core/test_errors.py +0 -0
- {bt_cli-0.4.56 → bt_cli-0.4.58}/tests/core/test_output.py +0 -0
- {bt_cli-0.4.56 → bt_cli-0.4.58}/tests/core/test_rest_debug.py +0 -0
- {bt_cli-0.4.56 → bt_cli-0.4.58}/tests/entitle/__init__.py +0 -0
- {bt_cli-0.4.56 → bt_cli-0.4.58}/tests/entitle/test_client.py +0 -0
- {bt_cli-0.4.56 → bt_cli-0.4.58}/tests/entitle/test_commands.py +0 -0
- {bt_cli-0.4.56 → bt_cli-0.4.58}/tests/entitle-smoke-test.sh +0 -0
- {bt_cli-0.4.56 → bt_cli-0.4.58}/tests/epml/__init__.py +0 -0
- {bt_cli-0.4.56 → bt_cli-0.4.58}/tests/epml/test_client.py +0 -0
- {bt_cli-0.4.56 → bt_cli-0.4.58}/tests/epml/test_commands.py +0 -0
- {bt_cli-0.4.56 → bt_cli-0.4.58}/tests/epmw/__init__.py +0 -0
- {bt_cli-0.4.56 → bt_cli-0.4.58}/tests/epmw/test_client.py +0 -0
- {bt_cli-0.4.56 → bt_cli-0.4.58}/tests/epmw/test_commands.py +0 -0
- {bt_cli-0.4.56 → bt_cli-0.4.58}/tests/epmw-quick-test-plan.md +0 -0
- {bt_cli-0.4.56 → bt_cli-0.4.58}/tests/fixtures/__init__.py +0 -0
- {bt_cli-0.4.56 → bt_cli-0.4.58}/tests/fixtures/responses.py +0 -0
- {bt_cli-0.4.56 → bt_cli-0.4.58}/tests/integration/__init__.py +0 -0
- {bt_cli-0.4.56 → bt_cli-0.4.58}/tests/integration/conftest.py +0 -0
- {bt_cli-0.4.56 → bt_cli-0.4.58}/tests/integration/helpers.py +0 -0
- {bt_cli-0.4.56 → bt_cli-0.4.58}/tests/integration/test_entitle_integration.py +0 -0
- {bt_cli-0.4.56 → bt_cli-0.4.58}/tests/integration/test_epmw_integration.py +0 -0
- {bt_cli-0.4.56 → bt_cli-0.4.58}/tests/integration/test_epmw_lifecycle.py +0 -0
- {bt_cli-0.4.56 → bt_cli-0.4.58}/tests/integration/test_pra_integration.py +0 -0
- {bt_cli-0.4.56 → bt_cli-0.4.58}/tests/integration/test_pra_lifecycle.py +0 -0
- {bt_cli-0.4.56 → bt_cli-0.4.58}/tests/integration/test_pws_integration.py +0 -0
- {bt_cli-0.4.56 → bt_cli-0.4.58}/tests/integration/test_pws_lifecycle.py +0 -0
- {bt_cli-0.4.56 → bt_cli-0.4.58}/tests/pf/__init__.py +0 -0
- {bt_cli-0.4.56 → bt_cli-0.4.58}/tests/pra/__init__.py +0 -0
- {bt_cli-0.4.56 → bt_cli-0.4.58}/tests/pra/test_client.py +0 -0
- {bt_cli-0.4.56 → bt_cli-0.4.58}/tests/pra/test_commands.py +0 -0
- {bt_cli-0.4.56 → bt_cli-0.4.58}/tests/pra-smoke-test.sh +0 -0
- {bt_cli-0.4.56 → bt_cli-0.4.58}/tests/pra-test-plan.md +0 -0
- {bt_cli-0.4.56 → bt_cli-0.4.58}/tests/pws/__init__.py +0 -0
- {bt_cli-0.4.56 → bt_cli-0.4.58}/tests/pws/test_client.py +0 -0
- {bt_cli-0.4.56 → bt_cli-0.4.58}/tests/pws/test_commands.py +0 -0
- {bt_cli-0.4.56 → bt_cli-0.4.58}/tests/pws-quick-test-plan.md +0 -0
- {bt_cli-0.4.56 → bt_cli-0.4.58}/tests/pws-smoke-test.sh +0 -0
- {bt_cli-0.4.56 → bt_cli-0.4.58}/tests/secrets/__init__.py +0 -0
- {bt_cli-0.4.56 → bt_cli-0.4.58}/tests/secrets/test_client.py +0 -0
- {bt_cli-0.4.56 → 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,13 +39,13 @@ 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` |
|
|
46
46
|
| EPM Linux | `bt epml` | `rbp` (cmdgrps/hostgrps/usergrps/tmdategrps/roles/policy/tests/tx), `settings`, `users`, `audit`, `siems`, `quick` |
|
|
47
47
|
| Secrets API | `bt secrets` | `folders`, `static`, `dynamic` (incl. `generate`), `leases`, `integrations` |
|
|
48
|
-
| Pathfinder | `bt pf` | `whoami`, `access`, `pats`, `mcp-tokens`, `machines` |
|
|
48
|
+
| Pathfinder | `bt pf` | `whoami`, `access`, `pats`, `mcp-tokens`, `machines`, `users` (invite/grant/revoke/site membership) |
|
|
49
49
|
|
|
50
50
|
## Common Patterns
|
|
51
51
|
|
|
@@ -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)
|
|
@@ -91,7 +92,7 @@ PASSWORD=$(bt pws quick checkout -s server -a admin --raw)
|
|
|
91
92
|
- **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.
|
|
92
93
|
- **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`.
|
|
93
94
|
- **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
|
|
95
|
+
- **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 GETs (`auth/Users`, `auth/Auditing`, `auth/SAMLProviders`, `auth/*access/org`) are 403 for PAT principals; token create/revoke still needs cookie auth on app.beyondtrust.io. Onboarding/access writes (`UserInvitation*`, `Users/delete`, `productaccess`/`siteaccess` grant/revoke) are deployed on the PAT gateway (`bt pf users ...`) but PAT principals get the same org-level 403 on POST (live-verified 2026-06-11) — commands exit 3 with a cookie-auth hint until the gateway opens up. Details in `src/bt_cli/data/skills/pf/SKILL.md`.
|
|
95
96
|
|
|
96
97
|
## Functional vs Managed Accounts
|
|
97
98
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: pf
|
|
3
|
-
description: Pathfinder platform commands — whoami, site/product access, PAT/MCP token listing, machine registry. Use when working with the Pathfinder portal API (Nomine), platform PATs, site IDs, or registered machine identities.
|
|
3
|
+
description: Pathfinder platform commands — whoami, site/product access, PAT/MCP token listing, machine registry, user invites and product/site access grants. Use when working with the Pathfinder portal API (Nomine), platform PATs, site IDs, user onboarding, or registered machine identities.
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# Pathfinder Platform (`bt pf`)
|
|
@@ -45,6 +45,37 @@ bt pf machines list # registered machine IDs
|
|
|
45
45
|
bt pf machines get <uuid> # one machine's record (secret hash never shown)
|
|
46
46
|
```
|
|
47
47
|
|
|
48
|
+
## User onboarding & access assignment (`bt pf users`)
|
|
49
|
+
|
|
50
|
+
```bash
|
|
51
|
+
# Invite a user to the org (sends a real email; confirms unless --yes).
|
|
52
|
+
# Default site = BT_PF_SITE_ID; -p grants products on that site at invite time.
|
|
53
|
+
bt pf users invite -e new.user@corp.com --first-name New --last-name User \
|
|
54
|
+
-p pra -p epml
|
|
55
|
+
|
|
56
|
+
bt pf users cancel-invite new.user@corp.com # cancel a pending invite
|
|
57
|
+
|
|
58
|
+
# Per-user product access on a site (product slugs are lowercase:
|
|
59
|
+
# epmwm, pra, psc, rs, entitle, epml, insights, secrets)
|
|
60
|
+
bt pf users grant -u <user-uuid> -p epml # default site
|
|
61
|
+
bt pf users revoke -u <user-uuid> -p epml -s <site-uuid>
|
|
62
|
+
|
|
63
|
+
# Site membership (optionally granting products in the same call)
|
|
64
|
+
bt pf users add-to-site -u <user-uuid> -s <site-uuid> -p pra -p psc
|
|
65
|
+
bt pf users remove-from-site -u <user-uuid> -s <site-uuid>
|
|
66
|
+
|
|
67
|
+
bt pf users delete <user-uuid> [...] # bulk delete (org admin; confirms)
|
|
68
|
+
bt pf users list # org-level — 403 over PAT, exits 3 with hint
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
**Write-path caveat (verified 2026-06-11):** all of these routes are
|
|
72
|
+
*deployed* on the PAT gateway (GET returns 405 = POST/DELETE exist), but PAT
|
|
73
|
+
principals are **not authorized** — a live productaccess grant POST returned
|
|
74
|
+
the same empty-body 403 as the org-level GETs. Every `bt pf users` write
|
|
75
|
+
currently exits 3 with the cookie-auth hint. The commands are correct and
|
|
76
|
+
will start working the moment the gateway grants PATs the org-level
|
|
77
|
+
permission (or a cookie-auth mode lands).
|
|
78
|
+
|
|
48
79
|
## What works over PAT vs what doesn't (verified 2026-06-11)
|
|
49
80
|
|
|
50
81
|
| Endpoint | PAT result |
|
|
@@ -52,7 +83,10 @@ bt pf machines get <uuid> # one machine's record (secret hash never shown)
|
|
|
52
83
|
| `auth/UserInfo`, `auth/User/Access`, `auth/User/Token`, `auth/User/McpToken` | 200 — any valid PAT |
|
|
53
84
|
| `auth/Machine`, `auth/Machine/{id}` | 200 — only with a PAT from a product-bearing site (Administration-site PATs get 403) |
|
|
54
85
|
| `auth/Users`, `auth/SAMLProviders`, `auth/Auditing` | 403 — org-level, blocked for PAT principals regardless of scopes seen so far |
|
|
55
|
-
| `auth/
|
|
86
|
+
| `auth/productaccess/org`, `auth/access/org`, `auth/siteaccess/org` | 403 — same org-level block |
|
|
87
|
+
| `auth/productaccess` (collection GET) | 404 — not deployed on the gateway (also 404 on the app host) |
|
|
88
|
+
| `auth/UserInvitation`, `auth/UserInvitation/Product`, `auth/UserInvitation/Cancel`, `auth/Users/delete` | 405 on GET — POST routes deployed, but PAT writes hit the org-level 403 (see below) |
|
|
89
|
+
| `auth/productaccess/{p}/site/{s}/user/{u}`, `auth/siteaccess/{s}/user/{u}` | 405 on GET — routes deployed; live grant POST → empty-body 403 (org-level block applies to writes too) |
|
|
56
90
|
|
|
57
91
|
PAT `access` scopes are the lowercase product IDs of the mint site (visible in
|
|
58
92
|
`bt pf pats list`). The org-level 403s may require a scope no UI-minted PAT
|
|
@@ -70,7 +104,13 @@ but never activated.
|
|
|
70
104
|
|
|
71
105
|
## Not exposed via PAT (cookie-auth only, app.beyondtrust.io/api/auth/*)
|
|
72
106
|
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
SignIn → /session/login flow —
|
|
107
|
+
Confirmed PAT-blocked (403): user listing, auditing, SAML providers, the
|
|
108
|
+
org-default access-rule reads. Still cookie-only: PAT/MCP token creation and
|
|
109
|
+
revocation, invitation Accept. These need the `access` session cookie
|
|
110
|
+
(browser JWT ~5min TTL, or the programmatic SignIn → /session/login flow —
|
|
111
|
+
password-auth users only, not federated).
|
|
112
|
+
|
|
113
|
+
Invite body shape (spec `InviteOrganizationUserInput`): `{email, firstName,
|
|
114
|
+
lastName, siteMetaData: [{siteId, name, residency}], productMetaData:
|
|
115
|
+
[{name, siteId, siteName}]}` — `bt pf users invite` builds this, defaulting
|
|
116
|
+
the site to `BT_PF_SITE_ID` and omitting name/residency unless given.
|
|
@@ -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
|
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
"""Pathfinder platform API client (PAT bearer auth, /site/<id>/platform/auth/... URLs)."""
|
|
2
|
+
|
|
3
|
+
import logging
|
|
4
|
+
from typing import Any, Dict, Optional
|
|
5
|
+
from urllib.parse import quote as _urlquote
|
|
6
|
+
|
|
7
|
+
import httpx
|
|
8
|
+
|
|
9
|
+
from bt_cli.core.client import _warn_ssl_disabled
|
|
10
|
+
from bt_cli.core.config import PFConfig, load_pf_config
|
|
11
|
+
from bt_cli.core.rest_debug import get_event_hooks
|
|
12
|
+
|
|
13
|
+
logger = logging.getLogger(__name__)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class PFClient:
|
|
17
|
+
"""Client for the Pathfinder platform API (internal name: Nomine).
|
|
18
|
+
|
|
19
|
+
URL composition:
|
|
20
|
+
|
|
21
|
+
{api_url}/site/{site_id}/platform/auth/<path>
|
|
22
|
+
|
|
23
|
+
The Nomine swagger writes paths as `/api/auth/X`; on the gateway the
|
|
24
|
+
product segment is `platform` and the path keeps the `auth/` prefix —
|
|
25
|
+
so spec `/api/auth/UserInfo` is gateway `/site/<id>/platform/auth/UserInfo`.
|
|
26
|
+
|
|
27
|
+
Auth: `Authorization: Bearer <PAT>`. PATs are site-bound — minted while
|
|
28
|
+
a site is active in the portal, valid only for that site's paths
|
|
29
|
+
(`401 Access denied for this site` elsewhere). A just-minted PAT can
|
|
30
|
+
take ~1s to propagate to the edge validator (transient
|
|
31
|
+
`401 Personal access token not found`).
|
|
32
|
+
|
|
33
|
+
Gateway-blocked for PAT principals (verified 2026-06-11): the entire
|
|
34
|
+
org-admin surface — reads (`auth/Users`, `auth/SAMLProviders`,
|
|
35
|
+
`auth/Auditing`, `auth/*access/org`) AND writes (`auth/UserInvitation*`,
|
|
36
|
+
`auth/Users/delete`, the productaccess/siteaccess grant/revoke routes).
|
|
37
|
+
The block's signature is an empty-body 403; the routes themselves are
|
|
38
|
+
deployed (GET returns 405). `auth/Machine` works only with a PAT for a
|
|
39
|
+
product-bearing site (not the Administration site).
|
|
40
|
+
"""
|
|
41
|
+
|
|
42
|
+
def __init__(self, config: PFConfig):
|
|
43
|
+
self.config = config
|
|
44
|
+
self.base_url = config.api_url.rstrip("/")
|
|
45
|
+
self.site_id = config.site_id
|
|
46
|
+
self._base = f"{self.base_url}/site/{self.site_id}/platform"
|
|
47
|
+
|
|
48
|
+
if not config.verify_ssl:
|
|
49
|
+
_warn_ssl_disabled()
|
|
50
|
+
|
|
51
|
+
self._client = httpx.Client(
|
|
52
|
+
verify=config.verify_ssl,
|
|
53
|
+
timeout=config.timeout,
|
|
54
|
+
event_hooks=get_event_hooks(),
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
# ---- lifecycle ------------------------------------------------------
|
|
58
|
+
|
|
59
|
+
def close(self) -> None:
|
|
60
|
+
self._client.close()
|
|
61
|
+
|
|
62
|
+
def __enter__(self) -> "PFClient":
|
|
63
|
+
return self
|
|
64
|
+
|
|
65
|
+
def __exit__(self, *args: Any) -> None:
|
|
66
|
+
self.close()
|
|
67
|
+
|
|
68
|
+
# ---- low-level ------------------------------------------------------
|
|
69
|
+
|
|
70
|
+
@staticmethod
|
|
71
|
+
def _q(value: Any) -> str:
|
|
72
|
+
"""URL-quote a path segment to prevent traversal / query injection."""
|
|
73
|
+
return _urlquote(str(value), safe="")
|
|
74
|
+
|
|
75
|
+
def _url(self, path: str) -> str:
|
|
76
|
+
if not path.startswith("/"):
|
|
77
|
+
path = "/" + path
|
|
78
|
+
return f"{self._base}{path}"
|
|
79
|
+
|
|
80
|
+
def _headers(self) -> Dict[str, str]:
|
|
81
|
+
return {
|
|
82
|
+
"Authorization": f"Bearer {self.config.pat}",
|
|
83
|
+
"Accept": "application/json",
|
|
84
|
+
"Content-Type": "application/json",
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
def get(self, path: str, params: Optional[Dict[str, Any]] = None) -> Any:
|
|
88
|
+
"""GET a platform path. Returns parsed JSON (None for empty bodies)."""
|
|
89
|
+
response = self._client.get(self._url(path), headers=self._headers(), params=params)
|
|
90
|
+
response.raise_for_status()
|
|
91
|
+
return self._parse(response)
|
|
92
|
+
|
|
93
|
+
def post(self, path: str, json: Any = None) -> Any:
|
|
94
|
+
"""POST a platform path. Returns parsed JSON (None for empty bodies)."""
|
|
95
|
+
response = self._client.post(self._url(path), headers=self._headers(), json=json)
|
|
96
|
+
response.raise_for_status()
|
|
97
|
+
return self._parse(response)
|
|
98
|
+
|
|
99
|
+
def delete(self, path: str) -> Any:
|
|
100
|
+
"""DELETE a platform path. Returns parsed JSON (None for empty bodies)."""
|
|
101
|
+
response = self._client.delete(self._url(path), headers=self._headers())
|
|
102
|
+
response.raise_for_status()
|
|
103
|
+
return self._parse(response)
|
|
104
|
+
|
|
105
|
+
@staticmethod
|
|
106
|
+
def _parse(response: httpx.Response) -> Any:
|
|
107
|
+
if not response.content:
|
|
108
|
+
return None
|
|
109
|
+
try:
|
|
110
|
+
return response.json()
|
|
111
|
+
except ValueError:
|
|
112
|
+
return response.text
|
|
113
|
+
|
|
114
|
+
# ---- verified endpoints ----------------------------------------------
|
|
115
|
+
|
|
116
|
+
def get_user_info(self) -> Dict[str, Any]:
|
|
117
|
+
"""GET /auth/UserInfo — the PAT owner's profile.
|
|
118
|
+
|
|
119
|
+
Returns `{sub, given_name, family_name, email, role, groups,
|
|
120
|
+
isFederated, mfaEnabled}`.
|
|
121
|
+
"""
|
|
122
|
+
return self.get("/auth/UserInfo")
|
|
123
|
+
|
|
124
|
+
def get_user_access(self) -> Dict[str, Any]:
|
|
125
|
+
"""GET /auth/User/Access — `{siteAccessRules: [...], productAccessRules: [...]}`."""
|
|
126
|
+
return self.get("/auth/User/Access")
|
|
127
|
+
|
|
128
|
+
def list_pats(self) -> Dict[str, Any]:
|
|
129
|
+
"""GET /auth/User/Token — `{personalAccessTokens: [...], success, ...}`."""
|
|
130
|
+
return self.get("/auth/User/Token")
|
|
131
|
+
|
|
132
|
+
def list_mcp_tokens(self) -> Dict[str, Any]:
|
|
133
|
+
"""GET /auth/User/McpToken — `{mcpTokens: [...], success, ...}`."""
|
|
134
|
+
return self.get("/auth/User/McpToken")
|
|
135
|
+
|
|
136
|
+
def list_machines(self) -> Dict[str, Any]:
|
|
137
|
+
"""GET /auth/Machine — `{machines: [uuid, ...], success, ...}`.
|
|
138
|
+
|
|
139
|
+
Only reachable with a PAT minted on a product-bearing site; an
|
|
140
|
+
Administration-site PAT gets 403.
|
|
141
|
+
"""
|
|
142
|
+
return self.get("/auth/Machine")
|
|
143
|
+
|
|
144
|
+
def get_machine(self, machine_id: str) -> Dict[str, Any]:
|
|
145
|
+
"""GET /auth/Machine/{id} — details for one registered machine."""
|
|
146
|
+
return self.get(f"/auth/Machine/{self._q(machine_id)}")
|
|
147
|
+
|
|
148
|
+
# ---- users & onboarding ----------------------------------------------
|
|
149
|
+
#
|
|
150
|
+
# Route status over PAT (probed 2026-06-11): the invitation POSTs,
|
|
151
|
+
# Users/delete POST, and the productaccess/siteaccess grant/revoke
|
|
152
|
+
# routes all exist (GET returns 405), but PAT principals are NOT
|
|
153
|
+
# authorized — a live productaccess grant POST returned the same
|
|
154
|
+
# empty-body 403 as the org-level GETs (`auth/Users`,
|
|
155
|
+
# `auth/productaccess/org`, `auth/access/org`, `auth/siteaccess/org`).
|
|
156
|
+
# The bare `auth/productaccess` collection GET is 404 (not deployed).
|
|
157
|
+
# These methods stay wired for cookie-auth support or a future gateway
|
|
158
|
+
# policy change; commands surface the 403 as exit 3 with a hint.
|
|
159
|
+
|
|
160
|
+
def list_users(self, params: Optional[Dict[str, Any]] = None) -> Any:
|
|
161
|
+
"""GET /auth/Users — org user list (plain array).
|
|
162
|
+
|
|
163
|
+
Filters: email, firstName, lastName, active, siteId, userId,
|
|
164
|
+
mfaEnabled, page (1-based), pageSize. 403 for PAT principals as of
|
|
165
|
+
2026-06-11 — works over cookie auth on app.beyondtrust.io.
|
|
166
|
+
"""
|
|
167
|
+
return self.get("/auth/Users", params=params)
|
|
168
|
+
|
|
169
|
+
def invite_user(self, payload: Dict[str, Any]) -> Dict[str, Any]:
|
|
170
|
+
"""POST /auth/UserInvitation — invite a user to the organization.
|
|
171
|
+
|
|
172
|
+
Payload: `{email, firstName, lastName, siteMetaData: [{siteId,
|
|
173
|
+
name, residency}], productMetaData: [{name, siteId, siteName}]}`.
|
|
174
|
+
Sends a real invitation email.
|
|
175
|
+
"""
|
|
176
|
+
return self.post("/auth/UserInvitation", json=payload)
|
|
177
|
+
|
|
178
|
+
def cancel_invitation(self, email: str) -> Dict[str, Any]:
|
|
179
|
+
"""POST /auth/UserInvitation/Cancel — cancel a pending invite."""
|
|
180
|
+
return self.post("/auth/UserInvitation/Cancel", json={"email": email})
|
|
181
|
+
|
|
182
|
+
def delete_users(self, user_ids: list) -> Dict[str, Any]:
|
|
183
|
+
"""POST /auth/Users/delete — bulk-delete platform users (org admin)."""
|
|
184
|
+
return self.post("/auth/Users/delete", json={"userIds": list(user_ids)})
|
|
185
|
+
|
|
186
|
+
def grant_product_access(self, product_id: str, site_id: str, user_id: str) -> Dict[str, Any]:
|
|
187
|
+
"""POST /auth/productaccess/{productId}/site/{siteId}/user/{userId}.
|
|
188
|
+
|
|
189
|
+
Product IDs are lowercase slugs (epmwm, pra, psc, rs, entitle,
|
|
190
|
+
epml, insights, secrets); normalized here so the contract holds
|
|
191
|
+
for every caller.
|
|
192
|
+
"""
|
|
193
|
+
return self.post(
|
|
194
|
+
f"/auth/productaccess/{self._q(str(product_id).lower())}/site/{self._q(site_id)}"
|
|
195
|
+
f"/user/{self._q(user_id)}"
|
|
196
|
+
)
|
|
197
|
+
|
|
198
|
+
def revoke_product_access(self, product_id: str, site_id: str, user_id: str) -> Dict[str, Any]:
|
|
199
|
+
"""DELETE /auth/productaccess/{productId}/site/{siteId}/user/{userId}."""
|
|
200
|
+
return self.delete(
|
|
201
|
+
f"/auth/productaccess/{self._q(str(product_id).lower())}/site/{self._q(site_id)}"
|
|
202
|
+
f"/user/{self._q(user_id)}"
|
|
203
|
+
)
|
|
204
|
+
|
|
205
|
+
def grant_site_access(self, site_id: str, user_id: str,
|
|
206
|
+
product_ids: Optional[list] = None) -> Dict[str, Any]:
|
|
207
|
+
"""POST /auth/siteaccess/{siteId}/user/{userId} — add a user to a site.
|
|
208
|
+
|
|
209
|
+
Optional body: array of lowercase product slugs to grant on that
|
|
210
|
+
site too (the spec marks the body itself optional).
|
|
211
|
+
"""
|
|
212
|
+
return self.post(
|
|
213
|
+
f"/auth/siteaccess/{self._q(site_id)}/user/{self._q(user_id)}",
|
|
214
|
+
json=[str(p).lower() for p in product_ids] if product_ids else None,
|
|
215
|
+
)
|
|
216
|
+
|
|
217
|
+
def revoke_site_access(self, site_id: str, user_id: str) -> Dict[str, Any]:
|
|
218
|
+
"""DELETE /auth/siteaccess/{siteId}/user/{userId} — remove a user from a site."""
|
|
219
|
+
return self.delete(f"/auth/siteaccess/{self._q(site_id)}/user/{self._q(user_id)}")
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
def get_client() -> PFClient:
|
|
223
|
+
"""Get a Pathfinder client with configuration from environment / config file."""
|
|
224
|
+
config = load_pf_config()
|
|
225
|
+
return PFClient(config)
|
|
@@ -7,6 +7,7 @@ from .machines import app as machines_app
|
|
|
7
7
|
from .tokens import mcp_tokens_app, pats_app
|
|
8
8
|
from .user import access as access_command
|
|
9
9
|
from .user import whoami as whoami_command
|
|
10
|
+
from .users import users_app
|
|
10
11
|
|
|
11
12
|
app = typer.Typer(
|
|
12
13
|
name="pf",
|
|
@@ -18,6 +19,7 @@ app.add_typer(auth_app, name="auth")
|
|
|
18
19
|
app.add_typer(pats_app, name="pats")
|
|
19
20
|
app.add_typer(mcp_tokens_app, name="mcp-tokens")
|
|
20
21
|
app.add_typer(machines_app, name="machines")
|
|
22
|
+
app.add_typer(users_app, name="users")
|
|
21
23
|
|
|
22
24
|
app.command("whoami")(whoami_command)
|
|
23
25
|
app.command("access")(access_command)
|