praetorian-cli 2.2.1__tar.gz → 2.2.3__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 (107) hide show
  1. {praetorian_cli-2.2.1/praetorian_cli.egg-info → praetorian_cli-2.2.3}/PKG-INFO +4 -1
  2. {praetorian_cli-2.2.1 → praetorian_cli-2.2.3}/praetorian_cli/handlers/add.py +25 -7
  3. praetorian_cli-2.2.3/praetorian_cli/handlers/aegis.py +107 -0
  4. {praetorian_cli-2.2.1 → praetorian_cli-2.2.3}/praetorian_cli/handlers/delete.py +3 -2
  5. {praetorian_cli-2.2.1 → praetorian_cli-2.2.3}/praetorian_cli/handlers/get.py +48 -2
  6. {praetorian_cli-2.2.1 → praetorian_cli-2.2.3}/praetorian_cli/handlers/list.py +41 -9
  7. praetorian_cli-2.2.3/praetorian_cli/handlers/ssh_utils.py +154 -0
  8. {praetorian_cli-2.2.1 → praetorian_cli-2.2.3}/praetorian_cli/handlers/test.py +7 -2
  9. {praetorian_cli-2.2.1 → praetorian_cli-2.2.3}/praetorian_cli/handlers/update.py +3 -3
  10. {praetorian_cli-2.2.1 → praetorian_cli-2.2.3}/praetorian_cli/main.py +1 -0
  11. {praetorian_cli-2.2.1 → praetorian_cli-2.2.3}/praetorian_cli/sdk/chariot.py +71 -12
  12. praetorian_cli-2.2.3/praetorian_cli/sdk/entities/aegis.py +437 -0
  13. {praetorian_cli-2.2.1 → praetorian_cli-2.2.3}/praetorian_cli/sdk/entities/assets.py +30 -12
  14. praetorian_cli-2.2.3/praetorian_cli/sdk/entities/scanners.py +13 -0
  15. praetorian_cli-2.2.3/praetorian_cli/sdk/entities/schema.py +27 -0
  16. praetorian_cli-2.2.3/praetorian_cli/sdk/entities/seeds.py +166 -0
  17. {praetorian_cli-2.2.1 → praetorian_cli-2.2.3}/praetorian_cli/sdk/mcp_server.py +2 -3
  18. praetorian_cli-2.2.3/praetorian_cli/sdk/model/aegis.py +156 -0
  19. {praetorian_cli-2.2.1 → praetorian_cli-2.2.3}/praetorian_cli/sdk/model/query.py +1 -1
  20. {praetorian_cli-2.2.1 → praetorian_cli-2.2.3}/praetorian_cli/sdk/model/utils.py +2 -8
  21. {praetorian_cli-2.2.1 → praetorian_cli-2.2.3}/praetorian_cli/sdk/test/pytest.ini +1 -0
  22. {praetorian_cli-2.2.1 → praetorian_cli-2.2.3}/praetorian_cli/sdk/test/test_asset.py +2 -2
  23. praetorian_cli-2.2.3/praetorian_cli/sdk/test/test_seed.py +40 -0
  24. {praetorian_cli-2.2.1 → praetorian_cli-2.2.3}/praetorian_cli/sdk/test/test_z_cli.py +22 -24
  25. praetorian_cli-2.2.3/praetorian_cli/sdk/test/ui_mocks.py +133 -0
  26. {praetorian_cli-2.2.1 → praetorian_cli-2.2.3}/praetorian_cli/sdk/test/utils.py +16 -4
  27. praetorian_cli-2.2.3/praetorian_cli/ui/__init__.py +3 -0
  28. praetorian_cli-2.2.3/praetorian_cli/ui/aegis/__init__.py +5 -0
  29. praetorian_cli-2.2.3/praetorian_cli/ui/aegis/commands/__init__.py +2 -0
  30. praetorian_cli-2.2.3/praetorian_cli/ui/aegis/commands/help.py +81 -0
  31. praetorian_cli-2.2.3/praetorian_cli/ui/aegis/commands/info.py +136 -0
  32. praetorian_cli-2.2.3/praetorian_cli/ui/aegis/commands/job.py +381 -0
  33. praetorian_cli-2.2.3/praetorian_cli/ui/aegis/commands/list.py +14 -0
  34. praetorian_cli-2.2.3/praetorian_cli/ui/aegis/commands/set.py +32 -0
  35. praetorian_cli-2.2.3/praetorian_cli/ui/aegis/commands/ssh.py +87 -0
  36. praetorian_cli-2.2.3/praetorian_cli/ui/aegis/constants.py +20 -0
  37. praetorian_cli-2.2.3/praetorian_cli/ui/aegis/menu.py +395 -0
  38. praetorian_cli-2.2.3/praetorian_cli/ui/aegis/utils.py +162 -0
  39. {praetorian_cli-2.2.1 → praetorian_cli-2.2.3/praetorian_cli.egg-info}/PKG-INFO +4 -1
  40. {praetorian_cli-2.2.1 → praetorian_cli-2.2.3}/praetorian_cli.egg-info/SOURCES.txt +20 -1
  41. {praetorian_cli-2.2.1 → praetorian_cli-2.2.3}/praetorian_cli.egg-info/requires.txt +3 -0
  42. {praetorian_cli-2.2.1 → praetorian_cli-2.2.3}/setup.cfg +4 -1
  43. praetorian_cli-2.2.1/praetorian_cli/sdk/entities/seeds.py +0 -114
  44. praetorian_cli-2.2.1/praetorian_cli/sdk/test/test_seed.py +0 -41
  45. {praetorian_cli-2.2.1 → praetorian_cli-2.2.3}/LICENSE +0 -0
  46. {praetorian_cli-2.2.1 → praetorian_cli-2.2.3}/MANIFEST.in +0 -0
  47. {praetorian_cli-2.2.1 → praetorian_cli-2.2.3}/README.md +0 -0
  48. {praetorian_cli-2.2.1 → praetorian_cli-2.2.3}/praetorian_cli/__init__.py +0 -0
  49. {praetorian_cli-2.2.1 → praetorian_cli-2.2.3}/praetorian_cli/handlers/__init__.py +0 -0
  50. {praetorian_cli-2.2.1 → praetorian_cli-2.2.3}/praetorian_cli/handlers/agent.py +0 -0
  51. {praetorian_cli-2.2.1 → praetorian_cli-2.2.3}/praetorian_cli/handlers/chariot.py +0 -0
  52. {praetorian_cli-2.2.1 → praetorian_cli-2.2.3}/praetorian_cli/handlers/cli_decorators.py +0 -0
  53. {praetorian_cli-2.2.1 → praetorian_cli-2.2.3}/praetorian_cli/handlers/configure.py +0 -0
  54. {praetorian_cli-2.2.1 → praetorian_cli-2.2.3}/praetorian_cli/handlers/enrich.py +0 -0
  55. {praetorian_cli-2.2.1 → praetorian_cli-2.2.3}/praetorian_cli/handlers/imports.py +0 -0
  56. {praetorian_cli-2.2.1 → praetorian_cli-2.2.3}/praetorian_cli/handlers/link.py +0 -0
  57. {praetorian_cli-2.2.1 → praetorian_cli-2.2.3}/praetorian_cli/handlers/script.py +0 -0
  58. {praetorian_cli-2.2.1 → praetorian_cli-2.2.3}/praetorian_cli/handlers/search.py +0 -0
  59. {praetorian_cli-2.2.1 → praetorian_cli-2.2.3}/praetorian_cli/handlers/unlink.py +0 -0
  60. {praetorian_cli-2.2.1 → praetorian_cli-2.2.3}/praetorian_cli/handlers/utils.py +0 -0
  61. {praetorian_cli-2.2.1 → praetorian_cli-2.2.3}/praetorian_cli/scripts/__init__.py +0 -0
  62. {praetorian_cli-2.2.1 → praetorian_cli-2.2.3}/praetorian_cli/scripts/commands/__init__.py +0 -0
  63. {praetorian_cli-2.2.1 → praetorian_cli-2.2.3}/praetorian_cli/scripts/commands/nmap-example.py +0 -0
  64. {praetorian_cli-2.2.1 → praetorian_cli-2.2.3}/praetorian_cli/scripts/utils.py +0 -0
  65. {praetorian_cli-2.2.1 → praetorian_cli-2.2.3}/praetorian_cli/sdk/__init__.py +0 -0
  66. {praetorian_cli-2.2.1 → praetorian_cli-2.2.3}/praetorian_cli/sdk/entities/__init__.py +0 -0
  67. {praetorian_cli-2.2.1 → praetorian_cli-2.2.3}/praetorian_cli/sdk/entities/accounts.py +0 -0
  68. {praetorian_cli-2.2.1 → praetorian_cli-2.2.3}/praetorian_cli/sdk/entities/agents.py +0 -0
  69. {praetorian_cli-2.2.1 → praetorian_cli-2.2.3}/praetorian_cli/sdk/entities/attributes.py +0 -0
  70. {praetorian_cli-2.2.1 → praetorian_cli-2.2.3}/praetorian_cli/sdk/entities/capabilities.py +0 -0
  71. {praetorian_cli-2.2.1 → praetorian_cli-2.2.3}/praetorian_cli/sdk/entities/configurations.py +0 -0
  72. {praetorian_cli-2.2.1 → praetorian_cli-2.2.3}/praetorian_cli/sdk/entities/credentials.py +0 -0
  73. {praetorian_cli-2.2.1 → praetorian_cli-2.2.3}/praetorian_cli/sdk/entities/definitions.py +0 -0
  74. {praetorian_cli-2.2.1 → praetorian_cli-2.2.3}/praetorian_cli/sdk/entities/files.py +0 -0
  75. {praetorian_cli-2.2.1 → praetorian_cli-2.2.3}/praetorian_cli/sdk/entities/integrations.py +0 -0
  76. {praetorian_cli-2.2.1 → praetorian_cli-2.2.3}/praetorian_cli/sdk/entities/jobs.py +0 -0
  77. {praetorian_cli-2.2.1 → praetorian_cli-2.2.3}/praetorian_cli/sdk/entities/keys.py +0 -0
  78. {praetorian_cli-2.2.1 → praetorian_cli-2.2.3}/praetorian_cli/sdk/entities/preseeds.py +0 -0
  79. {praetorian_cli-2.2.1 → praetorian_cli-2.2.3}/praetorian_cli/sdk/entities/risks.py +0 -0
  80. {praetorian_cli-2.2.1 → praetorian_cli-2.2.3}/praetorian_cli/sdk/entities/search.py +0 -0
  81. {praetorian_cli-2.2.1 → praetorian_cli-2.2.3}/praetorian_cli/sdk/entities/settings.py +0 -0
  82. {praetorian_cli-2.2.1 → praetorian_cli-2.2.3}/praetorian_cli/sdk/entities/statistics.py +0 -0
  83. {praetorian_cli-2.2.1 → praetorian_cli-2.2.3}/praetorian_cli/sdk/entities/webhook.py +0 -0
  84. {praetorian_cli-2.2.1 → praetorian_cli-2.2.3}/praetorian_cli/sdk/keychain.py +0 -0
  85. {praetorian_cli-2.2.1 → praetorian_cli-2.2.3}/praetorian_cli/sdk/model/__init__.py +0 -0
  86. {praetorian_cli-2.2.1 → praetorian_cli-2.2.3}/praetorian_cli/sdk/model/globals.py +0 -0
  87. {praetorian_cli-2.2.1 → praetorian_cli-2.2.3}/praetorian_cli/sdk/test/__init__.py +0 -0
  88. {praetorian_cli-2.2.1 → praetorian_cli-2.2.3}/praetorian_cli/sdk/test/test_account.py +0 -0
  89. {praetorian_cli-2.2.1 → praetorian_cli-2.2.3}/praetorian_cli/sdk/test/test_agent.py +0 -0
  90. {praetorian_cli-2.2.1 → praetorian_cli-2.2.3}/praetorian_cli/sdk/test/test_attribute.py +0 -0
  91. {praetorian_cli-2.2.1 → praetorian_cli-2.2.3}/praetorian_cli/sdk/test/test_capabilities.py +0 -0
  92. {praetorian_cli-2.2.1 → praetorian_cli-2.2.3}/praetorian_cli/sdk/test/test_configuration.py +0 -0
  93. {praetorian_cli-2.2.1 → praetorian_cli-2.2.3}/praetorian_cli/sdk/test/test_definition.py +0 -0
  94. {praetorian_cli-2.2.1 → praetorian_cli-2.2.3}/praetorian_cli/sdk/test/test_extend.py +0 -0
  95. {praetorian_cli-2.2.1 → praetorian_cli-2.2.3}/praetorian_cli/sdk/test/test_file.py +0 -0
  96. {praetorian_cli-2.2.1 → praetorian_cli-2.2.3}/praetorian_cli/sdk/test/test_job.py +0 -0
  97. {praetorian_cli-2.2.1 → praetorian_cli-2.2.3}/praetorian_cli/sdk/test/test_key.py +0 -0
  98. {praetorian_cli-2.2.1 → praetorian_cli-2.2.3}/praetorian_cli/sdk/test/test_mcp.py +0 -0
  99. {praetorian_cli-2.2.1 → praetorian_cli-2.2.3}/praetorian_cli/sdk/test/test_preseed.py +0 -0
  100. {praetorian_cli-2.2.1 → praetorian_cli-2.2.3}/praetorian_cli/sdk/test/test_risk.py +0 -0
  101. {praetorian_cli-2.2.1 → praetorian_cli-2.2.3}/praetorian_cli/sdk/test/test_search.py +0 -0
  102. {praetorian_cli-2.2.1 → praetorian_cli-2.2.3}/praetorian_cli/sdk/test/test_setting.py +0 -0
  103. {praetorian_cli-2.2.1 → praetorian_cli-2.2.3}/praetorian_cli/sdk/test/test_webhook.py +0 -0
  104. {praetorian_cli-2.2.1 → praetorian_cli-2.2.3}/praetorian_cli.egg-info/dependency_links.txt +0 -0
  105. {praetorian_cli-2.2.1 → praetorian_cli-2.2.3}/praetorian_cli.egg-info/entry_points.txt +0 -0
  106. {praetorian_cli-2.2.1 → praetorian_cli-2.2.3}/praetorian_cli.egg-info/top_level.txt +0 -0
  107. {praetorian_cli-2.2.1 → praetorian_cli-2.2.3}/pyproject.toml +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: praetorian-cli
