praetorian-cli 2.1.4__tar.gz → 2.2.0__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.1.4/praetorian_cli.egg-info → praetorian_cli-2.2.0}/PKG-INFO +4 -2
- {praetorian_cli-2.1.4 → praetorian_cli-2.2.0}/praetorian_cli/handlers/add.py +5 -4
- praetorian_cli-2.2.0/praetorian_cli/handlers/agent.py +63 -0
- {praetorian_cli-2.1.4 → praetorian_cli-2.2.0}/praetorian_cli/handlers/chariot.py +4 -2
- {praetorian_cli-2.1.4 → praetorian_cli-2.2.0}/praetorian_cli/handlers/cli_decorators.py +3 -1
- {praetorian_cli-2.1.4 → praetorian_cli-2.2.0}/praetorian_cli/handlers/get.py +1 -6
- {praetorian_cli-2.1.4 → praetorian_cli-2.2.0}/praetorian_cli/handlers/list.py +4 -3
- {praetorian_cli-2.1.4 → praetorian_cli-2.2.0}/praetorian_cli/main.py +3 -2
- {praetorian_cli-2.1.4 → praetorian_cli-2.2.0}/praetorian_cli/sdk/chariot.py +45 -21
- praetorian_cli-2.2.0/praetorian_cli/sdk/entities/accounts.py +130 -0
- {praetorian_cli-2.1.4 → praetorian_cli-2.2.0}/praetorian_cli/sdk/entities/agents.py +10 -0
- praetorian_cli-2.2.0/praetorian_cli/sdk/entities/assets.py +132 -0
- praetorian_cli-2.2.0/praetorian_cli/sdk/entities/attributes.py +83 -0
- praetorian_cli-2.2.0/praetorian_cli/sdk/entities/capabilities.py +58 -0
- praetorian_cli-2.2.0/praetorian_cli/sdk/entities/configurations.py +108 -0
- {praetorian_cli-2.1.4 → praetorian_cli-2.2.0}/praetorian_cli/sdk/entities/credentials.py +60 -40
- praetorian_cli-2.2.0/praetorian_cli/sdk/entities/definitions.py +64 -0
- praetorian_cli-2.2.0/praetorian_cli/sdk/entities/files.py +137 -0
- praetorian_cli-2.2.0/praetorian_cli/sdk/entities/integrations.py +69 -0
- praetorian_cli-2.2.0/praetorian_cli/sdk/entities/jobs.py +214 -0
- praetorian_cli-2.2.0/praetorian_cli/sdk/entities/keys.py +140 -0
- praetorian_cli-2.2.0/praetorian_cli/sdk/entities/preseeds.py +219 -0
- praetorian_cli-2.2.0/praetorian_cli/sdk/entities/risks.py +139 -0
- praetorian_cli-2.2.0/praetorian_cli/sdk/entities/search.py +429 -0
- praetorian_cli-2.2.0/praetorian_cli/sdk/entities/seeds.py +114 -0
- praetorian_cli-2.2.0/praetorian_cli/sdk/entities/settings.py +68 -0
- {praetorian_cli-2.1.4 → praetorian_cli-2.2.0}/praetorian_cli/sdk/entities/statistics.py +56 -4
- praetorian_cli-2.2.0/praetorian_cli/sdk/entities/webhook.py +167 -0
- praetorian_cli-2.2.0/praetorian_cli/sdk/mcp_server.py +236 -0
- {praetorian_cli-2.1.4 → praetorian_cli-2.2.0}/praetorian_cli/sdk/model/globals.py +3 -0
- {praetorian_cli-2.1.4 → praetorian_cli-2.2.0}/praetorian_cli/sdk/model/query.py +10 -2
- {praetorian_cli-2.1.4 → praetorian_cli-2.2.0}/praetorian_cli/sdk/model/utils.py +8 -0
- praetorian_cli-2.2.0/praetorian_cli/sdk/test/test_asset.py +83 -0
- praetorian_cli-2.2.0/praetorian_cli/sdk/test/test_mcp.py +28 -0
- {praetorian_cli-2.1.4 → praetorian_cli-2.2.0}/praetorian_cli/sdk/test/test_search.py +3 -3
- {praetorian_cli-2.1.4 → praetorian_cli-2.2.0}/praetorian_cli/sdk/test/utils.py +6 -1
- {praetorian_cli-2.1.4 → praetorian_cli-2.2.0/praetorian_cli.egg-info}/PKG-INFO +4 -2
- {praetorian_cli-2.1.4 → praetorian_cli-2.2.0}/praetorian_cli.egg-info/SOURCES.txt +2 -0
- {praetorian_cli-2.1.4 → praetorian_cli-2.2.0}/praetorian_cli.egg-info/requires.txt +2 -0
- {praetorian_cli-2.1.4 → praetorian_cli-2.2.0}/setup.cfg +4 -2
- praetorian_cli-2.1.4/praetorian_cli/handlers/agent.py +0 -28
- praetorian_cli-2.1.4/praetorian_cli/sdk/entities/accounts.py +0 -64
- praetorian_cli-2.1.4/praetorian_cli/sdk/entities/assets.py +0 -102
- praetorian_cli-2.1.4/praetorian_cli/sdk/entities/attributes.py +0 -26
- praetorian_cli-2.1.4/praetorian_cli/sdk/entities/capabilities.py +0 -10
- praetorian_cli-2.1.4/praetorian_cli/sdk/entities/configurations.py +0 -36
- praetorian_cli-2.1.4/praetorian_cli/sdk/entities/definitions.py +0 -29
- praetorian_cli-2.1.4/praetorian_cli/sdk/entities/files.py +0 -56
- praetorian_cli-2.1.4/praetorian_cli/sdk/entities/integrations.py +0 -33
- praetorian_cli-2.1.4/praetorian_cli/sdk/entities/jobs.py +0 -42
- praetorian_cli-2.1.4/praetorian_cli/sdk/entities/keys.py +0 -22
- praetorian_cli-2.1.4/praetorian_cli/sdk/entities/preseeds.py +0 -33
- praetorian_cli-2.1.4/praetorian_cli/sdk/entities/risks.py +0 -115
- praetorian_cli-2.1.4/praetorian_cli/sdk/entities/search.py +0 -76
- praetorian_cli-2.1.4/praetorian_cli/sdk/entities/seeds.py +0 -55
- praetorian_cli-2.1.4/praetorian_cli/sdk/entities/settings.py +0 -23
- praetorian_cli-2.1.4/praetorian_cli/sdk/entities/webhook.py +0 -39
- praetorian_cli-2.1.4/praetorian_cli/sdk/test/test_asset.py +0 -50
- {praetorian_cli-2.1.4 → praetorian_cli-2.2.0}/LICENSE +0 -0
- {praetorian_cli-2.1.4 → praetorian_cli-2.2.0}/MANIFEST.in +0 -0
- {praetorian_cli-2.1.4 → praetorian_cli-2.2.0}/README.md +0 -0
- {praetorian_cli-2.1.4 → praetorian_cli-2.2.0}/praetorian_cli/__init__.py +0 -0
- {praetorian_cli-2.1.4 → praetorian_cli-2.2.0}/praetorian_cli/handlers/__init__.py +0 -0
- {praetorian_cli-2.1.4 → praetorian_cli-2.2.0}/praetorian_cli/handlers/configure.py +0 -0
- {praetorian_cli-2.1.4 → praetorian_cli-2.2.0}/praetorian_cli/handlers/delete.py +0 -0
- {praetorian_cli-2.1.4 → praetorian_cli-2.2.0}/praetorian_cli/handlers/enrich.py +0 -0
- {praetorian_cli-2.1.4 → praetorian_cli-2.2.0}/praetorian_cli/handlers/imports.py +0 -0
- {praetorian_cli-2.1.4 → praetorian_cli-2.2.0}/praetorian_cli/handlers/link.py +0 -0
- {praetorian_cli-2.1.4 → praetorian_cli-2.2.0}/praetorian_cli/handlers/script.py +0 -0
- {praetorian_cli-2.1.4 → praetorian_cli-2.2.0}/praetorian_cli/handlers/search.py +0 -0
- {praetorian_cli-2.1.4 → praetorian_cli-2.2.0}/praetorian_cli/handlers/test.py +0 -0
- {praetorian_cli-2.1.4 → praetorian_cli-2.2.0}/praetorian_cli/handlers/unlink.py +0 -0
- {praetorian_cli-2.1.4 → praetorian_cli-2.2.0}/praetorian_cli/handlers/update.py +0 -0
- {praetorian_cli-2.1.4 → praetorian_cli-2.2.0}/praetorian_cli/handlers/utils.py +0 -0
- {praetorian_cli-2.1.4 → praetorian_cli-2.2.0}/praetorian_cli/scripts/__init__.py +0 -0
- {praetorian_cli-2.1.4 → praetorian_cli-2.2.0}/praetorian_cli/scripts/commands/__init__.py +0 -0
- {praetorian_cli-2.1.4 → praetorian_cli-2.2.0}/praetorian_cli/scripts/commands/nmap-example.py +0 -0
- {praetorian_cli-2.1.4 → praetorian_cli-2.2.0}/praetorian_cli/scripts/utils.py +0 -0
- {praetorian_cli-2.1.4 → praetorian_cli-2.2.0}/praetorian_cli/sdk/__init__.py +0 -0
- {praetorian_cli-2.1.4 → praetorian_cli-2.2.0}/praetorian_cli/sdk/entities/__init__.py +0 -0
- {praetorian_cli-2.1.4 → praetorian_cli-2.2.0}/praetorian_cli/sdk/keychain.py +0 -0
- {praetorian_cli-2.1.4 → praetorian_cli-2.2.0}/praetorian_cli/sdk/model/__init__.py +0 -0
- {praetorian_cli-2.1.4 → praetorian_cli-2.2.0}/praetorian_cli/sdk/test/__init__.py +0 -0
- {praetorian_cli-2.1.4 → praetorian_cli-2.2.0}/praetorian_cli/sdk/test/pytest.ini +0 -0
- {praetorian_cli-2.1.4 → praetorian_cli-2.2.0}/praetorian_cli/sdk/test/test_account.py +0 -0
- {praetorian_cli-2.1.4 → praetorian_cli-2.2.0}/praetorian_cli/sdk/test/test_agent.py +0 -0
- {praetorian_cli-2.1.4 → praetorian_cli-2.2.0}/praetorian_cli/sdk/test/test_attribute.py +0 -0
- {praetorian_cli-2.1.4 → praetorian_cli-2.2.0}/praetorian_cli/sdk/test/test_capabilities.py +0 -0
- {praetorian_cli-2.1.4 → praetorian_cli-2.2.0}/praetorian_cli/sdk/test/test_configuration.py +0 -0
- {praetorian_cli-2.1.4 → praetorian_cli-2.2.0}/praetorian_cli/sdk/test/test_definition.py +0 -0
- {praetorian_cli-2.1.4 → praetorian_cli-2.2.0}/praetorian_cli/sdk/test/test_extend.py +0 -0
- {praetorian_cli-2.1.4 → praetorian_cli-2.2.0}/praetorian_cli/sdk/test/test_file.py +0 -0
- {praetorian_cli-2.1.4 → praetorian_cli-2.2.0}/praetorian_cli/sdk/test/test_job.py +0 -0
- {praetorian_cli-2.1.4 → praetorian_cli-2.2.0}/praetorian_cli/sdk/test/test_key.py +0 -0
- {praetorian_cli-2.1.4 → praetorian_cli-2.2.0}/praetorian_cli/sdk/test/test_preseed.py +0 -0
- {praetorian_cli-2.1.4 → praetorian_cli-2.2.0}/praetorian_cli/sdk/test/test_risk.py +0 -0
- {praetorian_cli-2.1.4 → praetorian_cli-2.2.0}/praetorian_cli/sdk/test/test_seed.py +0 -0
- {praetorian_cli-2.1.4 → praetorian_cli-2.2.0}/praetorian_cli/sdk/test/test_setting.py +0 -0
- {praetorian_cli-2.1.4 → praetorian_cli-2.2.0}/praetorian_cli/sdk/test/test_webhook.py +0 -0
- {praetorian_cli-2.1.4 → praetorian_cli-2.2.0}/praetorian_cli/sdk/test/test_z_cli.py +0 -0
- {praetorian_cli-2.1.4 → praetorian_cli-2.2.0}/praetorian_cli.egg-info/dependency_links.txt +0 -0
- {praetorian_cli-2.1.4 → praetorian_cli-2.2.0}/praetorian_cli.egg-info/entry_points.txt +0 -0
- {praetorian_cli-2.1.4 → praetorian_cli-2.2.0}/praetorian_cli.egg-info/top_level.txt +0 -0
- {praetorian_cli-2.1.4 → praetorian_cli-2.2.0}/pyproject.toml +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: praetorian-cli
|
|
3
|
-
Version: 2.
|
|
3
|
+
Version: 2.2.0
|
|
4
4
|
Summary: For interacting with the Chariot API
|
|
5
5
|
Home-page: https://github.com/praetorian-inc/praetorian-cli
|
|
6
6
|
Author: Praetorian
|
|
@@ -8,13 +8,15 @@ Author-email: support@praetorian.com
|
|
|
8
8
|
Classifier: Programming Language :: Python :: 3
|
|
9
9
|
Classifier: License :: OSI Approved :: MIT License
|
|
10
10
|
Classifier: Operating System :: OS Independent
|
|
11
|
-
Requires-Python: >=3.
|
|
11
|
+
Requires-Python: >=3.10
|
|
12
12
|
Description-Content-Type: text/markdown
|
|
13
13
|
License-File: LICENSE
|
|
14
14
|
Requires-Dist: click>=8.1.7
|
|
15
15
|
Requires-Dist: boto3>=1.34.0
|
|
16
16
|
Requires-Dist: requests>=2.31.0
|
|
17
17
|
Requires-Dist: pytest>=8.0.2
|
|
18
|
+
Requires-Dist: mcp>=1.12.2
|
|
19
|
+
Requires-Dist: anyio>=3.0.0
|
|
18
20
|
Dynamic: license-file
|
|
19
21
|
|
|
20
22
|
# Praetorian CLI and SDK
|
|
@@ -6,7 +6,7 @@ import click
|
|
|
6
6
|
from praetorian_cli.handlers.chariot import chariot
|
|
7
7
|
from praetorian_cli.handlers.cli_decorators import cli_handler, praetorian_only
|
|
8
8
|
from praetorian_cli.handlers.utils import error
|
|
9
|
-
from praetorian_cli.sdk.model.globals import AddRisk, Asset, Seed
|
|
9
|
+
from praetorian_cli.sdk.model.globals import AddRisk, Asset, Seed, Kind
|
|
10
10
|
|
|
11
11
|
|
|
12
12
|
@chariot.group()
|
|
@@ -18,11 +18,12 @@ def add():
|
|
|
18
18
|
@add.command()
|
|
19
19
|
@cli_handler
|
|
20
20
|
@click.option('-d', '--dns', required=True, help='The DNS of the asset')
|
|
21
|
-
@click.option('-n', '--name', required=False, help='The name of the asset, e.g, IP address
|
|
21
|
+
@click.option('-n', '--name', required=False, help='The name of the asset, e.g, IP address')
|
|
22
|
+
@click.option('-t', '--type', 'asset_type', required=False, help='The type of the asset (asset, repository, etc.)', default=Kind.ASSET.value)
|
|
22
23
|
@click.option('-s', '--status', type=click.Choice([s.value for s in Asset]), required=False,
|
|
23
24
|
default=Asset.ACTIVE.value, help=f'Status of the asset', show_default=True)
|
|
24
25
|
@click.option('-f', '--surface', required=False, default='', help=f'Attack surface of the asset', show_default=False)
|
|
25
|
-
def asset(sdk, name, dns, status, surface):
|
|
26
|
+
def asset(sdk, name, dns, asset_type, status, surface):
|
|
26
27
|
""" Add an asset
|
|
27
28
|
|
|
28
29
|
Add an asset to the Chariot database. This command requires a DNS name for the asset.
|
|
@@ -43,7 +44,7 @@ def asset(sdk, name, dns, status, surface):
|
|
|
43
44
|
"""
|
|
44
45
|
if not name:
|
|
45
46
|
name = dns
|
|
46
|
-
sdk.assets.add(dns, name, status, surface)
|
|
47
|
+
sdk.assets.add(dns, name, asset_type, status, surface)
|
|
47
48
|
|
|
48
49
|
|
|
49
50
|
@add.command()
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import click
|
|
2
|
+
|
|
3
|
+
from praetorian_cli.handlers.chariot import chariot
|
|
4
|
+
from praetorian_cli.handlers.cli_decorators import cli_handler
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
@chariot.group()
|
|
8
|
+
def agent():
|
|
9
|
+
""" A collection of AI features """
|
|
10
|
+
pass
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@agent.command()
|
|
14
|
+
@cli_handler
|
|
15
|
+
@click.argument('key')
|
|
16
|
+
def affiliation(sdk, key):
|
|
17
|
+
""" Get affiliation data for risks and assets
|
|
18
|
+
|
|
19
|
+
The AI agent retrieves affiliation information for the asset or risk. This command
|
|
20
|
+
waits up to 3 minutes for the results.
|
|
21
|
+
|
|
22
|
+
\b
|
|
23
|
+
Example usages:
|
|
24
|
+
- praetorian chariot agent affiliation "#risk#www.praetorian.com#CVE-2024-1234"
|
|
25
|
+
- praetorian chariot agent affiliation "#asset#praetorian.com#www.praetorian.com"
|
|
26
|
+
"""
|
|
27
|
+
click.echo("Polling for the affiliation data for up to 3 minutes.")
|
|
28
|
+
click.echo(sdk.agents.affiliation(key))
|
|
29
|
+
|
|
30
|
+
@agent.group()
|
|
31
|
+
def mcp():
|
|
32
|
+
""" Chariot's MCP server """
|
|
33
|
+
pass
|
|
34
|
+
|
|
35
|
+
@mcp.command()
|
|
36
|
+
@cli_handler
|
|
37
|
+
@click.option('--allowed', '-a', type=str, multiple=True, default=['search_by_query', '*_list', '*_get'])
|
|
38
|
+
def start(sdk, allowed):
|
|
39
|
+
""" Starts the Chariot MCP server
|
|
40
|
+
|
|
41
|
+
\b
|
|
42
|
+
Example usages:
|
|
43
|
+
- praetorian chariot agent mcp start
|
|
44
|
+
- praetorian chariot agent mcp start -a search_by_term -a risk_add
|
|
45
|
+
- praetorian chariot agent mcp start -a search_* -a risk_add
|
|
46
|
+
"""
|
|
47
|
+
if len(allowed) == 0:
|
|
48
|
+
allowed = None
|
|
49
|
+
sdk.agents.start_mcp_server(allowed)
|
|
50
|
+
|
|
51
|
+
@mcp.command()
|
|
52
|
+
@click.option('--allowed', '-a', type=str, multiple=True, default=['search_by_query', '*_list', '*_get'])
|
|
53
|
+
@cli_handler
|
|
54
|
+
def tools(sdk, allowed):
|
|
55
|
+
""" Lists available mcp tools
|
|
56
|
+
|
|
57
|
+
\b
|
|
58
|
+
Example usages:
|
|
59
|
+
- praetorian chariot agent mcp tools
|
|
60
|
+
- praetorian chariot agent mcp tools -a search_* -a risk_add
|
|
61
|
+
"""
|
|
62
|
+
for tool in dict.keys(sdk.agents.list_mcp_tools(allowed)):
|
|
63
|
+
click.echo(tool)
|
|
@@ -10,6 +10,8 @@ def chariot(click_context):
|
|
|
10
10
|
""" Command group for interacting with the Chariot product """
|
|
11
11
|
# Replace the click context (previously a Keychain instance) with a Chariot
|
|
12
12
|
# instance, after creating it using the Keychain instance.
|
|
13
|
-
keychain = click_context.obj
|
|
14
|
-
|
|
13
|
+
keychain = click_context.obj['keychain']
|
|
14
|
+
proxy = click_context.obj['proxy']
|
|
15
|
+
|
|
16
|
+
chariot = Chariot(keychain=keychain, proxy=proxy)
|
|
15
17
|
click_context.obj = chariot
|
|
@@ -52,7 +52,7 @@ def cli_handler(func):
|
|
|
52
52
|
return func
|
|
53
53
|
|
|
54
54
|
|
|
55
|
-
def list_params(filter_by, has_details=True, has_filter=True):
|
|
55
|
+
def list_params(filter_by, has_details=True, has_filter=True, has_type=False):
|
|
56
56
|
def decorator(func):
|
|
57
57
|
func = pagination(func)
|
|
58
58
|
func = cli_handler(func)
|
|
@@ -60,6 +60,8 @@ def list_params(filter_by, has_details=True, has_filter=True):
|
|
|
60
60
|
func = click.option('-f', '--filter', default='', help=f'Filter by {filter_by}')(func)
|
|
61
61
|
if has_details:
|
|
62
62
|
func = click.option('-d', '--details', is_flag=True, default=False, help='Show detailed information')(func)
|
|
63
|
+
if has_type:
|
|
64
|
+
func = click.option('-t', '--type', "model_type", default='', help='Select only a subset by type')(func)
|
|
63
65
|
return func
|
|
64
66
|
|
|
65
67
|
return decorator
|
|
@@ -270,15 +270,10 @@ def credential(chariot, credential_id, category, type, format, parameters):
|
|
|
270
270
|
- praetorian chariot get credential aws-prod --category integration --type aws --format json
|
|
271
271
|
- praetorian chariot get credential ssh-key-1 --category cloud --type ssh_key --format pem
|
|
272
272
|
"""
|
|
273
|
-
import json
|
|
274
273
|
|
|
275
274
|
params = {}
|
|
276
275
|
if parameters:
|
|
277
|
-
|
|
278
|
-
params = json.loads(parameters)
|
|
279
|
-
except json.JSONDecodeError:
|
|
280
|
-
click.echo("Error: Invalid JSON format for parameters")
|
|
281
|
-
return
|
|
276
|
+
params = {key: value for key, value in parameters}
|
|
282
277
|
|
|
283
278
|
result = chariot.credentials.get(credential_id, category, type, [format], **params)
|
|
284
279
|
output = chariot.credentials.format_output(result)
|
|
@@ -12,8 +12,8 @@ def list():
|
|
|
12
12
|
|
|
13
13
|
|
|
14
14
|
@list.command()
|
|
15
|
-
@list_params('DNS')
|
|
16
|
-
def assets(chariot, filter, details, offset, page):
|
|
15
|
+
@list_params('DNS', has_type=True)
|
|
16
|
+
def assets(chariot, filter, model_type, details, offset, page):
|
|
17
17
|
""" List assets
|
|
18
18
|
|
|
19
19
|
Retrieve and display a list of assets.
|
|
@@ -24,8 +24,9 @@ def assets(chariot, filter, details, offset, page):
|
|
|
24
24
|
- praetorian chariot list assets --filter api.example.com
|
|
25
25
|
- praetorian chariot list assets --details
|
|
26
26
|
- praetorian chariot list assets --page all
|
|
27
|
+
- praetorian chariot list assets --type repository
|
|
27
28
|
"""
|
|
28
|
-
render_list_results(chariot.assets.list(filter, offset, pagination_size(page)), details)
|
|
29
|
+
render_list_results(chariot.assets.list(filter, model_type, offset, pagination_size(page)), details)
|
|
29
30
|
|
|
30
31
|
|
|
31
32
|
@list.command()
|
|
@@ -22,13 +22,14 @@ from praetorian_cli.sdk.keychain import Keychain
|
|
|
22
22
|
@click.option('--profile', default='United States', help='The profile to use in the keychain file', show_default=True)
|
|
23
23
|
@click.option('--account', default=None, help='Assume role into this account')
|
|
24
24
|
@click.option('--debug', is_flag=True, default=False, help='Run the CLI in debug mode')
|
|
25
|
+
@click.option('--proxy', default='', help='The proxy to use in the CLI')
|
|
25
26
|
@click.pass_context
|
|
26
27
|
@click.version_option()
|
|
27
|
-
def main(click_context, profile, account, debug):
|
|
28
|
+
def main(click_context, profile, account, debug, proxy):
|
|
28
29
|
if debug:
|
|
29
30
|
click.echo('Running in debug mode.')
|
|
30
31
|
chariot.is_debug = debug
|
|
31
|
-
click_context.obj = Keychain(profile, account)
|
|
32
|
+
click_context.obj = {'keychain': Keychain(profile, account), 'proxy': proxy}
|
|
32
33
|
praetorian_cli.handlers.script.load_dynamic_commands()
|
|
33
34
|
|
|
34
35
|
|
|
@@ -1,6 +1,4 @@
|
|
|
1
|
-
import json
|
|
2
|
-
|
|
3
|
-
import requests
|
|
1
|
+
import json, requests
|
|
4
2
|
|
|
5
3
|
from praetorian_cli.sdk.entities.accounts import Accounts
|
|
6
4
|
from praetorian_cli.sdk.entities.agents import Agents
|
|
@@ -28,7 +26,7 @@ from praetorian_cli.sdk.model.query import Query, my_params_to_query, DEFAULT_PA
|
|
|
28
26
|
|
|
29
27
|
class Chariot:
|
|
30
28
|
|
|
31
|
-
def __init__(self, keychain: Keychain):
|
|
29
|
+
def __init__(self, keychain: Keychain, proxy: str=''):
|
|
32
30
|
self.keychain = keychain
|
|
33
31
|
self.assets = Assets(self)
|
|
34
32
|
self.seeds = Seeds(self)
|
|
@@ -49,6 +47,23 @@ class Chariot:
|
|
|
49
47
|
self.keys = Keys(self)
|
|
50
48
|
self.capabilities = Capabilities(self)
|
|
51
49
|
self.credentials = Credentials(self)
|
|
50
|
+
self.proxy = proxy
|
|
51
|
+
|
|
52
|
+
if self.proxy:
|
|
53
|
+
import urllib3
|
|
54
|
+
urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
|
|
55
|
+
|
|
56
|
+
def _make_request(self, method: str, url: str, **kwargs) -> requests.Response:
|
|
57
|
+
"""
|
|
58
|
+
Centralized method to make HTTP requests with proxy configuration.
|
|
59
|
+
Automatically applies proxy settings and verify=False if proxy is defined.
|
|
60
|
+
"""
|
|
61
|
+
|
|
62
|
+
if self.proxy:
|
|
63
|
+
kwargs['proxies'] = {'http': self.proxy, 'https': self.proxy}
|
|
64
|
+
kwargs['verify'] = False
|
|
65
|
+
|
|
66
|
+
return requests.request(method, url, headers=self.keychain.headers(), **kwargs)
|
|
52
67
|
|
|
53
68
|
def my(self, params: dict, pages=1) -> dict:
|
|
54
69
|
final_resp = dict()
|
|
@@ -60,7 +75,7 @@ class Chariot:
|
|
|
60
75
|
|
|
61
76
|
# The search is on data in DynamoDB, which uses DynamoDB's native offset format.
|
|
62
77
|
for _ in range(pages):
|
|
63
|
-
resp =
|
|
78
|
+
resp = self._make_request('GET', self.url('/my'), params=params)
|
|
64
79
|
process_failure(resp)
|
|
65
80
|
resp = resp.json()
|
|
66
81
|
extend(final_resp, resp)
|
|
@@ -87,7 +102,7 @@ class Chariot:
|
|
|
87
102
|
final_resp = dict()
|
|
88
103
|
|
|
89
104
|
while pages > 0:
|
|
90
|
-
resp =
|
|
105
|
+
resp = self._make_request('POST', self.url('/my'), json=raw_query, params=params)
|
|
91
106
|
if is_query_limit_failure(resp):
|
|
92
107
|
# In this block, the data size is too large for the number of records requested in raw_query['limit'].
|
|
93
108
|
# We need to halve the page size: LIMIT = LIMIT / 2
|
|
@@ -114,22 +129,22 @@ class Chariot:
|
|
|
114
129
|
return final_resp
|
|
115
130
|
|
|
116
131
|
def post(self, type: str, body: dict, params: dict = {}) -> dict:
|
|
117
|
-
resp =
|
|
132
|
+
resp = self._make_request('POST', self.url(f'/{type}'), json=body, params=params)
|
|
118
133
|
process_failure(resp)
|
|
119
134
|
return resp.json()
|
|
120
135
|
|
|
121
136
|
def put(self, type: str, body: dict, params: dict = {}) -> dict:
|
|
122
|
-
resp =
|
|
137
|
+
resp = self._make_request('PUT', self.url(f'/{type}'), json=body, params=params)
|
|
123
138
|
process_failure(resp)
|
|
124
139
|
return resp.json()
|
|
125
140
|
|
|
126
141
|
def get(self, type: str, params: dict = {}) -> dict:
|
|
127
|
-
resp =
|
|
142
|
+
resp = self._make_request('GET', self.url(f'/{type}'), params=params)
|
|
128
143
|
process_failure(resp)
|
|
129
144
|
return resp.json()
|
|
130
145
|
|
|
131
146
|
def delete(self, type: str, body: dict, params: dict) -> dict:
|
|
132
|
-
resp =
|
|
147
|
+
resp = self._make_request('DELETE', self.url(f'/{type}'), json=body, params=params)
|
|
133
148
|
process_failure(resp)
|
|
134
149
|
return resp.json()
|
|
135
150
|
|
|
@@ -149,14 +164,12 @@ class Chariot:
|
|
|
149
164
|
return self.put(type, body, params)
|
|
150
165
|
|
|
151
166
|
def link_account(self, username: str, value: str = '', config: dict = {}) -> dict:
|
|
152
|
-
resp =
|
|
153
|
-
headers=self.keychain.headers())
|
|
167
|
+
resp = self._make_request('POST', self.url(f'/account/{username}'), json=dict(config=config, value=value))
|
|
154
168
|
process_failure(resp)
|
|
155
169
|
return resp.json()
|
|
156
170
|
|
|
157
171
|
def unlink(self, username: str, value: str = '', config: dict = {}) -> dict:
|
|
158
|
-
resp =
|
|
159
|
-
json=dict(value=value, config=config))
|
|
172
|
+
resp = self._make_request('DELETE', self.url(f'/account/{username}'), json=dict(value=value, config=config))
|
|
160
173
|
process_failure(resp)
|
|
161
174
|
return resp.json()
|
|
162
175
|
|
|
@@ -170,8 +183,7 @@ class Chariot:
|
|
|
170
183
|
def _upload(self, chariot_filepath: str, content: str) -> dict:
|
|
171
184
|
# It is a two-step upload. The PUT request to the /file endpoint is to get a presigned URL for S3.
|
|
172
185
|
# There is no data transfer.
|
|
173
|
-
presigned_url =
|
|
174
|
-
headers=self.keychain.headers())
|
|
186
|
+
presigned_url = self._make_request('PUT', self.url('/file'), params=dict(name=chariot_filepath))
|
|
175
187
|
process_failure(presigned_url)
|
|
176
188
|
resp = requests.put(presigned_url.json()['url'], data=content)
|
|
177
189
|
process_failure(resp)
|
|
@@ -182,12 +194,12 @@ class Chariot:
|
|
|
182
194
|
if global_:
|
|
183
195
|
params |= GLOBAL_FLAG
|
|
184
196
|
|
|
185
|
-
resp =
|
|
197
|
+
resp = self._make_request('GET', self.url('/file'), params=params, allow_redirects=True)
|
|
186
198
|
process_failure(resp)
|
|
187
199
|
return resp.content
|
|
188
200
|
|
|
189
201
|
def count(self, params: dict) -> dict:
|
|
190
|
-
resp =
|
|
202
|
+
resp = self._make_request('GET', self.url('/my/count'), params=params)
|
|
191
203
|
process_failure(resp)
|
|
192
204
|
return resp.json()
|
|
193
205
|
|
|
@@ -196,11 +208,11 @@ class Chariot:
|
|
|
196
208
|
return json.loads(self.download(f'enrichments/{type}/{filename}', True).decode('utf-8'))
|
|
197
209
|
|
|
198
210
|
def purge(self):
|
|
199
|
-
|
|
211
|
+
self._make_request('DELETE', self.url('/account/purge'))
|
|
200
212
|
|
|
201
213
|
def agent(self, agent: str, body: dict) -> dict:
|
|
202
214
|
body = body | dict(agent=agent)
|
|
203
|
-
resp =
|
|
215
|
+
resp = self._make_request('PUT', self.url('/agent'), json=body)
|
|
204
216
|
process_failure(resp)
|
|
205
217
|
return resp.json()
|
|
206
218
|
|
|
@@ -210,7 +222,19 @@ class Chariot:
|
|
|
210
222
|
def is_praetorian_user(self) -> bool:
|
|
211
223
|
return self.keychain.username().endswith('@praetorian.com')
|
|
212
224
|
|
|
213
|
-
|
|
225
|
+
def start_mcp_server(self, allowable_tools=None):
|
|
226
|
+
""" Start MCP server exposing SDK methods as tools
|
|
227
|
+
|
|
228
|
+
Arguments:
|
|
229
|
+
allowable_tools: list
|
|
230
|
+
Optional list of tool names to expose. If None, all tools are exposed.
|
|
231
|
+
Tool names should be in format 'entity.method' (e.g., 'assets.add', 'risks.list')
|
|
232
|
+
"""
|
|
233
|
+
from praetorian_cli.sdk.mcp_server import MCPServer
|
|
234
|
+
import anyio
|
|
235
|
+
|
|
236
|
+
server = MCPServer(self, allowable_tools)
|
|
237
|
+
return anyio.run(server.start)
|
|
214
238
|
def is_query_limit_failure(response: requests.Response) -> bool:
|
|
215
239
|
return response.status_code == 413 and 'reduce page size' in response.text
|
|
216
240
|
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
class Accounts:
|
|
2
|
+
""" The methods in this class are to be assessed from sdk.accounts, where sdk is an instance
|
|
3
|
+
of Chariot. """
|
|
4
|
+
|
|
5
|
+
def __init__(self, api):
|
|
6
|
+
self.api = api
|
|
7
|
+
|
|
8
|
+
def get(self, key):
|
|
9
|
+
"""
|
|
10
|
+
Get details of an account by its exact key.
|
|
11
|
+
|
|
12
|
+
:param key: The exact key of the account to retrieve
|
|
13
|
+
:type key: str
|
|
14
|
+
:return: The matching account entity or None if not found
|
|
15
|
+
:rtype: dict or None
|
|
16
|
+
"""
|
|
17
|
+
return self.api.search.by_exact_key(key)
|
|
18
|
+
|
|
19
|
+
def list(self, username_filter='', offset=None, pages=100000):
|
|
20
|
+
"""
|
|
21
|
+
List accounts of collaborators and master accounts that the current principal can access.
|
|
22
|
+
|
|
23
|
+
Optionally filtered by username of the collaborators or the authorized accounts.
|
|
24
|
+
Filters out integration accounts (those without '@' in member field).
|
|
25
|
+
|
|
26
|
+
:param username_filter: Filter results by username of collaborators or authorized accounts
|
|
27
|
+
:type username_filter: str
|
|
28
|
+
:param offset: The offset of the page you want to retrieve results
|
|
29
|
+
:type offset: str or None
|
|
30
|
+
:param pages: The number of pages of results to retrieve. <mcp>Start with one page of results unless specifically requested.</mcp>
|
|
31
|
+
:type pages: int
|
|
32
|
+
:return: A tuple containing (list of matching account entities, next page offset)
|
|
33
|
+
:rtype: tuple
|
|
34
|
+
"""
|
|
35
|
+
results, next_offset = self.api.search.by_key_prefix(f'#account#', offset, pages)
|
|
36
|
+
|
|
37
|
+
# filter out the integrations
|
|
38
|
+
results = [i for i in results if '@' in i['member']]
|
|
39
|
+
|
|
40
|
+
# filter for user emails
|
|
41
|
+
if username_filter:
|
|
42
|
+
results = [i for i in results if username_filter == i['name'] or username_filter == i['member']]
|
|
43
|
+
|
|
44
|
+
return results, next_offset
|
|
45
|
+
|
|
46
|
+
def add_collaborator(self, collaborator_email):
|
|
47
|
+
"""
|
|
48
|
+
Add a collaborator to the account of the current principal.
|
|
49
|
+
|
|
50
|
+
:param collaborator_email: Email address of the collaborator to add
|
|
51
|
+
:type collaborator_email: str
|
|
52
|
+
:return: The created account entity with member information
|
|
53
|
+
:rtype: dict
|
|
54
|
+
"""
|
|
55
|
+
return self.api.link_account(collaborator_email)
|
|
56
|
+
|
|
57
|
+
def delete_collaborator(self, collaborator_email):
|
|
58
|
+
"""
|
|
59
|
+
Delete a collaborator from the account of the current principal.
|
|
60
|
+
|
|
61
|
+
:param collaborator_email: Email address of the collaborator to remove
|
|
62
|
+
:type collaborator_email: str
|
|
63
|
+
:return: The deleted account entity with member information
|
|
64
|
+
:rtype: dict
|
|
65
|
+
"""
|
|
66
|
+
return self.api.unlink(collaborator_email)
|
|
67
|
+
|
|
68
|
+
def collaborators(self):
|
|
69
|
+
"""
|
|
70
|
+
Return emails of all users that are collaborating with the current principal.
|
|
71
|
+
|
|
72
|
+
The current principal can be an assume-role account.
|
|
73
|
+
|
|
74
|
+
:return: List of collaborator email addresses
|
|
75
|
+
:rtype: list
|
|
76
|
+
"""
|
|
77
|
+
accounts, _ = self.list()
|
|
78
|
+
return [a['member'] for a in accounts if a['name'] == self.current_principal()]
|
|
79
|
+
|
|
80
|
+
def authorized_accounts(self):
|
|
81
|
+
"""
|
|
82
|
+
Return emails of all users that the current principal is authorized to access.
|
|
83
|
+
|
|
84
|
+
The current principal can be an assume-role account.
|
|
85
|
+
|
|
86
|
+
:return: List of authorized account email addresses
|
|
87
|
+
:rtype: list
|
|
88
|
+
"""
|
|
89
|
+
accounts, _ = self.list()
|
|
90
|
+
return [a['name'] for a in accounts if a['member'] == self.current_principal()]
|
|
91
|
+
|
|
92
|
+
def assume_role(self, account_email):
|
|
93
|
+
"""
|
|
94
|
+
Switch session to assume-role account.
|
|
95
|
+
|
|
96
|
+
:param account_email: Email address of the account to assume role into
|
|
97
|
+
:type account_email: str
|
|
98
|
+
:return: None
|
|
99
|
+
:rtype: None
|
|
100
|
+
"""
|
|
101
|
+
self.api.keychain.assume_role(account_email)
|
|
102
|
+
|
|
103
|
+
def unassume_role(self):
|
|
104
|
+
"""
|
|
105
|
+
Switch back to the login principal account.
|
|
106
|
+
|
|
107
|
+
:return: None
|
|
108
|
+
:rtype: None
|
|
109
|
+
"""
|
|
110
|
+
self.api.keychain.unassume_role()
|
|
111
|
+
|
|
112
|
+
def current_principal(self):
|
|
113
|
+
"""
|
|
114
|
+
Tell you which account the current session is operating on.
|
|
115
|
+
|
|
116
|
+
Returns the assume-role account if one is active, otherwise the login principal.
|
|
117
|
+
|
|
118
|
+
:return: Email address of the current principal account
|
|
119
|
+
:rtype: str
|
|
120
|
+
"""
|
|
121
|
+
return self.api.keychain.account if self.api.keychain.account else self.api.keychain.username()
|
|
122
|
+
|
|
123
|
+
def login_principal(self):
|
|
124
|
+
"""
|
|
125
|
+
Tell you the user account that is used to login, regardless of assume-role account.
|
|
126
|
+
|
|
127
|
+
:return: Email address of the login principal account
|
|
128
|
+
:rtype: str
|
|
129
|
+
"""
|
|
130
|
+
return self.api.keychain.username()
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
from time import sleep, time
|
|
2
|
+
import asyncio
|
|
2
3
|
|
|
3
4
|
from praetorian_cli.sdk.model.globals import AgentType
|
|
5
|
+
from praetorian_cli.sdk.mcp_server import MCPServer
|
|
4
6
|
|
|
5
7
|
|
|
6
8
|
class Agents:
|
|
@@ -33,3 +35,11 @@ class Agents:
|
|
|
33
35
|
|
|
34
36
|
def affiliation_result(self, key: str) -> dict:
|
|
35
37
|
return self.api.files.get_utf8(self.affiliation_filename(AgentType.AFFILIATION.value, key))
|
|
38
|
+
|
|
39
|
+
def start_mcp_server(self, allowable_tools=None):
|
|
40
|
+
server = MCPServer(self.api, allowable_tools)
|
|
41
|
+
return asyncio.run(server.start())
|
|
42
|
+
|
|
43
|
+
def list_mcp_tools(self, allowable_tools=None):
|
|
44
|
+
server = MCPServer(self.api, allowable_tools)
|
|
45
|
+
return server.discovered_tools
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
from praetorian_cli.sdk.model.globals import Asset, Kind
|
|
2
|
+
from praetorian_cli.sdk.model.query import Relationship, Node, Query, asset_of_key, RISK_NODE, ATTRIBUTE_NODE
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
class Assets:
|
|
6
|
+
""" The methods in this class are to be assessed from sdk.assets, where sdk is an instance
|
|
7
|
+
of Chariot. """
|
|
8
|
+
|
|
9
|
+
def __init__(self, api):
|
|
10
|
+
self.api = api
|
|
11
|
+
|
|
12
|
+
def add(self, group, identifier, type=Kind.ASSET.value, status=Asset.ACTIVE.value, surface=''):
|
|
13
|
+
"""
|
|
14
|
+
Add an asset to the account.
|
|
15
|
+
|
|
16
|
+
:param group: The DNS or group identifier (e.g., domain name, repository URL)
|
|
17
|
+
:type group: str
|
|
18
|
+
:param identifier: The specific identifier (e.g., IP address, repository name)
|
|
19
|
+
:type identifier: str
|
|
20
|
+
:param type: Asset type from Kind enum (defaults to 'asset', can be 'addomain', 'repository')
|
|
21
|
+
:type type: str
|
|
22
|
+
:param status: Asset status from Asset enum ('A', 'F', 'D', 'P', 'FR')
|
|
23
|
+
:type status: str
|
|
24
|
+
:param surface: Attack surface classification (e.g., 'internal', 'external', 'web', 'api')
|
|
25
|
+
:type surface: str
|
|
26
|
+
:return: The asset that was added
|
|
27
|
+
:rtype: dict
|
|
28
|
+
"""
|
|
29
|
+
return self.api.upsert('asset', dict(group=group, identifier=identifier, status=status, surface=[surface], type=type))[0]
|
|
30
|
+
|
|
31
|
+
def get(self, key, details=False):
|
|
32
|
+
"""
|
|
33
|
+
Get details of an asset by key.
|
|
34
|
+
|
|
35
|
+
:param key: Entity key in format #asset#{dns}#{name}
|
|
36
|
+
:type key: str
|
|
37
|
+
:param details: Whether to fetch additional details like attributes and risks
|
|
38
|
+
:type details: bool
|
|
39
|
+
:return: The asset matching the specified key
|
|
40
|
+
:rtype: dict
|
|
41
|
+
"""
|
|
42
|
+
asset = self.api.search.by_exact_key(key, details)
|
|
43
|
+
if asset and details:
|
|
44
|
+
asset['associated_risks'] = self.associated_risks(key)
|
|
45
|
+
return asset
|
|
46
|
+
|
|
47
|
+
def update(self, key, status=None, surface=None):
|
|
48
|
+
"""
|
|
49
|
+
Update an asset.
|
|
50
|
+
|
|
51
|
+
:param key: Entity key in format #asset#{dns}#{name}. If you supply a prefix that matches multiple assets, all of them will be updated
|
|
52
|
+
:type key: str
|
|
53
|
+
:param status: Asset status from Asset enum ('A', 'F', 'D', 'P', 'FR'), if None status is not updated
|
|
54
|
+
:type status: str or None
|
|
55
|
+
:param surface: Attack surface classification (e.g., 'internal', 'external'), if None surface is not updated
|
|
56
|
+
:type surface: str or None
|
|
57
|
+
:return: None
|
|
58
|
+
:rtype: None
|
|
59
|
+
"""
|
|
60
|
+
if status:
|
|
61
|
+
self.api.upsert('asset', dict(key=key, status=status))
|
|
62
|
+
if surface:
|
|
63
|
+
self.api.attributes.add(key, 'surface', surface)
|
|
64
|
+
|
|
65
|
+
def delete(self, key):
|
|
66
|
+
"""
|
|
67
|
+
Delete an asset.
|
|
68
|
+
|
|
69
|
+
:param key: Entity key in format #asset#{dns}#{name}. If you supply a prefix that matches multiple assets, all of them will be deleted
|
|
70
|
+
:type key: str
|
|
71
|
+
:return: The asset that was deleted
|
|
72
|
+
:rtype: dict
|
|
73
|
+
"""
|
|
74
|
+
return self.api.delete_by_key('asset', key)
|
|
75
|
+
|
|
76
|
+
def list(self, prefix_filter='', asset_type='', offset=None, pages=100000) -> tuple:
|
|
77
|
+
"""
|
|
78
|
+
List assets.
|
|
79
|
+
|
|
80
|
+
:param prefix_filter: Supply this to perform prefix-filtering of the asset keys after the "#asset#" portion of the asset key. Asset keys read '#asset#{dns}#{name}'
|
|
81
|
+
:type prefix_filter: str
|
|
82
|
+
:param asset_type: The type of asset to filter by
|
|
83
|
+
:type asset_type: str
|
|
84
|
+
:param offset: The offset of the page you want to retrieve results. If this is not supplied, this function retrieves from the first page
|
|
85
|
+
:type offset: str or None
|
|
86
|
+
:param pages: The number of pages of results to retrieve. <mcp>Start with one page of results unless specifically requested.</mcp>
|
|
87
|
+
:type pages: int
|
|
88
|
+
:return: A tuple containing (list of assets, next page offset)
|
|
89
|
+
:rtype: tuple
|
|
90
|
+
"""
|
|
91
|
+
dns_prefix = ''
|
|
92
|
+
if prefix_filter:
|
|
93
|
+
dns_prefix = f'group:{prefix_filter}'
|
|
94
|
+
if asset_type == '':
|
|
95
|
+
asset_type = Kind.ASSET.value
|
|
96
|
+
return self.api.search.by_term(dns_prefix, asset_type, offset, pages)
|
|
97
|
+
|
|
98
|
+
def attributes(self, key):
|
|
99
|
+
"""
|
|
100
|
+
Get attributes associated with an asset.
|
|
101
|
+
|
|
102
|
+
:param key: Entity key in format #asset#{dns}#{name}
|
|
103
|
+
:type key: str
|
|
104
|
+
:return: List of attributes associated with the asset
|
|
105
|
+
:rtype: list
|
|
106
|
+
"""
|
|
107
|
+
attributes, _ = self.api.search.by_source(key, Kind.ATTRIBUTE.value)
|
|
108
|
+
return attributes
|
|
109
|
+
|
|
110
|
+
def associated_risks(self, key):
|
|
111
|
+
"""
|
|
112
|
+
Get risks associated with an asset.
|
|
113
|
+
|
|
114
|
+
:param key: Entity key in format #asset#{dns}#{name}
|
|
115
|
+
:type key: str
|
|
116
|
+
:return: List of risks associated with the asset (both directly and indirectly via attributes)
|
|
117
|
+
:rtype: list
|
|
118
|
+
"""
|
|
119
|
+
# risks directly linked to the asset
|
|
120
|
+
risks_from_this = Relationship(Relationship.Label.HAS_VULNERABILITY, source=asset_of_key(key))
|
|
121
|
+
query = Query(Node(RISK_NODE, relationships=[risks_from_this]))
|
|
122
|
+
risks, _ = self.api.search.by_query(query)
|
|
123
|
+
|
|
124
|
+
# risks indirectly linked to this asset via asset attributes
|
|
125
|
+
attributes_from_this = Relationship(Relationship.Label.HAS_ATTRIBUTE, source=asset_of_key(key))
|
|
126
|
+
attributes = Node(ATTRIBUTE_NODE, relationships=[attributes_from_this])
|
|
127
|
+
risks_from_attributes = Relationship(Relationship.Label.HAS_VULNERABILITY, source=attributes)
|
|
128
|
+
query = Query(Node(RISK_NODE, relationships=[risks_from_attributes]))
|
|
129
|
+
indirect_risks, _ = self.api.search.by_query(query)
|
|
130
|
+
|
|
131
|
+
risks.extend(indirect_risks)
|
|
132
|
+
return risks
|