bt-cli 0.4.13__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 +3 -0
- bt_cli/cli.py +830 -0
- bt_cli/commands/__init__.py +1 -0
- bt_cli/commands/configure.py +415 -0
- bt_cli/commands/learn.py +229 -0
- bt_cli/commands/quick.py +784 -0
- bt_cli/core/__init__.py +1 -0
- bt_cli/core/auth.py +213 -0
- bt_cli/core/client.py +313 -0
- bt_cli/core/config.py +393 -0
- bt_cli/core/config_file.py +420 -0
- bt_cli/core/csv_utils.py +91 -0
- bt_cli/core/errors.py +247 -0
- bt_cli/core/output.py +205 -0
- bt_cli/core/prompts.py +87 -0
- bt_cli/core/rest_debug.py +221 -0
- bt_cli/data/CLAUDE.md +94 -0
- bt_cli/data/__init__.py +0 -0
- bt_cli/data/skills/bt/SKILL.md +108 -0
- bt_cli/data/skills/entitle/SKILL.md +170 -0
- bt_cli/data/skills/epmw/SKILL.md +144 -0
- bt_cli/data/skills/pra/SKILL.md +150 -0
- bt_cli/data/skills/pws/SKILL.md +198 -0
- bt_cli/entitle/__init__.py +1 -0
- bt_cli/entitle/client/__init__.py +5 -0
- bt_cli/entitle/client/base.py +443 -0
- bt_cli/entitle/commands/__init__.py +24 -0
- bt_cli/entitle/commands/accounts.py +53 -0
- bt_cli/entitle/commands/applications.py +39 -0
- bt_cli/entitle/commands/auth.py +68 -0
- bt_cli/entitle/commands/bundles.py +218 -0
- bt_cli/entitle/commands/integrations.py +60 -0
- bt_cli/entitle/commands/permissions.py +70 -0
- bt_cli/entitle/commands/policies.py +97 -0
- bt_cli/entitle/commands/resources.py +131 -0
- bt_cli/entitle/commands/roles.py +74 -0
- bt_cli/entitle/commands/users.py +123 -0
- bt_cli/entitle/commands/workflows.py +187 -0
- bt_cli/entitle/models/__init__.py +31 -0
- bt_cli/entitle/models/bundle.py +28 -0
- bt_cli/entitle/models/common.py +37 -0
- bt_cli/entitle/models/integration.py +30 -0
- bt_cli/entitle/models/permission.py +27 -0
- bt_cli/entitle/models/policy.py +25 -0
- bt_cli/entitle/models/resource.py +29 -0
- bt_cli/entitle/models/role.py +28 -0
- bt_cli/entitle/models/user.py +24 -0
- bt_cli/entitle/models/workflow.py +55 -0
- bt_cli/epmw/__init__.py +1 -0
- bt_cli/epmw/client/__init__.py +5 -0
- bt_cli/epmw/client/base.py +848 -0
- bt_cli/epmw/commands/__init__.py +33 -0
- bt_cli/epmw/commands/audits.py +250 -0
- bt_cli/epmw/commands/auth.py +55 -0
- bt_cli/epmw/commands/computers.py +140 -0
- bt_cli/epmw/commands/events.py +233 -0
- bt_cli/epmw/commands/groups.py +215 -0
- bt_cli/epmw/commands/policies.py +673 -0
- bt_cli/epmw/commands/quick.py +348 -0
- bt_cli/epmw/commands/requests.py +224 -0
- bt_cli/epmw/commands/roles.py +78 -0
- bt_cli/epmw/commands/tasks.py +38 -0
- bt_cli/epmw/commands/users.py +219 -0
- bt_cli/epmw/models/__init__.py +1 -0
- bt_cli/pra/__init__.py +1 -0
- bt_cli/pra/client/__init__.py +5 -0
- bt_cli/pra/client/base.py +618 -0
- bt_cli/pra/commands/__init__.py +30 -0
- bt_cli/pra/commands/auth.py +55 -0
- bt_cli/pra/commands/import_export.py +442 -0
- bt_cli/pra/commands/jump_clients.py +139 -0
- bt_cli/pra/commands/jump_groups.py +146 -0
- bt_cli/pra/commands/jump_items.py +638 -0
- bt_cli/pra/commands/jumpoints.py +95 -0
- bt_cli/pra/commands/policies.py +197 -0
- bt_cli/pra/commands/quick.py +470 -0
- bt_cli/pra/commands/teams.py +81 -0
- bt_cli/pra/commands/users.py +87 -0
- bt_cli/pra/commands/vault.py +564 -0
- bt_cli/pra/models/__init__.py +27 -0
- bt_cli/pra/models/common.py +12 -0
- bt_cli/pra/models/jump_client.py +25 -0
- bt_cli/pra/models/jump_group.py +15 -0
- bt_cli/pra/models/jump_item.py +72 -0
- bt_cli/pra/models/jumpoint.py +19 -0
- bt_cli/pra/models/team.py +14 -0
- bt_cli/pra/models/user.py +17 -0
- bt_cli/pra/models/vault.py +45 -0
- bt_cli/pws/__init__.py +1 -0
- bt_cli/pws/client/__init__.py +5 -0
- bt_cli/pws/client/base.py +356 -0
- bt_cli/pws/client/beyondinsight.py +869 -0
- bt_cli/pws/client/passwordsafe.py +1786 -0
- bt_cli/pws/commands/__init__.py +33 -0
- bt_cli/pws/commands/accounts.py +372 -0
- bt_cli/pws/commands/assets.py +311 -0
- bt_cli/pws/commands/auth.py +166 -0
- bt_cli/pws/commands/clouds.py +221 -0
- bt_cli/pws/commands/config.py +344 -0
- bt_cli/pws/commands/credentials.py +347 -0
- bt_cli/pws/commands/databases.py +306 -0
- bt_cli/pws/commands/directories.py +199 -0
- bt_cli/pws/commands/functional.py +298 -0
- bt_cli/pws/commands/import_export.py +452 -0
- bt_cli/pws/commands/platforms.py +118 -0
- bt_cli/pws/commands/quick.py +1646 -0
- bt_cli/pws/commands/search.py +256 -0
- bt_cli/pws/commands/secrets.py +1343 -0
- bt_cli/pws/commands/systems.py +389 -0
- bt_cli/pws/commands/users.py +415 -0
- bt_cli/pws/commands/workgroups.py +166 -0
- bt_cli/pws/config.py +18 -0
- bt_cli/pws/models/__init__.py +19 -0
- bt_cli/pws/models/account.py +186 -0
- bt_cli/pws/models/asset.py +102 -0
- bt_cli/pws/models/common.py +132 -0
- bt_cli/pws/models/system.py +121 -0
- bt_cli-0.4.13.dist-info/METADATA +417 -0
- bt_cli-0.4.13.dist-info/RECORD +121 -0
- bt_cli-0.4.13.dist-info/WHEEL +4 -0
- bt_cli-0.4.13.dist-info/entry_points.txt +2 -0
|
@@ -0,0 +1,452 @@
|
|
|
1
|
+
"""Import/export commands for Password Safe bulk operations."""
|
|
2
|
+
|
|
3
|
+
from typing import Optional
|
|
4
|
+
import json
|
|
5
|
+
|
|
6
|
+
import httpx
|
|
7
|
+
import typer
|
|
8
|
+
from rich.console import Console
|
|
9
|
+
from rich.table import Table
|
|
10
|
+
|
|
11
|
+
from ...core.output import print_api_error, print_error, print_success, print_warning
|
|
12
|
+
from ...core.csv_utils import read_csv, write_csv, validate_required_fields, parse_bool, parse_int
|
|
13
|
+
from ..client.base import get_client
|
|
14
|
+
|
|
15
|
+
import_app = typer.Typer(no_args_is_help=True, help="Import resources from CSV files")
|
|
16
|
+
export_app = typer.Typer(no_args_is_help=True, help="Export sample CSV templates")
|
|
17
|
+
console = Console()
|
|
18
|
+
|
|
19
|
+
# Column definitions for CSV formats
|
|
20
|
+
SYSTEMS_COLUMNS = [
|
|
21
|
+
"name", "ip_address", "workgroup_id", "platform_id", "port",
|
|
22
|
+
"functional_account_id", "elevation_command", "auto_manage",
|
|
23
|
+
"account_name", "account_password", "account_description"
|
|
24
|
+
]
|
|
25
|
+
|
|
26
|
+
SECRETS_COLUMNS = [
|
|
27
|
+
"folder_path", "title", "username", "password", "description", "notes"
|
|
28
|
+
]
|
|
29
|
+
|
|
30
|
+
# Sample data for templates
|
|
31
|
+
SYSTEMS_SAMPLE = [
|
|
32
|
+
{
|
|
33
|
+
"name": "web-server-01", "ip_address": "10.0.1.50", "workgroup_id": "3",
|
|
34
|
+
"platform_id": "2", "port": "22", "functional_account_id": "7",
|
|
35
|
+
"elevation_command": "sudo", "auto_manage": "true",
|
|
36
|
+
"account_name": "root", "account_password": "", "account_description": "Root account"
|
|
37
|
+
},
|
|
38
|
+
{
|
|
39
|
+
"name": "web-server-01", "ip_address": "10.0.1.50", "workgroup_id": "3",
|
|
40
|
+
"platform_id": "2", "port": "22", "functional_account_id": "7",
|
|
41
|
+
"elevation_command": "sudo", "auto_manage": "true",
|
|
42
|
+
"account_name": "svc-backup", "account_password": "Backup#2026!", "account_description": "Backup service"
|
|
43
|
+
},
|
|
44
|
+
{
|
|
45
|
+
"name": "db-server-01", "ip_address": "10.0.1.60", "workgroup_id": "3",
|
|
46
|
+
"platform_id": "2", "port": "22", "functional_account_id": "7",
|
|
47
|
+
"elevation_command": "sudo", "auto_manage": "true",
|
|
48
|
+
"account_name": "postgres", "account_password": "", "account_description": "Database admin"
|
|
49
|
+
},
|
|
50
|
+
{
|
|
51
|
+
"name": "win-server-01", "ip_address": "10.0.2.10", "workgroup_id": "2",
|
|
52
|
+
"platform_id": "1", "port": "5985", "functional_account_id": "",
|
|
53
|
+
"elevation_command": "", "auto_manage": "false",
|
|
54
|
+
"account_name": "Administrator", "account_password": "InitialPass123!", "account_description": "Local admin"
|
|
55
|
+
},
|
|
56
|
+
]
|
|
57
|
+
|
|
58
|
+
SECRETS_SAMPLE = [
|
|
59
|
+
{
|
|
60
|
+
"folder_path": "Example Safe/Database", "title": "example-db-cred",
|
|
61
|
+
"username": "example_user", "password": "CHANGE_ME_123!",
|
|
62
|
+
"description": "Example database credential", "notes": ""
|
|
63
|
+
},
|
|
64
|
+
{
|
|
65
|
+
"folder_path": "Example Safe/API Keys", "title": "example-api-key",
|
|
66
|
+
"username": "api_service", "password": "example_api_key_here",
|
|
67
|
+
"description": "Example API key", "notes": '{"env":"example"}'
|
|
68
|
+
},
|
|
69
|
+
{
|
|
70
|
+
"folder_path": "Example Safe/Service Accounts", "title": "example-svc-account",
|
|
71
|
+
"username": "svc_example", "password": "CHANGE_ME_456!",
|
|
72
|
+
"description": "Example service account", "notes": ""
|
|
73
|
+
},
|
|
74
|
+
]
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
@import_app.command("systems")
|
|
78
|
+
def import_systems(
|
|
79
|
+
file: str = typer.Option(..., "--file", "-f", help="CSV file path"),
|
|
80
|
+
dry_run: bool = typer.Option(False, "--dry-run", help="Validate without creating"),
|
|
81
|
+
workgroup: Optional[int] = typer.Option(None, "--workgroup", "-w", help="Override workgroup ID for all rows"),
|
|
82
|
+
) -> None:
|
|
83
|
+
"""Import managed systems and accounts from CSV.
|
|
84
|
+
|
|
85
|
+
Each row creates a system + account. Multiple accounts per system use
|
|
86
|
+
multiple rows with the same system name (system is created only once).
|
|
87
|
+
|
|
88
|
+
Required columns: name, ip_address, workgroup_id, account_name
|
|
89
|
+
Optional: platform_id, port, functional_account_id, elevation_command,
|
|
90
|
+
auto_manage, account_password, account_description
|
|
91
|
+
|
|
92
|
+
Examples:
|
|
93
|
+
bt pws import systems --file systems.csv --dry-run
|
|
94
|
+
bt pws import systems --file systems.csv
|
|
95
|
+
bt pws import systems --file systems.csv --workgroup 3
|
|
96
|
+
"""
|
|
97
|
+
try:
|
|
98
|
+
rows = read_csv(file)
|
|
99
|
+
if not rows:
|
|
100
|
+
print_error("CSV file is empty")
|
|
101
|
+
raise typer.Exit(1)
|
|
102
|
+
|
|
103
|
+
console.print(f"[dim]Read {len(rows)} rows from {file}[/dim]")
|
|
104
|
+
|
|
105
|
+
# Validate all rows first
|
|
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
|
|
159
|
+
with get_client() as client:
|
|
160
|
+
client.authenticate()
|
|
161
|
+
|
|
162
|
+
created_systems = 0
|
|
163
|
+
created_accounts = 0
|
|
164
|
+
system_ids: dict[str, int] = {} # Map system name to ID
|
|
165
|
+
|
|
166
|
+
for name, sys_rows in systems_rows.items():
|
|
167
|
+
first = sys_rows[0]
|
|
168
|
+
wg_id = workgroup or parse_int(first.get("workgroup_id", ""))
|
|
169
|
+
|
|
170
|
+
if not wg_id:
|
|
171
|
+
print_warning(f"Skipping {name}: No workgroup ID")
|
|
172
|
+
continue
|
|
173
|
+
|
|
174
|
+
try:
|
|
175
|
+
# Create asset
|
|
176
|
+
console.print(f"[dim]Creating asset '{name}'...[/dim]")
|
|
177
|
+
asset = client.create_asset(
|
|
178
|
+
workgroup_id=wg_id,
|
|
179
|
+
ip_address=first.get("ip_address", "").strip(),
|
|
180
|
+
asset_name=name,
|
|
181
|
+
)
|
|
182
|
+
asset_id = asset.get("AssetID")
|
|
183
|
+
|
|
184
|
+
# Create managed system
|
|
185
|
+
console.print(f"[dim]Creating managed system '{name}'...[/dim]")
|
|
186
|
+
platform_id = parse_int(first.get("platform_id", ""), 2)
|
|
187
|
+
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
|
+
elevation = first.get("elevation_command", "").strip() or None
|
|
191
|
+
|
|
192
|
+
system = client.create_managed_system(
|
|
193
|
+
system_name=name,
|
|
194
|
+
platform_id=platform_id,
|
|
195
|
+
asset_id=asset_id,
|
|
196
|
+
port=port,
|
|
197
|
+
functional_account_id=func_acct,
|
|
198
|
+
auto_management_flag=auto_manage if func_acct else False,
|
|
199
|
+
elevation_command=elevation,
|
|
200
|
+
)
|
|
201
|
+
system_id = system.get("ManagedSystemID")
|
|
202
|
+
system_ids[name] = system_id
|
|
203
|
+
created_systems += 1
|
|
204
|
+
console.print(f" [green]Created system: {name} (ID: {system_id})[/green]")
|
|
205
|
+
|
|
206
|
+
# Create accounts for this system
|
|
207
|
+
for row in sys_rows:
|
|
208
|
+
account_name = row.get("account_name", "").strip()
|
|
209
|
+
password = row.get("account_password", "").strip() or None
|
|
210
|
+
description = row.get("account_description", "").strip() or None
|
|
211
|
+
|
|
212
|
+
console.print(f"[dim]Creating account '{account_name}' on {name}...[/dim]")
|
|
213
|
+
account = client.create_managed_account(
|
|
214
|
+
system_id=system_id,
|
|
215
|
+
account_name=account_name,
|
|
216
|
+
password=password,
|
|
217
|
+
auto_management_flag=auto_manage if func_acct else False,
|
|
218
|
+
)
|
|
219
|
+
account_id = account.get("ManagedAccountID")
|
|
220
|
+
created_accounts += 1
|
|
221
|
+
console.print(f" [green]Created account: {account_name} (ID: {account_id})[/green]")
|
|
222
|
+
|
|
223
|
+
except httpx.HTTPStatusError as e:
|
|
224
|
+
print_warning(f"Error creating {name}: {e.response.text}")
|
|
225
|
+
except Exception as e:
|
|
226
|
+
print_warning(f"Error creating {name}: {e}")
|
|
227
|
+
|
|
228
|
+
print_success(f"Import complete: {created_systems} systems, {created_accounts} accounts created")
|
|
229
|
+
|
|
230
|
+
except FileNotFoundError as e:
|
|
231
|
+
print_error(str(e))
|
|
232
|
+
raise typer.Exit(1)
|
|
233
|
+
except typer.Exit:
|
|
234
|
+
raise
|
|
235
|
+
except Exception as e:
|
|
236
|
+
print_api_error(e, "import systems")
|
|
237
|
+
raise typer.Exit(1)
|
|
238
|
+
|
|
239
|
+
|
|
240
|
+
@import_app.command("secrets")
|
|
241
|
+
def import_secrets(
|
|
242
|
+
file: str = typer.Option(..., "--file", "-f", help="CSV file path"),
|
|
243
|
+
dry_run: bool = typer.Option(False, "--dry-run", help="Validate without creating"),
|
|
244
|
+
folder: Optional[str] = typer.Option(None, "--folder", help="Override folder path for all rows"),
|
|
245
|
+
) -> None:
|
|
246
|
+
"""Import secrets from CSV.
|
|
247
|
+
|
|
248
|
+
Required columns: folder_path, title
|
|
249
|
+
Optional: username, password, description, notes
|
|
250
|
+
|
|
251
|
+
The folder_path should be the full path like "SafeName/FolderName/SubFolder".
|
|
252
|
+
|
|
253
|
+
Examples:
|
|
254
|
+
bt pws import secrets --file secrets.csv --dry-run
|
|
255
|
+
bt pws import secrets --file secrets.csv
|
|
256
|
+
bt pws import secrets --file secrets.csv --folder "PAM Demo/Database"
|
|
257
|
+
"""
|
|
258
|
+
try:
|
|
259
|
+
rows = read_csv(file)
|
|
260
|
+
if not rows:
|
|
261
|
+
print_error("CSV file is empty")
|
|
262
|
+
raise typer.Exit(1)
|
|
263
|
+
|
|
264
|
+
console.print(f"[dim]Read {len(rows)} rows from {file}[/dim]")
|
|
265
|
+
|
|
266
|
+
# Validate
|
|
267
|
+
errors = []
|
|
268
|
+
required = ["title"]
|
|
269
|
+
if not folder:
|
|
270
|
+
required.append("folder_path")
|
|
271
|
+
|
|
272
|
+
for i, row in enumerate(rows, 1):
|
|
273
|
+
row_errors = validate_required_fields(row, required, i)
|
|
274
|
+
errors.extend(row_errors)
|
|
275
|
+
|
|
276
|
+
if errors:
|
|
277
|
+
print_error("Validation errors:")
|
|
278
|
+
for err in errors[:20]:
|
|
279
|
+
console.print(f" [red]{err}[/red]")
|
|
280
|
+
raise typer.Exit(1)
|
|
281
|
+
|
|
282
|
+
if dry_run:
|
|
283
|
+
console.print("\n[yellow]DRY RUN - No changes will be made[/yellow]\n")
|
|
284
|
+
table = Table(title="Secrets to Create")
|
|
285
|
+
table.add_column("Folder", style="cyan")
|
|
286
|
+
table.add_column("Title", style="green")
|
|
287
|
+
table.add_column("Username", style="yellow")
|
|
288
|
+
|
|
289
|
+
for row in rows:
|
|
290
|
+
table.add_row(
|
|
291
|
+
folder or row.get("folder_path", ""),
|
|
292
|
+
row.get("title", ""),
|
|
293
|
+
row.get("username", "") or "-"
|
|
294
|
+
)
|
|
295
|
+
|
|
296
|
+
console.print(table)
|
|
297
|
+
console.print(f"\n[dim]Would create {len(rows)} secrets[/dim]")
|
|
298
|
+
return
|
|
299
|
+
|
|
300
|
+
# Import
|
|
301
|
+
with get_client() as client:
|
|
302
|
+
client.authenticate()
|
|
303
|
+
|
|
304
|
+
# Get all folders to map paths to IDs
|
|
305
|
+
all_folders = client.list_folders()
|
|
306
|
+
folder_map: dict[str, int] = {}
|
|
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")
|
|
311
|
+
|
|
312
|
+
created = 0
|
|
313
|
+
for row in rows:
|
|
314
|
+
folder_path = (folder or row.get("folder_path", "")).strip()
|
|
315
|
+
title = row.get("title", "").strip()
|
|
316
|
+
|
|
317
|
+
# Find folder ID
|
|
318
|
+
folder_id = folder_map.get(folder_path.lower())
|
|
319
|
+
if not folder_id:
|
|
320
|
+
print_warning(f"Folder not found: {folder_path}, skipping {title}")
|
|
321
|
+
continue
|
|
322
|
+
|
|
323
|
+
try:
|
|
324
|
+
console.print(f"[dim]Creating secret '{title}' in {folder_path}...[/dim]")
|
|
325
|
+
secret = client.create_secret(
|
|
326
|
+
folder_id=folder_id,
|
|
327
|
+
title=title,
|
|
328
|
+
username=row.get("username", "").strip() or None,
|
|
329
|
+
password=row.get("password", "").strip() or None,
|
|
330
|
+
description=row.get("description", "").strip() or None,
|
|
331
|
+
notes=row.get("notes", "").strip() or None,
|
|
332
|
+
)
|
|
333
|
+
secret_id = secret.get("Id")
|
|
334
|
+
created += 1
|
|
335
|
+
console.print(f" [green]Created secret: {title} (ID: {secret_id})[/green]")
|
|
336
|
+
except httpx.HTTPStatusError as e:
|
|
337
|
+
print_warning(f"Error creating {title}: {e.response.text}")
|
|
338
|
+
except Exception as e:
|
|
339
|
+
print_warning(f"Error creating {title}: {e}")
|
|
340
|
+
|
|
341
|
+
print_success(f"Import complete: {created} secrets created")
|
|
342
|
+
|
|
343
|
+
except FileNotFoundError as e:
|
|
344
|
+
print_error(str(e))
|
|
345
|
+
raise typer.Exit(1)
|
|
346
|
+
except typer.Exit:
|
|
347
|
+
raise
|
|
348
|
+
except Exception as e:
|
|
349
|
+
print_api_error(e, "import secrets")
|
|
350
|
+
raise typer.Exit(1)
|
|
351
|
+
|
|
352
|
+
|
|
353
|
+
@export_app.command("systems")
|
|
354
|
+
def export_systems(
|
|
355
|
+
file: str = typer.Option("pws-systems-template.csv", "--file", "-f", help="Output file path"),
|
|
356
|
+
sample: bool = typer.Option(True, "--sample/--no-sample", help="Export sample template (default) or current data"),
|
|
357
|
+
) -> None:
|
|
358
|
+
"""Export sample systems CSV template.
|
|
359
|
+
|
|
360
|
+
Examples:
|
|
361
|
+
bt pws export systems --file systems-template.csv
|
|
362
|
+
bt pws export systems --file systems-template.csv --sample
|
|
363
|
+
"""
|
|
364
|
+
try:
|
|
365
|
+
if sample:
|
|
366
|
+
write_csv(file, SYSTEMS_SAMPLE, SYSTEMS_COLUMNS)
|
|
367
|
+
print_success(f"Sample systems template exported to: {file}")
|
|
368
|
+
console.print(f"[dim]Contains {len(SYSTEMS_SAMPLE)} example rows[/dim]")
|
|
369
|
+
else:
|
|
370
|
+
# Export actual data from API
|
|
371
|
+
with get_client() as client:
|
|
372
|
+
client.authenticate()
|
|
373
|
+
|
|
374
|
+
systems = client.list_managed_systems()
|
|
375
|
+
rows = []
|
|
376
|
+
|
|
377
|
+
for sys in systems:
|
|
378
|
+
system_id = sys.get("ManagedSystemID")
|
|
379
|
+
accounts = client.list_managed_accounts(system_id=system_id)
|
|
380
|
+
|
|
381
|
+
for acc in accounts:
|
|
382
|
+
rows.append({
|
|
383
|
+
"name": sys.get("SystemName", ""),
|
|
384
|
+
"ip_address": sys.get("IPAddress", ""),
|
|
385
|
+
"workgroup_id": str(sys.get("WorkgroupID", "")),
|
|
386
|
+
"platform_id": str(sys.get("PlatformID", "")),
|
|
387
|
+
"port": str(sys.get("Port", "")),
|
|
388
|
+
"functional_account_id": str(sys.get("FunctionalAccountID", "") or ""),
|
|
389
|
+
"elevation_command": sys.get("ElevationCommand", "") or "",
|
|
390
|
+
"auto_manage": "true" if sys.get("AutoManagementFlag") else "false",
|
|
391
|
+
"account_name": acc.get("AccountName", ""),
|
|
392
|
+
"account_password": "", # Don't export passwords
|
|
393
|
+
"account_description": acc.get("Description", "") or "",
|
|
394
|
+
})
|
|
395
|
+
|
|
396
|
+
write_csv(file, rows, SYSTEMS_COLUMNS)
|
|
397
|
+
print_success(f"Exported {len(rows)} system/account rows to: {file}")
|
|
398
|
+
|
|
399
|
+
except typer.Exit:
|
|
400
|
+
raise
|
|
401
|
+
except Exception as e:
|
|
402
|
+
print_api_error(e, "export systems")
|
|
403
|
+
raise typer.Exit(1)
|
|
404
|
+
|
|
405
|
+
|
|
406
|
+
@export_app.command("secrets")
|
|
407
|
+
def export_secrets(
|
|
408
|
+
file: str = typer.Option("pws-secrets-template.csv", "--file", "-f", help="Output file path"),
|
|
409
|
+
sample: bool = typer.Option(True, "--sample/--no-sample", help="Export sample template (default) or current data"),
|
|
410
|
+
) -> None:
|
|
411
|
+
"""Export sample secrets CSV template.
|
|
412
|
+
|
|
413
|
+
Examples:
|
|
414
|
+
bt pws export secrets --file secrets-template.csv
|
|
415
|
+
bt pws export secrets --file secrets-template.csv --sample
|
|
416
|
+
"""
|
|
417
|
+
try:
|
|
418
|
+
if sample:
|
|
419
|
+
write_csv(file, SECRETS_SAMPLE, SECRETS_COLUMNS)
|
|
420
|
+
print_success(f"Sample secrets template exported to: {file}")
|
|
421
|
+
console.print(f"[dim]Contains {len(SECRETS_SAMPLE)} example rows[/dim]")
|
|
422
|
+
else:
|
|
423
|
+
# Export actual data from API
|
|
424
|
+
with get_client() as client:
|
|
425
|
+
client.authenticate()
|
|
426
|
+
|
|
427
|
+
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
|
+
|
|
433
|
+
rows = []
|
|
434
|
+
for s in secrets:
|
|
435
|
+
folder_id = s.get("FolderId")
|
|
436
|
+
rows.append({
|
|
437
|
+
"folder_path": folder_paths.get(folder_id, ""),
|
|
438
|
+
"title": s.get("Title", ""),
|
|
439
|
+
"username": s.get("Username", "") or "",
|
|
440
|
+
"password": "", # Don't export passwords
|
|
441
|
+
"description": s.get("Description", "") or "",
|
|
442
|
+
"notes": s.get("Notes", "") or "",
|
|
443
|
+
})
|
|
444
|
+
|
|
445
|
+
write_csv(file, rows, SECRETS_COLUMNS)
|
|
446
|
+
print_success(f"Exported {len(rows)} secrets to: {file}")
|
|
447
|
+
|
|
448
|
+
except typer.Exit:
|
|
449
|
+
raise
|
|
450
|
+
except Exception as e:
|
|
451
|
+
print_api_error(e, "export secrets")
|
|
452
|
+
raise typer.Exit(1)
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
"""CLI commands for managing platforms (OS types)."""
|
|
2
|
+
|
|
3
|
+
from typing import Optional
|
|
4
|
+
import json
|
|
5
|
+
|
|
6
|
+
import httpx
|
|
7
|
+
import typer
|
|
8
|
+
from rich.console import Console
|
|
9
|
+
from rich.table import Table
|
|
10
|
+
|
|
11
|
+
from ...core.output import print_api_error
|
|
12
|
+
from ..client.base import get_client
|
|
13
|
+
|
|
14
|
+
app = typer.Typer(no_args_is_help=True, help="View available platforms (OS types)")
|
|
15
|
+
console = Console()
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def print_platforms_table(platforms: list[dict], title: str = "Platforms") -> None:
|
|
19
|
+
"""Print platforms in a formatted table."""
|
|
20
|
+
table = Table(title=title)
|
|
21
|
+
table.add_column("ID", style="cyan", no_wrap=True)
|
|
22
|
+
table.add_column("Name", style="green")
|
|
23
|
+
table.add_column("Short Name", style="yellow")
|
|
24
|
+
table.add_column("Port", style="blue")
|
|
25
|
+
table.add_column("Session Type", style="magenta")
|
|
26
|
+
|
|
27
|
+
for platform in platforms:
|
|
28
|
+
port = platform.get("DefaultPort")
|
|
29
|
+
session_type = platform.get("DefaultSessionType")
|
|
30
|
+
table.add_row(
|
|
31
|
+
str(platform.get("PlatformID", "")),
|
|
32
|
+
platform.get("Name", ""),
|
|
33
|
+
platform.get("ShortName", "-"),
|
|
34
|
+
str(port) if port else "-",
|
|
35
|
+
session_type if session_type else "-",
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
console.print(table)
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
@app.command("list")
|
|
42
|
+
def list_platforms(
|
|
43
|
+
search: Optional[str] = typer.Option(None, "--search", "-s", help="Search filter"),
|
|
44
|
+
output: str = typer.Option("table", "--output", "-o", help="Output format: table or json"),
|
|
45
|
+
) -> None:
|
|
46
|
+
"""List all available platforms."""
|
|
47
|
+
try:
|
|
48
|
+
with get_client() as client:
|
|
49
|
+
client.authenticate()
|
|
50
|
+
platforms = client.list_platforms(search=search)
|
|
51
|
+
|
|
52
|
+
if output == "json":
|
|
53
|
+
console.print_json(json.dumps(platforms, default=str))
|
|
54
|
+
else:
|
|
55
|
+
if platforms:
|
|
56
|
+
print_platforms_table(platforms)
|
|
57
|
+
else:
|
|
58
|
+
console.print("[yellow]No platforms found.[/yellow]")
|
|
59
|
+
|
|
60
|
+
except httpx.HTTPStatusError as e:
|
|
61
|
+
print_api_error(e, "manage platforms")
|
|
62
|
+
raise typer.Exit(1)
|
|
63
|
+
except httpx.RequestError as e:
|
|
64
|
+
print_api_error(e, "manage platforms")
|
|
65
|
+
raise typer.Exit(1)
|
|
66
|
+
except Exception as e:
|
|
67
|
+
print_api_error(e, "manage platforms")
|
|
68
|
+
raise typer.Exit(1)
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
@app.command("get")
|
|
72
|
+
def get_platform(
|
|
73
|
+
platform_id: int = typer.Argument(..., help="Platform ID"),
|
|
74
|
+
output: str = typer.Option("table", "--output", "-o", help="Output format: table or json"),
|
|
75
|
+
) -> None:
|
|
76
|
+
"""Get a platform by ID."""
|
|
77
|
+
try:
|
|
78
|
+
with get_client() as client:
|
|
79
|
+
client.authenticate()
|
|
80
|
+
platform = client.get_platform(platform_id)
|
|
81
|
+
|
|
82
|
+
if output == "json":
|
|
83
|
+
console.print_json(json.dumps(platform, default=str))
|
|
84
|
+
else:
|
|
85
|
+
console.print(f"\n[bold cyan]Platform: {platform.get('Name', 'Unknown')}[/bold cyan]\n")
|
|
86
|
+
|
|
87
|
+
info_table = Table(show_header=False, box=None)
|
|
88
|
+
info_table.add_column("Field", style="dim")
|
|
89
|
+
info_table.add_column("Value")
|
|
90
|
+
|
|
91
|
+
fields = [
|
|
92
|
+
("ID", "PlatformID"),
|
|
93
|
+
("Name", "Name"),
|
|
94
|
+
("Short Name", "ShortName"),
|
|
95
|
+
("Default Port", "DefaultPort"),
|
|
96
|
+
("Session Type", "DefaultSessionType"),
|
|
97
|
+
("Auto Management", "AutoManagementFlag"),
|
|
98
|
+
("Elevation Support", "SupportsElevationFlag"),
|
|
99
|
+
]
|
|
100
|
+
|
|
101
|
+
for label, key in fields:
|
|
102
|
+
value = platform.get(key)
|
|
103
|
+
if value is not None:
|
|
104
|
+
if isinstance(value, bool):
|
|
105
|
+
value = "Yes" if value else "No"
|
|
106
|
+
info_table.add_row(label, str(value))
|
|
107
|
+
|
|
108
|
+
console.print(info_table)
|
|
109
|
+
|
|
110
|
+
except httpx.HTTPStatusError as e:
|
|
111
|
+
print_api_error(e, "manage platforms")
|
|
112
|
+
raise typer.Exit(1)
|
|
113
|
+
except httpx.RequestError as e:
|
|
114
|
+
print_api_error(e, "manage platforms")
|
|
115
|
+
raise typer.Exit(1)
|
|
116
|
+
except Exception as e:
|
|
117
|
+
print_api_error(e, "manage platforms")
|
|
118
|
+
raise typer.Exit(1)
|