3
- Version: 2.2.1
3
+ Version: 2.2.3
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
@@ -196,21 +196,39 @@ def attribute(sdk, key, name, value):
196
196
 
197
197
  @add.command()
198
198
  @cli_handler
199
- @click.option('-d', '--dns', required=True, help='The DNS of the asset')
199
+ @click.option('-t', '--type', 'seed_type', default='asset', help='Asset type (e.g., asset, addomain)')
200
200
  @click.option('-s', '--status', type=click.Choice([s.value for s in Seed]),
201
201
  default=Seed.PENDING.value, help='The status of the seed', show_default=True)
202
- def seed(sdk, dns, status):
202
+ @click.option('-f', '--field', 'field_list', multiple=True,
203
+ help='Field in format name:value (can be specified multiple times)')
204
+ def seed(sdk, seed_type, status, field_list):
203
205
  """ Add a seed
204
206
 
205
- Add a seed to the Chariot database. This command requires DNS of the seed to be
206
- specified. When status is not specified, the seed is added as PENDING.
207
+ Add a seed to the Chariot database. Seeds are now assets with special labeling.
208
+ You can specify the asset type and provide dynamic fields using --fields.
207
209
 
208
210
  \b
209
211
  Example usages:
210
- - praetorian chariot add seed --dns example.com
211
- - praetorian chariot add seed --dns example.com --status A
212
+ - praetorian chariot add seed --type asset --field dns:example.com
213
+ - praetorian chariot add seed --type asset --field dns:example.com --status A
214
+ - praetorian chariot add seed --type asset --field dns:example.com --field name:1.2.3.4
215
+ - praetorian chariot add seed --type addomain --field domain:corp.local --field objectid:S-1-5-21-2701466056-1043032755-2418290285
212
216
  """
