praetorian-cli 2.2.0__tar.gz → 2.2.2__tar.gz
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-2.2.0/praetorian_cli.egg-info → praetorian_cli-2.2.2}/PKG-INFO +4 -1
- praetorian_cli-2.2.2/praetorian_cli/handlers/aegis.py +107 -0
- {praetorian_cli-2.2.0 → praetorian_cli-2.2.2}/praetorian_cli/handlers/agent.py +8 -0
- {praetorian_cli-2.2.0 → praetorian_cli-2.2.2}/praetorian_cli/handlers/get.py +17 -0
- {praetorian_cli-2.2.0 → praetorian_cli-2.2.2}/praetorian_cli/handlers/list.py +33 -0
- praetorian_cli-2.2.2/praetorian_cli/handlers/ssh_utils.py +154 -0
- {praetorian_cli-2.2.0 → praetorian_cli-2.2.2}/praetorian_cli/handlers/test.py +8 -2
- {praetorian_cli-2.2.0 → praetorian_cli-2.2.2}/praetorian_cli/main.py +1 -0
- {praetorian_cli-2.2.0 → praetorian_cli-2.2.2}/praetorian_cli/sdk/chariot.py +89 -1
- praetorian_cli-2.2.2/praetorian_cli/sdk/entities/aegis.py +437 -0
- {praetorian_cli-2.2.0 → praetorian_cli-2.2.2}/praetorian_cli/sdk/entities/assets.py +6 -3
- praetorian_cli-2.2.2/praetorian_cli/sdk/entities/scanners.py +13 -0
- praetorian_cli-2.2.2/praetorian_cli/sdk/model/aegis.py +156 -0
- {praetorian_cli-2.2.0 → praetorian_cli-2.2.2}/praetorian_cli/sdk/test/pytest.ini +1 -0
- {praetorian_cli-2.2.0 → praetorian_cli-2.2.2}/praetorian_cli/sdk/test/test_asset.py +12 -14
- {praetorian_cli-2.2.0 → praetorian_cli-2.2.2}/praetorian_cli/sdk/test/test_z_cli.py +1 -1
- praetorian_cli-2.2.2/praetorian_cli/sdk/test/ui_mocks.py +133 -0
- praetorian_cli-2.2.2/praetorian_cli/ui/__init__.py +3 -0
- praetorian_cli-2.2.2/praetorian_cli/ui/aegis/__init__.py +5 -0
- praetorian_cli-2.2.2/praetorian_cli/ui/aegis/commands/__init__.py +2 -0
- praetorian_cli-2.2.2/praetorian_cli/ui/aegis/commands/help.py +81 -0
- praetorian_cli-2.2.2/praetorian_cli/ui/aegis/commands/info.py +136 -0
- praetorian_cli-2.2.2/praetorian_cli/ui/aegis/commands/job.py +381 -0
- praetorian_cli-2.2.2/praetorian_cli/ui/aegis/commands/list.py +14 -0
- praetorian_cli-2.2.2/praetorian_cli/ui/aegis/commands/set.py +32 -0
- praetorian_cli-2.2.2/praetorian_cli/ui/aegis/commands/ssh.py +87 -0
- praetorian_cli-2.2.2/praetorian_cli/ui/aegis/constants.py +20 -0
- praetorian_cli-2.2.2/praetorian_cli/ui/aegis/menu.py +395 -0
- praetorian_cli-2.2.2/praetorian_cli/ui/aegis/utils.py +162 -0
- {praetorian_cli-2.2.0 → praetorian_cli-2.2.2/praetorian_cli.egg-info}/PKG-INFO +4 -1
- {praetorian_cli-2.2.0 → praetorian_cli-2.2.2}/praetorian_cli.egg-info/SOURCES.txt +19 -1
- {praetorian_cli-2.2.0 → praetorian_cli-2.2.2}/praetorian_cli.egg-info/requires.txt +3 -0
- {praetorian_cli-2.2.0 → praetorian_cli-2.2.2}/setup.cfg +4 -1
- {praetorian_cli-2.2.0 → praetorian_cli-2.2.2}/LICENSE +0 -0
- {praetorian_cli-2.2.0 → praetorian_cli-2.2.2}/MANIFEST.in +0 -0
- {praetorian_cli-2.2.0 → praetorian_cli-2.2.2}/README.md +0 -0
- {praetorian_cli-2.2.0 → praetorian_cli-2.2.2}/praetorian_cli/__init__.py +0 -0
- {praetorian_cli-2.2.0 → praetorian_cli-2.2.2}/praetorian_cli/handlers/__init__.py +0 -0
- {praetorian_cli-2.2.0 → praetorian_cli-2.2.2}/praetorian_cli/handlers/add.py +0 -0
- {praetorian_cli-2.2.0 → praetorian_cli-2.2.2}/praetorian_cli/handlers/chariot.py +0 -0
- {praetorian_cli-2.2.0 → praetorian_cli-2.2.2}/praetorian_cli/handlers/cli_decorators.py +0 -0
- {praetorian_cli-2.2.0 → praetorian_cli-2.2.2}/praetorian_cli/handlers/configure.py +0 -0
- {praetorian_cli-2.2.0 → praetorian_cli-2.2.2}/praetorian_cli/handlers/delete.py +0 -0
- {praetorian_cli-2.2.0 → praetorian_cli-2.2.2}/praetorian_cli/handlers/enrich.py +0 -0
- {praetorian_cli-2.2.0 → praetorian_cli-2.2.2}/praetorian_cli/handlers/imports.py +0 -0
- {praetorian_cli-2.2.0 → praetorian_cli-2.2.2}/praetorian_cli/handlers/link.py +0 -0
- {praetorian_cli-2.2.0 → praetorian_cli-2.2.2}/praetorian_cli/handlers/script.py +0 -0
- {praetorian_cli-2.2.0 → praetorian_cli-2.2.2}/praetorian_cli/handlers/search.py +0 -0
- {praetorian_cli-2.2.0 → praetorian_cli-2.2.2}/praetorian_cli/handlers/unlink.py +0 -0
- {praetorian_cli-2.2.0 → praetorian_cli-2.2.2}/praetorian_cli/handlers/update.py +0 -0
- {praetorian_cli-2.2.0 → praetorian_cli-2.2.2}/praetorian_cli/handlers/utils.py +0 -0
- {praetorian_cli-2.2.0 → praetorian_cli-2.2.2}/praetorian_cli/scripts/__init__.py +0 -0
- {praetorian_cli-2.2.0 → praetorian_cli-2.2.2}/praetorian_cli/scripts/commands/__init__.py +0 -0
- {praetorian_cli-2.2.0 → praetorian_cli-2.2.2}/praetorian_cli/scripts/commands/nmap-example.py +0 -0
- {praetorian_cli-2.2.0 → praetorian_cli-2.2.2}/praetorian_cli/scripts/utils.py +0 -0
- {praetorian_cli-2.2.0 → praetorian_cli-2.2.2}/praetorian_cli/sdk/__init__.py +0 -0
- {praetorian_cli-2.2.0 → praetorian_cli-2.2.2}/praetorian_cli/sdk/entities/__init__.py +0 -0
- {praetorian_cli-2.2.0 → praetorian_cli-2.2.2}/praetorian_cli/sdk/entities/accounts.py +0 -0
- {praetorian_cli-2.2.0 → praetorian_cli-2.2.2}/praetorian_cli/sdk/entities/agents.py +0 -0
- {praetorian_cli-2.2.0 → praetorian_cli-2.2.2}/praetorian_cli/sdk/entities/attributes.py +0 -0
- {praetorian_cli-2.2.0 → praetorian_cli-2.2.2}/praetorian_cli/sdk/entities/capabilities.py +0 -0
- {praetorian_cli-2.2.0 → praetorian_cli-2.2.2}/praetorian_cli/sdk/entities/configurations.py +0 -0
- {praetorian_cli-2.2.0 → praetorian_cli-2.2.2}/praetorian_cli/sdk/entities/credentials.py +0 -0
- {praetorian_cli-2.2.0 → praetorian_cli-2.2.2}/praetorian_cli/sdk/entities/definitions.py +0 -0
- {praetorian_cli-2.2.0 → praetorian_cli-2.2.2}/praetorian_cli/sdk/entities/files.py +0 -0
- {praetorian_cli-2.2.0 → praetorian_cli-2.2.2}/praetorian_cli/sdk/entities/integrations.py +0 -0
- {praetorian_cli-2.2.0 → praetorian_cli-2.2.2}/praetorian_cli/sdk/entities/jobs.py +0 -0
- {praetorian_cli-2.2.0 → praetorian_cli-2.2.2}/praetorian_cli/sdk/entities/keys.py +0 -0
- {praetorian_cli-2.2.0 → praetorian_cli-2.2.2}/praetorian_cli/sdk/entities/preseeds.py +0 -0
- {praetorian_cli-2.2.0 → praetorian_cli-2.2.2}/praetorian_cli/sdk/entities/risks.py +0 -0
- {praetorian_cli-2.2.0 → praetorian_cli-2.2.2}/praetorian_cli/sdk/entities/search.py +0 -0
- {praetorian_cli-2.2.0 → praetorian_cli-2.2.2}/praetorian_cli/sdk/entities/seeds.py +0 -0
- {praetorian_cli-2.2.0 → praetorian_cli-2.2.2}/praetorian_cli/sdk/entities/settings.py +0 -0
- {praetorian_cli-2.2.0 → praetorian_cli-2.2.2}/praetorian_cli/sdk/entities/statistics.py +0 -0
- {praetorian_cli-2.2.0 → praetorian_cli-2.2.2}/praetorian_cli/sdk/entities/webhook.py +0 -0
- {praetorian_cli-2.2.0 → praetorian_cli-2.2.2}/praetorian_cli/sdk/keychain.py +0 -0
- {praetorian_cli-2.2.0 → praetorian_cli-2.2.2}/praetorian_cli/sdk/mcp_server.py +0 -0
- {praetorian_cli-2.2.0 → praetorian_cli-2.2.2}/praetorian_cli/sdk/model/__init__.py +0 -0
- {praetorian_cli-2.2.0 → praetorian_cli-2.2.2}/praetorian_cli/sdk/model/globals.py +0 -0
- {praetorian_cli-2.2.0 → praetorian_cli-2.2.2}/praetorian_cli/sdk/model/query.py +0 -0
- {praetorian_cli-2.2.0 → praetorian_cli-2.2.2}/praetorian_cli/sdk/model/utils.py +0 -0
- {praetorian_cli-2.2.0 → praetorian_cli-2.2.2}/praetorian_cli/sdk/test/__init__.py +0 -0
- {praetorian_cli-2.2.0 → praetorian_cli-2.2.2}/praetorian_cli/sdk/test/test_account.py +0 -0
- {praetorian_cli-2.2.0 → praetorian_cli-2.2.2}/praetorian_cli/sdk/test/test_agent.py +0 -0
- {praetorian_cli-2.2.0 → praetorian_cli-2.2.2}/praetorian_cli/sdk/test/test_attribute.py +0 -0
- {praetorian_cli-2.2.0 → praetorian_cli-2.2.2}/praetorian_cli/sdk/test/test_capabilities.py +0 -0
- {praetorian_cli-2.2.0 → praetorian_cli-2.2.2}/praetorian_cli/sdk/test/test_configuration.py +0 -0
- {praetorian_cli-2.2.0 → praetorian_cli-2.2.2}/praetorian_cli/sdk/test/test_definition.py +0 -0
- {praetorian_cli-2.2.0 → praetorian_cli-2.2.2}/praetorian_cli/sdk/test/test_extend.py +0 -0
- {praetorian_cli-2.2.0 → praetorian_cli-2.2.2}/praetorian_cli/sdk/test/test_file.py +0 -0
- {praetorian_cli-2.2.0 → praetorian_cli-2.2.2}/praetorian_cli/sdk/test/test_job.py +0 -0
- {praetorian_cli-2.2.0 → praetorian_cli-2.2.2}/praetorian_cli/sdk/test/test_key.py +0 -0
- {praetorian_cli-2.2.0 → praetorian_cli-2.2.2}/praetorian_cli/sdk/test/test_mcp.py +0 -0
- {praetorian_cli-2.2.0 → praetorian_cli-2.2.2}/praetorian_cli/sdk/test/test_preseed.py +0 -0
- {praetorian_cli-2.2.0 → praetorian_cli-2.2.2}/praetorian_cli/sdk/test/test_risk.py +0 -0
- {praetorian_cli-2.2.0 → praetorian_cli-2.2.2}/praetorian_cli/sdk/test/test_search.py +0 -0
- {praetorian_cli-2.2.0 → praetorian_cli-2.2.2}/praetorian_cli/sdk/test/test_seed.py +0 -0
- {praetorian_cli-2.2.0 → praetorian_cli-2.2.2}/praetorian_cli/sdk/test/test_setting.py +0 -0
- {praetorian_cli-2.2.0 → praetorian_cli-2.2.2}/praetorian_cli/sdk/test/test_webhook.py +0 -0
- {praetorian_cli-2.2.0 → praetorian_cli-2.2.2}/praetorian_cli/sdk/test/utils.py +0 -0
- {praetorian_cli-2.2.0 → praetorian_cli-2.2.2}/praetorian_cli.egg-info/dependency_links.txt +0 -0
- {praetorian_cli-2.2.0 → praetorian_cli-2.2.2}/praetorian_cli.egg-info/entry_points.txt +0 -0
- {praetorian_cli-2.2.0 → praetorian_cli-2.2.2}/praetorian_cli.egg-info/top_level.txt +0 -0
- {praetorian_cli-2.2.0 → praetorian_cli-2.2.2}/pyproject.toml +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: praetorian-cli
|
|
3
|
-
Version: 2.2.
|
|
3
|
+
Version: 2.2.2
|
|
4
4
|
Summary: For interacting with the Chariot API
|
|
5
5
|
Home-page: https://github.com/praetorian-inc/praetorian-cli
|
|
6
6
|
Author: Praetorian
|
|
@@ -17,6 +17,9 @@ Requires-Dist: requests>=2.31.0
|
|
|
17
17
|
Requires-Dist: pytest>=8.0.2
|
|
18
18
|
Requires-Dist: mcp>=1.12.2
|
|
19
19
|
Requires-Dist: anyio>=3.0.0
|
|
20
|
+
Requires-Dist: textual>=0.47.0
|
|
21
|
+
Requires-Dist: rich>=13.0.0
|
|
22
|
+
Requires-Dist: prompt_toolkit>=3.0.0
|
|
20
23
|
Dynamic: license-file
|
|
21
24
|
|
|
22
25
|
# Praetorian CLI and SDK
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import click
|
|
2
|
+
from praetorian_cli.handlers.chariot import chariot
|
|
3
|
+
from praetorian_cli.handlers.cli_decorators import cli_handler
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
@chariot.group(invoke_without_command=True)
|
|
7
|
+
@cli_handler
|
|
8
|
+
@click.pass_context
|
|
9
|
+
def aegis(ctx, sdk):
|
|
10
|
+
"""Aegis management commands"""
|
|
11
|
+
if ctx.invoked_subcommand is None:
|
|
12
|
+
# No subcommand was invoked, run the default interactive interface
|
|
13
|
+
from praetorian_cli.ui.aegis.menu import run_aegis_menu
|
|
14
|
+
run_aegis_menu(sdk)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
# Add the shared commands to the CLI group
|
|
18
|
+
# Each shared command gets wrapped to inject the CLI context
|
|
19
|
+
|
|
20
|
+
@aegis.command('list')
|
|
21
|
+
@cli_handler
|
|
22
|
+
@click.option('--details', is_flag=True, help='Show detailed agent information')
|
|
23
|
+
@click.option('--filter', help='Filter agents by hostname or other properties')
|
|
24
|
+
@click.pass_context
|
|
25
|
+
def list_agents(ctx, sdk, details, filter):
|
|
26
|
+
"""List Aegis agents with optional details"""
|
|
27
|
+
click.echo(sdk.aegis.format_agents_list(details=details, filter_text=filter))
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
@aegis.command('ssh')
|
|
31
|
+
@cli_handler
|
|
32
|
+
@click.argument('client_id', required=True)
|
|
33
|
+
@click.option('-u', '--user', help='SSH username (prepends user@ to hostname)')
|
|
34
|
+
@click.argument('args', nargs=-1)
|
|
35
|
+
@click.pass_context
|
|
36
|
+
def ssh(ctx, sdk, client_id, user, args):
|
|
37
|
+
"""Connect to an Aegis agent via SSH.
|
|
38
|
+
|
|
39
|
+
Pass native ssh flags after client_id; they are forwarded to ssh.
|
|
40
|
+
|
|
41
|
+
Common options (forwarded to ssh):
|
|
42
|
+
-L [bind_address:]port:host:hostport Local port forward (repeatable)
|
|
43
|
+
-R [bind_address:]port:host:hostport Remote port forward (repeatable)
|
|
44
|
+
-D [bind_address:]port Dynamic SOCKS proxy
|
|
45
|
+
-i IDENTITY_FILE Identity (private key) file
|
|
46
|
+
-l USER Remote username (alternative to -u/--user)
|
|
47
|
+
-o OPTION=VALUE Extra ssh config option
|
|
48
|
+
-p PORT SSH port
|
|
49
|
+
-v/-vv/-vvv Verbose output
|
|
50
|
+
"""
|
|
51
|
+
agent = sdk.aegis.get_by_client_id(client_id)
|
|
52
|
+
if not agent:
|
|
53
|
+
click.echo(f"Agent not found: {client_id}", err=True)
|
|
54
|
+
return
|
|
55
|
+
|
|
56
|
+
options = list(args)
|
|
57
|
+
sdk.aegis.ssh_to_agent(agent=agent, options=options, user=user, display_info=True)
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
@aegis.command('job')
|
|
61
|
+
@cli_handler
|
|
62
|
+
@click.option('-c', '--capability', 'capabilities', multiple=True, help='Capability to run (e.g., windows-smb-snaffler)')
|
|
63
|
+
@click.option('--config', help='JSON configuration string for the job')
|
|
64
|
+
@click.argument('client_id', required=True)
|
|
65
|
+
@click.pass_context
|
|
66
|
+
def job(ctx, sdk, capabilities, config, client_id):
|
|
67
|
+
"""Run a job on an Aegis agent"""
|
|
68
|
+
agent = sdk.aegis.get_by_client_id(client_id)
|
|
69
|
+
if not agent:
|
|
70
|
+
click.echo(f"Agent not found: {client_id}", err=True)
|
|
71
|
+
return
|
|
72
|
+
|
|
73
|
+
try:
|
|
74
|
+
result = sdk.aegis.run_job(
|
|
75
|
+
agent,
|
|
76
|
+
list(capabilities) if capabilities else None,
|
|
77
|
+
config
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
if 'capabilities' in result:
|
|
81
|
+
click.echo("Available capabilities:")
|
|
82
|
+
for cap in result['capabilities']:
|
|
83
|
+
name = cap.get('name', 'unknown')
|
|
84
|
+
desc = cap.get('description', '')[:50]
|
|
85
|
+
click.echo(f" {name:<25} {desc}")
|
|
86
|
+
elif result.get('success'):
|
|
87
|
+
click.echo("✓ Job queued successfully")
|
|
88
|
+
click.echo(f" Job ID: {result.get('job_id', 'unknown')}")
|
|
89
|
+
click.echo(f" Status: {result.get('status', 'unknown')}")
|
|
90
|
+
else:
|
|
91
|
+
click.echo("Error: Unknown error", err=True)
|
|
92
|
+
except Exception as e:
|
|
93
|
+
click.echo(f"Error: {e}", err=True)
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
@aegis.command('info')
|
|
97
|
+
@cli_handler
|
|
98
|
+
@click.argument('client_id', required=True)
|
|
99
|
+
@click.pass_context
|
|
100
|
+
def info(ctx, sdk, client_id):
|
|
101
|
+
"""Show detailed information for an agent"""
|
|
102
|
+
agent = sdk.aegis.get_by_client_id(client_id)
|
|
103
|
+
if not agent:
|
|
104
|
+
click.echo(f"Agent not found: {client_id}", err=True)
|
|
105
|
+
return
|
|
106
|
+
|
|
107
|
+
click.echo(agent.to_detailed_string())
|
|
@@ -43,6 +43,14 @@ def start(sdk, allowed):
|
|
|
43
43
|
- praetorian chariot agent mcp start
|
|
44
44
|
- praetorian chariot agent mcp start -a search_by_term -a risk_add
|
|
45
45
|
- praetorian chariot agent mcp start -a search_* -a risk_add
|
|
46
|
+
|
|
47
|
+
\b
|
|
48
|
+
Claude code configuration/usage:
|
|
49
|
+
- claude mcp add chariot -- praetorian chariot agent mcp start # read-only
|
|
50
|
+
- claude mcp add chariot -- praetorian chariot agent mcp start -a search_by_query -a risk_add -a asset_add # select write tools
|
|
51
|
+
- claude "show me my chariot assets from the example.com domain"
|
|
52
|
+
- claude "show me my chariot assets with port 22 open"
|
|
53
|
+
- claude "run a portscan on every discovered ip for example.com"
|
|
46
54
|
"""
|
|
47
55
|
if len(allowed) == 0:
|
|
48
56
|
allowed = None
|
|
@@ -278,3 +278,20 @@ def credential(chariot, credential_id, category, type, format, parameters):
|
|
|
278
278
|
result = chariot.credentials.get(credential_id, category, type, [format], **params)
|
|
279
279
|
output = chariot.credentials.format_output(result)
|
|
280
280
|
click.echo(output)
|
|
281
|
+
|
|
282
|
+
|
|
283
|
+
@get.command()
|
|
284
|
+
@cli_handler
|
|
285
|
+
@click.argument('key', required=True)
|
|
286
|
+
def scanner(chariot, key):
|
|
287
|
+
""" Get scanner details
|
|
288
|
+
|
|
289
|
+
\b
|
|
290
|
+
Argument:
|
|
291
|
+
- KEY: the key of an existing scanner record
|
|
292
|
+
|
|
293
|
+
\b
|
|
294
|
+
Example usage:
|
|
295
|
+
- praetorian chariot get scanner "#scanner#127.0.0.1"
|
|
296
|
+
"""
|
|
297
|
+
print_json(chariot.scanners.get(key))
|
|
@@ -64,6 +64,22 @@ def accounts(chariot, filter, details, offset, page):
|
|
|
64
64
|
render_list_results(chariot.accounts.list(filter, offset, pagination_size(page)), details)
|
|
65
65
|
|
|
66
66
|
|
|
67
|
+
@list.command()
|
|
68
|
+
@list_params('Aegis ID', has_filter=False)
|
|
69
|
+
def aegis(chariot, details, offset, page):
|
|
70
|
+
""" List Aegis
|
|
71
|
+
|
|
72
|
+
Retrieve and display a list of Aegis instances.
|
|
73
|
+
|
|
74
|
+
\b
|
|
75
|
+
Example usages:
|
|
76
|
+
- praetorian chariot list aegis
|
|
77
|
+
- praetorian chariot list aegis --details
|
|
78
|
+
- praetorian chariot list aegis --page all
|
|
79
|
+
"""
|
|
80
|
+
render_list_results(chariot.aegis.list(offset, pagination_size(page)), details)
|
|
81
|
+
|
|
82
|
+
|
|
67
83
|
@list.command()
|
|
68
84
|
@list_params('integration name')
|
|
69
85
|
def integrations(chariot, filter, details, offset, page):
|
|
@@ -336,3 +352,20 @@ def capabilities(chariot, name, target, executor):
|
|
|
336
352
|
- praetorian chariot list capabilities --name nuclei --target attribute --executor chariot
|
|
337
353
|
"""
|
|
338
354
|
print_json(chariot.capabilities.list(name, target, executor))
|
|
355
|
+
|
|
356
|
+
|
|
357
|
+
@list.command()
|
|
358
|
+
@list_params('IP address')
|
|
359
|
+
def scanners(chariot, filter, details, offset, page):
|
|
360
|
+
""" List scanners
|
|
361
|
+
|
|
362
|
+
Retrieve and display a list of scanner records that track IP addresses used by chariot.
|
|
363
|
+
|
|
364
|
+
\b
|
|
365
|
+
Example usages:
|
|
366
|
+
- praetorian chariot list scanners
|
|
367
|
+
- praetorian chariot list scanners --filter 127.0.0.1
|
|
368
|
+
- praetorian chariot list scanners --details
|
|
369
|
+
- praetorian chariot list scanners --page all
|
|
370
|
+
"""
|
|
371
|
+
render_list_results(chariot.scanners.list(filter, offset, pagination_size(page)), details)
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Shared SSH utilities for Aegis CLI and TUI commands
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from typing import List, Dict, Optional
|
|
6
|
+
from praetorian_cli.sdk.model.aegis import Agent
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class SSHArgumentParser:
|
|
10
|
+
"""Shared SSH argument parser for both CLI and TUI interfaces"""
|
|
11
|
+
|
|
12
|
+
def __init__(self, console=None):
|
|
13
|
+
self.console = console
|
|
14
|
+
|
|
15
|
+
def parse_ssh_args(self, args: List[str]) -> Optional[Dict]:
|
|
16
|
+
"""
|
|
17
|
+
Parse SSH command arguments and return options dict
|
|
18
|
+
Returns None if parsing fails with error messages displayed
|
|
19
|
+
"""
|
|
20
|
+
options = {
|
|
21
|
+
'local_forward': [],
|
|
22
|
+
'remote_forward': [],
|
|
23
|
+
'dynamic_forward': None,
|
|
24
|
+
'key': None,
|
|
25
|
+
'ssh_opts': None,
|
|
26
|
+
'user': None,
|
|
27
|
+
'passthrough': [] # collect unknown flags to pass through
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
i = 0
|
|
31
|
+
while i < len(args):
|
|
32
|
+
arg = args[i]
|
|
33
|
+
|
|
34
|
+
if arg in ['-L', '-l', '--local-forward']:
|
|
35
|
+
if i + 1 >= len(args):
|
|
36
|
+
self._print_error("Error: -L requires a port forwarding specification")
|
|
37
|
+
self._print_error("Example: ssh -L 8080:localhost:80", dim=True)
|
|
38
|
+
return None
|
|
39
|
+
options['local_forward'].append(args[i + 1])
|
|
40
|
+
i += 2
|
|
41
|
+
|
|
42
|
+
elif arg in ['-R', '-r', '--remote-forward']:
|
|
43
|
+
if i + 1 >= len(args):
|
|
44
|
+
self._print_error("Error: -R requires a port forwarding specification")
|
|
45
|
+
self._print_error("Example: ssh -R 9090:localhost:3000", dim=True)
|
|
46
|
+
return None
|
|
47
|
+
options['remote_forward'].append(args[i + 1])
|
|
48
|
+
i += 2
|
|
49
|
+
|
|
50
|
+
elif arg in ['-D', '-d', '--dynamic-forward']:
|
|
51
|
+
if i + 1 >= len(args):
|
|
52
|
+
self._print_error("Error: -D requires a port number")
|
|
53
|
+
self._print_error("Example: ssh -D 1080", dim=True)
|
|
54
|
+
return None
|
|
55
|
+
try:
|
|
56
|
+
port = int(args[i + 1])
|
|
57
|
+
if port < 1 or port > 65535:
|
|
58
|
+
raise ValueError()
|
|
59
|
+
options['dynamic_forward'] = str(port)
|
|
60
|
+
except ValueError:
|
|
61
|
+
self._print_error(f"Error: Invalid port number '{args[i + 1]}'")
|
|
62
|
+
self._print_error("Port must be a number between 1 and 65535", dim=True)
|
|
63
|
+
return None
|
|
64
|
+
i += 2
|
|
65
|
+
|
|
66
|
+
elif arg in ['-i', '-I', '--key']:
|
|
67
|
+
if i + 1 >= len(args):
|
|
68
|
+
self._print_error("Error: -i requires a key file path")
|
|
69
|
+
self._print_error("Example: ssh -i ~/.ssh/my_key", dim=True)
|
|
70
|
+
return None
|
|
71
|
+
options['key'] = args[i + 1]
|
|
72
|
+
i += 2
|
|
73
|
+
|
|
74
|
+
elif arg in ['-u', '-U', '--user']:
|
|
75
|
+
if i + 1 >= len(args):
|
|
76
|
+
self._print_error("Error: -u requires a username")
|
|
77
|
+
self._print_error("Example: ssh -u root", dim=True)
|
|
78
|
+
return None
|
|
79
|
+
options['user'] = args[i + 1]
|
|
80
|
+
i += 2
|
|
81
|
+
|
|
82
|
+
elif arg.startswith('-'):
|
|
83
|
+
# Collect unknown options and their arguments if any
|
|
84
|
+
options['passthrough'].append(arg)
|
|
85
|
+
# If next token is a value and current looks like expects arg (heuristic), include it
|
|
86
|
+
if i + 1 < len(args) and not args[i + 1].startswith('-'):
|
|
87
|
+
options['passthrough'].append(args[i + 1])
|
|
88
|
+
i += 2
|
|
89
|
+
else:
|
|
90
|
+
i += 1
|
|
91
|
+
else:
|
|
92
|
+
self._print_error(f"Error: Unexpected argument '{arg}'")
|
|
93
|
+
return None
|
|
94
|
+
|
|
95
|
+
return options
|
|
96
|
+
|
|
97
|
+
def validate_agent_ssh_availability(self, agent) -> bool:
|
|
98
|
+
"""
|
|
99
|
+
Check if SSH is available for the given agent
|
|
100
|
+
Returns True if available, False otherwise with error messages displayed
|
|
101
|
+
"""
|
|
102
|
+
is_valid, error_msg = validate_agent_for_ssh(agent)
|
|
103
|
+
if not is_valid:
|
|
104
|
+
self._print_error(error_msg)
|
|
105
|
+
return False
|
|
106
|
+
return True
|
|
107
|
+
|
|
108
|
+
def has_ssh_options(self, options: Dict) -> bool:
|
|
109
|
+
"""Check if any actual SSH options were provided (not just passthrough)"""
|
|
110
|
+
return bool(
|
|
111
|
+
options['local_forward'] or
|
|
112
|
+
options['remote_forward'] or
|
|
113
|
+
options['dynamic_forward'] or
|
|
114
|
+
options['key'] or
|
|
115
|
+
options['user']
|
|
116
|
+
)
|
|
117
|
+
|
|
118
|
+
def _print_error(self, message: str, dim: bool = False):
|
|
119
|
+
"""Print error message using console if available, otherwise plain print"""
|
|
120
|
+
if self.console:
|
|
121
|
+
if dim:
|
|
122
|
+
self.console.print(f"[dim]{message}[/dim]")
|
|
123
|
+
else:
|
|
124
|
+
self.console.print(f"[red]{message}[/red]")
|
|
125
|
+
else:
|
|
126
|
+
# Fallback for CLI usage
|
|
127
|
+
print(f"Error: {message}" if not dim else message)
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def validate_agent_for_ssh(agent: Agent) -> tuple[bool, str]:
|
|
131
|
+
"""
|
|
132
|
+
Validate if an agent is ready for SSH connections
|
|
133
|
+
Returns (is_valid, error_message)
|
|
134
|
+
"""
|
|
135
|
+
if not agent:
|
|
136
|
+
return False, "No agent specified"
|
|
137
|
+
|
|
138
|
+
client_id = agent.client_id
|
|
139
|
+
hostname = agent.hostname or 'Unknown'
|
|
140
|
+
has_tunnel = agent.has_tunnel
|
|
141
|
+
|
|
142
|
+
if not client_id:
|
|
143
|
+
return False, "Agent missing client_id"
|
|
144
|
+
|
|
145
|
+
# Check if Cloudflare tunnel is available
|
|
146
|
+
if not has_tunnel:
|
|
147
|
+
return False, f"SSH not available for {hostname} - no active tunnel"
|
|
148
|
+
|
|
149
|
+
# Check if tunnel has a public hostname
|
|
150
|
+
public_hostname = agent.health_check.cloudflared_status.hostname if has_tunnel else None
|
|
151
|
+
if not public_hostname:
|
|
152
|
+
return False, f"No public hostname found in tunnel configuration for {hostname}"
|
|
153
|
+
|
|
154
|
+
return True, ""
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import os
|
|
2
|
+
import sys
|
|
3
|
+
import subprocess
|
|
2
4
|
|
|
3
5
|
import click
|
|
4
6
|
import pytest
|
|
@@ -10,14 +12,18 @@ from praetorian_cli.handlers.cli_decorators import cli_handler
|
|
|
10
12
|
|
|
11
13
|
@chariot.command()
|
|
12
14
|
@cli_handler
|
|
13
|
-
@click.option('-s', '--suite', type=click.Choice(['coherence', 'cli']), help='Run a specific test suite')
|
|
15
|
+
@click.option('-s', '--suite', type=click.Choice(['coherence', 'cli', 'tui']), help='Run a specific test suite')
|
|
14
16
|
@click.argument('key', required=False)
|
|
15
17
|
def test(chariot, key, suite):
|
|
16
18
|
""" Run integration test suite """
|
|
17
19
|
os.environ['CHARIOT_TEST_PROFILE'] = chariot.keychain.profile
|
|
20
|
+
os.environ['CHARIOT_PROXY'] = chariot.proxy
|
|
18
21
|
command = [test_module.__path__[0]]
|
|
19
22
|
if key:
|
|
20
23
|
command.extend(['-k', key])
|
|
21
24
|
if suite:
|
|
22
25
|
command.extend(['-m', suite])
|
|
23
|
-
pytest
|
|
26
|
+
# Run pytest in a subprocess to isolate from CLI pre-imports
|
|
27
|
+
args = [sys.executable, '-m', 'pytest'] + command
|
|
28
|
+
result = subprocess.run(args)
|
|
29
|
+
raise SystemExit(result.returncode)
|
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
import json, requests
|
|
1
|
+
import json, requests, os
|
|
2
2
|
|
|
3
3
|
from praetorian_cli.sdk.entities.accounts import Accounts
|
|
4
|
+
from praetorian_cli.sdk.entities.aegis import Aegis
|
|
4
5
|
from praetorian_cli.sdk.entities.agents import Agents
|
|
5
6
|
from praetorian_cli.sdk.entities.assets import Assets
|
|
6
7
|
from praetorian_cli.sdk.entities.attributes import Attributes
|
|
@@ -14,6 +15,7 @@ from praetorian_cli.sdk.entities.jobs import Jobs
|
|
|
14
15
|
from praetorian_cli.sdk.entities.keys import Keys
|
|
15
16
|
from praetorian_cli.sdk.entities.preseeds import Preseeds
|
|
16
17
|
from praetorian_cli.sdk.entities.risks import Risks
|
|
18
|
+
from praetorian_cli.sdk.entities.scanners import Scanners
|
|
17
19
|
from praetorian_cli.sdk.entities.search import Search
|
|
18
20
|
from praetorian_cli.sdk.entities.seeds import Seeds
|
|
19
21
|
from praetorian_cli.sdk.entities.settings import Settings
|
|
@@ -39,8 +41,10 @@ class Chariot:
|
|
|
39
41
|
self.definitions = Definitions(self)
|
|
40
42
|
self.attributes = Attributes(self)
|
|
41
43
|
self.search = Search(self)
|
|
44
|
+
self.scanners = Scanners(self)
|
|
42
45
|
self.webhook = Webhook(self)
|
|
43
46
|
self.statistics = Statistics(self)
|
|
47
|
+
self.aegis = Aegis(self)
|
|
44
48
|
self.agents = Agents(self)
|
|
45
49
|
self.settings = Settings(self)
|
|
46
50
|
self.configurations = Configurations(self)
|
|
@@ -49,6 +53,9 @@ class Chariot:
|
|
|
49
53
|
self.credentials = Credentials(self)
|
|
50
54
|
self.proxy = proxy
|
|
51
55
|
|
|
56
|
+
if self.proxy == '' and os.environ.get('CHARIOT_PROXY'):
|
|
57
|
+
self.proxy = os.environ.get('CHARIOT_PROXY')
|
|
58
|
+
|
|
52
59
|
if self.proxy:
|
|
53
60
|
import urllib3
|
|
54
61
|
urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
|
|
@@ -62,9 +69,27 @@ class Chariot:
|
|
|
62
69
|
if self.proxy:
|
|
63
70
|
kwargs['proxies'] = {'http': self.proxy, 'https': self.proxy}
|
|
64
71
|
kwargs['verify'] = False
|
|
72
|
+
|
|
73
|
+
self._add_beta_filter(method, kwargs)
|
|
65
74
|
|
|
66
75
|
return requests.request(method, url, headers=self.keychain.headers(), **kwargs)
|
|
67
76
|
|
|
77
|
+
def _add_beta_filter(self, method: str, kwargs: dict):
|
|
78
|
+
if method == 'GET' or method == 'DELETE':
|
|
79
|
+
self._add_beta_url_param(kwargs)
|
|
80
|
+
else:
|
|
81
|
+
self._add_beta_json_param(kwargs)
|
|
82
|
+
|
|
83
|
+
def _add_beta_url_param(self, kwargs: dict):
|
|
84
|
+
if 'params' in kwargs:
|
|
85
|
+
kwargs['params']['beta'] = 'true'
|
|
86
|
+
else:
|
|
87
|
+
kwargs['params'] = {'beta': 'true'}
|
|
88
|
+
|
|
89
|
+
def _add_beta_json_param(self, kwargs: dict):
|
|
90
|
+
if 'json' in kwargs:
|
|
91
|
+
kwargs['json']['beta'] = True
|
|
92
|
+
|
|
68
93
|
def my(self, params: dict, pages=1) -> dict:
|
|
69
94
|
final_resp = dict()
|
|
70
95
|
|
|
@@ -235,6 +260,69 @@ class Chariot:
|
|
|
235
260
|
|
|
236
261
|
server = MCPServer(self, allowable_tools)
|
|
237
262
|
return anyio.run(server.start)
|
|
263
|
+
|
|
264
|
+
def get_current_user(self) -> tuple:
|
|
265
|
+
"""
|
|
266
|
+
Get current user information for Aegis functionality.
|
|
267
|
+
|
|
268
|
+
Returns:
|
|
269
|
+
tuple: (user_email, username) where user_email is the login email
|
|
270
|
+
and username is the SSH username derived from the email
|
|
271
|
+
"""
|
|
272
|
+
# Try to get username from keychain first (for username/password auth)
|
|
273
|
+
user_email = self.keychain.username()
|
|
274
|
+
|
|
275
|
+
# If no username in keychain (API key auth), try to get it from JWT token
|
|
276
|
+
if not user_email and self.keychain.has_api_key():
|
|
277
|
+
token = self.keychain.token()
|
|
278
|
+
payload = decode_jwt_payload(token)
|
|
279
|
+
if payload:
|
|
280
|
+
# Extract email from the 'email' field in the JWT payload
|
|
281
|
+
user_email = payload.get('email')
|
|
282
|
+
else:
|
|
283
|
+
# If JWT decoding fails, fall back to the account parameter
|
|
284
|
+
raise Exception("Failed to decode JWT token")
|
|
285
|
+
|
|
286
|
+
# Extract username from email (part before @) for SSH access
|
|
287
|
+
username = user_email.split('@')[0] if user_email and '@' in user_email else user_email
|
|
288
|
+
return user_email, username
|
|
289
|
+
|
|
290
|
+
|
|
291
|
+
def decode_jwt_payload(token: str) -> dict | None:
|
|
292
|
+
"""
|
|
293
|
+
Decode the payload from a JWT token.
|
|
294
|
+
|
|
295
|
+
Args:
|
|
296
|
+
token: JWT token string in format header.payload.signature
|
|
297
|
+
|
|
298
|
+
Returns:
|
|
299
|
+
dict: Decoded payload contents, or None if decoding fails
|
|
300
|
+
|
|
301
|
+
Example:
|
|
302
|
+
>>> token = "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJlbWFpbCI6InVzZXJAZXhhbXBsZS5jb20ifQ.signature"
|
|
303
|
+
>>> payload = decode_jwt_payload(token)
|
|
304
|
+
>>> print(payload.get('email'))
|
|
305
|
+
user@example.com
|
|
306
|
+
"""
|
|
307
|
+
try:
|
|
308
|
+
import json
|
|
309
|
+
import base64
|
|
310
|
+
|
|
311
|
+
# JWT tokens have 3 parts: header.payload.signature
|
|
312
|
+
parts = token.split('.')
|
|
313
|
+
if len(parts) != 3:
|
|
314
|
+
return None
|
|
315
|
+
|
|
316
|
+
payload_part = parts[1]
|
|
317
|
+
# Add padding if needed for base64 decoding
|
|
318
|
+
payload_part += '=' * (4 - len(payload_part) % 4)
|
|
319
|
+
payload = json.loads(base64.b64decode(payload_part))
|
|
320
|
+
|
|
321
|
+
return payload
|
|
322
|
+
except Exception:
|
|
323
|
+
return None
|
|
324
|
+
|
|
325
|
+
|
|
238
326
|
def is_query_limit_failure(response: requests.Response) -> bool:
|
|
239
327
|
return response.status_code == 413 and 'reduce page size' in response.text
|
|
240
328
|
|