praetorian-cli 2.2.1__py3-none-any.whl → 2.2.3__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.
- praetorian_cli/handlers/add.py +25 -7
- praetorian_cli/handlers/aegis.py +107 -0
- praetorian_cli/handlers/delete.py +3 -2
- praetorian_cli/handlers/get.py +48 -2
- praetorian_cli/handlers/list.py +41 -9
- praetorian_cli/handlers/ssh_utils.py +154 -0
- praetorian_cli/handlers/test.py +7 -2
- praetorian_cli/handlers/update.py +3 -3
- praetorian_cli/main.py +1 -0
- praetorian_cli/sdk/chariot.py +71 -12
- praetorian_cli/sdk/entities/aegis.py +437 -0
- praetorian_cli/sdk/entities/assets.py +30 -12
- praetorian_cli/sdk/entities/scanners.py +13 -0
- praetorian_cli/sdk/entities/schema.py +27 -0
- praetorian_cli/sdk/entities/seeds.py +108 -56
- praetorian_cli/sdk/mcp_server.py +2 -3
- praetorian_cli/sdk/model/aegis.py +156 -0
- praetorian_cli/sdk/model/query.py +1 -1
- praetorian_cli/sdk/model/utils.py +2 -8
- praetorian_cli/sdk/test/pytest.ini +1 -0
- praetorian_cli/sdk/test/test_asset.py +2 -2
- praetorian_cli/sdk/test/test_seed.py +13 -14
- praetorian_cli/sdk/test/test_z_cli.py +22 -24
- praetorian_cli/sdk/test/ui_mocks.py +133 -0
- praetorian_cli/sdk/test/utils.py +16 -4
- praetorian_cli/ui/__init__.py +3 -0
- praetorian_cli/ui/aegis/__init__.py +5 -0
- praetorian_cli/ui/aegis/commands/__init__.py +2 -0
- praetorian_cli/ui/aegis/commands/help.py +81 -0
- praetorian_cli/ui/aegis/commands/info.py +136 -0
- praetorian_cli/ui/aegis/commands/job.py +381 -0
- praetorian_cli/ui/aegis/commands/list.py +14 -0
- praetorian_cli/ui/aegis/commands/set.py +32 -0
- praetorian_cli/ui/aegis/commands/ssh.py +87 -0
- praetorian_cli/ui/aegis/constants.py +20 -0
- praetorian_cli/ui/aegis/menu.py +395 -0
- praetorian_cli/ui/aegis/utils.py +162 -0
- {praetorian_cli-2.2.1.dist-info → praetorian_cli-2.2.3.dist-info}/METADATA +4 -1
- {praetorian_cli-2.2.1.dist-info → praetorian_cli-2.2.3.dist-info}/RECORD +43 -24
- {praetorian_cli-2.2.1.dist-info → praetorian_cli-2.2.3.dist-info}/WHEEL +0 -0
- {praetorian_cli-2.2.1.dist-info → praetorian_cli-2.2.3.dist-info}/entry_points.txt +0 -0
- {praetorian_cli-2.2.1.dist-info → praetorian_cli-2.2.3.dist-info}/licenses/LICENSE +0 -0
- {praetorian_cli-2.2.1.dist-info → praetorian_cli-2.2.3.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,381 @@
|
|
|
1
|
+
import json
|
|
2
|
+
from rich.table import Table
|
|
3
|
+
from rich.box import MINIMAL
|
|
4
|
+
from rich.prompt import Prompt, Confirm
|
|
5
|
+
from ..utils import format_timestamp, format_job_status
|
|
6
|
+
from ..constants import DEFAULT_COLORS
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def handle_job(menu, args):
|
|
10
|
+
"""Handle job command with subcommands: list, run, capabilities."""
|
|
11
|
+
if not args:
|
|
12
|
+
show_job_help(menu)
|
|
13
|
+
return
|
|
14
|
+
|
|
15
|
+
subcommand = args[0].lower()
|
|
16
|
+
if subcommand == 'list':
|
|
17
|
+
list_jobs(menu)
|
|
18
|
+
elif subcommand == 'run':
|
|
19
|
+
run_job(menu, args[1:])
|
|
20
|
+
elif subcommand in ['capabilities', 'caps']:
|
|
21
|
+
list_capabilities(menu, args[1:])
|
|
22
|
+
else:
|
|
23
|
+
menu.console.print(f"\n Unknown job subcommand: {subcommand}")
|
|
24
|
+
show_job_help(menu)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def show_job_help(menu):
|
|
28
|
+
help_text = f"""
|
|
29
|
+
Job Commands
|
|
30
|
+
|
|
31
|
+
job list List recent jobs for selected agent
|
|
32
|
+
job run [capability] Run a capability on selected agent (interactive picker)
|
|
33
|
+
job capabilities List available capabilities (alias: caps)
|
|
34
|
+
[--details] Show full descriptions
|
|
35
|
+
|
|
36
|
+
Examples:
|
|
37
|
+
job list # List recent jobs
|
|
38
|
+
job capabilities # List capabilities with brief descriptions
|
|
39
|
+
job caps --details # List capabilities with full descriptions
|
|
40
|
+
job run # Interactive capability picker
|
|
41
|
+
job run windows-enum # Run specific capability with confirmation
|
|
42
|
+
"""
|
|
43
|
+
menu.console.print(help_text)
|
|
44
|
+
menu.pause()
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def list_jobs(menu):
|
|
48
|
+
if not menu.selected_agent:
|
|
49
|
+
menu.console.print("\n No agent selected. Use 'set <id>' to select one.\n")
|
|
50
|
+
menu.pause()
|
|
51
|
+
return
|
|
52
|
+
|
|
53
|
+
hostname = menu.selected_agent.hostname
|
|
54
|
+
|
|
55
|
+
try:
|
|
56
|
+
jobs, _ = menu.sdk.jobs.list(prefix_filter=hostname)
|
|
57
|
+
|
|
58
|
+
if not jobs:
|
|
59
|
+
menu.console.print(f"\n No jobs found for {hostname}\n")
|
|
60
|
+
menu.pause()
|
|
61
|
+
return
|
|
62
|
+
|
|
63
|
+
jobs.sort(key=lambda j: j.get('created', 0), reverse=True)
|
|
64
|
+
|
|
65
|
+
colors = getattr(menu, 'colors', DEFAULT_COLORS)
|
|
66
|
+
jobs_table = Table(
|
|
67
|
+
show_header=True,
|
|
68
|
+
header_style=f"bold {colors['primary']}",
|
|
69
|
+
border_style=colors['dim'],
|
|
70
|
+
box=MINIMAL,
|
|
71
|
+
show_lines=False,
|
|
72
|
+
padding=(0, 2),
|
|
73
|
+
pad_edge=False
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
jobs_table.add_column("JOB ID", style=f"bold {colors['accent']}", width=12, no_wrap=True)
|
|
77
|
+
jobs_table.add_column("CAPABILITY", style="white", min_width=20, no_wrap=True)
|
|
78
|
+
jobs_table.add_column("STATUS", width=10, justify="center", no_wrap=True)
|
|
79
|
+
jobs_table.add_column("CREATED", style=f"{colors['dim']}", width=12, justify="right", no_wrap=True)
|
|
80
|
+
|
|
81
|
+
menu.console.print()
|
|
82
|
+
menu.console.print(f" Recent Jobs for {hostname}")
|
|
83
|
+
menu.console.print()
|
|
84
|
+
|
|
85
|
+
for job in jobs[:10]:
|
|
86
|
+
capability = job.get('capabilities', ['unknown'])[0] if job.get('capabilities') else 'unknown'
|
|
87
|
+
status = job.get('status', 'unknown')
|
|
88
|
+
job_id = job.get('key', '').split('#')[-1][:10]
|
|
89
|
+
created = job.get('created', 0)
|
|
90
|
+
|
|
91
|
+
created_str = format_timestamp(created)
|
|
92
|
+
status_display = format_job_status(status, colors)
|
|
93
|
+
|
|
94
|
+
jobs_table.add_row(job_id, capability, status_display, created_str)
|
|
95
|
+
|
|
96
|
+
menu.console.print(jobs_table)
|
|
97
|
+
menu.console.print()
|
|
98
|
+
menu.pause()
|
|
99
|
+
|
|
100
|
+
except Exception as e:
|
|
101
|
+
menu.console.print(f"[red]Error listing jobs: {e}[/red]")
|
|
102
|
+
menu.pause()
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def run_job(menu, args):
|
|
106
|
+
if not menu.selected_agent:
|
|
107
|
+
menu.console.print("\n No agent selected. Use 'set <id>' to select one.\n")
|
|
108
|
+
menu.pause()
|
|
109
|
+
return
|
|
110
|
+
|
|
111
|
+
hostname = menu.selected_agent.hostname or 'Unknown'
|
|
112
|
+
|
|
113
|
+
# Use interactive capability picker if no capability provided, otherwise validate the provided one
|
|
114
|
+
suggested_capability = args[0] if args else None
|
|
115
|
+
capability = _interactive_capability_picker(menu, suggested_capability)
|
|
116
|
+
if not capability:
|
|
117
|
+
return # User cancelled
|
|
118
|
+
|
|
119
|
+
# Validate capability using SDK
|
|
120
|
+
capability_info = menu.sdk.aegis.validate_capability(capability)
|
|
121
|
+
if not capability_info:
|
|
122
|
+
colors = getattr(menu, 'colors', DEFAULT_COLORS)
|
|
123
|
+
menu.console.print(f" [{colors['error']}]Invalid capability: '{capability}'[/{colors['error']}]")
|
|
124
|
+
menu.console.print(" Use 'job capabilities' to see available options")
|
|
125
|
+
menu.pause()
|
|
126
|
+
return
|
|
127
|
+
|
|
128
|
+
target_type = capability_info.get('target', 'asset').lower()
|
|
129
|
+
|
|
130
|
+
# Create appropriate target key
|
|
131
|
+
if target_type == 'addomain':
|
|
132
|
+
# For AD capabilities, use interactive domain selection
|
|
133
|
+
domain = _select_domain(menu)
|
|
134
|
+
if not domain:
|
|
135
|
+
return # User cancelled
|
|
136
|
+
target_key = f"#addomain#{domain}#{domain}"
|
|
137
|
+
target_display = f"domain {domain}"
|
|
138
|
+
else:
|
|
139
|
+
target_key = f"#asset#{hostname}#{hostname}"
|
|
140
|
+
target_display = f"asset {hostname}"
|
|
141
|
+
|
|
142
|
+
# Handle credentials for capabilities that need them
|
|
143
|
+
credentials = None
|
|
144
|
+
if any(keyword in capability.lower() for keyword in ['ad-', 'smb-', 'domain-', 'ldap', 'winrm']):
|
|
145
|
+
if Confirm.ask(" This capability may require credentials. Add them?"):
|
|
146
|
+
username = Prompt.ask(" Username")
|
|
147
|
+
password = Prompt.ask(" Password", password=True)
|
|
148
|
+
credentials = {"Username": username, "Password": password}
|
|
149
|
+
|
|
150
|
+
# Create job configuration using SDK
|
|
151
|
+
config = menu.sdk.aegis.create_job_config(menu.selected_agent, credentials)
|
|
152
|
+
|
|
153
|
+
# Confirm job execution
|
|
154
|
+
if not Confirm.ask(f"\n Run '{capability}' on {target_display}?"):
|
|
155
|
+
menu.console.print(" Cancelled\n")
|
|
156
|
+
menu.pause()
|
|
157
|
+
return
|
|
158
|
+
|
|
159
|
+
try:
|
|
160
|
+
config_json = json.dumps(config)
|
|
161
|
+
|
|
162
|
+
# Add job using SDK
|
|
163
|
+
jobs = menu.sdk.jobs.add(target_key, [capability], config_json)
|
|
164
|
+
|
|
165
|
+
if jobs:
|
|
166
|
+
job = jobs[0] if isinstance(jobs, list) else jobs
|
|
167
|
+
job_key = job.get('key', '')
|
|
168
|
+
status = job.get('status', 'unknown')
|
|
169
|
+
job_id = job_key.split('#')[-1][:12] if job_key else 'unknown'
|
|
170
|
+
|
|
171
|
+
menu.console.print(f"\n[green]✓ Job {job_id} queued successfully[/green]")
|
|
172
|
+
menu.console.print(f" Capability: {capability}")
|
|
173
|
+
menu.console.print(f" Target: {target_display}")
|
|
174
|
+
menu.console.print(f" Status: {status}")
|
|
175
|
+
else:
|
|
176
|
+
menu.console.print("\n[red]Error: No job returned from API[/red]")
|
|
177
|
+
|
|
178
|
+
except Exception as e:
|
|
179
|
+
menu.console.print(f"\n[red]Job execution error: {e}[/red]")
|
|
180
|
+
|
|
181
|
+
menu.console.print()
|
|
182
|
+
menu.pause()
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
def list_capabilities(menu, args):
|
|
186
|
+
if not menu.selected_agent:
|
|
187
|
+
menu.console.print("\n No agent selected. Use 'set <id>' to select one.\n")
|
|
188
|
+
menu.pause()
|
|
189
|
+
return
|
|
190
|
+
|
|
191
|
+
show_details = '--details' in args or '-d' in args
|
|
192
|
+
|
|
193
|
+
try:
|
|
194
|
+
result = menu.sdk.aegis.run_job(
|
|
195
|
+
agent=menu.selected_agent,
|
|
196
|
+
capabilities=None,
|
|
197
|
+
config=None
|
|
198
|
+
)
|
|
199
|
+
|
|
200
|
+
colors = getattr(menu, 'colors', DEFAULT_COLORS)
|
|
201
|
+
if 'capabilities' in result:
|
|
202
|
+
capabilities_table = Table(
|
|
203
|
+
show_header=True,
|
|
204
|
+
header_style=f"bold {colors['primary']}",
|
|
205
|
+
border_style=colors['dim'],
|
|
206
|
+
box=MINIMAL,
|
|
207
|
+
show_lines=False,
|
|
208
|
+
padding=(0, 2),
|
|
209
|
+
pad_edge=False
|
|
210
|
+
)
|
|
211
|
+
|
|
212
|
+
capabilities_table.add_column("CAPABILITY", style=f"bold {colors['success']}", min_width=25, no_wrap=True)
|
|
213
|
+
capabilities_table.add_column("DESCRIPTION", style="white", no_wrap=False)
|
|
214
|
+
|
|
215
|
+
for cap in result['capabilities']:
|
|
216
|
+
name = cap.get('name', 'unknown')
|
|
217
|
+
full_desc = cap.get('description', '') or 'No description available'
|
|
218
|
+
if show_details:
|
|
219
|
+
desc = full_desc
|
|
220
|
+
else:
|
|
221
|
+
desc = full_desc[:80] + '...' if len(full_desc) > 80 else full_desc
|
|
222
|
+
capabilities_table.add_row(name, desc)
|
|
223
|
+
|
|
224
|
+
menu.console.print()
|
|
225
|
+
title = " Available Capabilities"
|
|
226
|
+
title += " (Detailed)" if show_details else " (use --details for full descriptions)"
|
|
227
|
+
menu.console.print(title)
|
|
228
|
+
menu.console.print()
|
|
229
|
+
menu.console.print(capabilities_table)
|
|
230
|
+
else:
|
|
231
|
+
menu.console.print(f"[yellow] No capabilities available for this agent[/yellow]")
|
|
232
|
+
|
|
233
|
+
menu.console.print()
|
|
234
|
+
menu.pause()
|
|
235
|
+
except Exception as e:
|
|
236
|
+
menu.console.print(f"[red]Error listing capabilities: {e}[/red]")
|
|
237
|
+
menu.pause()
|
|
238
|
+
|
|
239
|
+
|
|
240
|
+
def _interactive_capability_picker(menu, suggested=None):
|
|
241
|
+
"""Interactive capability picker with numbered options"""
|
|
242
|
+
colors = getattr(menu, 'colors', DEFAULT_COLORS)
|
|
243
|
+
|
|
244
|
+
if suggested:
|
|
245
|
+
# Validate the suggested capability first
|
|
246
|
+
capability_info = menu.sdk.aegis.validate_capability(suggested)
|
|
247
|
+
if capability_info:
|
|
248
|
+
# Show the suggested capability and ask for confirmation
|
|
249
|
+
desc = (capability_info.get('description', '') or '')[:60]
|
|
250
|
+
menu.console.print(f"\n Suggested capability:")
|
|
251
|
+
menu.console.print(f" {suggested}")
|
|
252
|
+
menu.console.print(f" [{colors['dim']}]{desc}[/{colors['dim']}]")
|
|
253
|
+
|
|
254
|
+
if Confirm.ask(" Use this capability?", default=True):
|
|
255
|
+
return suggested
|
|
256
|
+
# If they decline, continue to show the full picker below
|
|
257
|
+
|
|
258
|
+
try:
|
|
259
|
+
# Determine agent OS for filtering
|
|
260
|
+
agent_os = _detect_agent_os(menu)
|
|
261
|
+
|
|
262
|
+
# Get capabilities filtered by OS
|
|
263
|
+
caps = menu.sdk.aegis.get_capabilities(surface_filter='internal', agent_os=agent_os)
|
|
264
|
+
|
|
265
|
+
if not caps:
|
|
266
|
+
# Fallback to all capabilities
|
|
267
|
+
caps = menu.sdk.aegis.get_capabilities(surface_filter='internal')
|
|
268
|
+
|
|
269
|
+
if not caps:
|
|
270
|
+
menu.console.print(" No capabilities available.")
|
|
271
|
+
return None
|
|
272
|
+
|
|
273
|
+
if agent_os:
|
|
274
|
+
menu.console.print(f" [{colors['dim']}]Showing {agent_os.title()} capabilities[/{colors['dim']}]")
|
|
275
|
+
|
|
276
|
+
# Sort and display with numbers
|
|
277
|
+
caps.sort(key=lambda x: x.get('name', ''))
|
|
278
|
+
|
|
279
|
+
menu.console.print(f"\n Select capability:")
|
|
280
|
+
for i, cap in enumerate(caps[:20], 1): # Limit to 20 for usability
|
|
281
|
+
name = cap.get('name', 'unknown')
|
|
282
|
+
desc = (cap.get('description', '') or '')[:40]
|
|
283
|
+
menu.console.print(f" {i:2d}. {name:<25} {desc}")
|
|
284
|
+
|
|
285
|
+
if len(caps) > 20:
|
|
286
|
+
menu.console.print(f" [{colors['dim']}]... and {len(caps) - 20} more capabilities[/{colors['dim']}]")
|
|
287
|
+
|
|
288
|
+
menu.console.print(f" 0. Enter capability name manually")
|
|
289
|
+
|
|
290
|
+
while True:
|
|
291
|
+
try:
|
|
292
|
+
choice = Prompt.ask(" Choice", default="1")
|
|
293
|
+
choice_num = int(choice.strip())
|
|
294
|
+
|
|
295
|
+
if choice_num == 0:
|
|
296
|
+
return Prompt.ask(" Enter capability name")
|
|
297
|
+
elif 1 <= choice_num <= min(len(caps), 20):
|
|
298
|
+
return caps[choice_num - 1].get('name', '')
|
|
299
|
+
else:
|
|
300
|
+
menu.console.print(f" Please enter a number between 0 and {min(len(caps), 20)}")
|
|
301
|
+
|
|
302
|
+
except ValueError:
|
|
303
|
+
menu.console.print(" Please enter a valid number")
|
|
304
|
+
except KeyboardInterrupt:
|
|
305
|
+
menu.console.print(" Cancelled")
|
|
306
|
+
return None
|
|
307
|
+
|
|
308
|
+
except Exception as e:
|
|
309
|
+
menu.console.print(f" Error loading capabilities: {e}")
|
|
310
|
+
return Prompt.ask(" Enter capability name manually")
|
|
311
|
+
|
|
312
|
+
|
|
313
|
+
def _detect_agent_os(menu):
|
|
314
|
+
"""Detect the operating system of the selected agent"""
|
|
315
|
+
if not menu.selected_agent:
|
|
316
|
+
return None
|
|
317
|
+
|
|
318
|
+
os_field = (menu.selected_agent.os or '').lower()
|
|
319
|
+
|
|
320
|
+
if os_field:
|
|
321
|
+
if 'linux' in os_field or os_field in ['ubuntu', 'centos', 'debian', 'rhel', 'fedora', 'suse']:
|
|
322
|
+
return 'linux'
|
|
323
|
+
elif 'windows' in os_field or os_field in ['win32', 'win64', 'nt']:
|
|
324
|
+
return 'windows'
|
|
325
|
+
|
|
326
|
+
return None
|
|
327
|
+
|
|
328
|
+
|
|
329
|
+
def _select_domain(menu):
|
|
330
|
+
"""Interactive domain selection"""
|
|
331
|
+
colors = getattr(menu, 'colors', DEFAULT_COLORS)
|
|
332
|
+
|
|
333
|
+
try:
|
|
334
|
+
menu.console.print(f" [{colors['dim']}]Looking for available domains...[/{colors['dim']}]")
|
|
335
|
+
|
|
336
|
+
# Use SDK to get available AD domains
|
|
337
|
+
domains = menu.sdk.aegis.get_available_ad_domains()
|
|
338
|
+
|
|
339
|
+
menu.console.print(f" [{colors['dim']}]Found {len(domains)} domains[/{colors['dim']}]")
|
|
340
|
+
|
|
341
|
+
if domains:
|
|
342
|
+
menu.console.print(f"\n Available domains:")
|
|
343
|
+
for i, domain in enumerate(domains[:10], 1): # Limit to 10
|
|
344
|
+
menu.console.print(f" {i:2d}. {domain}")
|
|
345
|
+
|
|
346
|
+
if len(domains) > 10:
|
|
347
|
+
menu.console.print(f" [{colors['dim']}]... and {len(domains) - 10} more[/{colors['dim']}]")
|
|
348
|
+
|
|
349
|
+
menu.console.print(f" 0. Enter domain manually")
|
|
350
|
+
|
|
351
|
+
while True:
|
|
352
|
+
try:
|
|
353
|
+
choice = Prompt.ask(" Choose domain", default="1")
|
|
354
|
+
choice_num = int(choice.strip())
|
|
355
|
+
|
|
356
|
+
if choice_num == 0:
|
|
357
|
+
return Prompt.ask(" Enter domain name")
|
|
358
|
+
elif 1 <= choice_num <= min(len(domains), 10):
|
|
359
|
+
return domains[choice_num - 1]
|
|
360
|
+
else:
|
|
361
|
+
menu.console.print(f" Please enter a number between 0 and {min(len(domains), 10)}")
|
|
362
|
+
|
|
363
|
+
except ValueError:
|
|
364
|
+
menu.console.print(" Please enter a valid number")
|
|
365
|
+
except KeyboardInterrupt:
|
|
366
|
+
return None
|
|
367
|
+
else:
|
|
368
|
+
menu.console.print(f" [{colors['dim']}]No domains found in assets. You can still enter one manually.[/{colors['dim']}]")
|
|
369
|
+
return Prompt.ask(" Enter domain name (e.g., contoso.com, example.local)")
|
|
370
|
+
|
|
371
|
+
except Exception as e:
|
|
372
|
+
menu.console.print(f" [{colors['dim']}]Error during domain selection: {e}[/{colors['dim']}]")
|
|
373
|
+
return Prompt.ask(" Enter domain name (e.g., contoso.com, example.local)")
|
|
374
|
+
|
|
375
|
+
|
|
376
|
+
def complete(menu, text, tokens):
|
|
377
|
+
sub = ['list', 'run', 'capabilities', 'caps']
|
|
378
|
+
if len(tokens) <= 2:
|
|
379
|
+
return [s for s in sub if s.startswith(text)]
|
|
380
|
+
# Could extend to capability names later
|
|
381
|
+
return []
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
def handle_list(menu, args):
|
|
2
|
+
"""List agents with optional offline flag."""
|
|
3
|
+
show_offline = '--all' in args or '-a' in args
|
|
4
|
+
|
|
5
|
+
if not menu.agents:
|
|
6
|
+
menu.load_agents()
|
|
7
|
+
|
|
8
|
+
menu.show_agents_list(show_offline=show_offline)
|
|
9
|
+
menu.pause()
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def complete(menu, text, tokens):
|
|
13
|
+
return [o for o in ['--all', '-a'] if o.startswith(text)]
|
|
14
|
+
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
from ..utils import parse_agent_identifier
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
def handle_set(menu, args):
|
|
5
|
+
"""Select an agent by index, client_id, or hostname."""
|
|
6
|
+
if not args:
|
|
7
|
+
menu.console.print("\n No agent selected. Use 'set <id>' to select one.\n")
|
|
8
|
+
menu.pause()
|
|
9
|
+
return
|
|
10
|
+
|
|
11
|
+
selection = args[0]
|
|
12
|
+
selected_agent = parse_agent_identifier(selection, menu.agents)
|
|
13
|
+
|
|
14
|
+
if selected_agent:
|
|
15
|
+
menu.selected_agent = selected_agent
|
|
16
|
+
hostname = selected_agent.hostname
|
|
17
|
+
menu.console.print(f"\n Selected: {hostname}\n")
|
|
18
|
+
else:
|
|
19
|
+
menu.console.print(f"\n[red] Agent not found:[/red] {selection}")
|
|
20
|
+
menu.console.print(f"[dim] Use agent number (1-{len(menu.agents)}), client ID, or hostname[/dim]\n")
|
|
21
|
+
menu.pause()
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def complete(menu, text, tokens):
|
|
25
|
+
suggestions = []
|
|
26
|
+
for idx, agent in enumerate(menu.agents, 1):
|
|
27
|
+
suggestions.append(str(idx))
|
|
28
|
+
if agent.hostname:
|
|
29
|
+
suggestions.append(agent.hostname)
|
|
30
|
+
if agent.client_id:
|
|
31
|
+
suggestions.append(agent.client_id)
|
|
32
|
+
return [s for s in suggestions if s.startswith(text)]
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
from praetorian_cli.handlers.ssh_utils import SSHArgumentParser
|
|
2
|
+
|
|
3
|
+
def _print_help(menu):
|
|
4
|
+
help_text = """
|
|
5
|
+
SSH Command
|
|
6
|
+
|
|
7
|
+
Pass native ssh flags directly; we tunnel the host.
|
|
8
|
+
Username: use '-u/--user <user>' or native '-l <user>'.
|
|
9
|
+
|
|
10
|
+
Common options (forwarded to ssh):
|
|
11
|
+
-L [bind_address:]port:host:hostport Local port forward (repeatable)
|
|
12
|
+
-R [bind_address:]port:host:hostport Remote port forward (repeatable)
|
|
13
|
+
-D [bind_address:]port Dynamic SOCKS proxy
|
|
14
|
+
-i IDENTITY_FILE Identity (private key) file
|
|
15
|
+
-l USER Remote username (alternative to -u/--user)
|
|
16
|
+
-o OPTION=VALUE Extra ssh config option
|
|
17
|
+
-p PORT SSH port
|
|
18
|
+
-v/-vv/-vvv Verbose output
|
|
19
|
+
|
|
20
|
+
Examples:
|
|
21
|
+
ssh -L 8080:localhost:80 -D 1080 -i ~/.ssh/id_ed25519
|
|
22
|
+
ssh -l admin -o StrictHostKeyChecking=no
|
|
23
|
+
"""
|
|
24
|
+
menu.console.print(f"\n{help_text}")
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def handle_ssh(menu, args):
|
|
28
|
+
"""SSH into the selected agent with optional port forwarding."""
|
|
29
|
+
if not menu.selected_agent:
|
|
30
|
+
menu.console.print("\n No agent selected. Use 'set <id>' to select one.\n")
|
|
31
|
+
menu.pause()
|
|
32
|
+
return
|
|
33
|
+
|
|
34
|
+
# Accept `ssh help` too
|
|
35
|
+
if len(args) and args[0].lower() == 'help':
|
|
36
|
+
_print_help(menu)
|
|
37
|
+
menu.pause()
|
|
38
|
+
return
|
|
39
|
+
|
|
40
|
+
# Also support '-h/--help'
|
|
41
|
+
if any(a in ('-h', '--help') for a in args):
|
|
42
|
+
_print_help(menu)
|
|
43
|
+
menu.pause()
|
|
44
|
+
return
|
|
45
|
+
|
|
46
|
+
parser = SSHArgumentParser(console=menu.console)
|
|
47
|
+
|
|
48
|
+
# Validate agent first
|
|
49
|
+
if not parser.validate_agent_ssh_availability(menu.selected_agent):
|
|
50
|
+
menu.pause()
|
|
51
|
+
return
|
|
52
|
+
|
|
53
|
+
# Parse arguments using shared parser
|
|
54
|
+
parsed_options = parser.parse_ssh_args(args)
|
|
55
|
+
if not parsed_options:
|
|
56
|
+
# Error already displayed by parser
|
|
57
|
+
menu.pause()
|
|
58
|
+
return
|
|
59
|
+
|
|
60
|
+
# Build options list from parsed options for SDK
|
|
61
|
+
options = parsed_options.get('passthrough', [])
|
|
62
|
+
|
|
63
|
+
# Add structured options back as SSH flags
|
|
64
|
+
for forward in parsed_options.get('local_forward', []):
|
|
65
|
+
options.extend(['-L', forward])
|
|
66
|
+
for forward in parsed_options.get('remote_forward', []):
|
|
67
|
+
options.extend(['-R', forward])
|
|
68
|
+
if parsed_options.get('dynamic_forward'):
|
|
69
|
+
options.extend(['-D', parsed_options['dynamic_forward']])
|
|
70
|
+
if parsed_options.get('key'):
|
|
71
|
+
options.extend(['-i', parsed_options['key']])
|
|
72
|
+
|
|
73
|
+
try:
|
|
74
|
+
menu.sdk.aegis.ssh_to_agent(
|
|
75
|
+
agent=menu.selected_agent,
|
|
76
|
+
options=options,
|
|
77
|
+
user=parsed_options.get('user'),
|
|
78
|
+
display_info=True
|
|
79
|
+
)
|
|
80
|
+
except Exception as e:
|
|
81
|
+
menu.console.print(f"[red]SSH error: {e}[/red]")
|
|
82
|
+
menu.pause()
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def complete(menu, text, tokens):
|
|
86
|
+
opts = ['help', '-h', '--help', '-u', '--user', '-l', '-i', '-L', '-R', '-D', '-o', '-F', '-p', '-v', '-vv', '-vvv', '-4', '-6', '-A', '-a', '-C', '-N', '-T', '-q']
|
|
87
|
+
return [o for o in opts if o.startswith(text)]
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
Constants for the Aegis UI layer
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
DEFAULT_COLORS = {
|
|
7
|
+
'primary': '#5F47B7', # Primary purple
|
|
8
|
+
'secondary': '#8F7ECD', # Secondary purple
|
|
9
|
+
'accent': '#BFB5E2', # Tertiary purple
|
|
10
|
+
'dark': '#0D0D28', # Dark primary
|
|
11
|
+
'dark_sec': '#191933', # Dark secondary
|
|
12
|
+
'success': '#4CAF50', # Green
|
|
13
|
+
'error': '#F44336', # Red
|
|
14
|
+
'warning': '#FFC107', # Yellow
|
|
15
|
+
'info': '#2196F3', # Blue
|
|
16
|
+
'text': '#FFFFFF', # White text
|
|
17
|
+
'dim': '#B6B6BE' # Light secondary
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
|