213
- sdk.seeds.add(dns, status)
217
+ # Collect dynamic fields from the --fields option
218
+ dynamic_fields = {}
219
+
220
+ # Parse field_list entries (name:value format)
221
+ for field in field_list:
222
+ if ':' in field:
223
+ # Split only once to allow colons in the value
224
+ name, value = field.split(':', 1)
225
+ dynamic_fields[name] = value
226
+ else:
227
+ error(f"Field '{field}' is not in the format name:value")
228
+ return
229
+
230
+ # Call the updated add method with type and dynamic fields
231
+ sdk.seeds.add(status=status, seed_type=seed_type, **dynamic_fields)
214
232
 
215
233
 
216
234
  @add.command()
@@ -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())
@@ -88,11 +88,12 @@ def seed(chariot, key):
88
88
 
89
89
  \b
90
90
  Arguments:
91
- - KEY: the key of an existing seed
91
+ - KEY: the key of an existing seed (now uses asset key format)
92
92
 
93
93
  \b
94
94
  Example usage:
95
- - praetorian chariot delete seed "#seed#domain#example.com"
95
+ - praetorian chariot delete seed "#asset#example.com#example.com"
96
+ - praetorian chariot delete seed "#addomain#corp.local#corp.local"
96
97
  """
97
98
  chariot.seeds.delete(key)
98
99
 
@@ -186,11 +186,12 @@ def seed(chariot, key):
186
186
 
187
187
  \b
188
188
  Argument:
189
- - KEY: the key of an existing pre-seed
189
+ - KEY: the key of an existing seed (now uses asset key format)
190
190
 
191
191
  \b
192
192
  Example usages:
193
- - praetorian chariot get preseed "#preseed#domain#example.com"
193
+ - praetorian chariot get seed "#asset#example.com#example.com"
194
+ - praetorian chariot get seed "#addomain#corp.local#corp.local"
194
195
  """
