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,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)
|