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.
Files changed (121) hide show
  1. bt_cli/__init__.py +3 -0
  2. bt_cli/cli.py +830 -0
  3. bt_cli/commands/__init__.py +1 -0
  4. bt_cli/commands/configure.py +415 -0
  5. bt_cli/commands/learn.py +229 -0
  6. bt_cli/commands/quick.py +784 -0
  7. bt_cli/core/__init__.py +1 -0
  8. bt_cli/core/auth.py +213 -0
  9. bt_cli/core/client.py +313 -0
  10. bt_cli/core/config.py +393 -0
  11. bt_cli/core/config_file.py +420 -0
  12. bt_cli/core/csv_utils.py +91 -0
  13. bt_cli/core/errors.py +247 -0
  14. bt_cli/core/output.py +205 -0
  15. bt_cli/core/prompts.py +87 -0
  16. bt_cli/core/rest_debug.py +221 -0
  17. bt_cli/data/CLAUDE.md +94 -0
  18. bt_cli/data/__init__.py +0 -0
  19. bt_cli/data/skills/bt/SKILL.md +108 -0
  20. bt_cli/data/skills/entitle/SKILL.md +170 -0
  21. bt_cli/data/skills/epmw/SKILL.md +144 -0
  22. bt_cli/data/skills/pra/SKILL.md +150 -0
  23. bt_cli/data/skills/pws/SKILL.md +198 -0
  24. bt_cli/entitle/__init__.py +1 -0
  25. bt_cli/entitle/client/__init__.py +5 -0
  26. bt_cli/entitle/client/base.py +443 -0
  27. bt_cli/entitle/commands/__init__.py +24 -0
  28. bt_cli/entitle/commands/accounts.py +53 -0
  29. bt_cli/entitle/commands/applications.py +39 -0
  30. bt_cli/entitle/commands/auth.py +68 -0
  31. bt_cli/entitle/commands/bundles.py +218 -0
  32. bt_cli/entitle/commands/integrations.py +60 -0
  33. bt_cli/entitle/commands/permissions.py +70 -0
  34. bt_cli/entitle/commands/policies.py +97 -0
  35. bt_cli/entitle/commands/resources.py +131 -0
  36. bt_cli/entitle/commands/roles.py +74 -0
  37. bt_cli/entitle/commands/users.py +123 -0
  38. bt_cli/entitle/commands/workflows.py +187 -0
  39. bt_cli/entitle/models/__init__.py +31 -0
  40. bt_cli/entitle/models/bundle.py +28 -0
  41. bt_cli/entitle/models/common.py +37 -0
  42. bt_cli/entitle/models/integration.py +30 -0
  43. bt_cli/entitle/models/permission.py +27 -0
  44. bt_cli/entitle/models/policy.py +25 -0
  45. bt_cli/entitle/models/resource.py +29 -0
  46. bt_cli/entitle/models/role.py +28 -0
  47. bt_cli/entitle/models/user.py +24 -0
  48. bt_cli/entitle/models/workflow.py +55 -0
  49. bt_cli/epmw/__init__.py +1 -0
  50. bt_cli/epmw/client/__init__.py +5 -0
  51. bt_cli/epmw/client/base.py +848 -0
  52. bt_cli/epmw/commands/__init__.py +33 -0
  53. bt_cli/epmw/commands/audits.py +250 -0
  54. bt_cli/epmw/commands/auth.py +55 -0
  55. bt_cli/epmw/commands/computers.py +140 -0
  56. bt_cli/epmw/commands/events.py +233 -0
  57. bt_cli/epmw/commands/groups.py +215 -0
  58. bt_cli/epmw/commands/policies.py +673 -0
  59. bt_cli/epmw/commands/quick.py +348 -0
  60. bt_cli/epmw/commands/requests.py +224 -0
  61. bt_cli/epmw/commands/roles.py +78 -0
  62. bt_cli/epmw/commands/tasks.py +38 -0
  63. bt_cli/epmw/commands/users.py +219 -0
  64. bt_cli/epmw/models/__init__.py +1 -0
  65. bt_cli/pra/__init__.py +1 -0
  66. bt_cli/pra/client/__init__.py +5 -0
  67. bt_cli/pra/client/base.py +618 -0
  68. bt_cli/pra/commands/__init__.py +30 -0
  69. bt_cli/pra/commands/auth.py +55 -0
  70. bt_cli/pra/commands/import_export.py +442 -0
  71. bt_cli/pra/commands/jump_clients.py +139 -0
  72. bt_cli/pra/commands/jump_groups.py +146 -0
  73. bt_cli/pra/commands/jump_items.py +638 -0
  74. bt_cli/pra/commands/jumpoints.py +95 -0
  75. bt_cli/pra/commands/policies.py +197 -0
  76. bt_cli/pra/commands/quick.py +470 -0
  77. bt_cli/pra/commands/teams.py +81 -0
  78. bt_cli/pra/commands/users.py +87 -0
  79. bt_cli/pra/commands/vault.py +564 -0
  80. bt_cli/pra/models/__init__.py +27 -0
  81. bt_cli/pra/models/common.py +12 -0
  82. bt_cli/pra/models/jump_client.py +25 -0
  83. bt_cli/pra/models/jump_group.py +15 -0
  84. bt_cli/pra/models/jump_item.py +72 -0
  85. bt_cli/pra/models/jumpoint.py +19 -0
  86. bt_cli/pra/models/team.py +14 -0
  87. bt_cli/pra/models/user.py +17 -0
  88. bt_cli/pra/models/vault.py +45 -0
  89. bt_cli/pws/__init__.py +1 -0
  90. bt_cli/pws/client/__init__.py +5 -0
  91. bt_cli/pws/client/base.py +356 -0
  92. bt_cli/pws/client/beyondinsight.py +869 -0
  93. bt_cli/pws/client/passwordsafe.py +1786 -0
  94. bt_cli/pws/commands/__init__.py +33 -0
  95. bt_cli/pws/commands/accounts.py +372 -0
  96. bt_cli/pws/commands/assets.py +311 -0
  97. bt_cli/pws/commands/auth.py +166 -0
  98. bt_cli/pws/commands/clouds.py +221 -0
  99. bt_cli/pws/commands/config.py +344 -0
  100. bt_cli/pws/commands/credentials.py +347 -0
  101. bt_cli/pws/commands/databases.py +306 -0
  102. bt_cli/pws/commands/directories.py +199 -0
  103. bt_cli/pws/commands/functional.py +298 -0
  104. bt_cli/pws/commands/import_export.py +452 -0
  105. bt_cli/pws/commands/platforms.py +118 -0
  106. bt_cli/pws/commands/quick.py +1646 -0
  107. bt_cli/pws/commands/search.py +256 -0
  108. bt_cli/pws/commands/secrets.py +1343 -0
  109. bt_cli/pws/commands/systems.py +389 -0
  110. bt_cli/pws/commands/users.py +415 -0
  111. bt_cli/pws/commands/workgroups.py +166 -0
  112. bt_cli/pws/config.py +18 -0
  113. bt_cli/pws/models/__init__.py +19 -0
  114. bt_cli/pws/models/account.py +186 -0
  115. bt_cli/pws/models/asset.py +102 -0
  116. bt_cli/pws/models/common.py +132 -0
  117. bt_cli/pws/models/system.py +121 -0
  118. bt_cli-0.4.13.dist-info/METADATA +417 -0
  119. bt_cli-0.4.13.dist-info/RECORD +121 -0
  120. bt_cli-0.4.13.dist-info/WHEEL +4 -0
  121. bt_cli-0.4.13.dist-info/entry_points.txt +2 -0