195
196
  print_json(chariot.seeds.get(key))
196
197
 
@@ -278,3 +279,48 @@ def credential(chariot, credential_id, category, type, format, parameters):
278
279
  result = chariot.credentials.get(credential_id, category, type, [format], **params)
279
280
  output = chariot.credentials.format_output(result)
280
281
  click.echo(output)
282
+
283
+
284
+ @get.command()
285
+ @cli_handler
286
+ @click.argument('key', required=True)
287
+ def scanner(chariot, key):
288
+ """ Get scanner details
289
+
290
+ \b
291
+ Argument:
292
+ - KEY: the key of an existing scanner record
293
+
294
+ \b
295
+ Example usage:
296
+ - praetorian chariot get scanner "#scanner#127.0.0.1"
297
+ """
298
+ print_json(chariot.scanners.get(key))
299
+
300
+
301
+ @get.command()
302
+ @cli_handler
303
+ @click.option('-t', '--type', help='Optional specific entity type (e.g., asset, risk, attribute)')
304
+ @click.option('-d', '--details', is_flag=True, help='Further retrieve the details of the schema')
305
+ def schema(chariot, type, details):
306
+ """ Get Chariot entity schema
307
+
308
+ \b
309
+ Returns the JSON schema for Chariot entities. Optionally filter for a
310
+ specific entity type.
311
+
312
+ \b
313
+ Example usages:
314
+ - praetorian chariot get schema
315
+ - praetorian chariot get schema --type asset
316
+ - praetorian chariot get schema --type asset --details
317
+ """
318
+ data = chariot.schema.get(type)
319
+ if type:
320
+ data = {type: data[type]}
321
+
322
+ if details:
323
+ print_json(data)
324
+ else:
325
+ for hit in data:
326
+ click.echo(hit)
@@ -26,7 +26,7 @@ def assets(chariot, filter, model_type, details, offset, page):
26
26
  - praetorian chariot list assets --page all
