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.
Files changed (104) hide show
  1. {praetorian_cli-2.1.4/praetorian_cli.egg-info → praetorian_cli-2.2.0}/PKG-INFO +4 -2
  2. {praetorian_cli-2.1.4 → praetorian_cli-2.2.0}/praetorian_cli/handlers/add.py +5 -4
  3. praetorian_cli-2.2.0/praetorian_cli/handlers/agent.py +63 -0
  4. {praetorian_cli-2.1.4 → praetorian_cli-2.2.0}/praetorian_cli/handlers/chariot.py +4 -2
  5. {praetorian_cli-2.1.4 → praetorian_cli-2.2.0}/praetorian_cli/handlers/cli_decorators.py +3 -1
  6. {praetorian_cli-2.1.4 → praetorian_cli-2.2.0}/praetorian_cli/handlers/get.py +1 -6
  7. {praetorian_cli-2.1.4 → praetorian_cli-2.2.0}/praetorian_cli/handlers/list.py +4 -3
  8. {praetorian_cli-2.1.4 → praetorian_cli-2.2.0}/praetorian_cli/main.py +3 -2
  9. {praetorian_cli-2.1.4 → praetorian_cli-2.2.0}/praetorian_cli/sdk/chariot.py +45 -21
  10. praetorian_cli-2.2.0/praetorian_cli/sdk/entities/accounts.py +130 -0
  11. {praetorian_cli-2.1.4 → praetorian_cli-2.2.0}/praetorian_cli/sdk/entities/agents.py +10 -0
  12. praetorian_cli-2.2.0/praetorian_cli/sdk/entities/assets.py +132 -0
  13. praetorian_cli-2.2.0/praetorian_cli/sdk/entities/attributes.py +83 -0
  14. praetorian_cli-2.2.0/praetorian_cli/sdk/entities/capabilities.py +58 -0
  15. praetorian_cli-2.2.0/praetorian_cli/sdk/entities/configurations.py +108 -0
  16. {praetorian_cli-2.1.4 → praetorian_cli-2.2.0}/praetorian_cli/sdk/entities/credentials.py +60 -40
  17. praetorian_cli-2.2.0/praetorian_cli/sdk/entities/definitions.py +64 -0
  18. praetorian_cli-2.2.0/praetorian_cli/sdk/entities/files.py +137 -0
  19. praetorian_cli-2.2.0/praetorian_cli/sdk/entities/integrations.py +69 -0
  20. praetorian_cli-2.2.0/praetorian_cli/sdk/entities/jobs.py +214 -0
  21. praetorian_cli-2.2.0/praetorian_cli/sdk/entities/keys.py +140 -0
  22. praetorian_cli-2.2.0/praetorian_cli/sdk/entities/preseeds.py +219 -0
  23. praetorian_cli-2.2.0/praetorian_cli/sdk/entities/risks.py +139 -0
  24. praetorian_cli-2.2.0/praetorian_cli/sdk/entities/search.py +429 -0
  25. praetorian_cli-2.2.0/praetorian_cli/sdk/entities/seeds.py +114 -0
  26. praetorian_cli-2.2.0/praetorian_cli/sdk/entities/settings.py +68 -0
  27. {praetorian_cli-2.1.4 → praetorian_cli-2.2.0}/praetorian_cli/sdk/entities/statistics.py +56 -4
  28. praetorian_cli-2.2.0/praetorian_cli/sdk/entities/webhook.py +167 -0
  29. praetorian_cli-2.2.0/praetorian_cli/sdk/mcp_server.py +236 -0
  30. {praetorian_cli-2.1.4 → praetorian_cli-2.2.0}/praetorian_cli/sdk/model/globals.py +3 -0
  31. {praetorian_cli-2.1.4 → praetorian_cli-2.2.0}/praetorian_cli/sdk/model/query.py +10 -2
  32. {praetorian_cli-2.1.4 → praetorian_cli-2.2.0}/praetorian_cli/sdk/model/utils.py +8 -0
  33. praetorian_cli-2.2.0/praetorian_cli/sdk/test/test_asset.py +83 -0
  34. praetorian_cli-2.2.0/praetorian_cli/sdk/test/test_mcp.py +28 -0
  35. {praetorian_cli-2.1.4 → praetorian_cli-2.2.0}/praetorian_cli/sdk/test/test_search.py +3 -3
  36. {praetorian_cli-2.1.4 → praetorian_cli-2.2.0}/praetorian_cli/sdk/test/utils.py +6 -1
  37. {praetorian_cli-2.1.4 → praetorian_cli-2.2.0/praetorian_cli.egg-info}/PKG-INFO +4 -2
  38. {praetorian_cli-2.1.4 → praetorian_cli-2.2.0}/praetorian_cli.egg-info/SOURCES.txt +2 -0
  39. {praetorian_cli-2.1.4 → praetorian_cli-2.2.0}/praetorian_cli.egg-info/requires.txt +2 -0
  40. {praetorian_cli-2.1.4 → praetorian_cli-2.2.0}/setup.cfg +4 -2
  41. praetorian_cli-2.1.4/praetorian_cli/handlers/agent.py +0 -28
  42. praetorian_cli-2.1.4/praetorian_cli/sdk/entities/accounts.py +0 -64
  43. praetorian_cli-2.1.4/praetorian_cli/sdk/entities/assets.py +0 -102
  44. praetorian_cli-2.1.4/praetorian_cli/sdk/entities/attributes.py +0 -26
  45. praetorian_cli-2.1.4/praetorian_cli/sdk/entities/capabilities.py +0 -10
  46. praetorian_cli-2.1.4/praetorian_cli/sdk/entities/configurations.py +0 -36
  47. praetorian_cli-2.1.4/praetorian_cli/sdk/entities/definitions.py +0 -29
  48. praetorian_cli-2.1.4/praetorian_cli/sdk/entities/files.py +0 -56
  49. praetorian_cli-2.1.4/praetorian_cli/sdk/entities/integrations.py +0 -33
  50. praetorian_cli-2.1.4/praetorian_cli/sdk/entities/jobs.py +0 -42
  51. praetorian_cli-2.1.4/praetorian_cli/sdk/entities/keys.py +0 -22
  52. praetorian_cli-2.1.4/praetorian_cli/sdk/entities/preseeds.py +0 -33
  53. praetorian_cli-2.1.4/praetorian_cli/sdk/entities/risks.py +0 -115
  54. praetorian_cli-2.1.4/praetorian_cli/sdk/entities/search.py +0 -76
  55. praetorian_cli-2.1.4/praetorian_cli/sdk/entities/seeds.py +0 -55
  56. praetorian_cli-2.1.4/praetorian_cli/sdk/entities/settings.py +0 -23
  57. praetorian_cli-2.1.4/praetorian_cli/sdk/entities/webhook.py +0 -39
  58. praetorian_cli-2.1.4/praetorian_cli/sdk/test/test_asset.py +0 -50
  59. {praetorian_cli-2.1.4 → praetorian_cli-2.2.0}/LICENSE +0 -0
  60. {praetorian_cli-2.1.4 → praetorian_cli-2.2.0}/MANIFEST.in +0 -0
  61. {praetorian_cli-2.1.4 → praetorian_cli-2.2.0}/README.md +0 -0
  62. {praetorian_cli-2.1.4 → praetorian_cli-2.2.0}/praetorian_cli/__init__.py +0 -0
  63. {praetorian_cli-2.1.4 → praetorian_cli-2.2.0}/praetorian_cli/handlers/__init__.py +0 -0
  64. {praetorian_cli-2.1.4 → praetorian_cli-2.2.0}/praetorian_cli/handlers/configure.py +0 -0
  65. {praetorian_cli-2.1.4 → praetorian_cli-2.2.0}/praetorian_cli/handlers/delete.py +0 -0
  66. {praetorian_cli-2.1.4 → praetorian_cli-2.2.0}/praetorian_cli/handlers/enrich.py +0 -0
  67. {praetorian_cli-2.1.4 → praetorian_cli-2.2.0}/praetorian_cli/handlers/imports.py +0 -0
  68. {praetorian_cli-2.1.4 → praetorian_cli-2.2.0}/praetorian_cli/handlers/link.py +0 -0
  69. {praetorian_cli-2.1.4 → praetorian_cli-2.2.0}/praetorian_cli/handlers/script.py +0 -0
  70. {praetorian_cli-2.1.4 → praetorian_cli-2.2.0}/praetorian_cli/handlers/search.py +0 -0
  71. {praetorian_cli-2.1.4 → praetorian_cli-2.2.0}/praetorian_cli/handlers/test.py +0 -0
  72. {praetorian_cli-2.1.4 → praetorian_cli-2.2.0}/praetorian_cli/handlers/unlink.py +0 -0
  73. {praetorian_cli-2.1.4 → praetorian_cli-2.2.0}/praetorian_cli/handlers/update.py +0 -0
  74. {praetorian_cli-2.1.4 → praetorian_cli-2.2.0}/praetorian_cli/handlers/utils.py +0 -0
  75. {praetorian_cli-2.1.4 → praetorian_cli-2.2.0}/praetorian_cli/scripts/__init__.py +0 -0
  76. {praetorian_cli-2.1.4 → praetorian_cli-2.2.0}/praetorian_cli/scripts/commands/__init__.py +0 -0
  77. {praetorian_cli-2.1.4 → praetorian_cli-2.2.0}/praetorian_cli/scripts/commands/nmap-example.py +0 -0
  78. {praetorian_cli-2.1.4 → praetorian_cli-2.2.0}/praetorian_cli/scripts/utils.py +0 -0
  79. {praetorian_cli-2.1.4 → praetorian_cli-2.2.0}/praetorian_cli/sdk/__init__.py +0 -0
  80. {praetorian_cli-2.1.4 → praetorian_cli-2.2.0}/praetorian_cli/sdk/entities/__init__.py +0 -0
  81. {praetorian_cli-2.1.4 → praetorian_cli-2.2.0}/praetorian_cli/sdk/keychain.py +0 -0
  82. {praetorian_cli-2.1.4 → praetorian_cli-2.2.0}/praetorian_cli/sdk/model/__init__.py +0 -0
  83. {praetorian_cli-2.1.4 → praetorian_cli-2.2.0}/praetorian_cli/sdk/test/__init__.py +0 -0
  84. {praetorian_cli-2.1.4 → praetorian_cli-2.2.0}/praetorian_cli/sdk/test/pytest.ini +0 -0
  85. {praetorian_cli-2.1.4 → praetorian_cli-2.2.0}/praetorian_cli/sdk/test/test_account.py +0 -0
  86. {praetorian_cli-2.1.4 → praetorian_cli-2.2.0}/praetorian_cli/sdk/test/test_agent.py +0 -0
  87. {praetorian_cli-2.1.4 → praetorian_cli-2.2.0}/praetorian_cli/sdk/test/test_attribute.py +0 -0
  88. {praetorian_cli-2.1.4 → praetorian_cli-2.2.0}/praetorian_cli/sdk/test/test_capabilities.py +0 -0
  89. {praetorian_cli-2.1.4 → praetorian_cli-2.2.0}/praetorian_cli/sdk/test/test_configuration.py +0 -0
  90. {praetorian_cli-2.1.4 → praetorian_cli-2.2.0}/praetorian_cli/sdk/test/test_definition.py +0 -0
  91. {praetorian_cli-2.1.4 → praetorian_cli-2.2.0}/praetorian_cli/sdk/test/test_extend.py +0 -0
  92. {praetorian_cli-2.1.4 → praetorian_cli-2.2.0}/praetorian_cli/sdk/test/test_file.py +0 -0
  93. {praetorian_cli-2.1.4 → praetorian_cli-2.2.0}/praetorian_cli/sdk/test/test_job.py +0 -0
  94. {praetorian_cli-2.1.4 → praetorian_cli-2.2.0}/praetorian_cli/sdk/test/test_key.py +0 -0
  95. {praetorian_cli-2.1.4 → praetorian_cli-2.2.0}/praetorian_cli/sdk/test/test_preseed.py +0 -0
  96. {praetorian_cli-2.1.4 → praetorian_cli-2.2.0}/praetorian_cli/sdk/test/test_risk.py +0 -0
  97. {praetorian_cli-2.1.4 → praetorian_cli-2.2.0}/praetorian_cli/sdk/test/test_seed.py +0 -0
  98. {praetorian_cli-2.1.4 → praetorian_cli-2.2.0}/praetorian_cli/sdk/test/test_setting.py +0 -0
  99. {praetorian_cli-2.1.4 → praetorian_cli-2.2.0}/praetorian_cli/sdk/test/test_webhook.py +0 -0
  100. {praetorian_cli-2.1.4 → praetorian_cli-2.2.0}/praetorian_cli/sdk/test/test_z_cli.py +0 -0
  101. {praetorian_cli-2.1.4 → praetorian_cli-2.2.0}/praetorian_cli.egg-info/dependency_links.txt +0 -0
  102. {praetorian_cli-2.1.4 → praetorian_cli-2.2.0}/praetorian_cli.egg-info/entry_points.txt +0 -0
  103. {praetorian_cli-2.1.4 → praetorian_cli-2.2.0}/praetorian_cli.egg-info/top_level.txt +0 -0
  104. {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.1.4
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.9
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, GitHub repo URL')
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
- chariot = Chariot(keychain=keychain)
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
- try:
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 = requests.get(self.url('/my'), params=params, headers=self.keychain.headers())
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 = requests.post(self.url('/my'), json=raw_query, params=params, headers=self.keychain.headers())
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 = requests.post(self.url(f'/{type}'), json=body, params=params, headers=self.keychain.headers())
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 = requests.put(self.url(f'/{type}'), json=body, params=params, headers=self.keychain.headers())
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 = requests.get(self.url(f'/{type}'), params=params, headers=self.keychain.headers())
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 = requests.delete(self.url(f'/{type}'), json=body, params=params, headers=self.keychain.headers())
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 = requests.post(self.url(f'/account/{username}'), json=dict(config=config, value=value),
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 = requests.delete(self.url(f'/account/{username}'), headers=self.keychain.headers(),
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 = requests.put(self.url('/file'), params=dict(name=chariot_filepath),
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 = requests.get(self.url('/file'), params=params, allow_redirects=True, headers=self.keychain.headers())
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 = requests.get(self.url('/my/count'), params=params, headers=self.keychain.headers())
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
- requests.delete(self.url('/account/purge'), headers=self.keychain.headers())
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 = requests.put(self.url('/agent'), json=body, headers=self.keychain.headers())
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