bt-cli 0.4.67__tar.gz → 0.4.69__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.67 → bt_cli-0.4.69}/CLAUDE.md +1 -1
- {bt_cli-0.4.67 → bt_cli-0.4.69}/PKG-INFO +1 -1
- {bt_cli-0.4.67 → bt_cli-0.4.69}/pyproject.toml +1 -1
- {bt_cli-0.4.67 → bt_cli-0.4.69}/src/bt_cli/__init__.py +1 -1
- {bt_cli-0.4.67 → bt_cli-0.4.69}/src/bt_cli/data/skills/pws/SKILL.md +26 -0
- bt_cli-0.4.69/src/bt_cli/data/skills/session-analysis/SKILL.md +72 -0
- {bt_cli-0.4.67 → bt_cli-0.4.69}/src/bt_cli/pws/commands/sessions.py +78 -2
- bt_cli-0.4.67/src/bt_cli/data/skills/session-analysis/SKILL.md +0 -64
- {bt_cli-0.4.67 → bt_cli-0.4.69}/.claude/skills/bt/SKILL.md +0 -0
- {bt_cli-0.4.67 → bt_cli-0.4.69}/.claude/skills/entitle/SKILL.md +0 -0
- {bt_cli-0.4.67 → bt_cli-0.4.69}/.claude/skills/epml/SKILL.md +0 -0
- {bt_cli-0.4.67 → bt_cli-0.4.69}/.claude/skills/epmw/SKILL.md +0 -0
- {bt_cli-0.4.67 → bt_cli-0.4.69}/.claude/skills/pra/SKILL.md +0 -0
- {bt_cli-0.4.67 → bt_cli-0.4.69}/.claude/skills/pws/SKILL.md +0 -0
- {bt_cli-0.4.67 → bt_cli-0.4.69}/.env.example +0 -0
- {bt_cli-0.4.67 → bt_cli-0.4.69}/.github/workflows/ci.yml +0 -0
- {bt_cli-0.4.67 → bt_cli-0.4.69}/.github/workflows/release.yml +0 -0
- {bt_cli-0.4.67 → bt_cli-0.4.69}/.gitignore +0 -0
- {bt_cli-0.4.67 → bt_cli-0.4.69}/README.md +0 -0
- {bt_cli-0.4.67 → bt_cli-0.4.69}/assets/cli-help.png +0 -0
- {bt_cli-0.4.67 → bt_cli-0.4.69}/assets/cli-output.png +0 -0
- {bt_cli-0.4.67 → bt_cli-0.4.69}/bt-cli.spec +0 -0
- {bt_cli-0.4.67 → bt_cli-0.4.69}/bt_entry.py +0 -0
- {bt_cli-0.4.67 → bt_cli-0.4.69}/epml-clients-server-side-filters-plan.md +0 -0
- {bt_cli-0.4.67 → bt_cli-0.4.69}/epml-implementation-plan.md +0 -0
- {bt_cli-0.4.67 → bt_cli-0.4.69}/pf-implementation-plan.md +0 -0
- {bt_cli-0.4.67 → bt_cli-0.4.69}/scripts/bt_entry.py +0 -0
- {bt_cli-0.4.67 → bt_cli-0.4.69}/scripts/pf_onboard.py +0 -0
- {bt_cli-0.4.67 → bt_cli-0.4.69}/scripts/sync-package-data.sh +0 -0
- {bt_cli-0.4.67 → bt_cli-0.4.69}/src/bt_cli/cli.py +0 -0
- {bt_cli-0.4.67 → bt_cli-0.4.69}/src/bt_cli/commands/__init__.py +0 -0
- {bt_cli-0.4.67 → bt_cli-0.4.69}/src/bt_cli/commands/configure.py +0 -0
- {bt_cli-0.4.67 → bt_cli-0.4.69}/src/bt_cli/commands/learn.py +0 -0
- {bt_cli-0.4.67 → bt_cli-0.4.69}/src/bt_cli/commands/quick.py +0 -0
- {bt_cli-0.4.67 → bt_cli-0.4.69}/src/bt_cli/core/__init__.py +0 -0
- {bt_cli-0.4.67 → bt_cli-0.4.69}/src/bt_cli/core/auth.py +0 -0
- {bt_cli-0.4.67 → bt_cli-0.4.69}/src/bt_cli/core/client.py +0 -0
- {bt_cli-0.4.67 → bt_cli-0.4.69}/src/bt_cli/core/config.py +0 -0
- {bt_cli-0.4.67 → bt_cli-0.4.69}/src/bt_cli/core/config_file.py +0 -0
- {bt_cli-0.4.67 → bt_cli-0.4.69}/src/bt_cli/core/csv_utils.py +0 -0
- {bt_cli-0.4.67 → bt_cli-0.4.69}/src/bt_cli/core/errors.py +0 -0
- {bt_cli-0.4.67 → bt_cli-0.4.69}/src/bt_cli/core/output.py +0 -0
- {bt_cli-0.4.67 → bt_cli-0.4.69}/src/bt_cli/core/prompts.py +0 -0
- {bt_cli-0.4.67 → bt_cli-0.4.69}/src/bt_cli/core/rest_debug.py +0 -0
- {bt_cli-0.4.67 → bt_cli-0.4.69}/src/bt_cli/data/CLAUDE.md +0 -0
- {bt_cli-0.4.67 → bt_cli-0.4.69}/src/bt_cli/data/__init__.py +0 -0
- {bt_cli-0.4.67 → bt_cli-0.4.69}/src/bt_cli/data/skills/bt/SKILL.md +0 -0
- {bt_cli-0.4.67 → bt_cli-0.4.69}/src/bt_cli/data/skills/entitle/SKILL.md +0 -0
- {bt_cli-0.4.67 → bt_cli-0.4.69}/src/bt_cli/data/skills/epml/SKILL.md +0 -0
- {bt_cli-0.4.67 → bt_cli-0.4.69}/src/bt_cli/data/skills/epmw/SKILL.md +0 -0
- {bt_cli-0.4.67 → bt_cli-0.4.69}/src/bt_cli/data/skills/pf/SKILL.md +0 -0
- {bt_cli-0.4.67 → bt_cli-0.4.69}/src/bt_cli/data/skills/pra/SKILL.md +0 -0
- {bt_cli-0.4.67 → bt_cli-0.4.69}/src/bt_cli/data/skills/secrets/SKILL.md +0 -0
- {bt_cli-0.4.67 → bt_cli-0.4.69}/src/bt_cli/entitle/__init__.py +0 -0
- {bt_cli-0.4.67 → bt_cli-0.4.69}/src/bt_cli/entitle/client/__init__.py +0 -0
- {bt_cli-0.4.67 → bt_cli-0.4.69}/src/bt_cli/entitle/client/base.py +0 -0
- {bt_cli-0.4.67 → bt_cli-0.4.69}/src/bt_cli/entitle/commands/__init__.py +0 -0
- {bt_cli-0.4.67 → bt_cli-0.4.69}/src/bt_cli/entitle/commands/accounts.py +0 -0
- {bt_cli-0.4.67 → bt_cli-0.4.69}/src/bt_cli/entitle/commands/applications.py +0 -0
- {bt_cli-0.4.67 → bt_cli-0.4.69}/src/bt_cli/entitle/commands/auth.py +0 -0
- {bt_cli-0.4.67 → bt_cli-0.4.69}/src/bt_cli/entitle/commands/bundles.py +0 -0
- {bt_cli-0.4.67 → bt_cli-0.4.69}/src/bt_cli/entitle/commands/integrations.py +0 -0
- {bt_cli-0.4.67 → bt_cli-0.4.69}/src/bt_cli/entitle/commands/permissions.py +0 -0
- {bt_cli-0.4.67 → bt_cli-0.4.69}/src/bt_cli/entitle/commands/policies.py +0 -0
- {bt_cli-0.4.67 → bt_cli-0.4.69}/src/bt_cli/entitle/commands/requests.py +0 -0
- {bt_cli-0.4.67 → bt_cli-0.4.69}/src/bt_cli/entitle/commands/resources.py +0 -0
- {bt_cli-0.4.67 → bt_cli-0.4.69}/src/bt_cli/entitle/commands/roles.py +0 -0
- {bt_cli-0.4.67 → bt_cli-0.4.69}/src/bt_cli/entitle/commands/users.py +0 -0
- {bt_cli-0.4.67 → bt_cli-0.4.69}/src/bt_cli/entitle/commands/workflows.py +0 -0
- {bt_cli-0.4.67 → bt_cli-0.4.69}/src/bt_cli/entitle/models/__init__.py +0 -0
- {bt_cli-0.4.67 → bt_cli-0.4.69}/src/bt_cli/entitle/models/bundle.py +0 -0
- {bt_cli-0.4.67 → bt_cli-0.4.69}/src/bt_cli/entitle/models/common.py +0 -0
- {bt_cli-0.4.67 → bt_cli-0.4.69}/src/bt_cli/entitle/models/integration.py +0 -0
- {bt_cli-0.4.67 → bt_cli-0.4.69}/src/bt_cli/entitle/models/permission.py +0 -0
- {bt_cli-0.4.67 → bt_cli-0.4.69}/src/bt_cli/entitle/models/policy.py +0 -0
- {bt_cli-0.4.67 → bt_cli-0.4.69}/src/bt_cli/entitle/models/resource.py +0 -0
- {bt_cli-0.4.67 → bt_cli-0.4.69}/src/bt_cli/entitle/models/role.py +0 -0
- {bt_cli-0.4.67 → bt_cli-0.4.69}/src/bt_cli/entitle/models/user.py +0 -0
- {bt_cli-0.4.67 → bt_cli-0.4.69}/src/bt_cli/entitle/models/workflow.py +0 -0
- {bt_cli-0.4.67 → bt_cli-0.4.69}/src/bt_cli/epml/__init__.py +0 -0
- {bt_cli-0.4.67 → bt_cli-0.4.69}/src/bt_cli/epml/client/__init__.py +0 -0
- {bt_cli-0.4.67 → bt_cli-0.4.69}/src/bt_cli/epml/client/base.py +0 -0
- {bt_cli-0.4.67 → bt_cli-0.4.69}/src/bt_cli/epml/commands/__init__.py +0 -0
- {bt_cli-0.4.67 → bt_cli-0.4.69}/src/bt_cli/epml/commands/audit.py +0 -0
- {bt_cli-0.4.67 → bt_cli-0.4.69}/src/bt_cli/epml/commands/auth.py +0 -0
- {bt_cli-0.4.67 → bt_cli-0.4.69}/src/bt_cli/epml/commands/client_pkg.py +0 -0
- {bt_cli-0.4.67 → bt_cli-0.4.69}/src/bt_cli/epml/commands/clients.py +0 -0
- {bt_cli-0.4.67 → bt_cli-0.4.69}/src/bt_cli/epml/commands/external_apis.py +0 -0
- {bt_cli-0.4.67 → bt_cli-0.4.69}/src/bt_cli/epml/commands/hosts.py +0 -0
- {bt_cli-0.4.67 → bt_cli-0.4.69}/src/bt_cli/epml/commands/iolog.py +0 -0
- {bt_cli-0.4.67 → bt_cli-0.4.69}/src/bt_cli/epml/commands/license.py +0 -0
- {bt_cli-0.4.67 → bt_cli-0.4.69}/src/bt_cli/epml/commands/quick.py +0 -0
- {bt_cli-0.4.67 → bt_cli-0.4.69}/src/bt_cli/epml/commands/rbp_cmdgrps.py +0 -0
- {bt_cli-0.4.67 → bt_cli-0.4.69}/src/bt_cli/epml/commands/rbp_entitlement.py +0 -0
- {bt_cli-0.4.67 → bt_cli-0.4.69}/src/bt_cli/epml/commands/rbp_hostgrps.py +0 -0
- {bt_cli-0.4.67 → bt_cli-0.4.69}/src/bt_cli/epml/commands/rbp_policy.py +0 -0
- {bt_cli-0.4.67 → bt_cli-0.4.69}/src/bt_cli/epml/commands/rbp_roles.py +0 -0
- {bt_cli-0.4.67 → bt_cli-0.4.69}/src/bt_cli/epml/commands/rbp_tests.py +0 -0
- {bt_cli-0.4.67 → bt_cli-0.4.69}/src/bt_cli/epml/commands/rbp_tmdategrps.py +0 -0
- {bt_cli-0.4.67 → bt_cli-0.4.69}/src/bt_cli/epml/commands/rbp_tx.py +0 -0
- {bt_cli-0.4.67 → bt_cli-0.4.69}/src/bt_cli/epml/commands/rbp_usergrps.py +0 -0
- {bt_cli-0.4.67 → bt_cli-0.4.69}/src/bt_cli/epml/commands/settings.py +0 -0
- {bt_cli-0.4.67 → bt_cli-0.4.69}/src/bt_cli/epml/commands/siems.py +0 -0
- {bt_cli-0.4.67 → bt_cli-0.4.69}/src/bt_cli/epml/commands/users.py +0 -0
- {bt_cli-0.4.67 → bt_cli-0.4.69}/src/bt_cli/epml/models/__init__.py +0 -0
- {bt_cli-0.4.67 → bt_cli-0.4.69}/src/bt_cli/epmw/__init__.py +0 -0
- {bt_cli-0.4.67 → bt_cli-0.4.69}/src/bt_cli/epmw/client/__init__.py +0 -0
- {bt_cli-0.4.67 → bt_cli-0.4.69}/src/bt_cli/epmw/client/base.py +0 -0
- {bt_cli-0.4.67 → bt_cli-0.4.69}/src/bt_cli/epmw/commands/__init__.py +0 -0
- {bt_cli-0.4.67 → bt_cli-0.4.69}/src/bt_cli/epmw/commands/audits.py +0 -0
- {bt_cli-0.4.67 → bt_cli-0.4.69}/src/bt_cli/epmw/commands/auth.py +0 -0
- {bt_cli-0.4.67 → bt_cli-0.4.69}/src/bt_cli/epmw/commands/computers.py +0 -0
- {bt_cli-0.4.67 → bt_cli-0.4.69}/src/bt_cli/epmw/commands/events.py +0 -0
- {bt_cli-0.4.67 → bt_cli-0.4.69}/src/bt_cli/epmw/commands/groups.py +0 -0
- {bt_cli-0.4.67 → bt_cli-0.4.69}/src/bt_cli/epmw/commands/policies.py +0 -0
- {bt_cli-0.4.67 → bt_cli-0.4.69}/src/bt_cli/epmw/commands/quick.py +0 -0
- {bt_cli-0.4.67 → bt_cli-0.4.69}/src/bt_cli/epmw/commands/requests.py +0 -0
- {bt_cli-0.4.67 → bt_cli-0.4.69}/src/bt_cli/epmw/commands/roles.py +0 -0
- {bt_cli-0.4.67 → bt_cli-0.4.69}/src/bt_cli/epmw/commands/tasks.py +0 -0
- {bt_cli-0.4.67 → bt_cli-0.4.69}/src/bt_cli/epmw/commands/users.py +0 -0
- {bt_cli-0.4.67 → bt_cli-0.4.69}/src/bt_cli/epmw/models/__init__.py +0 -0
- {bt_cli-0.4.67 → bt_cli-0.4.69}/src/bt_cli/pf/__init__.py +0 -0
- {bt_cli-0.4.67 → bt_cli-0.4.69}/src/bt_cli/pf/client/__init__.py +0 -0
- {bt_cli-0.4.67 → bt_cli-0.4.69}/src/bt_cli/pf/client/base.py +0 -0
- {bt_cli-0.4.67 → bt_cli-0.4.69}/src/bt_cli/pf/commands/__init__.py +0 -0
- {bt_cli-0.4.67 → bt_cli-0.4.69}/src/bt_cli/pf/commands/auth.py +0 -0
- {bt_cli-0.4.67 → bt_cli-0.4.69}/src/bt_cli/pf/commands/machines.py +0 -0
- {bt_cli-0.4.67 → bt_cli-0.4.69}/src/bt_cli/pf/commands/tokens.py +0 -0
- {bt_cli-0.4.67 → bt_cli-0.4.69}/src/bt_cli/pf/commands/user.py +0 -0
- {bt_cli-0.4.67 → bt_cli-0.4.69}/src/bt_cli/pf/commands/users.py +0 -0
- {bt_cli-0.4.67 → bt_cli-0.4.69}/src/bt_cli/pra/__init__.py +0 -0
- {bt_cli-0.4.67 → bt_cli-0.4.69}/src/bt_cli/pra/client/__init__.py +0 -0
- {bt_cli-0.4.67 → bt_cli-0.4.69}/src/bt_cli/pra/client/base.py +0 -0
- {bt_cli-0.4.67 → bt_cli-0.4.69}/src/bt_cli/pra/commands/__init__.py +0 -0
- {bt_cli-0.4.67 → bt_cli-0.4.69}/src/bt_cli/pra/commands/auth.py +0 -0
- {bt_cli-0.4.67 → bt_cli-0.4.69}/src/bt_cli/pra/commands/group_policies.py +0 -0
- {bt_cli-0.4.67 → bt_cli-0.4.69}/src/bt_cli/pra/commands/import_export.py +0 -0
- {bt_cli-0.4.67 → bt_cli-0.4.69}/src/bt_cli/pra/commands/jump_clients.py +0 -0
- {bt_cli-0.4.67 → bt_cli-0.4.69}/src/bt_cli/pra/commands/jump_groups.py +0 -0
- {bt_cli-0.4.67 → bt_cli-0.4.69}/src/bt_cli/pra/commands/jump_items.py +0 -0
- {bt_cli-0.4.67 → bt_cli-0.4.69}/src/bt_cli/pra/commands/jumpoints.py +0 -0
- {bt_cli-0.4.67 → bt_cli-0.4.69}/src/bt_cli/pra/commands/policies.py +0 -0
- {bt_cli-0.4.67 → bt_cli-0.4.69}/src/bt_cli/pra/commands/quick.py +0 -0
- {bt_cli-0.4.67 → bt_cli-0.4.69}/src/bt_cli/pra/commands/teams.py +0 -0
- {bt_cli-0.4.67 → bt_cli-0.4.69}/src/bt_cli/pra/commands/users.py +0 -0
- {bt_cli-0.4.67 → bt_cli-0.4.69}/src/bt_cli/pra/commands/vault.py +0 -0
- {bt_cli-0.4.67 → bt_cli-0.4.69}/src/bt_cli/pra/models/__init__.py +0 -0
- {bt_cli-0.4.67 → bt_cli-0.4.69}/src/bt_cli/pra/models/common.py +0 -0
- {bt_cli-0.4.67 → bt_cli-0.4.69}/src/bt_cli/pra/models/group_policy.py +0 -0
- {bt_cli-0.4.67 → bt_cli-0.4.69}/src/bt_cli/pra/models/jump_client.py +0 -0
- {bt_cli-0.4.67 → bt_cli-0.4.69}/src/bt_cli/pra/models/jump_group.py +0 -0
- {bt_cli-0.4.67 → bt_cli-0.4.69}/src/bt_cli/pra/models/jump_item.py +0 -0
- {bt_cli-0.4.67 → bt_cli-0.4.69}/src/bt_cli/pra/models/jumpoint.py +0 -0
- {bt_cli-0.4.67 → bt_cli-0.4.69}/src/bt_cli/pra/models/team.py +0 -0
- {bt_cli-0.4.67 → bt_cli-0.4.69}/src/bt_cli/pra/models/user.py +0 -0
- {bt_cli-0.4.67 → bt_cli-0.4.69}/src/bt_cli/pra/models/vault.py +0 -0
- {bt_cli-0.4.67 → bt_cli-0.4.69}/src/bt_cli/pws/__init__.py +0 -0
- {bt_cli-0.4.67 → bt_cli-0.4.69}/src/bt_cli/pws/client/__init__.py +0 -0
- {bt_cli-0.4.67 → bt_cli-0.4.69}/src/bt_cli/pws/client/base.py +0 -0
- {bt_cli-0.4.67 → bt_cli-0.4.69}/src/bt_cli/pws/client/beyondinsight.py +0 -0
- {bt_cli-0.4.67 → bt_cli-0.4.69}/src/bt_cli/pws/client/passwordsafe.py +0 -0
- {bt_cli-0.4.67 → bt_cli-0.4.69}/src/bt_cli/pws/commands/__init__.py +0 -0
- {bt_cli-0.4.67 → bt_cli-0.4.69}/src/bt_cli/pws/commands/accounts.py +0 -0
- {bt_cli-0.4.67 → bt_cli-0.4.69}/src/bt_cli/pws/commands/assets.py +0 -0
- {bt_cli-0.4.67 → bt_cli-0.4.69}/src/bt_cli/pws/commands/attributes.py +0 -0
- {bt_cli-0.4.67 → bt_cli-0.4.69}/src/bt_cli/pws/commands/audit.py +0 -0
- {bt_cli-0.4.67 → bt_cli-0.4.69}/src/bt_cli/pws/commands/auth.py +0 -0
- {bt_cli-0.4.67 → bt_cli-0.4.69}/src/bt_cli/pws/commands/clouds.py +0 -0
- {bt_cli-0.4.67 → bt_cli-0.4.69}/src/bt_cli/pws/commands/config.py +0 -0
- {bt_cli-0.4.67 → bt_cli-0.4.69}/src/bt_cli/pws/commands/credentials.py +0 -0
- {bt_cli-0.4.67 → bt_cli-0.4.69}/src/bt_cli/pws/commands/databases.py +0 -0
- {bt_cli-0.4.67 → bt_cli-0.4.69}/src/bt_cli/pws/commands/directories.py +0 -0
- {bt_cli-0.4.67 → bt_cli-0.4.69}/src/bt_cli/pws/commands/functional.py +0 -0
- {bt_cli-0.4.67 → bt_cli-0.4.69}/src/bt_cli/pws/commands/import_export.py +0 -0
- {bt_cli-0.4.67 → bt_cli-0.4.69}/src/bt_cli/pws/commands/platforms.py +0 -0
- {bt_cli-0.4.67 → bt_cli-0.4.69}/src/bt_cli/pws/commands/quick.py +0 -0
- {bt_cli-0.4.67 → bt_cli-0.4.69}/src/bt_cli/pws/commands/search.py +0 -0
- {bt_cli-0.4.67 → bt_cli-0.4.69}/src/bt_cli/pws/commands/secrets.py +0 -0
- {bt_cli-0.4.67 → bt_cli-0.4.69}/src/bt_cli/pws/commands/systems.py +0 -0
- {bt_cli-0.4.67 → bt_cli-0.4.69}/src/bt_cli/pws/commands/users.py +0 -0
- {bt_cli-0.4.67 → bt_cli-0.4.69}/src/bt_cli/pws/commands/workgroups.py +0 -0
- {bt_cli-0.4.67 → bt_cli-0.4.69}/src/bt_cli/pws/config.py +0 -0
- {bt_cli-0.4.67 → bt_cli-0.4.69}/src/bt_cli/pws/models/__init__.py +0 -0
- {bt_cli-0.4.67 → bt_cli-0.4.69}/src/bt_cli/pws/models/account.py +0 -0
- {bt_cli-0.4.67 → bt_cli-0.4.69}/src/bt_cli/pws/models/asset.py +0 -0
- {bt_cli-0.4.67 → bt_cli-0.4.69}/src/bt_cli/pws/models/common.py +0 -0
- {bt_cli-0.4.67 → bt_cli-0.4.69}/src/bt_cli/pws/models/system.py +0 -0
- {bt_cli-0.4.67 → bt_cli-0.4.69}/src/bt_cli/secrets/__init__.py +0 -0
- {bt_cli-0.4.67 → bt_cli-0.4.69}/src/bt_cli/secrets/client/__init__.py +0 -0
- {bt_cli-0.4.67 → bt_cli-0.4.69}/src/bt_cli/secrets/client/base.py +0 -0
- {bt_cli-0.4.67 → bt_cli-0.4.69}/src/bt_cli/secrets/commands/__init__.py +0 -0
- {bt_cli-0.4.67 → bt_cli-0.4.69}/src/bt_cli/secrets/commands/_hints.py +0 -0
- {bt_cli-0.4.67 → bt_cli-0.4.69}/src/bt_cli/secrets/commands/auth.py +0 -0
- {bt_cli-0.4.67 → bt_cli-0.4.69}/src/bt_cli/secrets/commands/dynamic.py +0 -0
- {bt_cli-0.4.67 → bt_cli-0.4.69}/src/bt_cli/secrets/commands/folders.py +0 -0
- {bt_cli-0.4.67 → bt_cli-0.4.69}/src/bt_cli/secrets/commands/integrations.py +0 -0
- {bt_cli-0.4.67 → bt_cli-0.4.69}/src/bt_cli/secrets/commands/leases.py +0 -0
- {bt_cli-0.4.67 → bt_cli-0.4.69}/src/bt_cli/secrets/commands/static.py +0 -0
- {bt_cli-0.4.67 → bt_cli-0.4.69}/src/bt_cli/secrets/models/__init__.py +0 -0
- {bt_cli-0.4.67 → bt_cli-0.4.69}/tests/__init__.py +0 -0
- {bt_cli-0.4.67 → bt_cli-0.4.69}/tests/conftest.py +0 -0
- {bt_cli-0.4.67 → bt_cli-0.4.69}/tests/core/__init__.py +0 -0
- {bt_cli-0.4.67 → bt_cli-0.4.69}/tests/core/test_auth.py +0 -0
- {bt_cli-0.4.67 → bt_cli-0.4.69}/tests/core/test_config.py +0 -0
- {bt_cli-0.4.67 → bt_cli-0.4.69}/tests/core/test_config_file.py +0 -0
- {bt_cli-0.4.67 → bt_cli-0.4.69}/tests/core/test_errors.py +0 -0
- {bt_cli-0.4.67 → bt_cli-0.4.69}/tests/core/test_output.py +0 -0
- {bt_cli-0.4.67 → bt_cli-0.4.69}/tests/core/test_rest_debug.py +0 -0
- {bt_cli-0.4.67 → bt_cli-0.4.69}/tests/entitle/__init__.py +0 -0
- {bt_cli-0.4.67 → bt_cli-0.4.69}/tests/entitle/test_client.py +0 -0
- {bt_cli-0.4.67 → bt_cli-0.4.69}/tests/entitle/test_commands.py +0 -0
- {bt_cli-0.4.67 → bt_cli-0.4.69}/tests/entitle-smoke-test.sh +0 -0
- {bt_cli-0.4.67 → bt_cli-0.4.69}/tests/epml/__init__.py +0 -0
- {bt_cli-0.4.67 → bt_cli-0.4.69}/tests/epml/test_client.py +0 -0
- {bt_cli-0.4.67 → bt_cli-0.4.69}/tests/epml/test_commands.py +0 -0
- {bt_cli-0.4.67 → bt_cli-0.4.69}/tests/epmw/__init__.py +0 -0
- {bt_cli-0.4.67 → bt_cli-0.4.69}/tests/epmw/test_client.py +0 -0
- {bt_cli-0.4.67 → bt_cli-0.4.69}/tests/epmw/test_commands.py +0 -0
- {bt_cli-0.4.67 → bt_cli-0.4.69}/tests/epmw-quick-test-plan.md +0 -0
- {bt_cli-0.4.67 → bt_cli-0.4.69}/tests/fixtures/__init__.py +0 -0
- {bt_cli-0.4.67 → bt_cli-0.4.69}/tests/fixtures/responses.py +0 -0
- {bt_cli-0.4.67 → bt_cli-0.4.69}/tests/integration/__init__.py +0 -0
- {bt_cli-0.4.67 → bt_cli-0.4.69}/tests/integration/conftest.py +0 -0
- {bt_cli-0.4.67 → bt_cli-0.4.69}/tests/integration/helpers.py +0 -0
- {bt_cli-0.4.67 → bt_cli-0.4.69}/tests/integration/test_entitle_integration.py +0 -0
- {bt_cli-0.4.67 → bt_cli-0.4.69}/tests/integration/test_epmw_integration.py +0 -0
- {bt_cli-0.4.67 → bt_cli-0.4.69}/tests/integration/test_epmw_lifecycle.py +0 -0
- {bt_cli-0.4.67 → bt_cli-0.4.69}/tests/integration/test_pra_integration.py +0 -0
- {bt_cli-0.4.67 → bt_cli-0.4.69}/tests/integration/test_pra_lifecycle.py +0 -0
- {bt_cli-0.4.67 → bt_cli-0.4.69}/tests/integration/test_pws_integration.py +0 -0
- {bt_cli-0.4.67 → bt_cli-0.4.69}/tests/integration/test_pws_lifecycle.py +0 -0
- {bt_cli-0.4.67 → bt_cli-0.4.69}/tests/pf/__init__.py +0 -0
- {bt_cli-0.4.67 → bt_cli-0.4.69}/tests/pf/test_client.py +0 -0
- {bt_cli-0.4.67 → bt_cli-0.4.69}/tests/pf/test_commands.py +0 -0
- {bt_cli-0.4.67 → bt_cli-0.4.69}/tests/pra/__init__.py +0 -0
- {bt_cli-0.4.67 → bt_cli-0.4.69}/tests/pra/test_client.py +0 -0
- {bt_cli-0.4.67 → bt_cli-0.4.69}/tests/pra/test_commands.py +0 -0
- {bt_cli-0.4.67 → bt_cli-0.4.69}/tests/pra/test_jump_item_fixes.py +0 -0
- {bt_cli-0.4.67 → bt_cli-0.4.69}/tests/pra-smoke-test.sh +0 -0
- {bt_cli-0.4.67 → bt_cli-0.4.69}/tests/pra-test-plan.md +0 -0
- {bt_cli-0.4.67 → bt_cli-0.4.69}/tests/pws/__init__.py +0 -0
- {bt_cli-0.4.67 → bt_cli-0.4.69}/tests/pws/test_client.py +0 -0
- {bt_cli-0.4.67 → bt_cli-0.4.69}/tests/pws/test_commands.py +0 -0
- {bt_cli-0.4.67 → bt_cli-0.4.69}/tests/pws/test_quick_sessions.py +0 -0
- {bt_cli-0.4.67 → bt_cli-0.4.69}/tests/pws-quick-test-plan.md +0 -0
- {bt_cli-0.4.67 → bt_cli-0.4.69}/tests/pws-smoke-test.sh +0 -0
- {bt_cli-0.4.67 → bt_cli-0.4.69}/tests/secrets/__init__.py +0 -0
- {bt_cli-0.4.67 → bt_cli-0.4.69}/tests/secrets/test_client.py +0 -0
- {bt_cli-0.4.67 → bt_cli-0.4.69}/tests/secrets/test_commands.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# BT-CLI
|
|
2
2
|
|
|
3
|
-
BeyondTrust Platform CLI for Password Safe, Entitle, PRA, EPM Windows, EPM Linux, and the BeyondTrust Secrets API. **Version: 0.4.
|
|
3
|
+
BeyondTrust Platform CLI for Password Safe, Entitle, PRA, EPM Windows, EPM Linux, and the BeyondTrust Secrets API. **Version: 0.4.69**
|
|
4
4
|
|
|
5
5
|
## Setup
|
|
6
6
|
|
|
@@ -189,6 +189,11 @@ bt pws sessions list --active # only live sessions (started, not ended)
|
|
|
189
189
|
bt pws sessions list --all -o json # full history
|
|
190
190
|
bt pws sessions get <session_id>
|
|
191
191
|
|
|
192
|
+
# "The session from today" — date filters match on StartTime (client-side)
|
|
193
|
+
bt pws sessions list --today
|
|
194
|
+
bt pws sessions list --on 2026-07-02
|
|
195
|
+
bt pws sessions list --since 2026-06-01 --until 2026-06-30 # a range (e.g. "this week")
|
|
196
|
+
|
|
192
197
|
# Suspend = lock (user disconnected, session can be resumed)
|
|
193
198
|
bt pws sessions suspend <session_id>
|
|
194
199
|
|
|
@@ -241,6 +246,27 @@ prints the exact command + install instructions and leaves the frames. Works for
|
|
|
241
246
|
RDP (screen) and SSH (rendered terminal). Gotcha: the renderer needs ~0.5s
|
|
242
247
|
warm-up (first seek 404s), handled automatically.
|
|
243
248
|
|
|
249
|
+
### "What happened in this session?" — summarize efficiently
|
|
250
|
+
|
|
251
|
+
`replay` only *downloads* frames. To turn a session into a **timeline summary of
|
|
252
|
+
what the user did**, don't read every frame — use the analysis pipeline:
|
|
253
|
+
|
|
254
|
+
```bash
|
|
255
|
+
bt pws sessions replay <session_id> # 1. download the frames
|
|
256
|
+
bt pws sessions analyze <session_id> # 2. pick keyframes + anchor keystrokes + estimate cost
|
|
257
|
+
```
|
|
258
|
+
|
|
259
|
+
`analyze` is deterministic prep (no LLM): it selects the handful of frames worth
|
|
260
|
+
reading — screen-change frames **plus** the frames that render what the user
|
|
261
|
+
typed (keystroke-anchored, tagged `after-typing` with the `typed` text) — pulls
|
|
262
|
+
the transcript, and prints a token/USD cost estimate. Then **invoke the
|
|
263
|
+
`/session-analysis` skill**, which tells you (the agent) to read only those
|
|
264
|
+
keyframes (after-typing first) and produce a timeline + auditor highlights (EPM
|
|
265
|
+
elevation prompts, privileged tools, service/account changes) + a secrets-exposure
|
|
266
|
+
note. For **SSH**, skip frames entirely — `analyze` points you at the transcript,
|
|
267
|
+
which is the full input+output and far cheaper. This is the intended path for
|
|
268
|
+
"show me / summarize the session from today".
|
|
269
|
+
|
|
244
270
|
**Gotchas:**
|
|
245
271
|
- `/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.
|
|
246
272
|
- `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.
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: session-analysis
|
|
3
|
+
description: Analyze a dumped Password Safe session (replay frames + keystrokes) into a timeline summary with a cost estimate. Use when the user points at a `session-<id>-replay` folder or asks to summarize/analyze a recorded RDP or SSH session.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Session Analysis
|
|
7
|
+
|
|
8
|
+
Turn a bt-cli session dump into a **timeline summary of what happened**, efficiently, with a **cost estimate**. Works for RDP (screen recording → frames) and SSH (keystroke transcript). This is interactive analysis — *you* (the model) read the frames/transcript; there is no LLM call inside the CLI.
|
|
9
|
+
|
|
10
|
+
## The one rule: never read every frame
|
|
11
|
+
|
|
12
|
+
An RDP session sampled at ~1 frame per 2s is dozens of near-identical frames. Reading them all wastes tokens. Let the CLI pick the frames worth reading — the ones where the screen changed **and** the ones that render what the user typed — and read only those, guided by the per-frame `typed`/`reason` tags.
|
|
13
|
+
|
|
14
|
+
## Recipe
|
|
15
|
+
|
|
16
|
+
### 1. Prep with the CLI (does the heavy lifting)
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
# If the user has a replay folder already:
|
|
20
|
+
bt pws sessions analyze --dir <folder>
|
|
21
|
+
# If they have the session id (reads ./session-<id>-replay):
|
|
22
|
+
bt pws sessions analyze <id> --model opus # or --model sonnet for a cheaper estimate
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
`analyze` writes `analysis.json` into the folder and prints:
|
|
26
|
+
- **Who / what / when:** `user` (the initiator's real name + username, resolved from `UserID` via `/Users/{id}`), `user_email`, `account`@`system`, `start_time`. **Always name the human in the summary** — "UserID 219" is not an answer; "Amy Miller (amiller)" is.
|
|
27
|
+
- **RDP:** the **keyframes** to read. Each is tagged with a **reason** — `screen-change` (the picture moved) and/or `after-typing` (it renders the result of something the user typed). `after-typing` frames also carry the exact `typed` text.
|
|
28
|
+
- **SSH:** a pointer to the transcript (analyze it instead of frames).
|
|
29
|
+
- **`typed_bursts`** — typed input grouped into phrases with recording-relative offsets (`@14.9s "powershell"`), already anchored to the keyframes above.
|
|
30
|
+
- A **cost estimate**: `focused` (keyframes only) vs `all frames`.
|
|
31
|
+
|
|
32
|
+
If the user only has a folder (no live API access), `analyze --dir` still works — user name, keystrokes, and anchoring need the session metadata (`StartTime`/`UserID`), so offline it falls back to `screen-change` keyframes only, with a note. Keyframes + cost are still computed.
|
|
33
|
+
|
|
34
|
+
### 2. Combine both signals — transcript AND frames
|
|
35
|
+
|
|
36
|
+
Don't treat this as either/or. The transcript gives you *verbatim text* the screen can't render cleanly; the frames give you *visual state* the transcript can't capture. Use each for what it's good at, and correlate them by timestamp (the CLI already did the correlation — `typed`/`typed_bursts` carry offsets, keystrokes carry `time`).
|
|
37
|
+
|
|
38
|
+
- **Always pull the transcript** when there is one: `bt pws sessions transcript <id>`. It's cheap.
|
|
39
|
+
- **SSH:** the transcript *is* the analysis — full input+output (typed commands `»` + terminal stdout). Read it; frames are rarely worth opening.
|
|
40
|
+
- **RDP:** the transcript is sparse (typed keys / window titles, often empty) but still adds color frames miss — the exact command typed, a window title, a value entered in a field. Fold it into the narrative alongside the frames.
|
|
41
|
+
- **Read the frames** (RDP): **read every keyframe the CLI listed — do not sub-sample them.** The CLI already culled the full recording down to the frames that matter; picking "6 of 37 to confirm the story" is how you miss events (a service restart, a dialog, a config change happen in the frames you skipped). If the full keyframe set is genuinely too expensive, raise `--threshold` and re-run so the *CLI* drops frames deliberately — never silently skip listed keyframes. Prioritize `after-typing` frames (the `typed` field tells you what to look for before you open the image), then read the `screen-change` frames for the mouse-driven actions keystrokes don't cover (menus, service start/stop, window closes).
|
|
42
|
+
|
|
43
|
+
### 3. Anchoring is already done
|
|
44
|
+
|
|
45
|
+
The CLI has correlated keystrokes → frames for you (via the recording `StartTime`): the `typed` field on each keyframe and the `typed_bursts` list *are* the timeline anchors. Just narrate them — "at ~0:14 typed `powershell`, frame at 16s shows PowerShell 7 open". If RDP keystroke capture was off (bursts empty), fall back to reading every screen-change frame and inferring actions from the pictures.
|
|
46
|
+
|
|
47
|
+
### 4. Write the summary
|
|
48
|
+
|
|
49
|
+
Produce:
|
|
50
|
+
- **Who** — the initiating user by name (from `user`/`user_email`), the account@system, and the time window. Note if the initiator is an API/service credential (e.g. `cli-<name>`) vs an interactive login — that distinction matters for audit.
|
|
51
|
+
- **Timeline table** — approximate time → event (app launched, window opened, command run, dialog shown, service changed, logoff).
|
|
52
|
+
- **Highlights an auditor cares about** — EPM/Defendpoint elevation or justification prompts, privileged tools (`dsa.msc`, `services.msc`, PowerShell), AD/account/OU changes, service start/stop, file transfers.
|
|
53
|
+
- **Security note** — explicitly state whether any credentials, passwords, tokens, or secret values were visible in the frames read.
|
|
54
|
+
- **Coverage & cost** — say how many of the listed keyframes you actually read (ideally all of them) so the reader knows the summary isn't under-sampled, and report the estimate from `analysis.json`.
|
|
55
|
+
|
|
56
|
+
## Efficiency notes
|
|
57
|
+
|
|
58
|
+
- Two selection signals combine: PNG-size delta (`screen-change`, dependency-free) and keystroke anchoring (`after-typing`, when RDP keystroke capture is on). Anchoring is what makes the read *targeted* — it guarantees the frame that shows each typed action is included, even if its size barely moved.
|
|
59
|
+
- **Blind spot — mouse-driven, low-pixel-change actions.** A subtle action done with the mouse (a service going Running→Stopped is a few pixels of text, no keystroke) can slip past *both* signals: no `after-typing` anchor, and too small a PNG delta to trip `screen-change`. This is the classic "we missed the service restart." Mitigations: (a) **read all listed keyframes** — the restart's modal progress dialog usually *is* a keyframe, you just have to open it; (b) if you suspect a missed event in a quiet stretch, re-run `analyze --threshold 1.5` (or `1.0`) to surface smaller deltas; (c) for forensic certainty, `bt pws sessions replay <id> --all-frames --no-dedup` captures every recorded change. Under-sampling is usually the agent skipping listed keyframes, not the CLI mis-selecting them.
|
|
60
|
+
- If a session looks under-sampled (transitions missed), re-run `analyze --threshold 1.5` for more keyframes; over-sampled → raise it.
|
|
61
|
+
- Frame token cost ≈ `(width × height) / 750` per image (1024×768 ≈ ~1,050 tokens). The CLI's estimate already accounts for this.
|
|
62
|
+
- Pricing (per 1M tokens, for the estimate): Opus 4.8 $5/$25, Sonnet 4.6 $3/$15, Haiku 4.5 $1/$5. Use the `claude-api` skill for current numbers.
|
|
63
|
+
|
|
64
|
+
## Scaling up (optional, not built)
|
|
65
|
+
|
|
66
|
+
For summarizing **many** sessions unattended, this interactive recipe doesn't scale — that would be a `bt pws sessions summarize` command calling the Claude API (vision) with the same keyframe/transcript inputs and the Batch API for volume. Mention it if the user wants automation; today, analysis is interactive.
|
|
67
|
+
|
|
68
|
+
## Related
|
|
69
|
+
|
|
70
|
+
- `bt pws sessions replay <id>` — produce the frames (defaults to even coverage + dedup).
|
|
71
|
+
- `bt pws sessions transcript <id>` — the keystroke transcript (SSH gold; RDP sparse).
|
|
72
|
+
- `/pws` skill — Password Safe command reference.
|
|
@@ -5,7 +5,7 @@ import json
|
|
|
5
5
|
import shutil
|
|
6
6
|
import subprocess
|
|
7
7
|
import time
|
|
8
|
-
from datetime import datetime, timezone
|
|
8
|
+
from datetime import date, datetime, timezone
|
|
9
9
|
from pathlib import Path
|
|
10
10
|
|
|
11
11
|
import httpx
|
|
@@ -35,6 +35,41 @@ SESSION_STATUS = {
|
|
|
35
35
|
}
|
|
36
36
|
|
|
37
37
|
|
|
38
|
+
def _norm_date(value: str) -> str:
|
|
39
|
+
"""Validate a YYYY-MM-DD (or ISO) date arg and return its date portion."""
|
|
40
|
+
try:
|
|
41
|
+
datetime.strptime(value.strip()[:10], "%Y-%m-%d")
|
|
42
|
+
except (ValueError, AttributeError):
|
|
43
|
+
print_error(f"Invalid date '{value}'. Use YYYY-MM-DD.")
|
|
44
|
+
raise typer.Exit(2)
|
|
45
|
+
return value.strip()[:10]
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def _resolve_date_window(today: bool, on: str, since: str, until: str) -> tuple:
|
|
49
|
+
"""Collapse the date flags into an inclusive (lo, hi) date window (YYYY-MM-DD
|
|
50
|
+
strings, either may be None). --today/--on pin both ends to one day."""
|
|
51
|
+
if today:
|
|
52
|
+
d = date.today().isoformat()
|
|
53
|
+
return d, d
|
|
54
|
+
if on:
|
|
55
|
+
d = _norm_date(on)
|
|
56
|
+
return d, d
|
|
57
|
+
return (_norm_date(since) if since else None, _norm_date(until) if until else None)
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def _started_in_window(s: dict, lo: str, hi: str) -> bool:
|
|
61
|
+
"""True if the session's StartTime date falls within [lo, hi] (ISO date
|
|
62
|
+
strings sort lexicographically, so string compare on the date is correct)."""
|
|
63
|
+
d = (s.get("StartTime") or "")[:10]
|
|
64
|
+
if not d:
|
|
65
|
+
return False
|
|
66
|
+
if lo and d < lo:
|
|
67
|
+
return False
|
|
68
|
+
if hi and d > hi:
|
|
69
|
+
return False
|
|
70
|
+
return True
|
|
71
|
+
|
|
72
|
+
|
|
38
73
|
def is_active(s: dict) -> bool:
|
|
39
74
|
"""A session is live (suspend/terminate-able) when Status == 1.
|
|
40
75
|
|
|
@@ -99,6 +134,10 @@ def list_sessions(
|
|
|
99
134
|
system: int = typer.Option(None, "--system", help="Filter by managed system ID"),
|
|
100
135
|
active: bool = typer.Option(False, "--active", help="Show only active (started, not ended) sessions"),
|
|
101
136
|
status: str = typer.Option(None, "--status", "-s", help="Filter by raw numeric status code"),
|
|
137
|
+
today: bool = typer.Option(False, "--today", help="Only sessions that started today"),
|
|
138
|
+
on: str = typer.Option(None, "--on", help="Only sessions that started on this date (YYYY-MM-DD)"),
|
|
139
|
+
since: str = typer.Option(None, "--since", help="Only sessions on/after this date (YYYY-MM-DD)"),
|
|
140
|
+
until: str = typer.Option(None, "--until", help="Only sessions on/before this date (YYYY-MM-DD)"),
|
|
102
141
|
limit: int = typer.Option(25, "--limit", "-l", help="Max rows to show (most recent first)"),
|
|
103
142
|
fetch_all: bool = typer.Option(False, "--all", help="Show all sessions (the endpoint returns full history)"),
|
|
104
143
|
output: str = typer.Option("table", "--output", "-o", help="Output format: table or json"),
|
|
@@ -110,11 +149,17 @@ def list_sessions(
|
|
|
110
149
|
suspend/terminate), or --all to see everything. Output is newest-first and
|
|
111
150
|
capped at --limit by default.
|
|
112
151
|
|
|
152
|
+
Date filters (--today/--on/--since/--until) match on StartTime's date and are
|
|
153
|
+
applied client-side (the endpoint returns the full history every call).
|
|
154
|
+
|
|
113
155
|
Examples:
|
|
114
156
|
bt pws sessions list --active
|
|
157
|
+
bt pws sessions list --today
|
|
158
|
+
bt pws sessions list --since 2026-06-01 --until 2026-06-30
|
|
115
159
|
bt pws sessions list --system 13
|
|
116
160
|
bt pws sessions list --all -o json
|
|
117
161
|
"""
|
|
162
|
+
lo, hi = _resolve_date_window(today, on, since, until)
|
|
118
163
|
try:
|
|
119
164
|
with get_client() as client:
|
|
120
165
|
client.authenticate()
|
|
@@ -122,6 +167,8 @@ def list_sessions(
|
|
|
122
167
|
|
|
123
168
|
if active:
|
|
124
169
|
sessions = [s for s in sessions if is_active(s)]
|
|
170
|
+
if lo or hi:
|
|
171
|
+
sessions = [s for s in sessions if _started_in_window(s, lo, hi)]
|
|
125
172
|
|
|
126
173
|
total = len(sessions)
|
|
127
174
|
shown = sessions if fetch_all else sessions[:limit]
|
|
@@ -715,6 +762,17 @@ def _build_keyframes(
|
|
|
715
762
|
return keyframes
|
|
716
763
|
|
|
717
764
|
|
|
765
|
+
def _user_label(user: dict) -> str:
|
|
766
|
+
"""Human label for a /Users record: 'First Last (username)' or best available."""
|
|
767
|
+
if not user:
|
|
768
|
+
return ""
|
|
769
|
+
full = " ".join(p for p in (user.get("FirstName"), user.get("LastName")) if p).strip()
|
|
770
|
+
handle = user.get("UserName") or user.get("EmailAddress") or ""
|
|
771
|
+
if full and handle:
|
|
772
|
+
return f"{full} ({handle})"
|
|
773
|
+
return full or handle or (f"UserID {user.get('UserID')}" if user.get("UserID") else "")
|
|
774
|
+
|
|
775
|
+
|
|
718
776
|
@app.command("analyze")
|
|
719
777
|
def session_analyze(
|
|
720
778
|
session_id: int = typer.Argument(None, help="Session ID (defaults to reading ./session-<id>-replay)"),
|
|
@@ -758,18 +816,27 @@ def session_analyze(
|
|
|
758
816
|
frames = idx.get("frames", [])
|
|
759
817
|
sid = session_id or idx.get("session_id")
|
|
760
818
|
|
|
761
|
-
# Pull keystrokes + session meta (best-effort; folder
|
|
819
|
+
# Pull keystrokes + session meta + the initiating user (best-effort; folder
|
|
820
|
+
# may be analyzed offline with no API access).
|
|
762
821
|
keystrokes: list[dict] = []
|
|
763
822
|
meta: dict = {}
|
|
823
|
+
user: dict = {}
|
|
764
824
|
if sid:
|
|
765
825
|
try:
|
|
766
826
|
with get_client() as client:
|
|
767
827
|
client.authenticate()
|
|
768
828
|
meta = client.get_session(sid)
|
|
769
829
|
keystrokes = client.get_keystrokes(sid)
|
|
830
|
+
uid = meta.get("UserID")
|
|
831
|
+
if uid:
|
|
832
|
+
try:
|
|
833
|
+
user = client.get_user(uid)
|
|
834
|
+
except Exception:
|
|
835
|
+
pass # name resolution is a nicety; don't fail the analysis
|
|
770
836
|
except Exception as e:
|
|
771
837
|
if output != "json":
|
|
772
838
|
print_warning(f"Could not pull keystrokes for session {sid} ({e}); continuing with frames only.")
|
|
839
|
+
user_label = _user_label(user)
|
|
773
840
|
|
|
774
841
|
decoded_ks = [
|
|
775
842
|
{"time": _fmt_marker(k.get("TimeMarker")), "type": _keystroke_type(k.get("Type")), "data": k.get("Data", "")}
|
|
@@ -807,6 +874,12 @@ def session_analyze(
|
|
|
807
874
|
manifest = {
|
|
808
875
|
"session_id": sid,
|
|
809
876
|
"protocol": protocol,
|
|
877
|
+
"user_id": meta.get("UserID"),
|
|
878
|
+
"user": user_label,
|
|
879
|
+
"user_email": user.get("EmailAddress") if user else None,
|
|
880
|
+
"account": meta.get("ManagedAccountName"),
|
|
881
|
+
"system": meta.get("AssetName"),
|
|
882
|
+
"start_time": meta.get("StartTime"),
|
|
810
883
|
"frame_dims": [w, h],
|
|
811
884
|
"frame_count": len(frames),
|
|
812
885
|
"keyframe_count": len(keyframes),
|
|
@@ -826,6 +899,9 @@ def session_analyze(
|
|
|
826
899
|
acct = meta.get("ManagedAccountName", "?")
|
|
827
900
|
sysn = meta.get("AssetName", "?")
|
|
828
901
|
console.print(f"[bold]Session {sid}[/bold] {protocol} {acct}@{sysn}")
|
|
902
|
+
if user_label:
|
|
903
|
+
console.print(f" [green]Initiated by[/green] {rich_escape(user_label)}"
|
|
904
|
+
+ (f" [dim]· {meta.get('StartTime')}[/dim]" if meta.get("StartTime") else ""))
|
|
829
905
|
console.print(f" Frames: {len(frames)} ({w}x{h}) Keystroke events: {len(decoded_ks)}")
|
|
830
906
|
if protocol.upper() == "SSH":
|
|
831
907
|
console.print(" [green]SSH → analyze the transcript[/green] (cheap, full input+output):")
|
|
@@ -1,64 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
name: session-analysis
|
|
3
|
-
description: Analyze a dumped Password Safe session (replay frames + keystrokes) into a timeline summary with a cost estimate. Use when the user points at a `session-<id>-replay` folder or asks to summarize/analyze a recorded RDP or SSH session.
|
|
4
|
-
---
|
|
5
|
-
|
|
6
|
-
# Session Analysis
|
|
7
|
-
|
|
8
|
-
Turn a bt-cli session dump into a **timeline summary of what happened**, efficiently, with a **cost estimate**. Works for RDP (screen recording → frames) and SSH (keystroke transcript). This is interactive analysis — *you* (the model) read the frames/transcript; there is no LLM call inside the CLI.
|
|
9
|
-
|
|
10
|
-
## The one rule: never read every frame
|
|
11
|
-
|
|
12
|
-
An RDP session sampled at ~1 frame per 2s is dozens of near-identical frames. Reading them all wastes tokens. Let the CLI pick the frames worth reading — the ones where the screen changed **and** the ones that render what the user typed — and read only those, guided by the per-frame `typed`/`reason` tags.
|
|
13
|
-
|
|
14
|
-
## Recipe
|
|
15
|
-
|
|
16
|
-
### 1. Prep with the CLI (does the heavy lifting)
|
|
17
|
-
|
|
18
|
-
```bash
|
|
19
|
-
# If the user has a replay folder already:
|
|
20
|
-
bt pws sessions analyze --dir <folder>
|
|
21
|
-
# If they have the session id (reads ./session-<id>-replay):
|
|
22
|
-
bt pws sessions analyze <id> --model opus # or --model sonnet for a cheaper estimate
|
|
23
|
-
```
|
|
24
|
-
|
|
25
|
-
`analyze` writes `analysis.json` into the folder and prints:
|
|
26
|
-
- **RDP:** the **keyframes** to read (a handful, not all). Each keyframe is tagged with a **reason** — `screen-change` (the picture moved) and/or `after-typing` (it renders the result of something the user typed). `after-typing` frames also carry the exact `typed` text.
|
|
27
|
-
- **SSH:** a pointer to the transcript (analyze it instead of frames).
|
|
28
|
-
- **`typed_bursts`** — typed input grouped into phrases with recording-relative offsets (`@14.9s "powershell"`), already anchored to the keyframes above.
|
|
29
|
-
- A **cost estimate**: `focused` (keyframes only) vs `all frames`.
|
|
30
|
-
|
|
31
|
-
If the user only has a folder (no live API access), `analyze --dir` still works — keystrokes need session metadata (`StartTime`) to anchor, so offline it falls back to `screen-change` keyframes only, with a note. Keyframes + cost are still computed.
|
|
32
|
-
|
|
33
|
-
### 2. Read the right inputs — targeted, in `analysis.json` order
|
|
34
|
-
|
|
35
|
-
- **RDP:** `Read` only the keyframe PNGs the CLI listed. **Prioritize the `after-typing` frames** — they're where a user action produced a visible result (a launched app, a command's output, an EPM prompt). For each, the `typed` field tells you what to look for *before* you open the image, so you read with intent instead of scanning. The `screen-change`-only frames fill in transitions the keystrokes don't cover (mouse-driven actions: menus, service restarts, window closes). If a keyframe implies something happened between it and the next, that's fine — the summary is event-level, not frame-level.
|
|
36
|
-
- **SSH:** run `bt pws sessions transcript <id>` and read that — it's the full input+output, far richer and cheaper than frames. Frames are rarely worth reading for SSH.
|
|
37
|
-
|
|
38
|
-
### 3. Anchoring is already done
|
|
39
|
-
|
|
40
|
-
The CLI has correlated keystrokes → frames for you (via the recording `StartTime`): the `typed` field on each keyframe and the `typed_bursts` list *are* the timeline anchors. Just narrate them — "at ~0:14 typed `powershell`, frame at 16s shows PowerShell 7 open". If RDP keystroke capture was off (bursts empty), fall back to reading the screen-change frames and inferring actions from the pictures alone.
|
|
41
|
-
|
|
42
|
-
### 4. Write the summary
|
|
43
|
-
|
|
44
|
-
Produce:
|
|
45
|
-
- **Timeline table** — approximate time → event (app launched, window opened, command run, dialog shown, service changed, logoff).
|
|
46
|
-
- **Highlights an auditor cares about** — EPM/Defendpoint elevation or justification prompts, privileged tools (`dsa.msc`, `services.msc`, PowerShell), AD/account/OU changes, service start/stop, file transfers.
|
|
47
|
-
- **Security note** — explicitly state whether any credentials, passwords, tokens, or secret values were visible in the frames read.
|
|
48
|
-
- **Cost** — report the estimate from `analysis.json` (and, if you want, the actual: images read × ~`(w*h)/750` tokens + output).
|
|
49
|
-
|
|
50
|
-
## Efficiency notes
|
|
51
|
-
|
|
52
|
-
- Two selection signals combine: PNG-size delta (`screen-change`, dependency-free) and keystroke anchoring (`after-typing`, when RDP keystroke capture is on). Anchoring is what makes the read *targeted* — it guarantees the frame that shows each typed action is included, even if its size barely moved. If a session looks under-sampled (transitions missed), re-run `analyze --threshold 1.5` for more keyframes; over-sampled → raise it.
|
|
53
|
-
- Frame token cost ≈ `(width × height) / 750` per image (1024×768 ≈ ~1,050 tokens). The CLI's estimate already accounts for this.
|
|
54
|
-
- Pricing (per 1M tokens, for the estimate): Opus 4.8 $5/$25, Sonnet 4.6 $3/$15, Haiku 4.5 $1/$5. Use the `claude-api` skill for current numbers.
|
|
55
|
-
|
|
56
|
-
## Scaling up (optional, not built)
|
|
57
|
-
|
|
58
|
-
For summarizing **many** sessions unattended, this interactive recipe doesn't scale — that would be a `bt pws sessions summarize` command calling the Claude API (vision) with the same keyframe/transcript inputs and the Batch API for volume. Mention it if the user wants automation; today, analysis is interactive.
|
|
59
|
-
|
|
60
|
-
## Related
|
|
61
|
-
|
|
62
|
-
- `bt pws sessions replay <id>` — produce the frames (defaults to even coverage + dedup).
|
|
63
|
-
- `bt pws sessions transcript <id>` — the keystroke transcript (SSH gold; RDP sparse).
|
|
64
|
-
- `/pws` skill — Password Safe command reference.
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|