27
27
  - praetorian chariot list assets --type repository
28
28
  """
29
- render_list_results(chariot.assets.list(filter, model_type, offset, pagination_size(page)), details)
29
+ render_list_results(chariot.assets.list(filter, model_type, pagination_size(page)), details)
30
30
 
31
31
 
32
32
  @list.command()
@@ -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):
@@ -162,24 +178,23 @@ def attributes(chariot, filter, key, details, offset, page):
162
178
 
163
179
  @list.command()
164
180
  @list_params('DNS')
165
- @click.option('-t', '--type', type=click.Choice(['ip', 'domain']), help=f'Filter by type of the seeds')
181
+ @click.option('-t', '--type', help='Filter by seed type (e.g., asset, addomain)')
166
182
  def seeds(chariot, type, filter, details, offset, page):
167
183
  """ List seeds
168
184
 
169
- Retrieve and display a list of seeds.
185
+ Retrieve and display a list of seeds. Seeds are now assets with the 'Seed' label.
170
186
 
171
187
  \b
172
188
  Example usages:
173
189
  - praetorian chariot list seeds
174
- - praetorian chariot list seeds --type ip
175
- - praetorian chariot list seeds --type domain --filter example.com
190
+ - praetorian chariot list seeds --type asset
191
+ - praetorian chariot list seeds --type addomain
192
+ - praetorian chariot list seeds --type asset --filter example.com
176
193
  - praetorian chariot list seeds --details
