bt-cli 0.4.12__py3-none-any.whl → 0.4.14__py3-none-any.whl
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/__init__.py +1 -1
- bt_cli/data/CLAUDE.md +17 -11
- bt_cli/data/skills/bt/SKILL.md +36 -26
- bt_cli/data/skills/entitle/SKILL.md +25 -14
- bt_cli/data/skills/epmw/SKILL.md +15 -16
- bt_cli/data/skills/pra/SKILL.md +40 -39
- bt_cli/data/skills/pws/SKILL.md +22 -21
- bt_cli/epmw/commands/policies.py +17 -3
- bt_cli/pws/commands/import_export.py +280 -105
- bt_cli/pws/commands/secrets.py +5 -5
- {bt_cli-0.4.12.dist-info → bt_cli-0.4.14.dist-info}/METADATA +1 -1
- {bt_cli-0.4.12.dist-info → bt_cli-0.4.14.dist-info}/RECORD +14 -14
- {bt_cli-0.4.12.dist-info → bt_cli-0.4.14.dist-info}/WHEEL +0 -0
- {bt_cli-0.4.12.dist-info → bt_cli-0.4.14.dist-info}/entry_points.txt +0 -0
bt_cli/__init__.py
CHANGED
bt_cli/data/CLAUDE.md
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
|
-
# BT-
|
|
1
|
+
# BT-CLI
|
|
2
2
|
|
|
3
3
|
BeyondTrust Platform CLI for Password Safe, Entitle, PRA, and EPM Windows.
|
|
4
4
|
|
|
5
5
|
## Setup
|
|
6
6
|
|
|
7
7
|
```bash
|
|
8
|
-
|
|
8
|
+
# From project root
|
|
9
9
|
source .venv/bin/activate && source .env
|
|
10
10
|
bt whoami # Test all connections
|
|
11
11
|
```
|
|
@@ -62,16 +62,22 @@ PASSWORD=$(bt pws quick checkout -s server -a admin --raw)
|
|
|
62
62
|
- PWS assets: Must create via workgroup endpoint
|
|
63
63
|
- ECM integration: PWS system name must match PRA jump item name
|
|
64
64
|
|
|
65
|
-
##
|
|
65
|
+
## Functional vs Managed Accounts
|
|
66
66
|
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
67
|
+
**Functional accounts** (`bt pws functional`) - Service accounts used BY Password Safe to connect to systems for auto-management (password rotation, discovery). One functional account can manage many systems.
|
|
68
|
+
|
|
69
|
+
**Managed accounts** (`bt pws accounts`) - User accounts ON systems that Password Safe manages (stores/rotates passwords for). These are what users check out.
|
|
70
|
+
|
|
71
|
+
```bash
|
|
72
|
+
# List functional accounts to find the right one for your platform
|
|
73
|
+
bt pws functional list
|
|
74
|
+
|
|
75
|
+
# List workgroups to find the right one
|
|
76
|
+
bt pws workgroups list
|
|
77
|
+
|
|
78
|
+
# Use functional account when onboarding a system
|
|
79
|
+
bt pws quick onboard -n "server" -i "10.0.1.50" -w <WORKGROUP_ID> -f <FUNC_ACCT_ID> -e "sudo"
|
|
80
|
+
```
|
|
75
81
|
|
|
76
82
|
## Project Structure
|
|
77
83
|
|
bt_cli/data/skills/bt/SKILL.md
CHANGED
|
@@ -22,9 +22,10 @@ Before executing any destructive operation:
|
|
|
22
22
|
|
|
23
23
|
## Setup
|
|
24
24
|
|
|
25
|
+
The CLI should be installed and configured. Test with:
|
|
26
|
+
|
|
25
27
|
```bash
|
|
26
|
-
|
|
27
|
-
source .venv/bin/activate && source .env
|
|
28
|
+
bt whoami # Verify all products are connected
|
|
28
29
|
```
|
|
29
30
|
|
|
30
31
|
## Test All Connections
|
|
@@ -47,27 +48,32 @@ bt quick pasm-search web-server -o json
|
|
|
47
48
|
### PASM Onboard (PWS + PRA)
|
|
48
49
|
Onboard a host to both Password Safe and PRA in one command.
|
|
49
50
|
|
|
51
|
+
**First, discover your environment IDs** (see "Discover Environment IDs" below).
|
|
52
|
+
|
|
50
53
|
```bash
|
|
51
|
-
# Linux SSH host
|
|
52
|
-
bt quick pasm-onboard -n "my-server" -i "10.0.1.50"
|
|
54
|
+
# Linux SSH host (use IDs from your environment)
|
|
55
|
+
bt quick pasm-onboard -n "my-server" -i "10.0.1.50" \
|
|
56
|
+
-w <workgroup_id> -j <jumpoint_id> -g <jump_group_id>
|
|
53
57
|
|
|
54
58
|
# Full options
|
|
55
59
|
bt quick pasm-onboard \
|
|
56
60
|
--name "web-01" \
|
|
57
61
|
--ip "10.0.1.100" \
|
|
58
|
-
--workgroup
|
|
59
|
-
--jumpoint
|
|
60
|
-
--jump-group
|
|
61
|
-
--functional-account
|
|
62
|
+
--workgroup <workgroup_id> \
|
|
63
|
+
--jumpoint <jumpoint_id> \
|
|
64
|
+
--jump-group <jump_group_id> \
|
|
65
|
+
--functional-account <func_acct_id> \
|
|
62
66
|
--elevation "sudo" \
|
|
63
|
-
--pra-username "
|
|
67
|
+
--pra-username "admin"
|
|
64
68
|
|
|
65
69
|
# Windows RDP host
|
|
66
|
-
bt quick pasm-onboard -n "win-srv" -i "10.0.2.10"
|
|
70
|
+
bt quick pasm-onboard -n "win-srv" -i "10.0.2.10" \
|
|
71
|
+
-w <workgroup_id> -p <platform_id> -j <jumpoint_id> -g <jump_group_id> \
|
|
72
|
+
--jump-type rdp --port 3389
|
|
67
73
|
|
|
68
74
|
# Skip one product
|
|
69
|
-
bt quick pasm-onboard -n "pra-only" -i "10.0.1.5" -j
|
|
70
|
-
bt quick pasm-onboard -n "pws-only" -i "10.0.1.6" -w
|
|
75
|
+
bt quick pasm-onboard -n "pra-only" -i "10.0.1.5" -j <jumpoint_id> -g <jump_group_id> --skip-pws
|
|
76
|
+
bt quick pasm-onboard -n "pws-only" -i "10.0.1.6" -w <workgroup_id> --skip-pra
|
|
71
77
|
```
|
|
72
78
|
|
|
73
79
|
### PASM Offboard
|
|
@@ -78,20 +84,24 @@ bt quick pasm-offboard -n "my-server"
|
|
|
78
84
|
bt quick pasm-offboard -n "web-01" --force
|
|
79
85
|
```
|
|
80
86
|
|
|
81
|
-
## Environment
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
87
|
+
## Discover Environment IDs
|
|
88
|
+
|
|
89
|
+
Use these commands to find the IDs for your environment:
|
|
90
|
+
|
|
91
|
+
```bash
|
|
92
|
+
# PWS resources
|
|
93
|
+
bt pws workgroups list # Find workgroup IDs
|
|
94
|
+
bt pws functional list # Find functional account IDs
|
|
95
|
+
bt pws platforms list # Find platform IDs
|
|
96
|
+
|
|
97
|
+
# PRA resources
|
|
98
|
+
bt pra jumpoint list # Find jumpoint IDs
|
|
99
|
+
bt pra jump-groups list # Find jump group IDs
|
|
100
|
+
bt pra vault groups list # Find vault account group IDs
|
|
101
|
+
|
|
102
|
+
# Entitle resources
|
|
103
|
+
bt entitle integrations list # Find integration IDs
|
|
104
|
+
```
|
|
95
105
|
|
|
96
106
|
## Product-Specific Skills
|
|
97
107
|
|
|
@@ -25,10 +25,10 @@ bt entitle integrations get <integration_id>
|
|
|
25
25
|
bt entitle resources list --integration <integration_id>
|
|
26
26
|
bt entitle resources get <resource_id>
|
|
27
27
|
|
|
28
|
-
# Virtual integration resources
|
|
28
|
+
# Virtual integration resources (use integration ID from `bt entitle integrations list`)
|
|
29
29
|
bt entitle resources create-virtual \
|
|
30
30
|
--name "Customer-05 (Bing7)" \
|
|
31
|
-
--integration
|
|
31
|
+
--integration <virtual_integration_id> \
|
|
32
32
|
--source-role-id <role_id> \
|
|
33
33
|
--role-name "Start Session"
|
|
34
34
|
bt entitle resources delete <resource_id>
|
|
@@ -101,18 +101,21 @@ bt entitle policies get <policy_id>
|
|
|
101
101
|
- Or manually trigger sync in Entitle UI: Integrations → PRA → Sync Now
|
|
102
102
|
|
|
103
103
|
```bash
|
|
104
|
-
# 1.
|
|
104
|
+
# 1. Find your PRA integration ID
|
|
105
|
+
bt entitle integrations list | grep -i pra
|
|
105
106
|
|
|
106
|
-
# 2.
|
|
107
|
-
bt entitle resources list --integration bb2a3c79-02a9-45d9-be7f-f209d97cb1d7
|
|
107
|
+
# 2. Trigger PRA sync in Entitle UI (or wait for hourly sync)
|
|
108
108
|
|
|
109
|
-
# 3.
|
|
109
|
+
# 3. Find the new PRA resource
|
|
110
|
+
bt entitle resources list --integration <pra_integration_id>
|
|
111
|
+
|
|
112
|
+
# 4. Get "Start Sessions Only" role ID
|
|
110
113
|
bt entitle roles list --resource <pra_resource_id>
|
|
111
114
|
|
|
112
|
-
#
|
|
115
|
+
# 5. Add to virtual integration (find ID with `bt entitle integrations list`)
|
|
113
116
|
bt entitle resources create-virtual \
|
|
114
117
|
--name "Customer-05" \
|
|
115
|
-
--integration
|
|
118
|
+
--integration <virtual_integration_id> \
|
|
116
119
|
--source-role-id <role_id> \
|
|
117
120
|
--role-name "Start Session"
|
|
118
121
|
```
|
|
@@ -123,13 +126,21 @@ bt entitle resources create-virtual \
|
|
|
123
126
|
bt entitle permissions list --user "user@example.com" -o json
|
|
124
127
|
```
|
|
125
128
|
|
|
126
|
-
##
|
|
129
|
+
## Discover IDs
|
|
130
|
+
|
|
131
|
+
```bash
|
|
132
|
+
# Find integration IDs
|
|
133
|
+
bt entitle integrations list
|
|
134
|
+
|
|
135
|
+
# Find workflow IDs
|
|
136
|
+
bt entitle workflows list
|
|
137
|
+
|
|
138
|
+
# Find resource IDs within an integration
|
|
139
|
+
bt entitle resources list --integration <integration_id>
|
|
127
140
|
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
| Customer Access (Virtual) | 22f4960f-b2ee-435f-9fa2-b82baeca06b2 |
|
|
132
|
-
| AWS SandBox | (check `bt entitle integrations list`) |
|
|
141
|
+
# Find role IDs for a resource
|
|
142
|
+
bt entitle roles list --resource <resource_id>
|
|
143
|
+
```
|
|
133
144
|
|
|
134
145
|
## Integrations Available
|
|
135
146
|
|
bt_cli/data/skills/epmw/SKILL.md
CHANGED
|
@@ -118,22 +118,21 @@ bt epmw audits requests list
|
|
|
118
118
|
bt epmw audits requests get <audit_id>
|
|
119
119
|
```
|
|
120
120
|
|
|
121
|
-
##
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
| CorpWS02 | nexusdyn.corp | Workstations | Connected |
|
|
121
|
+
## Discover IDs
|
|
122
|
+
|
|
123
|
+
```bash
|
|
124
|
+
# Find group IDs
|
|
125
|
+
bt epmw groups list
|
|
126
|
+
|
|
127
|
+
# Find computer IDs
|
|
128
|
+
bt epmw computers list
|
|
129
|
+
|
|
130
|
+
# Find policy IDs
|
|
131
|
+
bt epmw policies list
|
|
132
|
+
|
|
133
|
+
# Find user IDs
|
|
134
|
+
bt epmw users list
|
|
135
|
+
```
|
|
137
136
|
|
|
138
137
|
## API Notes
|
|
139
138
|
|
bt_cli/data/skills/pra/SKILL.md
CHANGED
|
@@ -35,28 +35,28 @@ bt pra quick search admin -o json
|
|
|
35
35
|
|
|
36
36
|
```bash
|
|
37
37
|
bt pra jump-items shell list
|
|
38
|
-
bt pra jump-items shell get
|
|
38
|
+
bt pra jump-items shell get <jump_item_id>
|
|
39
39
|
bt pra jump-items shell create \
|
|
40
40
|
--name "web-server-01" \
|
|
41
41
|
--hostname "10.0.1.50" \
|
|
42
|
-
--jumpoint
|
|
43
|
-
--jump-group
|
|
42
|
+
--jumpoint <jumpoint_id> \
|
|
43
|
+
--jump-group <jump_group_id> \
|
|
44
44
|
--protocol ssh \
|
|
45
45
|
--port 22 \
|
|
46
|
-
--username "
|
|
47
|
-
bt pra jump-items shell delete
|
|
46
|
+
--username "admin"
|
|
47
|
+
bt pra jump-items shell delete <jump_item_id>
|
|
48
48
|
```
|
|
49
49
|
|
|
50
50
|
### RDP Jump
|
|
51
51
|
|
|
52
52
|
```bash
|
|
53
53
|
bt pra jump-items rdp list
|
|
54
|
-
bt pra jump-items rdp get
|
|
54
|
+
bt pra jump-items rdp get <jump_item_id>
|
|
55
55
|
bt pra jump-items rdp create \
|
|
56
56
|
--name "win-server-01" \
|
|
57
57
|
--hostname "10.0.2.10" \
|
|
58
|
-
--jumpoint
|
|
59
|
-
--jump-group
|
|
58
|
+
--jumpoint <jumpoint_id> \
|
|
59
|
+
--jump-group <jump_group_id> \
|
|
60
60
|
--port 3389
|
|
61
61
|
```
|
|
62
62
|
|
|
@@ -67,8 +67,8 @@ bt pra jump-items tunnel list
|
|
|
67
67
|
bt pra jump-items tunnel create \
|
|
68
68
|
--name "production-k8s" \
|
|
69
69
|
--hostname "k8s-api.example.com" \
|
|
70
|
-
--jumpoint
|
|
71
|
-
--jump-group
|
|
70
|
+
--jumpoint <jumpoint_id> \
|
|
71
|
+
--jump-group <jump_group_id> \
|
|
72
72
|
--type k8s \
|
|
73
73
|
--url "https://k8s-api.example.com:6443" \
|
|
74
74
|
--ca-cert "$(cat /path/to/ca.crt)"
|
|
@@ -78,25 +78,25 @@ bt pra jump-items tunnel create \
|
|
|
78
78
|
|
|
79
79
|
```bash
|
|
80
80
|
bt pra jump-groups list
|
|
81
|
-
bt pra jump-groups get
|
|
81
|
+
bt pra jump-groups get <group_id>
|
|
82
82
|
bt pra jump-groups create \
|
|
83
|
-
--name "Customer-05
|
|
84
|
-
--code-name
|
|
85
|
-
--comments "
|
|
86
|
-
bt pra jump-groups delete
|
|
83
|
+
--name "Customer-05" \
|
|
84
|
+
--code-name customer05 \
|
|
85
|
+
--comments "New customer environment"
|
|
86
|
+
bt pra jump-groups delete <group_id>
|
|
87
87
|
```
|
|
88
88
|
|
|
89
89
|
## Vault Accounts
|
|
90
90
|
|
|
91
91
|
```bash
|
|
92
92
|
bt pra vault accounts list
|
|
93
|
-
bt pra vault accounts get
|
|
94
|
-
bt pra vault accounts checkout
|
|
95
|
-
bt pra vault accounts checkin
|
|
96
|
-
bt pra vault accounts rotate
|
|
93
|
+
bt pra vault accounts get <account_id>
|
|
94
|
+
bt pra vault accounts checkout <account_id>
|
|
95
|
+
bt pra vault accounts checkin <account_id>
|
|
96
|
+
bt pra vault accounts rotate <account_id>
|
|
97
97
|
|
|
98
98
|
# SSH CA - get public key for provisioning
|
|
99
|
-
bt pra vault accounts get-public-key
|
|
99
|
+
bt pra vault accounts get-public-key <ssh_ca_account_id>
|
|
100
100
|
```
|
|
101
101
|
|
|
102
102
|
## SSH CA Authentication
|
|
@@ -104,21 +104,18 @@ bt pra vault accounts get-public-key 31
|
|
|
104
104
|
PRA supports SSH CA for ephemeral access - no static keys on hosts.
|
|
105
105
|
|
|
106
106
|
```bash
|
|
107
|
+
# Find SSH CA vault accounts
|
|
108
|
+
bt pra vault accounts list | grep -i ssh
|
|
109
|
+
|
|
107
110
|
# Get CA public key (ready for authorized_keys)
|
|
108
|
-
bt pra vault accounts get-public-key
|
|
111
|
+
bt pra vault accounts get-public-key <ssh_ca_account_id>
|
|
109
112
|
# Output: cert-authority ssh-rsa AAAAB3NzaC1yc2E...
|
|
110
113
|
|
|
111
114
|
# Provision EC2 with SSH CA
|
|
112
|
-
PRA_CA_KEY=$(bt pra vault accounts get-public-key
|
|
115
|
+
PRA_CA_KEY=$(bt pra vault accounts get-public-key <ssh_ca_account_id>)
|
|
113
116
|
# Embed in user-data script for EC2
|
|
114
117
|
```
|
|
115
118
|
|
|
116
|
-
**SSH CA Vault Accounts:**
|
|
117
|
-
| ID | Name | Username |
|
|
118
|
-
|----|------|----------|
|
|
119
|
-
| 3 | Ephemeral Admin SSH CA | admin-ephemeral |
|
|
120
|
-
| 31 | ec2-admin | ec2-admin |
|
|
121
|
-
|
|
122
119
|
## CSV Import/Export
|
|
123
120
|
|
|
124
121
|
```bash
|
|
@@ -128,17 +125,21 @@ bt pra import jump-items --file jump-items.csv --dry-run
|
|
|
128
125
|
bt pra import jump-items --file jump-items.csv
|
|
129
126
|
```
|
|
130
127
|
|
|
131
|
-
##
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
128
|
+
## Discover IDs
|
|
129
|
+
|
|
130
|
+
```bash
|
|
131
|
+
# Find jumpoint IDs
|
|
132
|
+
bt pra jumpoint list
|
|
133
|
+
|
|
134
|
+
# Find jump group IDs
|
|
135
|
+
bt pra jump-groups list
|
|
136
|
+
|
|
137
|
+
# Find vault account IDs
|
|
138
|
+
bt pra vault accounts list
|
|
139
|
+
|
|
140
|
+
# Find vault account group IDs
|
|
141
|
+
bt pra vault groups list
|
|
142
|
+
```
|
|
142
143
|
|
|
143
144
|
## API Notes
|
|
144
145
|
|
bt_cli/data/skills/pws/SKILL.md
CHANGED
|
@@ -39,8 +39,9 @@ bt pws quick search root -o json
|
|
|
39
39
|
bt pws quick rotate -s "axion-finapp-01" -a "root"
|
|
40
40
|
|
|
41
41
|
# Onboard system (asset + system + account)
|
|
42
|
-
|
|
43
|
-
bt pws quick onboard -n "
|
|
42
|
+
# First find your workgroup and functional account IDs (see "Discover IDs" below)
|
|
43
|
+
bt pws quick onboard -n "my-server" -i "10.0.1.50" -w <workgroup_id>
|
|
44
|
+
bt pws quick onboard -n "web-01" -i "10.0.1.100" -w <workgroup_id> -f <func_acct_id> -e "sudo"
|
|
44
45
|
|
|
45
46
|
# Offboard system
|
|
46
47
|
bt pws quick offboard -s "my-server"
|
|
@@ -52,15 +53,15 @@ bt pws quick offboard -s "web-01" --force
|
|
|
52
53
|
```bash
|
|
53
54
|
# List systems
|
|
54
55
|
bt pws systems list
|
|
55
|
-
bt pws systems list --workgroup
|
|
56
|
+
bt pws systems list --workgroup <workgroup_id>
|
|
56
57
|
bt pws systems list -o json
|
|
57
58
|
|
|
58
59
|
# Get system details
|
|
59
|
-
bt pws systems get
|
|
60
|
+
bt pws systems get <system_id>
|
|
60
61
|
|
|
61
62
|
# List accounts on a system
|
|
62
|
-
bt pws accounts list --system
|
|
63
|
-
bt pws accounts get
|
|
63
|
+
bt pws accounts list --system <system_id>
|
|
64
|
+
bt pws accounts get <account_id>
|
|
64
65
|
```
|
|
65
66
|
|
|
66
67
|
## Secrets Safe
|
|
@@ -128,13 +129,13 @@ bt pws quick user-entitlements dave
|
|
|
128
129
|
|
|
129
130
|
```bash
|
|
130
131
|
# 1. Find system
|
|
131
|
-
bt pws systems list -o json | jq '.[] | select(.SystemName=="
|
|
132
|
+
bt pws systems list -o json | jq '.[] | select(.SystemName=="my-server")'
|
|
132
133
|
|
|
133
134
|
# 2. Find account
|
|
134
|
-
bt pws accounts list --system
|
|
135
|
+
bt pws accounts list --system <system_id>
|
|
135
136
|
|
|
136
137
|
# 3. Checkout
|
|
137
|
-
bt pws credentials checkout --system "
|
|
138
|
+
bt pws credentials checkout --system "my-server" --account "root"
|
|
138
139
|
|
|
139
140
|
# 4. Get password
|
|
140
141
|
bt pws credentials show <request_id>
|
|
@@ -158,18 +159,18 @@ bt pws import systems --file systems.csv
|
|
|
158
159
|
bt pws import secrets --file secrets.csv
|
|
159
160
|
```
|
|
160
161
|
|
|
161
|
-
##
|
|
162
|
+
## Discover IDs
|
|
162
163
|
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
164
|
+
```bash
|
|
165
|
+
# Find workgroup IDs
|
|
166
|
+
bt pws workgroups list
|
|
167
|
+
|
|
168
|
+
# Find platform IDs
|
|
169
|
+
bt pws platforms list
|
|
170
|
+
|
|
171
|
+
# Find functional account IDs (for auto-management)
|
|
172
|
+
bt pws functional list
|
|
173
|
+
```
|
|
173
174
|
|
|
174
175
|
## EC2 Systems in AWS
|
|
175
176
|
|
|
@@ -179,7 +180,7 @@ When onboarding EC2 instances to Password Safe, use the **internal AWS DNS name*
|
|
|
179
180
|
# Use internal DNS for EC2 systems
|
|
180
181
|
bt pws quick onboard -n "web-prod-01" \
|
|
181
182
|
-i "ip-10-0-12-45.us-east-1.compute.internal" \
|
|
182
|
-
-w
|
|
183
|
+
-w <workgroup_id> -f <func_acct_id>
|
|
183
184
|
|
|
184
185
|
# Or set DNS separately
|
|
185
186
|
bt pws systems update <system_id> --dns "ip-10-0-12-45.us-east-1.compute.internal"
|
bt_cli/epmw/commands/policies.py
CHANGED
|
@@ -288,15 +288,29 @@ def revert_policy(
|
|
|
288
288
|
@app.command("download")
|
|
289
289
|
def download_policy(
|
|
290
290
|
policy_id: str = typer.Argument(..., help="Policy ID (UUID)"),
|
|
291
|
+
file: Optional[str] = typer.Option(None, "--file", "-f", help="Save to file instead of stdout"),
|
|
291
292
|
):
|
|
292
|
-
"""Download policy content (XML format).
|
|
293
|
+
"""Download policy content (XML format).
|
|
294
|
+
|
|
295
|
+
Examples:
|
|
296
|
+
bt epmw policies download <policy_id>
|
|
297
|
+
bt epmw policies download <policy_id> --file policy.xml
|
|
298
|
+
bt epmw policies download <policy_id> > policy.xml
|
|
299
|
+
"""
|
|
293
300
|
from bt_cli.epmw.client import get_client
|
|
294
301
|
|
|
295
302
|
try:
|
|
296
303
|
client = get_client()
|
|
297
304
|
content = client.download_policy(policy_id)
|
|
298
|
-
|
|
299
|
-
|
|
305
|
+
|
|
306
|
+
if file:
|
|
307
|
+
# Write to file
|
|
308
|
+
with open(file, "w", encoding="utf-8") as f:
|
|
309
|
+
f.write(content)
|
|
310
|
+
print_success(f"Policy saved to: {file}")
|
|
311
|
+
else:
|
|
312
|
+
# Output to stdout
|
|
313
|
+
typer.echo(content)
|
|
300
314
|
except httpx.HTTPStatusError as e:
|
|
301
315
|
print_api_error(e, "download policy")
|
|
302
316
|
raise typer.Exit(1)
|
|
@@ -16,10 +16,10 @@ import_app = typer.Typer(no_args_is_help=True, help="Import resources from CSV f
|
|
|
16
16
|
export_app = typer.Typer(no_args_is_help=True, help="Export sample CSV templates")
|
|
17
17
|
console = Console()
|
|
18
18
|
|
|
19
|
-
# Column definitions for CSV formats
|
|
19
|
+
# Column definitions for CSV formats - using NAMES not IDs for readability
|
|
20
20
|
SYSTEMS_COLUMNS = [
|
|
21
|
-
"
|
|
22
|
-
"
|
|
21
|
+
"system_name", "ip_address", "workgroup", "platform", "port",
|
|
22
|
+
"functional_account", "elevation_command", "auto_manage",
|
|
23
23
|
"account_name", "account_password", "account_description"
|
|
24
24
|
]
|
|
25
25
|
|
|
@@ -27,31 +27,36 @@ SECRETS_COLUMNS = [
|
|
|
27
27
|
"folder_path", "title", "username", "password", "description", "notes"
|
|
28
28
|
]
|
|
29
29
|
|
|
30
|
-
# Sample data for templates
|
|
30
|
+
# Sample data for templates - uses names that will be resolved to IDs
|
|
31
|
+
# NOTE: account_password only required if auto_manage=false
|
|
32
|
+
# If auto_manage=true with functional_account, PWS rotates password automatically
|
|
31
33
|
SYSTEMS_SAMPLE = [
|
|
32
34
|
{
|
|
33
|
-
"
|
|
34
|
-
"
|
|
35
|
+
"system_name": "linux-managed-01", "ip_address": "10.0.1.50",
|
|
36
|
+
"workgroup": "Default", # Use workgroup NAME (run: bt pws workgroups list)
|
|
37
|
+
"platform": "Linux SSH", # Use platform NAME (run: bt pws platforms list)
|
|
38
|
+
"port": "22",
|
|
39
|
+
"functional_account": "Linux - Local", # FA DisplayName (run: bt pws functional list)
|
|
35
40
|
"elevation_command": "sudo", "auto_manage": "true",
|
|
36
|
-
"account_name": "root", "account_password": "", "account_description": "
|
|
41
|
+
"account_name": "root", "account_password": "", "account_description": "Auto-managed root"
|
|
37
42
|
},
|
|
38
43
|
{
|
|
39
|
-
"
|
|
40
|
-
"
|
|
41
|
-
"elevation_command": "sudo", "auto_manage": "true",
|
|
42
|
-
"account_name": "
|
|
44
|
+
"system_name": "linux-managed-01", "ip_address": "10.0.1.50",
|
|
45
|
+
"workgroup": "Default", "platform": "Linux SSH", "port": "22",
|
|
46
|
+
"functional_account": "Linux - Local", "elevation_command": "sudo", "auto_manage": "true",
|
|
47
|
+
"account_name": "appuser", "account_password": "", "account_description": "Auto-managed app account"
|
|
43
48
|
},
|
|
44
49
|
{
|
|
45
|
-
"
|
|
46
|
-
"
|
|
47
|
-
"elevation_command": "
|
|
48
|
-
"account_name": "
|
|
50
|
+
"system_name": "linux-manual-01", "ip_address": "10.0.1.60",
|
|
51
|
+
"workgroup": "Default", "platform": "Linux SSH", "port": "22",
|
|
52
|
+
"functional_account": "", "elevation_command": "", "auto_manage": "false",
|
|
53
|
+
"account_name": "dbadmin", "account_password": "InitialP@ss123!", "account_description": "Manual - password required"
|
|
49
54
|
},
|
|
50
55
|
{
|
|
51
|
-
"
|
|
52
|
-
"
|
|
53
|
-
"elevation_command": "", "auto_manage": "false",
|
|
54
|
-
"account_name": "Administrator", "account_password": "
|
|
56
|
+
"system_name": "win-manual-01", "ip_address": "10.0.2.10",
|
|
57
|
+
"workgroup": "Default", "platform": "Windows", "port": "5985",
|
|
58
|
+
"functional_account": "", "elevation_command": "", "auto_manage": "false",
|
|
59
|
+
"account_name": "Administrator", "account_password": "WinP@ss456!", "account_description": "Manual - password required"
|
|
55
60
|
},
|
|
56
61
|
]
|
|
57
62
|
|
|
@@ -74,25 +79,89 @@ SECRETS_SAMPLE = [
|
|
|
74
79
|
]
|
|
75
80
|
|
|
76
81
|
|
|
82
|
+
def _resolve_name_to_id(name: str, lookup: dict, resource_type: str) -> Optional[int]:
|
|
83
|
+
"""Resolve a name to ID using case-insensitive partial matching."""
|
|
84
|
+
if not name:
|
|
85
|
+
return None
|
|
86
|
+
|
|
87
|
+
# Try exact match first (case-insensitive)
|
|
88
|
+
name_lower = name.lower().strip()
|
|
89
|
+
for key, value in lookup.items():
|
|
90
|
+
if key.lower() == name_lower:
|
|
91
|
+
return value
|
|
92
|
+
|
|
93
|
+
# Try partial match (name contains search term)
|
|
94
|
+
for key, value in lookup.items():
|
|
95
|
+
if name_lower in key.lower():
|
|
96
|
+
return value
|
|
97
|
+
|
|
98
|
+
return None
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def _build_folder_path_map(safes: list, folders: list) -> dict[str, str]:
|
|
102
|
+
"""Build a map of folder paths to folder IDs.
|
|
103
|
+
|
|
104
|
+
Safes are top-level containers, folders can be nested.
|
|
105
|
+
Returns a dict like {"SafeName/FolderName": "folder-guid", ...}
|
|
106
|
+
"""
|
|
107
|
+
# Build ID -> object lookups
|
|
108
|
+
safe_by_id = {s.get("Id"): s for s in safes}
|
|
109
|
+
folder_by_id = {f.get("Id"): f for f in folders}
|
|
110
|
+
|
|
111
|
+
# Build path for each folder
|
|
112
|
+
path_map: dict[str, str] = {}
|
|
113
|
+
|
|
114
|
+
def get_path(folder_id: str) -> str:
|
|
115
|
+
"""Recursively build path for a folder."""
|
|
116
|
+
if folder_id in safe_by_id:
|
|
117
|
+
# It's a safe (top-level)
|
|
118
|
+
return safe_by_id[folder_id].get("Name", "")
|
|
119
|
+
|
|
120
|
+
folder = folder_by_id.get(folder_id)
|
|
121
|
+
if not folder:
|
|
122
|
+
return ""
|
|
123
|
+
|
|
124
|
+
parent_id = folder.get("ParentId")
|
|
125
|
+
folder_name = folder.get("Name", "")
|
|
126
|
+
|
|
127
|
+
if parent_id:
|
|
128
|
+
parent_path = get_path(parent_id)
|
|
129
|
+
if parent_path:
|
|
130
|
+
return f"{parent_path}/{folder_name}"
|
|
131
|
+
|
|
132
|
+
return folder_name
|
|
133
|
+
|
|
134
|
+
# Map all folders
|
|
135
|
+
for folder in folders:
|
|
136
|
+
folder_id = folder.get("Id")
|
|
137
|
+
path = get_path(folder_id)
|
|
138
|
+
if path:
|
|
139
|
+
path_map[path.lower()] = folder_id
|
|
140
|
+
|
|
141
|
+
return path_map
|
|
142
|
+
|
|
143
|
+
|
|
77
144
|
@import_app.command("systems")
|
|
78
145
|
def import_systems(
|
|
79
146
|
file: str = typer.Option(..., "--file", "-f", help="CSV file path"),
|
|
80
147
|
dry_run: bool = typer.Option(False, "--dry-run", help="Validate without creating"),
|
|
81
|
-
|
|
148
|
+
workgroup_override: Optional[str] = typer.Option(None, "--workgroup", "-w", help="Override workgroup for all rows (name or ID)"),
|
|
82
149
|
) -> None:
|
|
83
150
|
"""Import managed systems and accounts from CSV.
|
|
84
151
|
|
|
85
152
|
Each row creates a system + account. Multiple accounts per system use
|
|
86
153
|
multiple rows with the same system name (system is created only once).
|
|
87
154
|
|
|
88
|
-
|
|
89
|
-
|
|
155
|
+
Uses NAMES for workgroup, platform, functional_account - resolved to IDs automatically.
|
|
156
|
+
|
|
157
|
+
Required columns: system_name, ip_address, workgroup, account_name
|
|
158
|
+
Optional: platform, port, functional_account, elevation_command,
|
|
90
159
|
auto_manage, account_password, account_description
|
|
91
160
|
|
|
92
161
|
Examples:
|
|
93
162
|
bt pws import systems --file systems.csv --dry-run
|
|
94
163
|
bt pws import systems --file systems.csv
|
|
95
|
-
bt pws import systems --file systems.csv --workgroup
|
|
164
|
+
bt pws import systems --file systems.csv --workgroup "Default"
|
|
96
165
|
"""
|
|
97
166
|
try:
|
|
98
167
|
rows = read_csv(file)
|
|
@@ -102,73 +171,148 @@ def import_systems(
|
|
|
102
171
|
|
|
103
172
|
console.print(f"[dim]Read {len(rows)} rows from {file}[/dim]")
|
|
104
173
|
|
|
105
|
-
#
|
|
106
|
-
errors = []
|
|
107
|
-
required = ["name", "ip_address", "account_name"]
|
|
108
|
-
if not workgroup:
|
|
109
|
-
required.append("workgroup_id")
|
|
110
|
-
|
|
111
|
-
for i, row in enumerate(rows, 1):
|
|
112
|
-
row_errors = validate_required_fields(row, required, i)
|
|
113
|
-
errors.extend(row_errors)
|
|
114
|
-
|
|
115
|
-
if errors:
|
|
116
|
-
print_error("Validation errors:")
|
|
117
|
-
for err in errors[:20]:
|
|
118
|
-
console.print(f" [red]{err}[/red]")
|
|
119
|
-
if len(errors) > 20:
|
|
120
|
-
console.print(f" [red]... and {len(errors) - 20} more errors[/red]")
|
|
121
|
-
raise typer.Exit(1)
|
|
122
|
-
|
|
123
|
-
# Group rows by system name
|
|
124
|
-
systems_rows: dict[str, list[dict]] = {}
|
|
125
|
-
for row in rows:
|
|
126
|
-
name = row["name"].strip()
|
|
127
|
-
if name not in systems_rows:
|
|
128
|
-
systems_rows[name] = []
|
|
129
|
-
systems_rows[name].append(row)
|
|
130
|
-
|
|
131
|
-
console.print(f"[dim]Found {len(systems_rows)} unique systems with {len(rows)} total accounts[/dim]")
|
|
132
|
-
|
|
133
|
-
if dry_run:
|
|
134
|
-
console.print("\n[yellow]DRY RUN - No changes will be made[/yellow]\n")
|
|
135
|
-
table = Table(title="Systems to Create")
|
|
136
|
-
table.add_column("System", style="green")
|
|
137
|
-
table.add_column("IP", style="cyan")
|
|
138
|
-
table.add_column("Workgroup", style="yellow")
|
|
139
|
-
table.add_column("Platform", style="magenta")
|
|
140
|
-
table.add_column("Accounts", style="blue")
|
|
141
|
-
|
|
142
|
-
for name, sys_rows in systems_rows.items():
|
|
143
|
-
first = sys_rows[0]
|
|
144
|
-
wg = workgroup or parse_int(first.get("workgroup_id", ""))
|
|
145
|
-
accounts = ", ".join(r["account_name"] for r in sys_rows)
|
|
146
|
-
table.add_row(
|
|
147
|
-
name,
|
|
148
|
-
first.get("ip_address", ""),
|
|
149
|
-
str(wg),
|
|
150
|
-
first.get("platform_id", "2"),
|
|
151
|
-
accounts
|
|
152
|
-
)
|
|
153
|
-
|
|
154
|
-
console.print(table)
|
|
155
|
-
console.print(f"\n[dim]Would create {len(systems_rows)} systems with {len(rows)} accounts[/dim]")
|
|
156
|
-
return
|
|
157
|
-
|
|
158
|
-
# Actually import
|
|
174
|
+
# Connect and build lookup tables for name resolution
|
|
159
175
|
with get_client() as client:
|
|
160
176
|
client.authenticate()
|
|
161
177
|
|
|
178
|
+
console.print("[dim]Loading workgroups, platforms, functional accounts...[/dim]")
|
|
179
|
+
|
|
180
|
+
# Build name -> ID lookup tables
|
|
181
|
+
workgroups = client.list_workgroups()
|
|
182
|
+
workgroup_lookup = {w.get("Name", ""): w.get("ID") for w in workgroups}
|
|
183
|
+
|
|
184
|
+
platforms = client.list_platforms()
|
|
185
|
+
platform_lookup = {p.get("Name", ""): p.get("PlatformID") for p in platforms}
|
|
186
|
+
|
|
187
|
+
func_accounts = client.list_functional_accounts()
|
|
188
|
+
# Use DisplayName as the primary lookup key, also add AccountName as fallback
|
|
189
|
+
func_acct_lookup = {}
|
|
190
|
+
for f in func_accounts:
|
|
191
|
+
fa_id = f.get("FunctionalAccountID")
|
|
192
|
+
# Primary: DisplayName (e.g., "Linux - Local")
|
|
193
|
+
if f.get("DisplayName"):
|
|
194
|
+
func_acct_lookup[f["DisplayName"]] = fa_id
|
|
195
|
+
# Fallback: AccountName (e.g., "svc_passwordsafe")
|
|
196
|
+
if f.get("AccountName"):
|
|
197
|
+
func_acct_lookup[f["AccountName"]] = fa_id
|
|
198
|
+
|
|
199
|
+
# Resolve workgroup override
|
|
200
|
+
wg_override_id = None
|
|
201
|
+
if workgroup_override:
|
|
202
|
+
# Try as ID first
|
|
203
|
+
try:
|
|
204
|
+
wg_override_id = int(workgroup_override)
|
|
205
|
+
except ValueError:
|
|
206
|
+
wg_override_id = _resolve_name_to_id(workgroup_override, workgroup_lookup, "workgroup")
|
|
207
|
+
if not wg_override_id:
|
|
208
|
+
print_error(f"Workgroup not found: {workgroup_override}")
|
|
209
|
+
console.print("[dim]Available workgroups:[/dim]")
|
|
210
|
+
for name in workgroup_lookup.keys():
|
|
211
|
+
console.print(f" - {name}")
|
|
212
|
+
raise typer.Exit(1)
|
|
213
|
+
|
|
214
|
+
# Validate all rows first
|
|
215
|
+
errors = []
|
|
216
|
+
# Support both old column names (backward compat) and new names
|
|
217
|
+
name_col = "system_name" if "system_name" in rows[0] else "name"
|
|
218
|
+
wg_col = "workgroup" if "workgroup" in rows[0] else "workgroup_id"
|
|
219
|
+
|
|
220
|
+
required = [name_col, "ip_address", "account_name"]
|
|
221
|
+
if not wg_override_id:
|
|
222
|
+
required.append(wg_col)
|
|
223
|
+
|
|
224
|
+
for i, row in enumerate(rows, 1):
|
|
225
|
+
row_errors = validate_required_fields(row, required, i)
|
|
226
|
+
errors.extend(row_errors)
|
|
227
|
+
|
|
228
|
+
if errors:
|
|
229
|
+
print_error("Validation errors:")
|
|
230
|
+
for err in errors[:20]:
|
|
231
|
+
console.print(f" [red]{err}[/red]")
|
|
232
|
+
if len(errors) > 20:
|
|
233
|
+
console.print(f" [red]... and {len(errors) - 20} more errors[/red]")
|
|
234
|
+
raise typer.Exit(1)
|
|
235
|
+
|
|
236
|
+
# Group rows by system name
|
|
237
|
+
systems_rows: dict[str, list[dict]] = {}
|
|
238
|
+
for row in rows:
|
|
239
|
+
name = row.get(name_col, "").strip()
|
|
240
|
+
if name not in systems_rows:
|
|
241
|
+
systems_rows[name] = []
|
|
242
|
+
systems_rows[name].append(row)
|
|
243
|
+
|
|
244
|
+
console.print(f"[dim]Found {len(systems_rows)} unique systems with {len(rows)} total accounts[/dim]")
|
|
245
|
+
|
|
246
|
+
if dry_run:
|
|
247
|
+
console.print("\n[yellow]DRY RUN - No changes will be made[/yellow]\n")
|
|
248
|
+
table = Table(title="Systems to Create")
|
|
249
|
+
table.add_column("System", style="green")
|
|
250
|
+
table.add_column("IP", style="cyan")
|
|
251
|
+
table.add_column("Workgroup", style="yellow")
|
|
252
|
+
table.add_column("Platform", style="magenta")
|
|
253
|
+
table.add_column("Accounts", style="blue")
|
|
254
|
+
|
|
255
|
+
for name, sys_rows in systems_rows.items():
|
|
256
|
+
first = sys_rows[0]
|
|
257
|
+
wg_name = first.get(wg_col, "")
|
|
258
|
+
plat_name = first.get("platform", first.get("platform_id", "Linux"))
|
|
259
|
+
accounts = ", ".join(r["account_name"] for r in sys_rows)
|
|
260
|
+
table.add_row(name, first.get("ip_address", ""), wg_name, plat_name, accounts)
|
|
261
|
+
|
|
262
|
+
console.print(table)
|
|
263
|
+
console.print(f"\n[dim]Would create {len(systems_rows)} systems with {len(rows)} accounts[/dim]")
|
|
264
|
+
return
|
|
265
|
+
|
|
266
|
+
# Actually import
|
|
162
267
|
created_systems = 0
|
|
163
268
|
created_accounts = 0
|
|
164
|
-
system_ids: dict[str, int] = {}
|
|
269
|
+
system_ids: dict[str, int] = {}
|
|
165
270
|
|
|
166
271
|
for name, sys_rows in systems_rows.items():
|
|
167
272
|
first = sys_rows[0]
|
|
168
|
-
|
|
273
|
+
|
|
274
|
+
# Resolve workgroup
|
|
275
|
+
if wg_override_id:
|
|
276
|
+
wg_id = wg_override_id
|
|
277
|
+
else:
|
|
278
|
+
wg_val = first.get(wg_col, "").strip()
|
|
279
|
+
try:
|
|
280
|
+
wg_id = int(wg_val)
|
|
281
|
+
except ValueError:
|
|
282
|
+
wg_id = _resolve_name_to_id(wg_val, workgroup_lookup, "workgroup")
|
|
169
283
|
|
|
170
284
|
if not wg_id:
|
|
171
|
-
print_warning(f"Skipping {name}:
|
|
285
|
+
print_warning(f"Skipping {name}: Could not resolve workgroup '{first.get(wg_col, '')}'")
|
|
286
|
+
continue
|
|
287
|
+
|
|
288
|
+
# Resolve platform
|
|
289
|
+
plat_val = first.get("platform", first.get("platform_id", "")).strip()
|
|
290
|
+
try:
|
|
291
|
+
platform_id = int(plat_val) if plat_val else 2
|
|
292
|
+
except ValueError:
|
|
293
|
+
platform_id = _resolve_name_to_id(plat_val, platform_lookup, "platform") or 2
|
|
294
|
+
|
|
295
|
+
# Resolve functional account
|
|
296
|
+
func_val = first.get("functional_account", first.get("functional_account_id", "")).strip()
|
|
297
|
+
func_acct_id = None
|
|
298
|
+
if func_val:
|
|
299
|
+
try:
|
|
300
|
+
func_acct_id = int(func_val)
|
|
301
|
+
except ValueError:
|
|
302
|
+
func_acct_id = _resolve_name_to_id(func_val, func_acct_lookup, "functional account")
|
|
303
|
+
|
|
304
|
+
# Get auto_manage flag early for validation
|
|
305
|
+
auto_manage = parse_bool(first.get("auto_manage", ""))
|
|
306
|
+
|
|
307
|
+
# Validate auto_manage + functional account combination
|
|
308
|
+
if auto_manage and func_val and not func_acct_id:
|
|
309
|
+
print_warning(f"Skipping {name}: auto_manage=true but functional account '{func_val}' not found")
|
|
310
|
+
console.print("[dim]Available functional accounts:[/dim]")
|
|
311
|
+
for fa_name in func_acct_lookup.keys():
|
|
312
|
+
console.print(f" - {fa_name}")
|
|
313
|
+
continue
|
|
314
|
+
if auto_manage and not func_val:
|
|
315
|
+
print_warning(f"Skipping {name}: auto_manage=true requires a functional_account")
|
|
172
316
|
continue
|
|
173
317
|
|
|
174
318
|
try:
|
|
@@ -183,10 +327,7 @@ def import_systems(
|
|
|
183
327
|
|
|
184
328
|
# Create managed system
|
|
185
329
|
console.print(f"[dim]Creating managed system '{name}'...[/dim]")
|
|
186
|
-
platform_id = parse_int(first.get("platform_id", ""), 2)
|
|
187
330
|
port = parse_int(first.get("port", ""), 22)
|
|
188
|
-
func_acct = parse_int(first.get("functional_account_id", ""))
|
|
189
|
-
auto_manage = parse_bool(first.get("auto_manage", ""))
|
|
190
331
|
elevation = first.get("elevation_command", "").strip() or None
|
|
191
332
|
|
|
192
333
|
system = client.create_managed_system(
|
|
@@ -194,8 +335,8 @@ def import_systems(
|
|
|
194
335
|
platform_id=platform_id,
|
|
195
336
|
asset_id=asset_id,
|
|
196
337
|
port=port,
|
|
197
|
-
functional_account_id=
|
|
198
|
-
auto_management_flag=auto_manage if
|
|
338
|
+
functional_account_id=func_acct_id,
|
|
339
|
+
auto_management_flag=auto_manage if func_acct_id else False,
|
|
199
340
|
elevation_command=elevation,
|
|
200
341
|
)
|
|
201
342
|
system_id = system.get("ManagedSystemID")
|
|
@@ -214,7 +355,7 @@ def import_systems(
|
|
|
214
355
|
system_id=system_id,
|
|
215
356
|
account_name=account_name,
|
|
216
357
|
password=password,
|
|
217
|
-
auto_management_flag=auto_manage if
|
|
358
|
+
auto_management_flag=auto_manage if func_acct_id else False,
|
|
218
359
|
)
|
|
219
360
|
account_id = account.get("ManagedAccountID")
|
|
220
361
|
created_accounts += 1
|
|
@@ -301,13 +442,11 @@ def import_secrets(
|
|
|
301
442
|
with get_client() as client:
|
|
302
443
|
client.authenticate()
|
|
303
444
|
|
|
304
|
-
#
|
|
445
|
+
# Build folder path -> ID map from safes and folders
|
|
446
|
+
console.print("[dim]Loading safes and folders...[/dim]")
|
|
447
|
+
safes = client.list_safes()
|
|
305
448
|
all_folders = client.list_folders()
|
|
306
|
-
folder_map
|
|
307
|
-
for f in all_folders:
|
|
308
|
-
path = f.get("FolderPath", "") or f.get("Name", "")
|
|
309
|
-
if path:
|
|
310
|
-
folder_map[path.lower()] = f.get("Id")
|
|
449
|
+
folder_map = _build_folder_path_map(safes, all_folders)
|
|
311
450
|
|
|
312
451
|
created = 0
|
|
313
452
|
for row in rows:
|
|
@@ -318,6 +457,11 @@ def import_secrets(
|
|
|
318
457
|
folder_id = folder_map.get(folder_path.lower())
|
|
319
458
|
if not folder_id:
|
|
320
459
|
print_warning(f"Folder not found: {folder_path}, skipping {title}")
|
|
460
|
+
console.print("[dim]Available folder paths:[/dim]")
|
|
461
|
+
for path in sorted(folder_map.keys())[:10]:
|
|
462
|
+
console.print(f" - {path}")
|
|
463
|
+
if len(folder_map) > 10:
|
|
464
|
+
console.print(f" ... and {len(folder_map) - 10} more")
|
|
321
465
|
continue
|
|
322
466
|
|
|
323
467
|
try:
|
|
@@ -355,22 +499,46 @@ def export_systems(
|
|
|
355
499
|
file: str = typer.Option("pws-systems-template.csv", "--file", "-f", help="Output file path"),
|
|
356
500
|
sample: bool = typer.Option(True, "--sample/--no-sample", help="Export sample template (default) or current data"),
|
|
357
501
|
) -> None:
|
|
358
|
-
"""Export
|
|
502
|
+
"""Export systems CSV template or current data.
|
|
503
|
+
|
|
504
|
+
Uses human-readable NAMES for workgroup, platform, functional_account.
|
|
359
505
|
|
|
360
506
|
Examples:
|
|
361
507
|
bt pws export systems --file systems-template.csv
|
|
362
|
-
bt pws export systems --file systems
|
|
508
|
+
bt pws export systems --file current-systems.csv --no-sample
|
|
363
509
|
"""
|
|
364
510
|
try:
|
|
365
511
|
if sample:
|
|
366
512
|
write_csv(file, SYSTEMS_SAMPLE, SYSTEMS_COLUMNS)
|
|
367
513
|
print_success(f"Sample systems template exported to: {file}")
|
|
368
514
|
console.print(f"[dim]Contains {len(SYSTEMS_SAMPLE)} example rows[/dim]")
|
|
515
|
+
console.print("\n[dim]Column descriptions:[/dim]")
|
|
516
|
+
console.print(" system_name - Unique name for the managed system")
|
|
517
|
+
console.print(" ip_address - IP or hostname")
|
|
518
|
+
console.print(" workgroup - Workgroup NAME (run: bt pws workgroups list)")
|
|
519
|
+
console.print(" platform - Platform NAME, partial match OK (run: bt pws platforms list)")
|
|
520
|
+
console.print(" port - Connection port (22 for SSH, 5985 for WinRM)")
|
|
521
|
+
console.print(" functional_account - Functional account NAME for auto-management (bt pws functional list)")
|
|
522
|
+
console.print(" elevation_command - 'sudo' or other elevation command (optional)")
|
|
523
|
+
console.print(" auto_manage - true=PWS rotates password, false=manual management")
|
|
524
|
+
console.print(" account_name - Account username to manage")
|
|
525
|
+
console.print(" account_password - Required if auto_manage=false, empty if auto_manage=true")
|
|
526
|
+
console.print(" account_description- Description for the account")
|
|
369
527
|
else:
|
|
370
|
-
# Export actual data from API
|
|
528
|
+
# Export actual data from API with names resolved
|
|
371
529
|
with get_client() as client:
|
|
372
530
|
client.authenticate()
|
|
373
531
|
|
|
532
|
+
# Build ID -> name lookup tables
|
|
533
|
+
workgroups = client.list_workgroups()
|
|
534
|
+
wg_names = {w.get("ID"): w.get("Name", "") for w in workgroups}
|
|
535
|
+
|
|
536
|
+
platforms = client.list_platforms()
|
|
537
|
+
plat_names = {p.get("PlatformID"): p.get("Name", "") for p in platforms}
|
|
538
|
+
|
|
539
|
+
func_accounts = client.list_functional_accounts()
|
|
540
|
+
func_names = {f.get("FunctionalAccountID"): f.get("FunctionalAccountName", "") for f in func_accounts}
|
|
541
|
+
|
|
374
542
|
systems = client.list_managed_systems()
|
|
375
543
|
rows = []
|
|
376
544
|
|
|
@@ -378,14 +546,18 @@ def export_systems(
|
|
|
378
546
|
system_id = sys.get("ManagedSystemID")
|
|
379
547
|
accounts = client.list_managed_accounts(system_id=system_id)
|
|
380
548
|
|
|
549
|
+
wg_id = sys.get("WorkgroupID")
|
|
550
|
+
plat_id = sys.get("PlatformID")
|
|
551
|
+
func_id = sys.get("FunctionalAccountID")
|
|
552
|
+
|
|
381
553
|
for acc in accounts:
|
|
382
554
|
rows.append({
|
|
383
|
-
"
|
|
555
|
+
"system_name": sys.get("SystemName", ""),
|
|
384
556
|
"ip_address": sys.get("IPAddress", ""),
|
|
385
|
-
"
|
|
386
|
-
"
|
|
557
|
+
"workgroup": wg_names.get(wg_id, str(wg_id or "")),
|
|
558
|
+
"platform": plat_names.get(plat_id, str(plat_id or "")),
|
|
387
559
|
"port": str(sys.get("Port", "")),
|
|
388
|
-
"
|
|
560
|
+
"functional_account": func_names.get(func_id, "") if func_id else "",
|
|
389
561
|
"elevation_command": sys.get("ElevationCommand", "") or "",
|
|
390
562
|
"auto_manage": "true" if sys.get("AutoManagementFlag") else "false",
|
|
391
563
|
"account_name": acc.get("AccountName", ""),
|
|
@@ -419,22 +591,25 @@ def export_secrets(
|
|
|
419
591
|
write_csv(file, SECRETS_SAMPLE, SECRETS_COLUMNS)
|
|
420
592
|
print_success(f"Sample secrets template exported to: {file}")
|
|
421
593
|
console.print(f"[dim]Contains {len(SECRETS_SAMPLE)} example rows[/dim]")
|
|
594
|
+
console.print("\n[dim]Column descriptions:[/dim]")
|
|
595
|
+
console.print(" folder_path - Full path: SafeName/FolderName (run: bt pws secrets folders list)")
|
|
596
|
+
console.print(" title - Unique title for the secret")
|
|
597
|
+
console.print(" username - Username/identifier (optional)")
|
|
598
|
+
console.print(" password - Password/secret value")
|
|
599
|
+
console.print(" description - Description of the secret")
|
|
600
|
+
console.print(" notes - Additional notes (can be JSON)")
|
|
422
601
|
else:
|
|
423
602
|
# Export actual data from API
|
|
424
603
|
with get_client() as client:
|
|
425
604
|
client.authenticate()
|
|
426
605
|
|
|
427
606
|
secrets = client.list_secrets()
|
|
428
|
-
folders = client.list_folders()
|
|
429
|
-
|
|
430
|
-
# Map folder IDs to paths
|
|
431
|
-
folder_paths = {f.get("Id"): f.get("FolderPath", "") or f.get("Name", "") for f in folders}
|
|
432
607
|
|
|
433
608
|
rows = []
|
|
434
609
|
for s in secrets:
|
|
435
|
-
|
|
610
|
+
# Secrets have FolderPath directly (e.g., "SafeName/FolderName")
|
|
436
611
|
rows.append({
|
|
437
|
-
"folder_path":
|
|
612
|
+
"folder_path": s.get("FolderPath", "") or s.get("Folder", ""),
|
|
438
613
|
"title": s.get("Title", ""),
|
|
439
614
|
"username": s.get("Username", "") or "",
|
|
440
615
|
"password": "", # Don't export passwords
|
bt_cli/pws/commands/secrets.py
CHANGED
|
@@ -568,7 +568,7 @@ def print_secrets_table(secrets: list[dict], title: str = "Secrets") -> None:
|
|
|
568
568
|
table.add_column("ID", style="cyan", no_wrap=True, max_width=36)
|
|
569
569
|
table.add_column("Title", style="green")
|
|
570
570
|
table.add_column("Username", style="yellow")
|
|
571
|
-
table.add_column("Folder", style="blue")
|
|
571
|
+
table.add_column("Folder Path", style="blue")
|
|
572
572
|
table.add_column("Modified", style="dim")
|
|
573
573
|
|
|
574
574
|
for secret in secrets:
|
|
@@ -576,7 +576,7 @@ def print_secrets_table(secrets: list[dict], title: str = "Secrets") -> None:
|
|
|
576
576
|
secret.get("Id", "")[:36],
|
|
577
577
|
secret.get("Title", ""),
|
|
578
578
|
secret.get("Username", "-"),
|
|
579
|
-
secret.get("
|
|
579
|
+
secret.get("FolderPath", secret.get("Folder", "-")),
|
|
580
580
|
str(secret.get("ModifiedOn", "-"))[:19],
|
|
581
581
|
)
|
|
582
582
|
|
|
@@ -653,7 +653,7 @@ def get_secret(
|
|
|
653
653
|
console.print(f" Password: {secret.get('Password', '-')}")
|
|
654
654
|
else:
|
|
655
655
|
console.print(" Password: ********")
|
|
656
|
-
console.print(f" Folder: {secret.get('
|
|
656
|
+
console.print(f" Folder Path: {secret.get('FolderPath', secret.get('Folder', '-'))}")
|
|
657
657
|
console.print(f" Description: {secret.get('Description', '-') or '-'}")
|
|
658
658
|
if secret.get("Notes"):
|
|
659
659
|
console.print(f" Notes: {secret.get('Notes')}")
|
|
@@ -961,7 +961,7 @@ def get_text_secret(
|
|
|
961
961
|
else:
|
|
962
962
|
console.print(f"\n[bold cyan]Text Secret: {secret.get('Title', 'Unknown')}[/bold cyan]\n")
|
|
963
963
|
console.print(f" ID: {secret.get('Id', 'N/A')}")
|
|
964
|
-
console.print(f" Folder: {secret.get('
|
|
964
|
+
console.print(f" Folder Path: {secret.get('FolderPath', secret.get('Folder', '-'))}")
|
|
965
965
|
console.print(f" Description: {secret.get('Description', '-') or '-'}")
|
|
966
966
|
if show_text:
|
|
967
967
|
console.print(f" Text Content:\n{secret.get('Password', '-')}")
|
|
@@ -1065,7 +1065,7 @@ def get_file_secret(
|
|
|
1065
1065
|
console.print(f"\n[bold cyan]File Secret: {secret.get('Title', 'Unknown')}[/bold cyan]\n")
|
|
1066
1066
|
console.print(f" ID: {secret.get('Id', 'N/A')}")
|
|
1067
1067
|
console.print(f" FileName: {secret.get('FileName', '-')}")
|
|
1068
|
-
console.print(f" Folder: {secret.get('
|
|
1068
|
+
console.print(f" Folder Path: {secret.get('FolderPath', secret.get('Folder', '-'))}")
|
|
1069
1069
|
console.print(f" Description: {secret.get('Description', '-') or '-'}")
|
|
1070
1070
|
console.print(f" Created: {secret.get('CreatedOn', '-')}")
|
|
1071
1071
|
console.print(f" Modified: {secret.get('ModifiedOn', '-')}")
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
bt_cli/__init__.py,sha256=
|
|
1
|
+
bt_cli/__init__.py,sha256=2Ec_fVFIfrTcwoI2dlXG3AashEKBkbaNUp9YUdyZiOw,61
|
|
2
2
|
bt_cli/cli.py,sha256=-V7Q_DcP7Ryw-mRBLMyj-iwlFpof8_eLgFAx-fN5seM,29640
|
|
3
3
|
bt_cli/commands/__init__.py,sha256=Wrf3ZV1sf7JCilbv93VqoWWTyj0d-y4saAaVFD5apU8,38
|
|
4
4
|
bt_cli/commands/configure.py,sha256=f3tn09eRDqlGQIq1gpuxj984S4CARYbmKI4XrqxPAAM,14270
|
|
@@ -14,13 +14,13 @@ bt_cli/core/errors.py,sha256=-LWipqdn8w-FMAC1W1jFbTMAlooiJqmyuR5j1VWMPyM,8783
|
|
|
14
14
|
bt_cli/core/output.py,sha256=ACazDf3XAfJhAlns5fh6bQXkRIIGlBFuq1z2M-IBrwQ,5156
|
|
15
15
|
bt_cli/core/prompts.py,sha256=VeB8on5VN_D_4r85rJBO6AP_yHJUxBVwWqwZHCoOiqs,2387
|
|
16
16
|
bt_cli/core/rest_debug.py,sha256=7KsqsrsPXUqaYLePjZbwnoqMcg5ivJgYGS35ygaeLfM,5973
|
|
17
|
-
bt_cli/data/CLAUDE.md,sha256=
|
|
17
|
+
bt_cli/data/CLAUDE.md,sha256=3Y_afs1RGTKLkn4Vr9wMKiReIr4EEtBdjFrqSs2NymQ,3228
|
|
18
18
|
bt_cli/data/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
19
|
-
bt_cli/data/skills/bt/SKILL.md,sha256=
|
|
20
|
-
bt_cli/data/skills/entitle/SKILL.md,sha256=
|
|
21
|
-
bt_cli/data/skills/epmw/SKILL.md,sha256=
|
|
22
|
-
bt_cli/data/skills/pra/SKILL.md,sha256
|
|
23
|
-
bt_cli/data/skills/pws/SKILL.md,sha256=
|
|
19
|
+
bt_cli/data/skills/bt/SKILL.md,sha256=DZTvRtSYjiDxDbg7WTpgklYs5Dn-2Z-9w3X8RQJbGNo,3077
|
|
20
|
+
bt_cli/data/skills/entitle/SKILL.md,sha256=yOHP9U_1vKh1yLmrAqKl4rNo16n4f3hia0xj_ZJRRYM,4413
|
|
21
|
+
bt_cli/data/skills/epmw/SKILL.md,sha256=brfIXR9MnB5_FwRa-SyX1TwqVRyefgtr2foQfErpVVA,3541
|
|
22
|
+
bt_cli/data/skills/pra/SKILL.md,sha256=-LTUJvjvLvZn0rMmy0KOeEBtUsotjbHvXX3RCk7mDBw,3749
|
|
23
|
+
bt_cli/data/skills/pws/SKILL.md,sha256=lrOIN06hVrbHfph5rdu78W5N4UKzjrjuHxcJG4eK5hA,5401
|
|
24
24
|
bt_cli/entitle/__init__.py,sha256=l2fasFHO_0zgCS1FgmGdAikumU8QimpaFD9Gvh3Jyqg,30
|
|
25
25
|
bt_cli/entitle/client/__init__.py,sha256=kIQohXlEFGrRAyVWMO9GdESrnAg5J4dmoyJQ6q9NxSo,114
|
|
26
26
|
bt_cli/entitle/client/base.py,sha256=Jov6v7iM8DZ6o_DL7esBpdnYyM-WVFzI8u_0Bjh9MgA,14623
|
|
@@ -55,7 +55,7 @@ bt_cli/epmw/commands/auth.py,sha256=2oC3h01_eh4XyEvsar_ffJAqGKXGE_PrhHQJWFswlN8,
|
|
|
55
55
|
bt_cli/epmw/commands/computers.py,sha256=K0eBd8HOaVCe8hhHPlUuqCrZduhPuvwlkxdYt6YjJxM,4146
|
|
56
56
|
bt_cli/epmw/commands/events.py,sha256=not-9_poxQWRBB4sqZSzJo6031A3IZTdg6uAl2Agk2g,7707
|
|
57
57
|
bt_cli/epmw/commands/groups.py,sha256=PaMcjqfr2b3xS3W3V2A9CFTlMOAs38UmJbi5LKXbbg0,6759
|
|
58
|
-
bt_cli/epmw/commands/policies.py,sha256=
|
|
58
|
+
bt_cli/epmw/commands/policies.py,sha256=0jFQ-ZLzTAC6rPbMWL3DfWm0jdgvE2iwR_dHUHI-QG4,24600
|
|
59
59
|
bt_cli/epmw/commands/quick.py,sha256=X2iTwnDgAvatW8wVY4nqQztLmIEoGmbn1YeCVwHyvzY,13876
|
|
60
60
|
bt_cli/epmw/commands/requests.py,sha256=qcx6MsPXVm2UkaBIkhpf3RZh6Lo-BBz2OPF4Tl3h4p8,8251
|
|
61
61
|
bt_cli/epmw/commands/roles.py,sha256=WYAelLUwzJkBB7rlKoR4FqSxfdoDjV310KcNl6ixCCM,2396
|
|
@@ -102,11 +102,11 @@ bt_cli/pws/commands/credentials.py,sha256=p1efDBCI35exGS-6iHQ5IHBQIo0TJMfWuCBoek
|
|
|
102
102
|
bt_cli/pws/commands/databases.py,sha256=hHGg7hNtE5mwClCpWoSmVSB1-LdY3PORu9iMRuhbxOg,11961
|
|
103
103
|
bt_cli/pws/commands/directories.py,sha256=NBZfivK-gh3BlC7nXyM_iWeE-qjPIPMlsptkNGtsQJk,7985
|
|
104
104
|
bt_cli/pws/commands/functional.py,sha256=C9iMSjNfG2VF-kDAM8a3rlMwp_wMePZpGRPNWwkSTBI,12705
|
|
105
|
-
bt_cli/pws/commands/import_export.py,sha256=
|
|
105
|
+
bt_cli/pws/commands/import_export.py,sha256=jGrGqInxmWAGWVdUxVBt1hHtu_CqmXO65oV5RoUsaAA,27395
|
|
106
106
|
bt_cli/pws/commands/platforms.py,sha256=1NZM8BFMG8-AL3AzRZjU0X8zZwt584Z4FOxQf6mGemc,3999
|
|
107
107
|
bt_cli/pws/commands/quick.py,sha256=3i985r7A8_0zhXTDNxG9NLbGHXlXAAxZL2tMyAAIUyg,70769
|
|
108
108
|
bt_cli/pws/commands/search.py,sha256=6AHJgk30mLneviODOYX4xL9VwPZo1bDUoix3oC99LMM,9875
|
|
109
|
-
bt_cli/pws/commands/secrets.py,sha256
|
|
109
|
+
bt_cli/pws/commands/secrets.py,sha256=-8WInmkRxhq2kESDyMwvtx3hS7OvB-60-puj_2GnH_4,51664
|
|
110
110
|
bt_cli/pws/commands/systems.py,sha256=UFWNgyfI--NjKXqSWVbw_g_Ar_td4-Roa0tK1AJSuQE,16905
|
|
111
111
|
bt_cli/pws/commands/users.py,sha256=yMMC1I32QOjsMr2taU4qD7WajdJ7W3mNDQITbqKCEP4,14078
|
|
112
112
|
bt_cli/pws/commands/workgroups.py,sha256=fvjGniHE6T_oktQSvGhjFXeeDxeubkJXTwR4RNtrA4E,6080
|
|
@@ -115,7 +115,7 @@ bt_cli/pws/models/account.py,sha256=OSCMyULPOH1Yu2WOzK0ZQhSRrggGpb2JPHScwGLqUgI,
|
|
|
115
115
|
bt_cli/pws/models/asset.py,sha256=Fl0AlR4_9Yyyu36FL1eKF29DNsxsB-r7FaOBRlfOg2Q,4081
|
|
116
116
|
bt_cli/pws/models/common.py,sha256=D9Ah4ob5CIiFhTt_IR9nF2cBWRHS2z9OyBR2Sss5yzw,3487
|
|
117
117
|
bt_cli/pws/models/system.py,sha256=D_J0x1A92H2n6BsaBEK9PSAAcs3BTifA5-M9SQqQFGA,5856
|
|
118
|
-
bt_cli-0.4.
|
|
119
|
-
bt_cli-0.4.
|
|
120
|
-
bt_cli-0.4.
|
|
121
|
-
bt_cli-0.4.
|
|
118
|
+
bt_cli-0.4.14.dist-info/METADATA,sha256=1Xp74W_sSHvwqqEe6JPwvZpkU0ENadn1PfS4ZGK_ySM,11803
|
|
119
|
+
bt_cli-0.4.14.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
|
|
120
|
+
bt_cli-0.4.14.dist-info/entry_points.txt,sha256=NCOEqTI-XKpJOux0JKKhbRElz0B7upayh_d99X5hoLs,38
|
|
121
|
+
bt_cli-0.4.14.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|