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
bt_cli/commands/quick.py
ADDED
|
@@ -0,0 +1,784 @@
|
|
|
1
|
+
"""Global quick commands for cross-product operations."""
|
|
2
|
+
|
|
3
|
+
from typing import Optional
|
|
4
|
+
import json
|
|
5
|
+
import secrets
|
|
6
|
+
import string
|
|
7
|
+
|
|
8
|
+
import httpx
|
|
9
|
+
import typer
|
|
10
|
+
from rich.console import Console
|
|
11
|
+
from rich.panel import Panel
|
|
12
|
+
from rich.table import Table
|
|
13
|
+
|
|
14
|
+
from ..core.output import print_api_error, print_error, print_success, print_warning
|
|
15
|
+
from ..core.prompts import prompt_if_missing, prompt_from_list
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def generate_password(length: int = 24) -> str:
|
|
19
|
+
"""Generate a secure random password."""
|
|
20
|
+
alphabet = string.ascii_letters + string.digits + "!@#$%^&*"
|
|
21
|
+
# Ensure at least one of each character type
|
|
22
|
+
password = [
|
|
23
|
+
secrets.choice(string.ascii_uppercase),
|
|
24
|
+
secrets.choice(string.ascii_lowercase),
|
|
25
|
+
secrets.choice(string.digits),
|
|
26
|
+
secrets.choice("!@#$%^&*"),
|
|
27
|
+
]
|
|
28
|
+
password.extend(secrets.choice(alphabet) for _ in range(length - 4))
|
|
29
|
+
# Shuffle the password
|
|
30
|
+
password_list = list(password)
|
|
31
|
+
secrets.SystemRandom().shuffle(password_list)
|
|
32
|
+
return "".join(password_list)
|
|
33
|
+
|
|
34
|
+
app = typer.Typer(no_args_is_help=True, help="Cross-product quick commands (Total PASM workflows)")
|
|
35
|
+
console = Console()
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
@app.command("pasm-onboard")
|
|
39
|
+
def pasm_onboard(
|
|
40
|
+
name: Optional[str] = typer.Option(None, "--name", "-n", help="System/jump item name (used in both PWS and PRA)"),
|
|
41
|
+
ip: Optional[str] = typer.Option(None, "--ip", "-i", help="IP address"),
|
|
42
|
+
dns: Optional[str] = typer.Option(None, "--dns", "-d", help="DNS name (optional)"),
|
|
43
|
+
workgroup: Optional[int] = typer.Option(None, "--workgroup", "-w", help="PWS Workgroup ID"),
|
|
44
|
+
platform: int = typer.Option(2, "--platform", "-p", help="PWS Platform ID (default: 2=Linux)"),
|
|
45
|
+
jumpoint: Optional[int] = typer.Option(None, "--jumpoint", "-j", help="PRA Jumpoint ID"),
|
|
46
|
+
jump_group: Optional[int] = typer.Option(None, "--jump-group", "-g", help="PRA Jump Group ID"),
|
|
47
|
+
account: Optional[str] = typer.Option(None, "--account", "-a", help="Account name to create (prompts if not provided)"),
|
|
48
|
+
password: Optional[str] = typer.Option(None, "--password", help="Account password (auto-generated if not provided)"),
|
|
49
|
+
pra_username: Optional[str] = typer.Option(None, "--pra-username", "-u", help="PRA jump item username (default: ec2-admin)"),
|
|
50
|
+
functional_account: Optional[int] = typer.Option(None, "--functional-account", "-f", help="PWS Functional account ID for auto-management"),
|
|
51
|
+
port: int = typer.Option(22, "--port", help="SSH port (default: 22)"),
|
|
52
|
+
elevation: Optional[str] = typer.Option(None, "--elevation", "-e", help="Elevation command (e.g., 'sudo')"),
|
|
53
|
+
jump_type: str = typer.Option("shell", "--jump-type", "-t", help="PRA jump type: shell or rdp"),
|
|
54
|
+
skip_pws: bool = typer.Option(False, "--skip-pws", help="Skip PWS onboarding (PRA only)"),
|
|
55
|
+
skip_pra: bool = typer.Option(False, "--skip-pra", help="Skip PRA onboarding (PWS only)"),
|
|
56
|
+
skip_account: bool = typer.Option(False, "--skip-account", help="Skip managed account creation in PWS"),
|
|
57
|
+
from_csv: Optional[str] = typer.Option(None, "--from-csv", help="Bulk onboard from CSV file"),
|
|
58
|
+
output: str = typer.Option("table", "--output", "-o", help="Output format: table or json"),
|
|
59
|
+
) -> None:
|
|
60
|
+
"""Onboard a host for Total PASM (Password Safe + PRA).
|
|
61
|
+
|
|
62
|
+
Creates resources in both Password Safe and PRA with consistent naming
|
|
63
|
+
for ECM (Enterprise Credential Manager) integration.
|
|
64
|
+
|
|
65
|
+
Consistent naming is CRITICAL: The PWS asset name, managed system name,
|
|
66
|
+
and PRA jump item name must all match for ECM credential lookup to work.
|
|
67
|
+
|
|
68
|
+
What gets created:
|
|
69
|
+
- PWS: Asset -> Managed System -> Managed Account
|
|
70
|
+
- PRA: Shell or RDP Jump Item
|
|
71
|
+
|
|
72
|
+
If required options are not provided, prompts interactively.
|
|
73
|
+
|
|
74
|
+
Examples:
|
|
75
|
+
# Interactive mode - prompts for missing info
|
|
76
|
+
bt quick pasm-onboard
|
|
77
|
+
|
|
78
|
+
# Full specification
|
|
79
|
+
bt quick pasm-onboard -n "my-server" -i "10.0.1.50" -w 3 -j 3 -g 24
|
|
80
|
+
|
|
81
|
+
# With functional account for auto-management
|
|
82
|
+
bt quick pasm-onboard -n "web-01" -i "10.0.1.100" -w 3 -j 3 -g 24 -f 7 -e "sudo"
|
|
83
|
+
|
|
84
|
+
# Windows RDP host
|
|
85
|
+
bt quick pasm-onboard -n "win-srv" -i "10.0.2.10" -w 2 -p 1 -j 3 -g 31 --jump-type rdp --port 3389
|
|
86
|
+
|
|
87
|
+
# PRA only (skip PWS)
|
|
88
|
+
bt quick pasm-onboard -n "jump-host" -i "10.0.1.5" -j 3 -g 24 --skip-pws
|
|
89
|
+
|
|
90
|
+
# PWS only (skip PRA)
|
|
91
|
+
bt quick pasm-onboard -n "db-server" -i "10.0.1.60" -w 3 --skip-pra
|
|
92
|
+
|
|
93
|
+
# Bulk onboard from CSV
|
|
94
|
+
bt quick pasm-onboard --from-csv hosts.csv
|
|
95
|
+
|
|
96
|
+
See also:
|
|
97
|
+
bt pws functional list - List functional accounts for auto-management
|
|
98
|
+
bt pws functional create - Create a new functional account
|
|
99
|
+
bt pra jump-groups list - List available jump groups
|
|
100
|
+
bt pra jumpoint list - List available jumpoints
|
|
101
|
+
|
|
102
|
+
CSV format (header row required):
|
|
103
|
+
name,ip,dns,workgroup,jumpoint,jump_group,account,functional_account,port,elevation
|
|
104
|
+
web-01,10.0.1.50,web-01.internal,3,3,24,root,7,22,sudo
|
|
105
|
+
db-01,10.0.1.51,,3,3,24,postgres,7,22,sudo
|
|
106
|
+
"""
|
|
107
|
+
import csv
|
|
108
|
+
from pathlib import Path
|
|
109
|
+
|
|
110
|
+
# Handle CSV bulk import
|
|
111
|
+
if from_csv:
|
|
112
|
+
csv_path = Path(from_csv).expanduser()
|
|
113
|
+
if not csv_path.exists():
|
|
114
|
+
print_error(f"CSV file not found: {from_csv}")
|
|
115
|
+
raise typer.Exit(1)
|
|
116
|
+
|
|
117
|
+
console.print(f"[bold]Bulk onboarding from: {from_csv}[/bold]\n")
|
|
118
|
+
|
|
119
|
+
with open(csv_path, newline='') as f:
|
|
120
|
+
reader = csv.DictReader(f)
|
|
121
|
+
rows = list(reader)
|
|
122
|
+
|
|
123
|
+
if not rows:
|
|
124
|
+
print_warning("CSV file is empty")
|
|
125
|
+
raise typer.Exit(0)
|
|
126
|
+
|
|
127
|
+
console.print(f"Found {len(rows)} hosts to onboard:\n")
|
|
128
|
+
for i, row in enumerate(rows, 1):
|
|
129
|
+
console.print(f" {i}. {row.get('name', '?')} ({row.get('ip', '?')})")
|
|
130
|
+
|
|
131
|
+
if not typer.confirm("\nProceed with bulk onboarding?"):
|
|
132
|
+
console.print("[yellow]Cancelled.[/yellow]")
|
|
133
|
+
raise typer.Exit(0)
|
|
134
|
+
|
|
135
|
+
results = []
|
|
136
|
+
for i, row in enumerate(rows, 1):
|
|
137
|
+
console.print(f"\n[bold cyan]--- Onboarding {i}/{len(rows)}: {row.get('name', '?')} ---[/bold cyan]")
|
|
138
|
+
try:
|
|
139
|
+
# Call this function recursively for each row
|
|
140
|
+
_onboard_single_host(
|
|
141
|
+
name=row.get('name'),
|
|
142
|
+
ip=row.get('ip'),
|
|
143
|
+
dns=row.get('dns') or None,
|
|
144
|
+
workgroup=int(row['workgroup']) if row.get('workgroup') else workgroup,
|
|
145
|
+
platform=int(row['platform']) if row.get('platform') else platform,
|
|
146
|
+
jumpoint=int(row['jumpoint']) if row.get('jumpoint') else jumpoint,
|
|
147
|
+
jump_group=int(row['jump_group']) if row.get('jump_group') else jump_group,
|
|
148
|
+
account=row.get('account') or account,
|
|
149
|
+
password=row.get('password') or password,
|
|
150
|
+
pra_username=row.get('pra_username') or pra_username,
|
|
151
|
+
functional_account=int(row['functional_account']) if row.get('functional_account') else functional_account,
|
|
152
|
+
port=int(row['port']) if row.get('port') else port,
|
|
153
|
+
elevation=row.get('elevation') or elevation,
|
|
154
|
+
jump_type=row.get('jump_type') or jump_type,
|
|
155
|
+
skip_pws=skip_pws,
|
|
156
|
+
skip_pra=skip_pra,
|
|
157
|
+
skip_account=skip_account,
|
|
158
|
+
)
|
|
159
|
+
results.append({"name": row.get('name'), "status": "success"})
|
|
160
|
+
except Exception as e:
|
|
161
|
+
console.print(f"[red]Failed: {e}[/red]")
|
|
162
|
+
results.append({"name": row.get('name'), "status": "failed", "error": str(e)})
|
|
163
|
+
|
|
164
|
+
# Summary
|
|
165
|
+
console.print(f"\n[bold]Bulk Onboarding Summary:[/bold]")
|
|
166
|
+
success = sum(1 for r in results if r["status"] == "success")
|
|
167
|
+
failed = sum(1 for r in results if r["status"] == "failed")
|
|
168
|
+
console.print(f" [green]Success: {success}[/green]")
|
|
169
|
+
if failed:
|
|
170
|
+
console.print(f" [red]Failed: {failed}[/red]")
|
|
171
|
+
for r in results:
|
|
172
|
+
if r["status"] == "failed":
|
|
173
|
+
console.print(f" - {r['name']}: {r.get('error', 'Unknown error')}")
|
|
174
|
+
|
|
175
|
+
if output == "json":
|
|
176
|
+
console.print_json(json.dumps(results, default=str))
|
|
177
|
+
return
|
|
178
|
+
|
|
179
|
+
# Single host onboarding
|
|
180
|
+
_onboard_single_host(
|
|
181
|
+
name=name, ip=ip, dns=dns, workgroup=workgroup, platform=platform,
|
|
182
|
+
jumpoint=jumpoint, jump_group=jump_group, account=account, password=password,
|
|
183
|
+
pra_username=pra_username, functional_account=functional_account, port=port,
|
|
184
|
+
elevation=elevation, jump_type=jump_type, skip_pws=skip_pws, skip_pra=skip_pra,
|
|
185
|
+
skip_account=skip_account, output=output,
|
|
186
|
+
)
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
def _onboard_single_host(
|
|
190
|
+
name: Optional[str],
|
|
191
|
+
ip: Optional[str],
|
|
192
|
+
dns: Optional[str],
|
|
193
|
+
workgroup: Optional[int],
|
|
194
|
+
platform: int,
|
|
195
|
+
jumpoint: Optional[int],
|
|
196
|
+
jump_group: Optional[int],
|
|
197
|
+
account: Optional[str],
|
|
198
|
+
password: Optional[str],
|
|
199
|
+
pra_username: Optional[str],
|
|
200
|
+
functional_account: Optional[int],
|
|
201
|
+
port: int,
|
|
202
|
+
elevation: Optional[str],
|
|
203
|
+
jump_type: str,
|
|
204
|
+
skip_pws: bool,
|
|
205
|
+
skip_pra: bool,
|
|
206
|
+
skip_account: bool,
|
|
207
|
+
output: str = "table",
|
|
208
|
+
) -> None:
|
|
209
|
+
"""Internal function to onboard a single host."""
|
|
210
|
+
try:
|
|
211
|
+
# Import clients
|
|
212
|
+
from ..pws.client.base import get_client as get_pws_client
|
|
213
|
+
from ..pra.client import get_client as get_pra_client
|
|
214
|
+
|
|
215
|
+
result = {
|
|
216
|
+
"name": None,
|
|
217
|
+
"ip": None,
|
|
218
|
+
"pws": {"created": False},
|
|
219
|
+
"pra": {"created": False},
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
# ============================================================
|
|
223
|
+
# PHASE 1: Collect all inputs upfront (interactive prompts)
|
|
224
|
+
# ============================================================
|
|
225
|
+
name = prompt_if_missing(name, "System name (used in both PWS and PRA)")
|
|
226
|
+
ip = prompt_if_missing(ip, "IP address")
|
|
227
|
+
|
|
228
|
+
result["name"] = name
|
|
229
|
+
result["ip"] = ip
|
|
230
|
+
|
|
231
|
+
# PWS prompts
|
|
232
|
+
if not skip_pws:
|
|
233
|
+
with get_pws_client() as pws:
|
|
234
|
+
pws.authenticate()
|
|
235
|
+
|
|
236
|
+
# Prompt for workgroup if missing
|
|
237
|
+
if workgroup is None:
|
|
238
|
+
workgroups = pws.list_workgroups()
|
|
239
|
+
workgroup = prompt_from_list(
|
|
240
|
+
workgroups, "Workgroup ID", "ID", "Name", "Available Workgroups", int
|
|
241
|
+
)
|
|
242
|
+
|
|
243
|
+
# Prompt for account name if not provided and not skipping account
|
|
244
|
+
if not skip_account and account is None:
|
|
245
|
+
account = prompt_if_missing(account, "Account name (e.g., root, admin)")
|
|
246
|
+
|
|
247
|
+
# PRA prompts
|
|
248
|
+
if not skip_pra:
|
|
249
|
+
pra = get_pra_client()
|
|
250
|
+
|
|
251
|
+
# Prompt for jumpoint if missing
|
|
252
|
+
if jumpoint is None:
|
|
253
|
+
jumpoints = pra.list_jumpoints()
|
|
254
|
+
jumpoint = prompt_from_list(
|
|
255
|
+
jumpoints, "Jumpoint ID", "id", "name", "Available Jumpoints", int
|
|
256
|
+
)
|
|
257
|
+
|
|
258
|
+
# Prompt for jump group if missing
|
|
259
|
+
if jump_group is None:
|
|
260
|
+
groups = pra.list_jump_groups()
|
|
261
|
+
jump_group = prompt_from_list(
|
|
262
|
+
groups, "Jump Group ID", "id", "name", "Available Jump Groups", int
|
|
263
|
+
)
|
|
264
|
+
|
|
265
|
+
# ============================================================
|
|
266
|
+
# PHASE 2: Execute operations (no more prompts after this)
|
|
267
|
+
# ============================================================
|
|
268
|
+
console.print() # Blank line before operations
|
|
269
|
+
|
|
270
|
+
# PWS Onboarding
|
|
271
|
+
if not skip_pws:
|
|
272
|
+
with get_pws_client() as pws:
|
|
273
|
+
pws.authenticate()
|
|
274
|
+
|
|
275
|
+
# Create asset
|
|
276
|
+
console.print(f"[dim]PWS: Creating asset '{name}'...[/dim]")
|
|
277
|
+
asset = pws.create_asset(
|
|
278
|
+
workgroup_id=workgroup,
|
|
279
|
+
ip_address=ip,
|
|
280
|
+
asset_name=name,
|
|
281
|
+
dns_name=dns,
|
|
282
|
+
)
|
|
283
|
+
asset_id = asset.get("AssetID")
|
|
284
|
+
console.print(f" [green]Created asset ID: {asset_id}[/green]")
|
|
285
|
+
|
|
286
|
+
# Create managed system
|
|
287
|
+
console.print(f"[dim]PWS: Creating managed system '{name}'...[/dim]")
|
|
288
|
+
system = pws.create_managed_system(
|
|
289
|
+
system_name=name,
|
|
290
|
+
platform_id=platform,
|
|
291
|
+
asset_id=asset_id,
|
|
292
|
+
port=port,
|
|
293
|
+
functional_account_id=functional_account,
|
|
294
|
+
auto_management_flag=True if functional_account else False,
|
|
295
|
+
elevation_command=elevation,
|
|
296
|
+
)
|
|
297
|
+
system_id = system.get("ManagedSystemID")
|
|
298
|
+
console.print(f" [green]Created managed system ID: {system_id}[/green]")
|
|
299
|
+
|
|
300
|
+
result["pws"] = {
|
|
301
|
+
"created": True,
|
|
302
|
+
"asset_id": asset_id,
|
|
303
|
+
"system_id": system_id,
|
|
304
|
+
"workgroup_id": workgroup,
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
# Create managed account (unless skipped)
|
|
308
|
+
if not skip_account:
|
|
309
|
+
# If no functional account (no auto-management), password is required
|
|
310
|
+
# Generate one if not provided
|
|
311
|
+
account_password = password
|
|
312
|
+
generated_password = False
|
|
313
|
+
if not functional_account and not account_password:
|
|
314
|
+
account_password = generate_password()
|
|
315
|
+
generated_password = True
|
|
316
|
+
|
|
317
|
+
console.print(f"[dim]PWS: Creating managed account '{account}'...[/dim]")
|
|
318
|
+
account_obj = pws.create_managed_account(
|
|
319
|
+
system_id=system_id,
|
|
320
|
+
account_name=account,
|
|
321
|
+
password=account_password,
|
|
322
|
+
auto_management_flag=True if functional_account else False,
|
|
323
|
+
)
|
|
324
|
+
account_id = account_obj.get("ManagedAccountID")
|
|
325
|
+
console.print(f" [green]Created managed account ID: {account_id}[/green]")
|
|
326
|
+
|
|
327
|
+
if generated_password:
|
|
328
|
+
console.print(f" [yellow]Generated password (save this!):[/yellow] [bold]{account_password}[/bold]")
|
|
329
|
+
|
|
330
|
+
result["pws"]["account_id"] = account_id
|
|
331
|
+
result["pws"]["account_name"] = account
|
|
332
|
+
result["pws"]["password_generated"] = generated_password
|
|
333
|
+
|
|
334
|
+
# PRA Onboarding
|
|
335
|
+
if not skip_pra:
|
|
336
|
+
pra = get_pra_client()
|
|
337
|
+
|
|
338
|
+
# Determine PRA username
|
|
339
|
+
pra_user = pra_username or ("ec2-admin" if jump_type == "shell" else None)
|
|
340
|
+
|
|
341
|
+
if jump_type == "shell":
|
|
342
|
+
console.print(f"[dim]PRA: Creating shell jump item '{name}'...[/dim]")
|
|
343
|
+
jump_item = pra.create_shell_jump(
|
|
344
|
+
name=name,
|
|
345
|
+
hostname=ip,
|
|
346
|
+
jumpoint_id=jumpoint,
|
|
347
|
+
jump_group_id=jump_group,
|
|
348
|
+
port=port,
|
|
349
|
+
protocol="ssh",
|
|
350
|
+
username=pra_user,
|
|
351
|
+
)
|
|
352
|
+
jump_id = jump_item.get("id")
|
|
353
|
+
console.print(f" [green]Created shell jump ID: {jump_id}[/green]")
|
|
354
|
+
else:
|
|
355
|
+
console.print(f"[dim]PRA: Creating RDP jump item '{name}'...[/dim]")
|
|
356
|
+
jump_item = pra.create_rdp_jump(
|
|
357
|
+
name=name,
|
|
358
|
+
hostname=ip,
|
|
359
|
+
jumpoint_id=jumpoint,
|
|
360
|
+
jump_group_id=jump_group,
|
|
361
|
+
rdp_port=port if port != 22 else 3389,
|
|
362
|
+
)
|
|
363
|
+
jump_id = jump_item.get("id")
|
|
364
|
+
console.print(f" [green]Created RDP jump ID: {jump_id}[/green]")
|
|
365
|
+
|
|
366
|
+
result["pra"] = {
|
|
367
|
+
"created": True,
|
|
368
|
+
"jump_type": jump_type,
|
|
369
|
+
"jump_id": jump_id,
|
|
370
|
+
"jumpoint_id": jumpoint,
|
|
371
|
+
"jump_group_id": jump_group,
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
# Output
|
|
375
|
+
if output == "json":
|
|
376
|
+
console.print_json(json.dumps(result))
|
|
377
|
+
else:
|
|
378
|
+
pws_info = ""
|
|
379
|
+
if result["pws"].get("created"):
|
|
380
|
+
pws_info = (
|
|
381
|
+
f"[green]PWS Created:[/green]\n"
|
|
382
|
+
f" Asset ID: [cyan]{result['pws']['asset_id']}[/cyan]\n"
|
|
383
|
+
f" System ID: [cyan]{result['pws']['system_id']}[/cyan]\n"
|
|
384
|
+
)
|
|
385
|
+
if result["pws"].get("account_id"):
|
|
386
|
+
pws_info += (
|
|
387
|
+
f" Account ID: [cyan]{result['pws']['account_id']}[/cyan]\n"
|
|
388
|
+
f" Account: [bold]{result['pws'].get('account_name', account)}[/bold]\n"
|
|
389
|
+
)
|
|
390
|
+
if result["pws"].get("password_generated"):
|
|
391
|
+
pws_info += f" [yellow](Password was auto-generated)[/yellow]\n"
|
|
392
|
+
pws_info += "\n"
|
|
393
|
+
elif skip_pws:
|
|
394
|
+
pws_info = "[yellow]PWS: Skipped[/yellow]\n\n"
|
|
395
|
+
|
|
396
|
+
pra_info = ""
|
|
397
|
+
if result["pra"].get("created"):
|
|
398
|
+
pra_info = (
|
|
399
|
+
f"[green]PRA Created:[/green]\n"
|
|
400
|
+
f" Jump Type: [cyan]{result['pra']['jump_type'].upper()}[/cyan]\n"
|
|
401
|
+
f" Jump ID: [cyan]{result['pra']['jump_id']}[/cyan]\n"
|
|
402
|
+
f" Jumpoint ID: {result['pra']['jumpoint_id']}\n"
|
|
403
|
+
f" Jump Group ID: {result['pra']['jump_group_id']}\n"
|
|
404
|
+
)
|
|
405
|
+
elif skip_pra:
|
|
406
|
+
pra_info = "[yellow]PRA: Skipped[/yellow]\n"
|
|
407
|
+
|
|
408
|
+
console.print(Panel(
|
|
409
|
+
f"[bold green]Total PASM Onboarding Complete![/bold green]\n\n"
|
|
410
|
+
f"Name: [bold]{name}[/bold]\n"
|
|
411
|
+
f"IP: {ip}\n\n"
|
|
412
|
+
f"{pws_info}"
|
|
413
|
+
f"{pra_info}\n"
|
|
414
|
+
f"[dim]ECM Note: PWS system name and PRA jump item name match for credential lookup.[/dim]",
|
|
415
|
+
title="PASM Onboard",
|
|
416
|
+
))
|
|
417
|
+
|
|
418
|
+
except httpx.HTTPStatusError as e:
|
|
419
|
+
print_api_error(e, "pasm-onboard")
|
|
420
|
+
raise typer.Exit(1)
|
|
421
|
+
except httpx.RequestError as e:
|
|
422
|
+
print_api_error(e, "pasm-onboard")
|
|
423
|
+
raise typer.Exit(1)
|
|
424
|
+
except typer.Exit:
|
|
425
|
+
raise
|
|
426
|
+
except Exception as e:
|
|
427
|
+
print_api_error(e, "pasm-onboard")
|
|
428
|
+
raise typer.Exit(1)
|
|
429
|
+
|
|
430
|
+
|
|
431
|
+
@app.command("pasm-offboard")
|
|
432
|
+
def pasm_offboard(
|
|
433
|
+
name: str = typer.Option(..., "--name", "-n", help="System/jump item name (searches both PWS and PRA)"),
|
|
434
|
+
force: bool = typer.Option(False, "--force", "-f", help="Skip confirmation"),
|
|
435
|
+
skip_pws: bool = typer.Option(False, "--skip-pws", help="Skip PWS offboarding"),
|
|
436
|
+
skip_pra: bool = typer.Option(False, "--skip-pra", help="Skip PRA offboarding"),
|
|
437
|
+
output: str = typer.Option("table", "--output", "-o", help="Output format: table or json"),
|
|
438
|
+
) -> None:
|
|
439
|
+
"""Offboard a host from Total PASM (Password Safe + PRA).
|
|
440
|
+
|
|
441
|
+
Removes resources from both Password Safe and PRA by name.
|
|
442
|
+
|
|
443
|
+
What gets deleted:
|
|
444
|
+
- PWS: Managed Accounts -> Managed System -> Asset
|
|
445
|
+
- PRA: Shell or RDP Jump Item
|
|
446
|
+
|
|
447
|
+
Examples:
|
|
448
|
+
bt quick pasm-offboard -n "my-server"
|
|
449
|
+
bt quick pasm-offboard -n "web-01" --force
|
|
450
|
+
bt quick pasm-offboard -n "jump-host" --skip-pws
|
|
451
|
+
"""
|
|
452
|
+
try:
|
|
453
|
+
from ..pws.client.base import get_client as get_pws_client
|
|
454
|
+
from ..pra.client import get_client as get_pra_client
|
|
455
|
+
|
|
456
|
+
result = {
|
|
457
|
+
"name": name,
|
|
458
|
+
"pws": {"deleted": False},
|
|
459
|
+
"pra": {"deleted": False},
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
# Find resources in both products
|
|
463
|
+
pws_system = None
|
|
464
|
+
pws_accounts = []
|
|
465
|
+
pws_asset_id = None
|
|
466
|
+
pra_jump = None
|
|
467
|
+
pra_jump_type = None
|
|
468
|
+
|
|
469
|
+
# Search PWS
|
|
470
|
+
if not skip_pws:
|
|
471
|
+
try:
|
|
472
|
+
with get_pws_client() as pws:
|
|
473
|
+
pws.authenticate()
|
|
474
|
+
systems = pws.list_managed_systems(search=name)
|
|
475
|
+
for s in systems:
|
|
476
|
+
if s.get("SystemName", "").lower() == name.lower():
|
|
477
|
+
pws_system = s
|
|
478
|
+
break
|
|
479
|
+
if not pws_system and systems:
|
|
480
|
+
pws_system = systems[0]
|
|
481
|
+
|
|
482
|
+
if pws_system:
|
|
483
|
+
system_id = pws_system.get("ManagedSystemID")
|
|
484
|
+
pws_accounts = pws.list_managed_accounts(system_id=system_id)
|
|
485
|
+
# Get asset ID
|
|
486
|
+
if not pws_system.get("AssetID"):
|
|
487
|
+
full_system = pws.get_managed_system(system_id)
|
|
488
|
+
pws_asset_id = full_system.get("AssetID")
|
|
489
|
+
else:
|
|
490
|
+
pws_asset_id = pws_system.get("AssetID")
|
|
491
|
+
except Exception as e:
|
|
492
|
+
console.print(f"[yellow]PWS search error: {e}[/yellow]")
|
|
493
|
+
|
|
494
|
+
# Search PRA
|
|
495
|
+
if not skip_pra:
|
|
496
|
+
try:
|
|
497
|
+
pra = get_pra_client()
|
|
498
|
+
# Search shell jumps
|
|
499
|
+
shell_jumps = pra.list_shell_jumps()
|
|
500
|
+
for j in shell_jumps:
|
|
501
|
+
if j.get("name", "").lower() == name.lower():
|
|
502
|
+
pra_jump = j
|
|
503
|
+
pra_jump_type = "shell"
|
|
504
|
+
break
|
|
505
|
+
|
|
506
|
+
# Search RDP jumps if not found
|
|
507
|
+
if not pra_jump:
|
|
508
|
+
rdp_jumps = pra.list_rdp_jumps()
|
|
509
|
+
for j in rdp_jumps:
|
|
510
|
+
if j.get("name", "").lower() == name.lower():
|
|
511
|
+
pra_jump = j
|
|
512
|
+
pra_jump_type = "rdp"
|
|
513
|
+
break
|
|
514
|
+
except Exception as e:
|
|
515
|
+
console.print(f"[yellow]PRA search error: {e}[/yellow]")
|
|
516
|
+
|
|
517
|
+
# Show what will be deleted
|
|
518
|
+
console.print(f"\n[bold]Will delete '{name}':[/bold]")
|
|
519
|
+
|
|
520
|
+
if pws_system:
|
|
521
|
+
console.print(f"\n[cyan]PWS:[/cyan]")
|
|
522
|
+
console.print(f" System: {pws_system.get('SystemName')} (ID: {pws_system.get('ManagedSystemID')})")
|
|
523
|
+
if pws_accounts:
|
|
524
|
+
console.print(f" Accounts ({len(pws_accounts)}):")
|
|
525
|
+
for acc in pws_accounts:
|
|
526
|
+
console.print(f" - {acc.get('AccountName')} (ID: {acc.get('ManagedAccountID', acc.get('AccountId'))})")
|
|
527
|
+
if pws_asset_id:
|
|
528
|
+
console.print(f" Asset ID: {pws_asset_id}")
|
|
529
|
+
elif not skip_pws:
|
|
530
|
+
console.print(f"\n[yellow]PWS: No system found matching '{name}'[/yellow]")
|
|
531
|
+
|
|
532
|
+
if pra_jump:
|
|
533
|
+
console.print(f"\n[cyan]PRA:[/cyan]")
|
|
534
|
+
console.print(f" {pra_jump_type.upper()} Jump: {pra_jump.get('name')} (ID: {pra_jump.get('id')})")
|
|
535
|
+
elif not skip_pra:
|
|
536
|
+
console.print(f"\n[yellow]PRA: No jump item found matching '{name}'[/yellow]")
|
|
537
|
+
|
|
538
|
+
if not pws_system and not pra_jump:
|
|
539
|
+
print_error(f"No resources found matching '{name}' in either PWS or PRA")
|
|
540
|
+
raise typer.Exit(1)
|
|
541
|
+
|
|
542
|
+
# Confirm
|
|
543
|
+
if not force:
|
|
544
|
+
confirm = typer.confirm("\nProceed with deletion?")
|
|
545
|
+
if not confirm:
|
|
546
|
+
console.print("[yellow]Cancelled.[/yellow]")
|
|
547
|
+
raise typer.Exit(0)
|
|
548
|
+
|
|
549
|
+
# Delete PWS resources
|
|
550
|
+
if pws_system and not skip_pws:
|
|
551
|
+
with get_pws_client() as pws:
|
|
552
|
+
pws.authenticate()
|
|
553
|
+
system_id = pws_system.get("ManagedSystemID")
|
|
554
|
+
|
|
555
|
+
# Delete accounts
|
|
556
|
+
deleted_accounts = []
|
|
557
|
+
for acc in pws_accounts:
|
|
558
|
+
acc_id = acc.get("ManagedAccountID", acc.get("AccountId"))
|
|
559
|
+
acc_name = acc.get("AccountName")
|
|
560
|
+
console.print(f"[dim]PWS: Deleting account {acc_name}...[/dim]")
|
|
561
|
+
pws.delete_managed_account(acc_id)
|
|
562
|
+
deleted_accounts.append({"id": acc_id, "name": acc_name})
|
|
563
|
+
console.print(f" [green]Deleted account: {acc_name}[/green]")
|
|
564
|
+
|
|
565
|
+
# Delete system
|
|
566
|
+
console.print(f"[dim]PWS: Deleting system {pws_system.get('SystemName')}...[/dim]")
|
|
567
|
+
pws.delete_managed_system(system_id)
|
|
568
|
+
console.print(f" [green]Deleted system: {pws_system.get('SystemName')}[/green]")
|
|
569
|
+
|
|
570
|
+
# Delete asset
|
|
571
|
+
if pws_asset_id:
|
|
572
|
+
console.print(f"[dim]PWS: Deleting asset {pws_asset_id}...[/dim]")
|
|
573
|
+
pws.delete_asset(pws_asset_id)
|
|
574
|
+
console.print(f" [green]Deleted asset ID: {pws_asset_id}[/green]")
|
|
575
|
+
|
|
576
|
+
result["pws"] = {
|
|
577
|
+
"deleted": True,
|
|
578
|
+
"system_id": system_id,
|
|
579
|
+
"asset_id": pws_asset_id,
|
|
580
|
+
"accounts_deleted": len(deleted_accounts),
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
# Delete PRA resources
|
|
584
|
+
if pra_jump and not skip_pra:
|
|
585
|
+
pra = get_pra_client()
|
|
586
|
+
jump_id = pra_jump.get("id")
|
|
587
|
+
|
|
588
|
+
console.print(f"[dim]PRA: Deleting {pra_jump_type} jump item {pra_jump.get('name')}...[/dim]")
|
|
589
|
+
if pra_jump_type == "shell":
|
|
590
|
+
pra.delete_shell_jump(jump_id)
|
|
591
|
+
else:
|
|
592
|
+
pra.delete_rdp_jump(jump_id)
|
|
593
|
+
console.print(f" [green]Deleted {pra_jump_type} jump: {pra_jump.get('name')}[/green]")
|
|
594
|
+
|
|
595
|
+
result["pra"] = {
|
|
596
|
+
"deleted": True,
|
|
597
|
+
"jump_type": pra_jump_type,
|
|
598
|
+
"jump_id": jump_id,
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
# Output
|
|
602
|
+
if output == "json":
|
|
603
|
+
console.print_json(json.dumps(result))
|
|
604
|
+
else:
|
|
605
|
+
console.print(f"\n[bold green]'{name}' offboarded from Total PASM![/bold green]")
|
|
606
|
+
if result["pws"].get("deleted"):
|
|
607
|
+
console.print(f" PWS: System + {result['pws']['accounts_deleted']} accounts + asset deleted")
|
|
608
|
+
if result["pra"].get("deleted"):
|
|
609
|
+
console.print(f" PRA: {result['pra']['jump_type'].upper()} jump item deleted")
|
|
610
|
+
|
|
611
|
+
except httpx.HTTPStatusError as e:
|
|
612
|
+
print_api_error(e, "pasm-offboard")
|
|
613
|
+
raise typer.Exit(1)
|
|
614
|
+
except httpx.RequestError as e:
|
|
615
|
+
print_api_error(e, "pasm-offboard")
|
|
616
|
+
raise typer.Exit(1)
|
|
617
|
+
except typer.Exit:
|
|
618
|
+
raise
|
|
619
|
+
except Exception as e:
|
|
620
|
+
print_api_error(e, "pasm-offboard")
|
|
621
|
+
raise typer.Exit(1)
|
|
622
|
+
|
|
623
|
+
|
|
624
|
+
@app.command("pasm-search")
|
|
625
|
+
def pasm_search(
|
|
626
|
+
query: str = typer.Argument(..., help="Search term (searches both PWS and PRA)"),
|
|
627
|
+
output: str = typer.Option("table", "--output", "-o", help="Output format: table or json"),
|
|
628
|
+
) -> None:
|
|
629
|
+
"""Search across both Password Safe and PRA.
|
|
630
|
+
|
|
631
|
+
Finds matching systems, accounts, and jump items in both products.
|
|
632
|
+
Useful for finding resources before onboarding or checking ECM alignment.
|
|
633
|
+
|
|
634
|
+
Examples:
|
|
635
|
+
bt quick pasm-search axion
|
|
636
|
+
bt quick pasm-search web-server -o json
|
|
637
|
+
"""
|
|
638
|
+
try:
|
|
639
|
+
from ..pws.client.base import get_client as get_pws_client
|
|
640
|
+
from ..pra.client import get_client as get_pra_client
|
|
641
|
+
|
|
642
|
+
query_lower = query.lower()
|
|
643
|
+
result = {
|
|
644
|
+
"query": query,
|
|
645
|
+
"pws": {"systems": [], "accounts": []},
|
|
646
|
+
"pra": {"shell_jumps": [], "rdp_jumps": []},
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
# Search PWS
|
|
650
|
+
try:
|
|
651
|
+
with get_pws_client() as pws:
|
|
652
|
+
pws.authenticate()
|
|
653
|
+
console.print(f"[dim]Searching PWS for '{query}'...[/dim]")
|
|
654
|
+
|
|
655
|
+
systems = pws.list_managed_systems()
|
|
656
|
+
result["pws"]["systems"] = [
|
|
657
|
+
s for s in systems
|
|
658
|
+
if query_lower in s.get("SystemName", "").lower()
|
|
659
|
+
][:20]
|
|
660
|
+
|
|
661
|
+
accounts = pws.list_managed_accounts(account_name=query)
|
|
662
|
+
result["pws"]["accounts"] = accounts[:20]
|
|
663
|
+
except Exception as e:
|
|
664
|
+
console.print(f"[yellow]PWS search error: {e}[/yellow]")
|
|
665
|
+
|
|
666
|
+
# Search PRA
|
|
667
|
+
try:
|
|
668
|
+
pra = get_pra_client()
|
|
669
|
+
console.print(f"[dim]Searching PRA for '{query}'...[/dim]")
|
|
670
|
+
|
|
671
|
+
shell_jumps = pra.list_shell_jumps()
|
|
672
|
+
result["pra"]["shell_jumps"] = [
|
|
673
|
+
j for j in shell_jumps
|
|
674
|
+
if query_lower in j.get("name", "").lower()
|
|
675
|
+
or query_lower in j.get("hostname", "").lower()
|
|
676
|
+
][:20]
|
|
677
|
+
|
|
678
|
+
rdp_jumps = pra.list_rdp_jumps()
|
|
679
|
+
result["pra"]["rdp_jumps"] = [
|
|
680
|
+
j for j in rdp_jumps
|
|
681
|
+
if query_lower in j.get("name", "").lower()
|
|
682
|
+
or query_lower in j.get("hostname", "").lower()
|
|
683
|
+
][:20]
|
|
684
|
+
except Exception as e:
|
|
685
|
+
console.print(f"[yellow]PRA search error: {e}[/yellow]")
|
|
686
|
+
|
|
687
|
+
if output == "json":
|
|
688
|
+
console.print_json(json.dumps(result, default=str))
|
|
689
|
+
else:
|
|
690
|
+
# PWS Systems
|
|
691
|
+
if result["pws"]["systems"]:
|
|
692
|
+
table = Table(title=f"PWS Systems matching '{query}'")
|
|
693
|
+
table.add_column("ID", style="cyan")
|
|
694
|
+
table.add_column("Name", style="green")
|
|
695
|
+
table.add_column("IP", style="yellow")
|
|
696
|
+
table.add_column("Platform")
|
|
697
|
+
|
|
698
|
+
for s in result["pws"]["systems"]:
|
|
699
|
+
table.add_row(
|
|
700
|
+
str(s.get("ManagedSystemID", "")),
|
|
701
|
+
s.get("SystemName", ""),
|
|
702
|
+
s.get("IPAddress", ""),
|
|
703
|
+
str(s.get("PlatformID", "")),
|
|
704
|
+
)
|
|
705
|
+
console.print(table)
|
|
706
|
+
else:
|
|
707
|
+
console.print(f"[yellow]No PWS systems found matching '{query}'[/yellow]")
|
|
708
|
+
|
|
709
|
+
console.print()
|
|
710
|
+
|
|
711
|
+
# PWS Accounts
|
|
712
|
+
if result["pws"]["accounts"]:
|
|
713
|
+
table = Table(title=f"PWS Accounts matching '{query}'")
|
|
714
|
+
table.add_column("ID", style="cyan")
|
|
715
|
+
table.add_column("Account", style="green")
|
|
716
|
+
table.add_column("System", style="yellow")
|
|
717
|
+
|
|
718
|
+
for a in result["pws"]["accounts"]:
|
|
719
|
+
table.add_row(
|
|
720
|
+
str(a.get("ManagedAccountID", a.get("AccountId", ""))),
|
|
721
|
+
a.get("AccountName", ""),
|
|
722
|
+
a.get("SystemName", ""),
|
|
723
|
+
)
|
|
724
|
+
console.print(table)
|
|
725
|
+
|
|
726
|
+
console.print()
|
|
727
|
+
|
|
728
|
+
# PRA Shell Jumps
|
|
729
|
+
if result["pra"]["shell_jumps"]:
|
|
730
|
+
table = Table(title=f"PRA Shell Jumps matching '{query}'")
|
|
731
|
+
table.add_column("ID", style="cyan")
|
|
732
|
+
table.add_column("Name", style="green")
|
|
733
|
+
table.add_column("Hostname", style="yellow")
|
|
734
|
+
table.add_column("Username", style="magenta")
|
|
735
|
+
|
|
736
|
+
for j in result["pra"]["shell_jumps"]:
|
|
737
|
+
table.add_row(
|
|
738
|
+
str(j.get("id", "")),
|
|
739
|
+
j.get("name", ""),
|
|
740
|
+
j.get("hostname", ""),
|
|
741
|
+
j.get("username", "") or "-",
|
|
742
|
+
)
|
|
743
|
+
console.print(table)
|
|
744
|
+
else:
|
|
745
|
+
console.print(f"[yellow]No PRA shell jumps found matching '{query}'[/yellow]")
|
|
746
|
+
|
|
747
|
+
console.print()
|
|
748
|
+
|
|
749
|
+
# PRA RDP Jumps
|
|
750
|
+
if result["pra"]["rdp_jumps"]:
|
|
751
|
+
table = Table(title=f"PRA RDP Jumps matching '{query}'")
|
|
752
|
+
table.add_column("ID", style="cyan")
|
|
753
|
+
table.add_column("Name", style="green")
|
|
754
|
+
table.add_column("Hostname", style="yellow")
|
|
755
|
+
table.add_column("Domain", style="magenta")
|
|
756
|
+
|
|
757
|
+
for j in result["pra"]["rdp_jumps"]:
|
|
758
|
+
table.add_row(
|
|
759
|
+
str(j.get("id", "")),
|
|
760
|
+
j.get("name", ""),
|
|
761
|
+
j.get("hostname", ""),
|
|
762
|
+
j.get("domain", "") or "-",
|
|
763
|
+
)
|
|
764
|
+
console.print(table)
|
|
765
|
+
else:
|
|
766
|
+
console.print(f"[yellow]No PRA RDP jumps found matching '{query}'[/yellow]")
|
|
767
|
+
|
|
768
|
+
# ECM alignment check
|
|
769
|
+
console.print()
|
|
770
|
+
pws_names = {s.get("SystemName", "").lower() for s in result["pws"]["systems"]}
|
|
771
|
+
pra_names = {j.get("name", "").lower() for j in result["pra"]["shell_jumps"]}
|
|
772
|
+
pra_names.update(j.get("name", "").lower() for j in result["pra"]["rdp_jumps"])
|
|
773
|
+
|
|
774
|
+
aligned = pws_names & pra_names
|
|
775
|
+
if aligned:
|
|
776
|
+
console.print(f"[green]ECM Aligned:[/green] {len(aligned)} name(s) match in both PWS and PRA")
|
|
777
|
+
else:
|
|
778
|
+
console.print("[yellow]ECM Note:[/yellow] No exact name matches between PWS and PRA")
|
|
779
|
+
|
|
780
|
+
except typer.Exit:
|
|
781
|
+
raise
|
|
782
|
+
except Exception as e:
|
|
783
|
+
print_api_error(e, "pasm-search")
|
|
784
|
+
raise typer.Exit(1)
|