177
194
  - praetorian chariot list seeds --page all
178
195
  """
179
- if filter and not type:
180
- error('When the DNS filter is specified, you also need to specify the type of the filter: ip or domain.')
181
-
182
- render_list_results(chariot.seeds.list(type, filter, offset, pagination_size(page)), details)
196
+ # Note: filter restriction removed since we're using different key format now
197
+ render_list_results(chariot.seeds.list(type, filter, pagination_size(page)), details)
183
198
 
184
199
 
185
200
  @list.command()
@@ -336,3 +351,20 @@ def capabilities(chariot, name, target, executor):
336
351
  - praetorian chariot list capabilities --name nuclei --target attribute --executor chariot
337
352
  """
338
353
  print_json(chariot.capabilities.list(name, target, executor))
354
+
355
+
356
+ @list.command()
357
+ @list_params('IP address')
358
+ def scanners(chariot, filter, details, offset, page):
359
+ """ List scanners
360
+
361
+ Retrieve and display a list of scanner records that track IP addresses used by chariot.
362
+
363
+ \b
364
+ Example usages:
365
+ - praetorian chariot list scanners
366
+ - praetorian chariot list scanners --filter 127.0.0.1
367
+ - praetorian chariot list scanners --details
368
+ - praetorian chariot list scanners --page all
369
+ """
370
+ 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,7 +12,7 @@ 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 """
@@ -21,4 +23,7 @@ def test(chariot, key, suite):
21
23
  command.extend(['-k', key])
22
24
  if suite:
23
25
  command.extend(['-m', suite])
24
- pytest.main(command)
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)
@@ -65,12 +65,12 @@ def seed(chariot, key, status):
65
65
 
66
66
  \b
67
67
  Example usages:
68
- - praetorian chariot update seed "#seed#domain#example.com" -s A
69
- - praetorian chariot update seed "#seed#ip#1.1.1.0/24" -s F
68
+ - praetorian chariot update seed "#asset#example.com#example.com" -s A
69
+ - praetorian chariot update seed "#asset#1.1.1.0/24#1.1.1.0/24" -s F
70
70
  """
71
+
71
72
  chariot.seeds.update(key, status)
72
73
 
73
-
74
74
  @update.command()
75
75
  @cli_handler
76
76
  @click.argument('key', required=True)
@@ -1,6 +1,7 @@
1
1
  import click
2
2
 
3
3
  import praetorian_cli.handlers.add
4
+ import praetorian_cli.handlers.aegis
4
5
  import praetorian_cli.handlers.agent
5
6
  import praetorian_cli.handlers.delete
6
7
  import praetorian_cli.handlers.enrich
@@ -1,6 +1,7 @@
1
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,8 @@ 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
19
+ from praetorian_cli.sdk.entities.schema import Schema
17
20
  from praetorian_cli.sdk.entities.search import Search
18
21
  from praetorian_cli.sdk.entities.seeds import Seeds
19
22
  from praetorian_cli.sdk.entities.settings import Settings
@@ -39,14 +42,17 @@ class Chariot:
39
42
  self.definitions = Definitions(self)
40
43
  self.attributes = Attributes(self)
41
44
  self.search = Search(self)
45
+ self.scanners = Scanners(self)
42
46
  self.webhook = Webhook(self)
43
47
  self.statistics = Statistics(self)
48
+ self.aegis = Aegis(self)
44
49
  self.agents = Agents(self)
45
50
  self.settings = Settings(self)