@@ -0,0 +1,470 @@
1
+ """Quick commands for PRA - combine multiple API calls into single 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.panel import Panel
10
+ from rich.table import Table
11
+
12
+ from ...core.output import print_api_error, print_error, print_success
13
+ from ...core.prompts import prompt_if_missing, prompt_from_list
14
+
15
+ app = typer.Typer(no_args_is_help=True, help="Quick commands - common multi-step operations in one command")
16
+ console = Console()
17
+
18
+
19
+ @app.command("vault")
20
+ def quick_vault(
21
+ name: Optional[str] = typer.Option(None, "--name", "-n", help="Vault account name (partial match)"),
22
+ raw: bool = typer.Option(False, "--raw", help="Output only the password/key (for scripts)"),
23
+ no_auto_checkin: bool = typer.Option(False, "--no-auto-checkin", help="Don't auto-checkin after displaying"),
24
+ output: str = typer.Option("table", "--output", "-o", help="Output format: table or json"),
25
+ ) -> None:
26
+ """Find and checkout a vault credential by name, then auto-checkin.
27
+
28
+ Combines: find vault account -> checkout -> show credential -> checkin
29
+
30
+ If name not provided, shows list of vault accounts to choose from.
31
+
32
+ Examples:
33
+ bt pra quick vault # Interactive mode
34
+ bt pra quick vault -n "server-admin"
35
+ bt pra quick vault -n postgres --raw
36
+ bt pra quick vault -n mssql-admin --no-auto-checkin
37
+ PASSWORD=$(bt pra quick vault -n mssql-admin --raw)
38
+ """
39
+ from ..client import get_client
40
+
41
+ try:
42
+ client = get_client()
43
+
44
+ # Interactive prompt for name if not provided
45
+ if not name:
46
+ all_accounts = client.list_vault_accounts()
47
+ if not all_accounts:
48
+ print_error("No vault accounts found")
49
+ raise typer.Exit(1)
50
+ name = prompt_from_list(
51
+ all_accounts, "Account name", "name", "name",
52
+ "Available Vault Accounts", str
53
+ )
54
+
55
+ # Step 1: Find the vault account
56
+ if not raw:
57
+ console.print(f"[dim]Finding vault account '{name}'...[/dim]")
58
+ accounts = client.list_vault_accounts(name=name)
59
+
60
+ if not accounts:
61
+ print_error(f"No vault account found matching '{name}'")
62
+ raise typer.Exit(1)
63
+
64
+ # Try exact match first, then partial
65
+ matched_account = None
66
+ name_lower = name.lower()
67
+ for acc in accounts:
68
+ if acc.get("name", "").lower() == name_lower:
69
+ matched_account = acc
70
+ break
71
+
72
+ if not matched_account:
73
+ # Use first partial match
74
+ matched_account = accounts[0]
75
+ if len(accounts) > 1 and not raw:
76
+ console.print(f"[yellow]Multiple matches found, using: {matched_account.get('name')}[/yellow]")
77
+
78
+ account_id = matched_account.get("id")
79
+ account_name = matched_account.get("name")
80
+ account_type = matched_account.get("type")
81
+ username = matched_account.get("username", "")
82
+
83
+ # Step 2: Checkout the credential
84
+ if not raw:
85
+ console.print(f"[dim]Checking out {account_name}...[/dim]")
86
+ credential = client.checkout_vault_account(account_id)
87
+
88
+ # Output
89
+ password = credential.get("password", "")
90
+ private_key = credential.get("private_key", "")
91
+ passphrase = credential.get("passphrase", "")
92
+
93
+ if raw:
94
+ # Output the credential value for scripts
95
+ if password:
96
+ print(password, end="")
97
+ elif private_key:
98
+ print(private_key, end="")
99
+ elif output == "json":
100
+ result = {
101
+ "account_id": account_id,
102
+ "account_name": account_name,
103
+ "account_type": account_type,
104
+ "username": username,
105
+ "password": password,
106
+ "private_key": private_key,
107
+ "passphrase": passphrase,
108
+ "auto_checkin": not no_auto_checkin,
109
+ }
110
+ console.print_json(json.dumps(result))
111
+ else:
112
+ content = f"[green]Credential checked out successfully![/green]\n\n"
113
+ content += f"Account: [cyan]{account_name}[/cyan] (ID: {account_id})\n"
114
+ content += f"Type: [yellow]{account_type}[/yellow]\n"
115
+ if username:
116
+ content += f"Username: [cyan]{username}[/cyan]\n"
117
+ content += "\n"
118
+ if password:
119
+ content += f"Password: [bold green]{password}[/bold green]\n"
120
+ if private_key:
121
+ content += f"Private Key:\n[dim]{private_key[:100]}...[/dim]\n"
122
+ if passphrase:
123
+ content += f"Passphrase: [bold green]{passphrase}[/bold green]\n"
124
+
125
+ if no_auto_checkin:
126
+ content += f"\n[dim]Checkin: bt pra vault accounts checkin {account_id}[/dim]"
127
+ else:
128
+ content += f"\n[dim]Auto-checkin enabled[/dim]"
129
+
130
+ console.print(Panel(content, title="Quick Vault Checkout"))
131
+
132
+ # Step 3: Auto-checkin unless disabled
133
+ if not no_auto_checkin:
134
+ if not raw:
135
+ console.print(f"[dim]Checking in {account_name}...[/dim]")
136
+ client.checkin_vault_account(account_id)
137
+ if not raw and output != "json":
138
+ console.print(f"[green]Credential checked back in.[/green]")
139
+
140
+ except httpx.HTTPStatusError as e:
141
+ print_api_error(e, "quick vault")
142
+ raise typer.Exit(1)
143
+ except httpx.RequestError as e:
144
+ print_api_error(e, "quick vault")
145
+ raise typer.Exit(1)
146
+ except typer.Exit:
147
+ raise
148
+ except Exception as e:
149
+ print_api_error(e, "quick vault")
150
+ raise typer.Exit(1)
151
+
152
+
153
+ @app.command("jump-item")
154
+ def quick_jump_item(
155
+ name: Optional[str] = typer.Option(None, "--name", "-n", help="Jump item name"),
156
+ hostname: Optional[str] = typer.Option(None, "--hostname", "-h", help="Target hostname or IP"),
157
+ jump_type: Optional[str] = typer.Option(None, "--type", "-t", help="Jump type: shell, rdp, or tunnel"),
158
+ jumpoint_id: Optional[int] = typer.Option(None, "--jumpoint", "-j", help="Jumpoint ID"),
159
+ jump_group_id: Optional[int] = typer.Option(None, "--jump-group", "-g", help="Jump Group ID"),
160
+ output: str = typer.Option("table", "--output", "-o", help="Output format: table or json"),
161
+ ) -> None:
162
+ """Create a jump item interactively.
163
+
164
+ Prompts for name and hostname, then lists available jumpoints and jump groups
165
+ to select from. Sets sensible defaults for other options.
166
+
167
+ Examples:
168
+ bt pra quick jump-item # Full interactive mode
169
+ bt pra quick jump-item -n "my-server" -h "10.0.1.50" # Partial args
170
+ bt pra quick jump-item -t shell -j 3 -g 24 # Specify type and IDs
171
+ """
172
+ from ..client import get_client
173
+
174
+ try:
175
+ client = get_client()
176
+
177
+ # Step 1: Get name
178
+ if not name:
179
+ name = prompt_if_missing(name, "Jump item name")
180
+
181
+ # Step 2: Get hostname/IP
182
+ if not hostname:
183
+ hostname = prompt_if_missing(hostname, "Target hostname or IP")
184
+
185
+ # Step 3: List and select jumpoint
186
+ if not jumpoint_id:
187
+ console.print("[dim]Fetching jumpoints...[/dim]")
188
+ jumpoints = client.list_jumpoints()
189
+ if not jumpoints:
190
+ print_error("No jumpoints found")
191
+ raise typer.Exit(1)
192
+ jumpoint_id = prompt_from_list(
193
+ jumpoints, "Jumpoint", "id", "name",
194
+ "Available Jumpoints", int
195
+ )
196
+
197
+ # Step 4: List and select jump group
198
+ if not jump_group_id:
199
+ console.print("[dim]Fetching jump groups...[/dim]")
200
+ jump_groups = client.list_jump_groups()
201
+ if not jump_groups:
202
+ print_error("No jump groups found")
203
+ raise typer.Exit(1)
204
+ jump_group_id = prompt_from_list(
205
+ jump_groups, "Jump Group", "id", "name",
206
+ "Available Jump Groups", int
207
+ )
208
+
209
+ # Step 5: Select jump type
210
+ if not jump_type:
211
+ console.print("\n[bold]Jump Types:[/bold]")
212
+ console.print(" 1. shell - SSH/Telnet connections")
213
+ console.print(" 2. rdp - Remote Desktop (Windows)")
214
+ console.print(" 3. tunnel - Protocol tunnel (database, TCP, K8s)")
215
+ choice = typer.prompt("Jump type (1/2/3 or name)", default="1")
216
+ type_map = {"1": "shell", "2": "rdp", "3": "tunnel"}
217
+ jump_type = type_map.get(choice, choice.lower())
218
+
219
+ # Step 6: Create based on type
220
+ if jump_type == "shell":
221
+ # Shell jump defaults
222
+ console.print("\n[bold]Shell Jump Configuration:[/bold]")
223
+ protocol = typer.prompt("Protocol (ssh/telnet)", default="ssh")
224
+ port = typer.prompt("Port", default="22" if protocol == "ssh" else "23")
225
+ username = typer.prompt("Username (optional, press Enter to skip)", default="")
226
+
227
+ item = client.create_shell_jump(
228
+ name=name,
229
+ hostname=hostname,
230
+ jumpoint_id=jumpoint_id,
231
+ jump_group_id=jump_group_id,
232
+ protocol=protocol,
233
+ port=int(port),
234
+ username=username if username else None,
235
+ )
236
+ print_success(f"Created shell jump: {item.get('name')} (ID: {item.get('id')})")
237
+
238
+ elif jump_type == "rdp":
239
+ # RDP jump defaults
240
+ console.print("\n[bold]RDP Jump Configuration:[/bold]")
241
+ domain = typer.prompt("Domain (optional, press Enter to skip)", default="")
242
+
243
+ item = client.create_rdp_jump(
244
+ name=name,
245
+ hostname=hostname,
246
+ jumpoint_id=jumpoint_id,
247
+ jump_group_id=jump_group_id,
248
+ domain=domain if domain else None,
249
+ )
250
+ print_success(f"Created RDP jump: {item.get('name')} (ID: {item.get('id')})")
251
+
252
+ elif jump_type == "tunnel":
253
+ # Protocol tunnel
254
+ console.print("\n[bold]Tunnel Types:[/bold]")
255
+ console.print(" 1. tcp - Generic TCP tunnel")
256
+ console.print(" 2. mssql - Microsoft SQL Server")
257
+ console.print(" 3. psql - PostgreSQL")
258
+ console.print(" 4. mysql - MySQL")
259
+ console.print(" 5. k8s - Kubernetes API")
260
+ tunnel_choice = typer.prompt("Tunnel type (1-5 or name)", default="1")
261
+ tunnel_map = {"1": "tcp", "2": "mssql", "3": "psql", "4": "mysql", "5": "k8s"}
262
+ tunnel_type = tunnel_map.get(tunnel_choice, tunnel_choice.lower())
263
+
264
+ username = None
265
+ database = None
266
+ tunnel_definitions = None
267
+ url = None
268
+ ca_certificates = None
269
+
270
+ if tunnel_type in ("mssql", "psql", "mysql"):
271
+ console.print(f"\n[bold]{tunnel_type.upper()} Configuration:[/bold]")
272
+ username = typer.prompt("Database username (optional)", default="")
273
+ database = typer.prompt("Database name (optional)", default="")
274
+ elif tunnel_type == "tcp":
275
+ console.print("\n[bold]TCP Tunnel Configuration:[/bold]")
276
+ tunnel_definitions = typer.prompt(
277
+ "Port definitions (e.g., '5432;5432' for local;remote, optional)",
278
+ default=""
279
+ )
280
+ elif tunnel_type == "k8s":
281
+ console.print("\n[bold]Kubernetes Configuration:[/bold]")
282
+ url = typer.prompt("K8s API URL (required)")
283
+ ca_cert_path = typer.prompt("CA certificate path (optional)", default="")
284
+ if ca_cert_path:
285
+ try:
286
+ with open(ca_cert_path) as f:
287
+ ca_certificates = f.read()
288
+ except Exception as e:
289
+ print_error(f"Could not read CA cert: {e}")
290
+
291
+ item = client.create_protocol_tunnel(
292
+ name=name,
293
+ hostname=hostname,
294
+ jumpoint_id=jumpoint_id,
295
+ jump_group_id=jump_group_id,
296
+ tunnel_type=tunnel_type,
297
+ username=username if username else None,
298
+ database=database if database else None,
299
+ tunnel_definitions=tunnel_definitions if tunnel_definitions else None,
300
+ url=url if url else None,
301
+ ca_certificates=ca_certificates if ca_certificates else None,
302
+ )
303
+ print_success(f"Created {tunnel_type} tunnel: {item.get('name')} (ID: {item.get('id')})")
304
+
305
+ else:
306
+ print_error(f"Unknown jump type: {jump_type}")
307
+ raise typer.Exit(1)
308
+
309
+ # Show result
310
+ if output == "json":
311
+ console.print_json(json.dumps(item, default=str))
312
+ else:
313
+ console.print(Panel(
314
+ f"[green]Jump item created successfully![/green]\n\n"
315
+ f"Name: [cyan]{item.get('name')}[/cyan]\n"
316
+ f"ID: [bold]{item.get('id')}[/bold]\n"
317
+ f"Hostname: {hostname}\n"
318
+ f"Type: {jump_type}",
319
+ title="Quick Jump Item",
320
+ ))
321
+
322
+ except httpx.HTTPStatusError as e:
323
+ print_api_error(e, "quick jump-item")
324
+ raise typer.Exit(1)
325
+ except httpx.RequestError as e:
326
+ print_api_error(e, "quick jump-item")
327
+ raise typer.Exit(1)
328
+ except typer.Exit:
329
+ raise
330
+ except Exception as e:
331
+ print_api_error(e, "quick jump-item")
332
+ raise typer.Exit(1)
333
+
334
+
335
+ @app.command("search")
336
+ def quick_search(
337
+ query: str = typer.Argument(..., help="Search term (searches jump items and vault accounts)"),
338
+ limit: int = typer.Option(20, "--limit", "-l", help="Maximum results per category"),
339
+ output: str = typer.Option("table", "--output", "-o", help="Output format: table or json"),
340
+ ) -> None:
341
+ """Search across jump items and vault accounts.
342
+
343
+ Examples:
344
+ bt pra quick search axion
345
+ bt pra quick search postgres
346
+ bt pra quick search admin -o json
347
+ """
348
+ from ..client import get_client
349
+
350
+ try:
351
+ client = get_client()
352
+ query_lower = query.lower()
353
+
354
+ # Search shell jump items
355
+ console.print(f"[dim]Searching shell jump items for '{query}'...[/dim]")
356
+ all_shell = client.list_shell_jumps()
357
+ shell_items = [
358
+ s for s in all_shell
359
+ if query_lower in s.get("name", "").lower()
360
+ or query_lower in s.get("hostname", "").lower()
361
+ ][:limit]
362
+
363
+ # Search RDP jump items
364
+ console.print(f"[dim]Searching RDP jump items for '{query}'...[/dim]")
365
+ all_rdp = client.list_rdp_jumps()
366
+ rdp_items = [
367
+ r for r in all_rdp
368
+ if query_lower in r.get("name", "").lower()
369
+ or query_lower in r.get("hostname", "").lower()
370
+ ][:limit]
371
+
372
+ # Search vault accounts
373
+ console.print(f"[dim]Searching vault accounts for '{query}'...[/dim]")
374
+ all_vault = client.list_vault_accounts()
375
+ vault_matches = [
376
+ v for v in all_vault
377
+ if query_lower in v.get("name", "").lower()
378
+ or query_lower in (v.get("description") or "").lower()
379
+ ][:limit]
380
+
381
+ # Fetch full details for vault accounts to get username
382
+ vault_accounts = []
383
+ for v in vault_matches:
384
+ try:
385
+ full = client.get_vault_account(v["id"])
386
+ vault_accounts.append(full)
387
+ except Exception:
388
+ vault_accounts.append(v)
389
+
390
+ if output == "json":
391
+ result = {
392
+ "query": query,
393
+ "shell_jumps": shell_items,
394
+ "rdp_jumps": rdp_items,
395
+ "vault_accounts": vault_accounts,
396
+ }
397
+ console.print_json(json.dumps(result, default=str))
398
+ else:
399
+ # Show shell jump items
400
+ if shell_items:
401
+ table = Table(title=f"Shell Jump Items matching '{query}'")
402
+ table.add_column("ID", style="cyan")
403
+ table.add_column("Name", style="green")
404
+ table.add_column("Hostname", style="yellow")
405
+ table.add_column("Username", style="magenta")
406
+ table.add_column("Port")
407
+
408
+ for item in shell_items:
409
+ table.add_row(
410
+ str(item.get("id", "")),
411
+ item.get("name", ""),
412
+ item.get("hostname", ""),
413
+ item.get("username", "") or "-",
414
+ str(item.get("port", "")),
415
+ )
416
+ console.print(table)
417
+ else:
418
+ console.print(f"[yellow]No shell jump items found matching '{query}'[/yellow]")
419
+
420
+ console.print()
421
+
422
+ # Show RDP jump items
423
+ if rdp_items:
424
+ table = Table(title=f"RDP Jump Items matching '{query}'")
425
+ table.add_column("ID", style="cyan")
426
+ table.add_column("Name", style="green")
427
+ table.add_column("Hostname", style="yellow")
428
+ table.add_column("Domain", style="magenta")
429
+
430
+ for item in rdp_items:
431
+ table.add_row(
432
+ str(item.get("id", "")),
433
+ item.get("name", ""),
434
+ item.get("hostname", ""),
435
+ item.get("domain", "") or "-",
436
+ )
437
+ console.print(table)
438
+ else:
439
+ console.print(f"[yellow]No RDP jump items found matching '{query}'[/yellow]")
440
+
441
+ console.print()
442
+
443
+ # Show vault accounts
444
+ if vault_accounts:
445
+ table = Table(title=f"Vault Accounts matching '{query}'")
446
+ table.add_column("ID", style="cyan")
447
+ table.add_column("Name", style="green")
448
+ table.add_column("Type", style="yellow")
449
+ table.add_column("Username", style="magenta")
450
+
451
+ for acc in vault_accounts:
452
+ table.add_row(
453
+ str(acc.get("id", "")),
454
+ acc.get("name", ""),
455
+ acc.get("type", ""),
456
+ acc.get("username", "") or "-",
457
+ )
458
+ console.print(table)
459
+ else:
460
+ console.print(f"[yellow]No vault accounts found matching '{query}'[/yellow]")
461
+
462
+ except httpx.HTTPStatusError as e:
463
+ print_api_error(e, "quick search")
464
+ raise typer.Exit(1)
465
+ except httpx.RequestError as e:
466
+ print_api_error(e, "quick search")
467
+ raise typer.Exit(1)
468
+ except Exception as e:
469
+ print_api_error(e, "quick search")
470
+ raise typer.Exit(1)
@@ -0,0 +1,81 @@
1
+ """Team commands."""
2
+
3
+ import httpx
4
+ import typer
5
+
6
+ from bt_cli.core.output import OutputFormat, print_table, print_json, print_error, print_api_error
7
+
8
+ app = typer.Typer(no_args_is_help=True)
9
+
10
+
11
+ @app.command("list")
12
+ def list_teams(
13
+ output: OutputFormat = typer.Option(OutputFormat.TABLE, "--output", "-o"),
14
+ ):
15
+ """List all teams."""
16
+ from bt_cli.pra.client import get_client
17
+
18
+ try:
19
+ client = get_client()
20
+ teams = client.list_teams()
21
+
22
+ if output == OutputFormat.JSON:
23
+ print_json(teams)
24
+ else:
25
+ columns = [
26
+ ("ID", "id"),
27
+ ("Name", "name"),
28
+ ("Code Name", "code_name"),
29
+ ("Comments", "comments"),
30
+ ]
31
+ print_table(teams, columns, title="Teams")
32
+ except httpx.HTTPStatusError as e:
33
+ print_api_error(e, "list teams")
34
+ raise typer.Exit(1)
35
+ except httpx.RequestError as e:
36
+ print_api_error(e, "list teams")
37
+ raise typer.Exit(1)
38
+ except Exception as e:
39
+ print_api_error(e, "list teams")
40
+ raise typer.Exit(1)
41
+
42
+
43
+ @app.command("get")
44
+ def get_team(
45
+ team_id: int = typer.Argument(..., help="Team ID"),
46
+ output: OutputFormat = typer.Option(OutputFormat.TABLE, "--output", "-o"),
47
+ ):
48
+ """Get team details."""
49
+ from bt_cli.pra.client import get_client
50
+ from rich.console import Console
51
+ from rich.panel import Panel
52
+
53
+ console = Console()
54
+
55
+ try:
56
+ client = get_client()
57
+ team = client.get_team(team_id)
58
+
59
+ if output == OutputFormat.JSON:
60
+ print_json(team)
61
+ else:
62
+ name = team.get("name", "")
63
+ code_name = team.get("code_name", "") or "-"
64
+ comments = team.get("comments", "") or "-"
65
+
66
+ console.print(Panel(
67
+ f"[bold]{name}[/bold]\n\n"
68
+ f"[dim]Code Name:[/dim] {code_name}\n"
69
+ f"[dim]Comments:[/dim] {comments}",
70
+ title="Team Details",
71
+ subtitle=f"ID: {team.get('id', '')}",
72
+ ))
73
+ except httpx.HTTPStatusError as e:
74
+ print_api_error(e, "get team")
75
+ raise typer.Exit(1)
76
+ except httpx.RequestError as e:
77
+ print_api_error(e, "get team")
78
+ raise typer.Exit(1)
79
+ except Exception as e:
80
+ print_api_error(e, "get team")
81
+ raise typer.Exit(1)
@@ -0,0 +1,87 @@
1
+ """User commands."""
2
+
3
+ import httpx
4
+ import typer
5
+
6
+ from bt_cli.core.output import OutputFormat, print_table, print_json, print_error, print_api_error
7
+
8
+ app = typer.Typer(no_args_is_help=True)
9
+
10
+
11
+ @app.command("list")
12
+ def list_users(
13
+ output: OutputFormat = typer.Option(OutputFormat.TABLE, "--output", "-o"),
14
+ ):
15
+ """List all users."""
16
+ from bt_cli.pra.client import get_client
17
+
18
+ try:
19
+ client = get_client()
20
+ users = client.list_users()
21
+
22
+ if output == OutputFormat.JSON:
23
+ print_json(users)
24
+ else:
25
+ columns = [
26
+ ("ID", "id"),
27
+ ("Username", "username"),
28
+ ("Display Name", "display_name"),
29
+ ("Email", "email"),
30
+ ("Enabled", "enabled"),
31
+ ("Last Login", "last_login_time"),
32
+ ]
33
+ print_table(users, columns, title="Users")
34
+ except httpx.HTTPStatusError as e:
35
+ print_api_error(e, "list users")
36
+ raise typer.Exit(1)
37
+ except httpx.RequestError as e:
38
+ print_api_error(e, "list users")
39
+ raise typer.Exit(1)
40
+ except Exception as e:
41
+ print_api_error(e, "list users")
42
+ raise typer.Exit(1)
43
+
44
+
45
+ @app.command("get")
46
+ def get_user(
47
+ user_id: int = typer.Argument(..., help="User ID"),
48
+ output: OutputFormat = typer.Option(OutputFormat.TABLE, "--output", "-o"),
49
+ ):
50
+ """Get user details."""
51
+ from bt_cli.pra.client import get_client
52
+ from rich.console import Console
53
+ from rich.panel import Panel
54
+
55
+ console = Console()
56
+
57
+ try:
58
+ client = get_client()
59
+ user = client.get_user(user_id)
60
+
61
+ if output == OutputFormat.JSON:
62
+ print_json(user)
63
+ else:
64
+ username = user.get("username", "")
65
+ display_name = user.get("display_name", "") or "-"
66
+ email = user.get("email_address", "") or "-"
67
+ enabled = "Yes" if user.get("enabled") else "No"
68
+ public_display_name = user.get("public_display_name", "") or "-"
69
+
70
+ console.print(Panel(
71
+ f"[bold]{username}[/bold]\n\n"
72
+ f"[dim]Display Name:[/dim] {display_name}\n"
73
+ f"[dim]Email:[/dim] {email}\n"
74
+ f"[dim]Enabled:[/dim] {enabled}\n"
75
+ f"[dim]Public Display Name:[/dim] {public_display_name}",
76
+ title="PRA User Details",
77
+ subtitle=f"ID: {user.get('id', '')}",
78
+ ))
79
+ except httpx.HTTPStatusError as e:
80
+ print_api_error(e, "get user")
81
+ raise typer.Exit(1)
82
+ except httpx.RequestError as e:
83
+ print_api_error(e, "get user")
84
+ raise typer.Exit(1)
85
+ except Exception as e:
86
+ print_api_error(e, "get user")
87
+ raise typer.Exit(1)