46
51
  self.configurations = Configurations(self)
47
52
  self.keys = Keys(self)
48
53
  self.capabilities = Capabilities(self)
49
54
  self.credentials = Credentials(self)
55
+ self.schema = Schema(self)
50
56
  self.proxy = proxy
51
57
 
52
58
  if self.proxy == '' and os.environ.get('CHARIOT_PROXY'):
@@ -66,26 +72,16 @@ class Chariot:
66
72
  kwargs['proxies'] = {'http': self.proxy, 'https': self.proxy}
67
73
  kwargs['verify'] = False
68
74
 
69
- self._add_beta_filter(method, kwargs)
75
+ self.add_beta_url_param(kwargs)
70
76
 
71
77
  return requests.request(method, url, headers=self.keychain.headers(), **kwargs)
72
78
 
73
- def _add_beta_filter(self, method: str, kwargs: dict):
74
- if method == 'GET' or method == 'DELETE':
75
- self._add_beta_url_param(kwargs)
76
- else:
77
- self._add_beta_json_param(kwargs)
78
-
79
- def _add_beta_url_param(self, kwargs: dict):
79
+ def add_beta_url_param(self, kwargs: dict):
80
80
  if 'params' in kwargs:
81
81
  kwargs['params']['beta'] = 'true'
82
82
  else:
83
83
  kwargs['params'] = {'beta': 'true'}
84
84
 
85
- def _add_beta_json_param(self, kwargs: dict):
86
- if 'json' in kwargs:
87
- kwargs['json']['beta'] = True
88
-
89
85
  def my(self, params: dict, pages=1) -> dict:
90
86
  final_resp = dict()
91
87
 
@@ -256,6 +252,69 @@ class Chariot:
256
252
 
257
253
  server = MCPServer(self, allowable_tools)
258
254
  return anyio.run(server.start)
255
+
256
+ def get_current_user(self) -> tuple:
257
+ """
258
+ Get current user information for Aegis functionality.
259
+
260
+ Returns:
261
+ tuple: (user_email, username) where user_email is the login email
262
+ and username is the SSH username derived from the email
263
+ """
264
+ # Try to get username from keychain first (for username/password auth)
265
+ user_email = self.keychain.username()
266
+
267
+ # If no username in keychain (API key auth), try to get it from JWT token
268
+ if not user_email and self.keychain.has_api_key():
269
+ token = self.keychain.token()
270
+ payload = decode_jwt_payload(token)
271
+ if payload:
272
+ # Extract email from the 'email' field in the JWT payload
273
+ user_email = payload.get('email')
274
+ else:
275
+ # If JWT decoding fails, fall back to the account parameter
276
+ raise Exception("Failed to decode JWT token")
277
+
278
+ # Extract username from email (part before @) for SSH access
279
+ username = user_email.split('@')[0] if user_email and '@' in user_email else user_email
280
+ return user_email, username
281
+
282
+
283
+ def decode_jwt_payload(token: str) -> dict | None:
284
+ """
285
+ Decode the payload from a JWT token.
286
+
287
+ Args:
288
+ token: JWT token string in format header.payload.signature
289
+
290
+ Returns:
291
+ dict: Decoded payload contents, or None if decoding fails
292
+
293
+ Example:
294
+ >>> token = "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJlbWFpbCI6InVzZXJAZXhhbXBsZS5jb20ifQ.signature"
295
+ >>> payload = decode_jwt_payload(token)
296
+ >>> print(payload.get('email'))
297
+ user@example.com
298
+ """
299
+ try:
300
+ import json
301
+ import base64
302
+
303
+ # JWT tokens have 3 parts: header.payload.signature
304
+ parts = token.split('.')
305
+ if len(parts) != 3:
306
+ return None
307
+
308
+ payload_part = parts[1]
309
+ # Add padding if needed for base64 decoding
310
+ payload_part += '=' * (4 - len(payload_part) % 4)
311
+ payload = json.loads(base64.b64decode(payload_part))
312
+
313
+ return payload
314
+ except Exception:
315
+ return None
316
+
317
+
259
318
  def is_query_limit_failure(response: requests.Response) -> bool:
260
319
  return response.status_code == 413 and 'reduce page size' in response.text
261
320