praetorian-cli 2.4.0__tar.gz → 2.4.2__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (147) hide show
  1. {praetorian_cli-2.4.0/praetorian_cli.egg-info → praetorian_cli-2.4.2}/PKG-INFO +1 -1
  2. praetorian_cli-2.4.2/praetorian_cli/handlers/access.py +209 -0
  3. {praetorian_cli-2.4.0 → praetorian_cli-2.4.2}/praetorian_cli/handlers/configure.py +1 -1
  4. {praetorian_cli-2.4.0 → praetorian_cli-2.4.2}/praetorian_cli/handlers/engagement.py +7 -4
  5. {praetorian_cli-2.4.0 → praetorian_cli-2.4.2}/praetorian_cli/handlers/export.py +4 -1
  6. {praetorian_cli-2.4.0 → praetorian_cli-2.4.2}/praetorian_cli/handlers/list.py +21 -2
  7. {praetorian_cli-2.4.0 → praetorian_cli-2.4.2}/praetorian_cli/handlers/run.py +96 -12
  8. {praetorian_cli-2.4.0 → praetorian_cli-2.4.2}/praetorian_cli/main.py +1 -0
  9. {praetorian_cli-2.4.0 → praetorian_cli-2.4.2}/praetorian_cli/runners/local.py +4 -1
  10. praetorian_cli-2.4.2/praetorian_cli/sdk/entities/account_discovery.py +443 -0
  11. {praetorian_cli-2.4.0 → praetorian_cli-2.4.2}/praetorian_cli/sdk/entities/assets.py +25 -0
  12. {praetorian_cli-2.4.0 → praetorian_cli-2.4.2}/praetorian_cli/sdk/entities/attributes.py +15 -0
  13. {praetorian_cli-2.4.0 → praetorian_cli-2.4.2}/praetorian_cli/sdk/entities/credentials.py +20 -3
  14. {praetorian_cli-2.4.0 → praetorian_cli-2.4.2}/praetorian_cli/sdk/entities/jobs.py +171 -0
  15. {praetorian_cli-2.4.0 → praetorian_cli-2.4.2}/praetorian_cli/sdk/entities/reports.py +5 -1
  16. {praetorian_cli-2.4.0 → praetorian_cli-2.4.2}/praetorian_cli/sdk/entities/risks.py +24 -0
  17. praetorian_cli-2.4.2/praetorian_cli/sdk/test/test_access_aws.py +198 -0
  18. praetorian_cli-2.4.2/praetorian_cli/sdk/test/test_bulk.py +315 -0
  19. praetorian_cli-2.4.2/praetorian_cli/sdk/test/test_credential_process.py +63 -0
  20. {praetorian_cli-2.4.0 → praetorian_cli-2.4.2}/praetorian_cli/sdk/test/test_job.py +44 -0
  21. {praetorian_cli-2.4.0 → praetorian_cli-2.4.2}/praetorian_cli/sdk/test/test_report.py +3 -0
  22. {praetorian_cli-2.4.0 → praetorian_cli-2.4.2}/praetorian_cli/sdk/test/test_z_cli.py +2 -1
  23. {praetorian_cli-2.4.0 → praetorian_cli-2.4.2}/praetorian_cli/sdk/test/ui_mocks.py +5 -0
  24. praetorian_cli-2.4.2/praetorian_cli/ui/aegis/account_selector.py +250 -0
  25. {praetorian_cli-2.4.0 → praetorian_cli-2.4.2}/praetorian_cli/ui/aegis/commands/job.py +2 -1
  26. {praetorian_cli-2.4.0 → praetorian_cli-2.4.2}/praetorian_cli/ui/aegis/commands/job_helpers.py +10 -0
  27. {praetorian_cli-2.4.0 → praetorian_cli-2.4.2}/praetorian_cli/ui/aegis/commands/list.py +1 -3
  28. {praetorian_cli-2.4.0 → praetorian_cli-2.4.2}/praetorian_cli/ui/aegis/commands/schedule.py +262 -166
  29. {praetorian_cli-2.4.0 → praetorian_cli-2.4.2}/praetorian_cli/ui/aegis/commands/set.py +27 -1
  30. {praetorian_cli-2.4.0 → praetorian_cli-2.4.2}/praetorian_cli/ui/aegis/menu.py +120 -19
  31. {praetorian_cli-2.4.0 → praetorian_cli-2.4.2}/praetorian_cli/ui/console/commands/__init__.py +1 -0
  32. {praetorian_cli-2.4.0 → praetorian_cli-2.4.2}/praetorian_cli/ui/console/commands/accounts.py +6 -2
  33. {praetorian_cli-2.4.0 → praetorian_cli-2.4.2}/praetorian_cli/ui/console/commands/context.py +70 -2
  34. praetorian_cli-2.4.2/praetorian_cli/ui/console/commands/data.py +162 -0
  35. {praetorian_cli-2.4.0 → praetorian_cli-2.4.2}/praetorian_cli/ui/console/commands/marcus.py +109 -14
  36. {praetorian_cli-2.4.0 → praetorian_cli-2.4.2}/praetorian_cli/ui/console/commands/search.py +36 -15
  37. {praetorian_cli-2.4.0 → praetorian_cli-2.4.2}/praetorian_cli/ui/console/console.py +41 -4
  38. {praetorian_cli-2.4.0 → praetorian_cli-2.4.2}/praetorian_cli/ui/console/context.py +1 -0
  39. {praetorian_cli-2.4.0 → praetorian_cli-2.4.2}/praetorian_cli/ui/console/renderer.py +151 -6
  40. {praetorian_cli-2.4.0 → praetorian_cli-2.4.2/praetorian_cli.egg-info}/PKG-INFO +1 -1
  41. {praetorian_cli-2.4.0 → praetorian_cli-2.4.2}/praetorian_cli.egg-info/SOURCES.txt +7 -0
  42. {praetorian_cli-2.4.0 → praetorian_cli-2.4.2}/setup.cfg +1 -1
  43. {praetorian_cli-2.4.0 → praetorian_cli-2.4.2}/LICENSE +0 -0
  44. {praetorian_cli-2.4.0 → praetorian_cli-2.4.2}/MANIFEST.in +0 -0
  45. {praetorian_cli-2.4.0 → praetorian_cli-2.4.2}/README.md +0 -0
  46. {praetorian_cli-2.4.0 → praetorian_cli-2.4.2}/praetorian_cli/__init__.py +0 -0
  47. {praetorian_cli-2.4.0 → praetorian_cli-2.4.2}/praetorian_cli/handlers/__init__.py +0 -0
  48. {praetorian_cli-2.4.0 → praetorian_cli-2.4.2}/praetorian_cli/handlers/add.py +0 -0
  49. {praetorian_cli-2.4.0 → praetorian_cli-2.4.2}/praetorian_cli/handlers/aegis.py +0 -0
  50. {praetorian_cli-2.4.0 → praetorian_cli-2.4.2}/praetorian_cli/handlers/agent.py +0 -0
  51. {praetorian_cli-2.4.0 → praetorian_cli-2.4.2}/praetorian_cli/handlers/chariot.py +0 -0
  52. {praetorian_cli-2.4.0 → praetorian_cli-2.4.2}/praetorian_cli/handlers/cli_decorators.py +0 -0
  53. {praetorian_cli-2.4.0 → praetorian_cli-2.4.2}/praetorian_cli/handlers/critfinder.py +0 -0
  54. {praetorian_cli-2.4.0 → praetorian_cli-2.4.2}/praetorian_cli/handlers/delete.py +0 -0
  55. {praetorian_cli-2.4.0 → praetorian_cli-2.4.2}/praetorian_cli/handlers/enrich.py +0 -0
  56. {praetorian_cli-2.4.0 → praetorian_cli-2.4.2}/praetorian_cli/handlers/find.py +0 -0
  57. {praetorian_cli-2.4.0 → praetorian_cli-2.4.2}/praetorian_cli/handlers/get.py +0 -0
  58. {praetorian_cli-2.4.0 → praetorian_cli-2.4.2}/praetorian_cli/handlers/imports.py +0 -0
  59. {praetorian_cli-2.4.0 → praetorian_cli-2.4.2}/praetorian_cli/handlers/link.py +0 -0
  60. {praetorian_cli-2.4.0 → praetorian_cli-2.4.2}/praetorian_cli/handlers/report.py +0 -0
  61. {praetorian_cli-2.4.0 → praetorian_cli-2.4.2}/praetorian_cli/handlers/script.py +0 -0
  62. {praetorian_cli-2.4.0 → praetorian_cli-2.4.2}/praetorian_cli/handlers/search.py +0 -0
  63. {praetorian_cli-2.4.0 → praetorian_cli-2.4.2}/praetorian_cli/handlers/ssh_utils.py +0 -0
  64. {praetorian_cli-2.4.0 → praetorian_cli-2.4.2}/praetorian_cli/handlers/test.py +0 -0
  65. {praetorian_cli-2.4.0 → praetorian_cli-2.4.2}/praetorian_cli/handlers/unlink.py +0 -0
  66. {praetorian_cli-2.4.0 → praetorian_cli-2.4.2}/praetorian_cli/handlers/update.py +0 -0
  67. {praetorian_cli-2.4.0 → praetorian_cli-2.4.2}/praetorian_cli/handlers/utils.py +0 -0
  68. {praetorian_cli-2.4.0 → praetorian_cli-2.4.2}/praetorian_cli/runners/__init__.py +0 -0
  69. {praetorian_cli-2.4.0 → praetorian_cli-2.4.2}/praetorian_cli/scripts/__init__.py +0 -0
  70. {praetorian_cli-2.4.0 → praetorian_cli-2.4.2}/praetorian_cli/scripts/commands/__init__.py +0 -0
  71. {praetorian_cli-2.4.0 → praetorian_cli-2.4.2}/praetorian_cli/scripts/commands/nmap-example.py +0 -0
  72. {praetorian_cli-2.4.0 → praetorian_cli-2.4.2}/praetorian_cli/scripts/utils.py +0 -0
  73. {praetorian_cli-2.4.0 → praetorian_cli-2.4.2}/praetorian_cli/sdk/__init__.py +0 -0
  74. {praetorian_cli-2.4.0 → praetorian_cli-2.4.2}/praetorian_cli/sdk/chariot.py +0 -0
  75. {praetorian_cli-2.4.0 → praetorian_cli-2.4.2}/praetorian_cli/sdk/entities/__init__.py +0 -0
  76. {praetorian_cli-2.4.0 → praetorian_cli-2.4.2}/praetorian_cli/sdk/entities/accounts.py +0 -0
  77. {praetorian_cli-2.4.0 → praetorian_cli-2.4.2}/praetorian_cli/sdk/entities/ad.py +0 -0
  78. {praetorian_cli-2.4.0 → praetorian_cli-2.4.2}/praetorian_cli/sdk/entities/aegis.py +0 -0
  79. {praetorian_cli-2.4.0 → praetorian_cli-2.4.2}/praetorian_cli/sdk/entities/agents.py +0 -0
  80. {praetorian_cli-2.4.0 → praetorian_cli-2.4.2}/praetorian_cli/sdk/entities/capabilities.py +0 -0
  81. {praetorian_cli-2.4.0 → praetorian_cli-2.4.2}/praetorian_cli/sdk/entities/configurations.py +0 -0
  82. {praetorian_cli-2.4.0 → praetorian_cli-2.4.2}/praetorian_cli/sdk/entities/definitions.py +0 -0
  83. {praetorian_cli-2.4.0 → praetorian_cli-2.4.2}/praetorian_cli/sdk/entities/files.py +0 -0
  84. {praetorian_cli-2.4.0 → praetorian_cli-2.4.2}/praetorian_cli/sdk/entities/integrations.py +0 -0
  85. {praetorian_cli-2.4.0 → praetorian_cli-2.4.2}/praetorian_cli/sdk/entities/keys.py +0 -0
  86. {praetorian_cli-2.4.0 → praetorian_cli-2.4.2}/praetorian_cli/sdk/entities/preseeds.py +0 -0
  87. {praetorian_cli-2.4.0 → praetorian_cli-2.4.2}/praetorian_cli/sdk/entities/scanners.py +0 -0
  88. {praetorian_cli-2.4.0 → praetorian_cli-2.4.2}/praetorian_cli/sdk/entities/schedules.py +0 -0
  89. {praetorian_cli-2.4.0 → praetorian_cli-2.4.2}/praetorian_cli/sdk/entities/schema.py +0 -0
  90. {praetorian_cli-2.4.0 → praetorian_cli-2.4.2}/praetorian_cli/sdk/entities/search.py +0 -0
  91. {praetorian_cli-2.4.0 → praetorian_cli-2.4.2}/praetorian_cli/sdk/entities/seeds.py +0 -0
  92. {praetorian_cli-2.4.0 → praetorian_cli-2.4.2}/praetorian_cli/sdk/entities/settings.py +0 -0
  93. {praetorian_cli-2.4.0 → praetorian_cli-2.4.2}/praetorian_cli/sdk/entities/statistics.py +0 -0
  94. {praetorian_cli-2.4.0 → praetorian_cli-2.4.2}/praetorian_cli/sdk/entities/webhook.py +0 -0
  95. {praetorian_cli-2.4.0 → praetorian_cli-2.4.2}/praetorian_cli/sdk/entities/webpage.py +0 -0
  96. {praetorian_cli-2.4.0 → praetorian_cli-2.4.2}/praetorian_cli/sdk/guard.py +0 -0
  97. {praetorian_cli-2.4.0 → praetorian_cli-2.4.2}/praetorian_cli/sdk/keychain.py +0 -0
  98. {praetorian_cli-2.4.0 → praetorian_cli-2.4.2}/praetorian_cli/sdk/mcp_server.py +0 -0
  99. {praetorian_cli-2.4.0 → praetorian_cli-2.4.2}/praetorian_cli/sdk/model/__init__.py +0 -0
  100. {praetorian_cli-2.4.0 → praetorian_cli-2.4.2}/praetorian_cli/sdk/model/aegis.py +0 -0
  101. {praetorian_cli-2.4.0 → praetorian_cli-2.4.2}/praetorian_cli/sdk/model/globals.py +0 -0
  102. {praetorian_cli-2.4.0 → praetorian_cli-2.4.2}/praetorian_cli/sdk/model/query.py +0 -0
  103. {praetorian_cli-2.4.0 → praetorian_cli-2.4.2}/praetorian_cli/sdk/model/utils.py +0 -0
  104. {praetorian_cli-2.4.0 → praetorian_cli-2.4.2}/praetorian_cli/sdk/test/__init__.py +0 -0
  105. {praetorian_cli-2.4.0 → praetorian_cli-2.4.2}/praetorian_cli/sdk/test/pytest.ini +0 -0
  106. {praetorian_cli-2.4.0 → praetorian_cli-2.4.2}/praetorian_cli/sdk/test/test_account.py +0 -0
  107. {praetorian_cli-2.4.0 → praetorian_cli-2.4.2}/praetorian_cli/sdk/test/test_agent.py +0 -0
  108. {praetorian_cli-2.4.0 → praetorian_cli-2.4.2}/praetorian_cli/sdk/test/test_asset.py +0 -0
  109. {praetorian_cli-2.4.0 → praetorian_cli-2.4.2}/praetorian_cli/sdk/test/test_attribute.py +0 -0
  110. {praetorian_cli-2.4.0 → praetorian_cli-2.4.2}/praetorian_cli/sdk/test/test_capabilities.py +0 -0
  111. {praetorian_cli-2.4.0 → praetorian_cli-2.4.2}/praetorian_cli/sdk/test/test_configuration.py +0 -0
  112. {praetorian_cli-2.4.0 → praetorian_cli-2.4.2}/praetorian_cli/sdk/test/test_conversation.py +0 -0
  113. {praetorian_cli-2.4.0 → praetorian_cli-2.4.2}/praetorian_cli/sdk/test/test_definition.py +0 -0
  114. {praetorian_cli-2.4.0 → praetorian_cli-2.4.2}/praetorian_cli/sdk/test/test_extend.py +0 -0
  115. {praetorian_cli-2.4.0 → praetorian_cli-2.4.2}/praetorian_cli/sdk/test/test_file.py +0 -0
  116. {praetorian_cli-2.4.0 → praetorian_cli-2.4.2}/praetorian_cli/sdk/test/test_key.py +0 -0
  117. {praetorian_cli-2.4.0 → praetorian_cli-2.4.2}/praetorian_cli/sdk/test/test_mcp.py +0 -0
  118. {praetorian_cli-2.4.0 → praetorian_cli-2.4.2}/praetorian_cli/sdk/test/test_preseed.py +0 -0
  119. {praetorian_cli-2.4.0 → praetorian_cli-2.4.2}/praetorian_cli/sdk/test/test_risk.py +0 -0
  120. {praetorian_cli-2.4.0 → praetorian_cli-2.4.2}/praetorian_cli/sdk/test/test_search.py +0 -0
  121. {praetorian_cli-2.4.0 → praetorian_cli-2.4.2}/praetorian_cli/sdk/test/test_seed.py +0 -0
  122. {praetorian_cli-2.4.0 → praetorian_cli-2.4.2}/praetorian_cli/sdk/test/test_setting.py +0 -0
  123. {praetorian_cli-2.4.0 → praetorian_cli-2.4.2}/praetorian_cli/sdk/test/test_webhook.py +0 -0
  124. {praetorian_cli-2.4.0 → praetorian_cli-2.4.2}/praetorian_cli/sdk/test/test_webpage.py +0 -0
  125. {praetorian_cli-2.4.0 → praetorian_cli-2.4.2}/praetorian_cli/sdk/test/utils.py +0 -0
  126. {praetorian_cli-2.4.0 → praetorian_cli-2.4.2}/praetorian_cli/ui/__init__.py +0 -0
  127. {praetorian_cli-2.4.0 → praetorian_cli-2.4.2}/praetorian_cli/ui/aegis/__init__.py +0 -0
  128. {praetorian_cli-2.4.0 → praetorian_cli-2.4.2}/praetorian_cli/ui/aegis/commands/__init__.py +0 -0
  129. {praetorian_cli-2.4.0 → praetorian_cli-2.4.2}/praetorian_cli/ui/aegis/commands/cp.py +0 -0
  130. {praetorian_cli-2.4.0 → praetorian_cli-2.4.2}/praetorian_cli/ui/aegis/commands/help.py +0 -0
  131. {praetorian_cli-2.4.0 → praetorian_cli-2.4.2}/praetorian_cli/ui/aegis/commands/info.py +0 -0
  132. {praetorian_cli-2.4.0 → praetorian_cli-2.4.2}/praetorian_cli/ui/aegis/commands/proxy.py +0 -0
  133. {praetorian_cli-2.4.0 → praetorian_cli-2.4.2}/praetorian_cli/ui/aegis/commands/schedule_helpers.py +0 -0
  134. {praetorian_cli-2.4.0 → praetorian_cli-2.4.2}/praetorian_cli/ui/aegis/commands/ssh.py +0 -0
  135. {praetorian_cli-2.4.0 → praetorian_cli-2.4.2}/praetorian_cli/ui/aegis/constants.py +0 -0
  136. {praetorian_cli-2.4.0 → praetorian_cli-2.4.2}/praetorian_cli/ui/aegis/theme.py +0 -0
  137. {praetorian_cli-2.4.0 → praetorian_cli-2.4.2}/praetorian_cli/ui/aegis/utils.py +0 -0
  138. {praetorian_cli-2.4.0 → praetorian_cli-2.4.2}/praetorian_cli/ui/console/__init__.py +0 -0
  139. {praetorian_cli-2.4.0 → praetorian_cli-2.4.2}/praetorian_cli/ui/console/commands/reporting.py +0 -0
  140. {praetorian_cli-2.4.0 → praetorian_cli-2.4.2}/praetorian_cli/ui/console/commands/tools.py +0 -0
  141. {praetorian_cli-2.4.0 → praetorian_cli-2.4.2}/praetorian_cli/ui/conversation/__init__.py +0 -0
  142. {praetorian_cli-2.4.0 → praetorian_cli-2.4.2}/praetorian_cli/ui/conversation/textual_chat.py +0 -0
  143. {praetorian_cli-2.4.0 → praetorian_cli-2.4.2}/praetorian_cli.egg-info/dependency_links.txt +0 -0
  144. {praetorian_cli-2.4.0 → praetorian_cli-2.4.2}/praetorian_cli.egg-info/entry_points.txt +0 -0
  145. {praetorian_cli-2.4.0 → praetorian_cli-2.4.2}/praetorian_cli.egg-info/requires.txt +0 -0
  146. {praetorian_cli-2.4.0 → praetorian_cli-2.4.2}/praetorian_cli.egg-info/top_level.txt +0 -0
  147. {praetorian_cli-2.4.0 → praetorian_cli-2.4.2}/pyproject.toml +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: praetorian-cli
3
- Version: 2.4.0
3
+ Version: 2.4.2
4
4
  Summary: For interacting with the Guard API
5
5
  Home-page: https://github.com/praetorian-inc/praetorian-cli
6
6
  Author: Praetorian
@@ -0,0 +1,209 @@
1
+ import os
2
+
3
+ import click
4
+
5
+ from praetorian_cli.handlers.chariot import chariot
6
+ from praetorian_cli.handlers.cli_decorators import cli_handler
7
+
8
+
9
+ def extract_prefix(email):
10
+ """Extract the profile prefix from a Guard account email.
11
+
12
+ Takes the portion after '+' and before '@'.
13
+ If no '+' exists, uses the full local part (before '@').
14
+
15
+ Examples:
16
+ chariot+proceptbiorobotics@praetorian.com -> proceptbiorobotics
17
+ chariot+grant_street_group-ztw@praetorian.com -> grant_street_group-ztw
18
+ user@example.com -> user
19
+ """
20
+ local_part = email.split('@')[0]
21
+ if '+' in local_part:
22
+ return local_part.split('+', 1)[1]
23
+ return local_part
24
+
25
+
26
+ def extract_account_id(credential_key):
27
+ """Extract the AWS account ID from a credential key.
28
+
29
+ Keys look like: #account#chariot+client@praetorian.com#amazon#325281727610
30
+ The account ID is the last segment.
31
+ """
32
+ return credential_key.rstrip('#').split('#')[-1]
33
+
34
+
35
+ def build_aws_config_profiles(account_email, prefix, credential_id, root_account_id, sub_accounts, category='env-integration'):
36
+ """Build a list of (profile_name, profile_dict) tuples for AWS config.
37
+
38
+ Args:
39
+ account_email: The Guard account email (used in credential_process command)
40
+ prefix: The profile name prefix
41
+ credential_id: The credential UUID
42
+ root_account_id: The root AWS account ID
43
+ sub_accounts: List of dicts with 'Id' key from Organizations list_accounts,
44
+ or empty list if not an org.
45
+ category: The credential category (e.g., 'env-integration', 'cloud')
46
+ Returns:
47
+ List of (profile_name, dict) where dict has credential_process, region, output keys.
48
+ """
49
+ profiles = []
50
+
51
+ # Collect all account IDs (root + sub-accounts), deduplicated
52
+ all_account_ids = [root_account_id]
53
+ for acct in sub_accounts:
54
+ if acct['Id'] != root_account_id:
55
+ all_account_ids.append(acct['Id'])
56
+
57
+ for account_id in all_account_ids:
58
+ profile_name = f'{prefix}-{account_id}'
59
+ credential_process = (
60
+ f'guard --account {account_email} get credential {credential_id} '
61
+ f'--category {category} --type aws --format credential-process '
62
+ f'--parameters accountId {account_id}'
63
+ )
64
+ profiles.append((profile_name, {
65
+ 'credential_process': credential_process,
66
+ 'region': 'us-east-1',
67
+ 'output': 'json',
68
+ }))
69
+
70
+ return profiles
71
+
72
+
73
+ def write_aws_config(profiles, config_path=None):
74
+ """Write AWS config profiles, preserving existing unrelated profiles.
75
+
76
+ Args:
77
+ profiles: List of (profile_name, profile_dict) tuples.
78
+ config_path: Path to AWS config file. Defaults to ~/.aws/config.
79
+ """
80
+ from configparser import ConfigParser
81
+ from pathlib import Path
82
+
83
+ if config_path is None:
84
+ config_path = os.path.join(Path.home(), '.aws', 'config')
85
+
86
+ config = ConfigParser()
87
+ if os.path.exists(config_path):
88
+ config.read(config_path)
89
+
90
+ for profile_name, profile_data in profiles:
91
+ section = f'profile {profile_name}'
92
+ if not config.has_section(section):
93
+ config.add_section(section)
94
+ for key, value in profile_data.items():
95
+ config.set(section, key, value)
96
+
97
+ parent_dir = os.path.dirname(config_path)
98
+ if parent_dir:
99
+ os.makedirs(parent_dir, exist_ok=True)
100
+ with open(config_path, 'w') as f:
101
+ config.write(f)
102
+
103
+
104
+ @chariot.group()
105
+ def access():
106
+ """Configure credential access for cloud providers."""
107
+ pass
108
+
109
+
110
+ @access.command()
111
+ @cli_handler
112
+ @click.option('--account', default=None, help='Guard account email to discover AWS credentials for (also inherited from guard --account)')
113
+ @click.option('--prefix', default=None, help='Override the profile name prefix (default: derived from email)')
114
+ def aws(sdk, account, prefix):
115
+ """Generate AWS CLI config profiles from Guard credentials.
116
+
117
+ Discovers AWS credentials for the given account, enumerates sub-accounts
118
+ via Organizations (if applicable), and writes ~/.aws/config profiles that
119
+ use guard as the credential_process.
120
+
121
+ The --account can be provided here or as the top-level guard --account flag.
122
+
123
+ \b
124
+ Example usages:
125
+ - guard --account chariot+client@praetorian.com access aws
126
+ - guard access aws --account chariot+client@praetorian.com
127
+ - guard access aws --account chariot+client@praetorian.com --prefix myclient
128
+ """
129
+ import boto3
130
+
131
+ # Resolve account: explicit --account on this command, or inherited from guard --account
132
+ if account is None:
133
+ account = sdk.keychain.account
134
+ if account is None:
135
+ raise click.ClickException('--account is required. Provide it here or as guard --account.')
136
+
137
+ sdk.keychain.assume_role(account)
138
+
139
+ if prefix is None:
140
+ prefix = extract_prefix(account)
141
+
142
+ # List credentials and filter to AWS type
143
+ creds_response = sdk.credentials.list()
144
+ credentials_list = creds_response[0] if isinstance(creds_response, tuple) else []
145
+
146
+ aws_credentials = [c for c in credentials_list if c.get('type', '') == 'aws']
147
+
148
+ if not aws_credentials:
149
+ click.echo(f'No AWS credentials found for account {account}')
150
+ return
151
+
152
+ all_profiles = []
153
+
154
+ for cred in aws_credentials:
155
+ credential_id = cred.get('credentialId', '')
156
+ account_key = cred.get('accountKey', '')
157
+ category = cred.get('category', 'env-integration')
158
+ root_account_id = extract_account_id(account_key)
159
+
160
+ # Fetch the root credential to get temp AWS creds
161
+ try:
162
+ result = sdk.credentials.get(credential_id, category, 'aws', ['token'])
163
+ except Exception as e:
164
+ click.echo(f'Warning: failed to get credential {credential_id}: {e}')
165
+ continue
166
+
167
+ access_key = result.get('credentialValue', {}).get('accessKeyId')
168
+ secret_key = result.get('credentialValue', {}).get('secretAccessKey')
169
+ session_token = result.get('credentialValue', {}).get('sessionToken')
170
+
171
+ if not access_key:
172
+ click.echo(f'Warning: credential {credential_id} returned no access key')
173
+ continue
174
+
175
+ # Try Organizations to discover sub-accounts
176
+ sub_accounts = []
177
+ try:
178
+ org_client = boto3.client(
179
+ 'organizations',
180
+ aws_access_key_id=access_key,
181
+ aws_secret_access_key=secret_key,
182
+ aws_session_token=session_token,
183
+ region_name='us-east-1'
184
+ )
185
+ paginator = org_client.get_paginator('list_accounts')
186
+ for page in paginator.paginate():
187
+ sub_accounts.extend(page['Accounts'])
188
+ except Exception:
189
+ # Not an org account — that's fine, just use the root
190
+ pass
191
+
192
+ profiles = build_aws_config_profiles(
193
+ account_email=account,
194
+ prefix=prefix,
195
+ credential_id=credential_id,
196
+ root_account_id=root_account_id,
197
+ sub_accounts=sub_accounts,
198
+ category=category,
199
+ )
200
+ all_profiles.extend(profiles)
201
+
202
+ if not all_profiles:
203
+ click.echo(f'No AWS profiles could be generated for account {account}')
204
+ return
205
+
206
+ write_aws_config(all_profiles)
207
+ click.echo(f'Wrote {len(all_profiles)} AWS profile(s) to ~/.aws/config:')
208
+ for profile_name, _ in all_profiles:
209
+ click.echo(f' {profile_name}')
@@ -6,7 +6,7 @@ from praetorian_cli.sdk.keychain import Keychain, DEFAULT_API, DEFAULT_CLIENT_ID
6
6
  @click.pass_context
7
7
  def configure(click_context):
8
8
  """Configure the CLI with API key authentication."""
9
-
9
+
10
10
  api_key_id = click.prompt("Enter your API Key ID")
11
11
  api_key_secret = click.prompt("Enter your API Key secret", hide_input=True)
12
12
 
@@ -18,16 +18,17 @@ from praetorian_cli.handlers.add import add
18
18
  @click.option('--type', 'customer_type', type=click.Choice(['ENGAGEMENT', 'MANAGED', 'SAAS', 'PILOT', 'FREEMIUM']),
19
19
  default='ENGAGEMENT', show_default=True, help='Customer type')
20
20
  @click.option('--collaborator', multiple=True, help='Additional collaborator emails (format: email:role)')
21
- def create_customer(sdk, email, display_name, scan_level, customer_type, collaborator):
21
+ @click.option('--allowed-domain', multiple=True, help='Allowed email domains for collaborators (e.g., acme.com)')
22
+ def create_customer(sdk, email, display_name, scan_level, customer_type, collaborator, allowed_domain):
22
23
  """ Create a new customer account on the Guard platform
23
24
 
24
25
  Creates a Cognito user, Guard account, and adds default collaborators.
25
26
 
26
27
  \b
27
28
  Example usages:
28
- - guard add customer --email ops@acme.com --name "ACME Corp"
29
- - guard add customer --email ops@acme.com --name "ACME Corp" --type MANAGED
30
- - guard add customer --email ops@acme.com --name "ACME Corp" --collaborator "analyst@praetorian.com:admin"
29
+ - guard add customer --email ops@acme.com --name "ACME Corp" --allowed-domain acme.com
30
+ - guard add customer --email ops@acme.com --name "ACME Corp" --type MANAGED --allowed-domain acme.com
31
+ - guard add customer --email ops@acme.com --name "ACME Corp" --allowed-domain acme.com --collaborator "analyst@praetorian.com:admin"
31
32
  """
32
33
  body = {
33
34
  'username': email,
@@ -47,6 +48,8 @@ def create_customer(sdk, email, display_name, scan_level, customer_type, collabo
47
48
  collabs.append({'email': c, 'role': 'admin'})
48
49
  body['collaborators'] = collabs
49
50
 
51
+ body['allowed_domains'] = [d.lower().strip() for d in allowed_domain]
52
+
50
53
  result = sdk.post('customer/onboard', body)
51
54
  click.echo(f'Customer created: {email} ({display_name})')
52
55
  print_json(result)
@@ -25,6 +25,8 @@ def export():
25
25
  @click.option('--end-date', default='', help='Engagement end date (ISO format)')
26
26
  @click.option('--report-date', default='', help='Report date (ISO format). Default: today')
27
27
  @click.option('--draft/--no-draft', default=False, help='Add DRAFT watermark')
28
+ @click.option('--retest/--no-retest', default=False,
29
+ help='Include retest status badges and sections')
28
30
  @click.option('--version', 'report_version', default='1.0', help='Report version string')
29
31
  @click.option('--format', 'export_format', type=click.Choice(['pdf', 'zip']),
30
32
  default='pdf', help='Export format')
@@ -45,7 +47,7 @@ def export():
45
47
  @click.option('--no-download', is_flag=True, default=False,
46
48
  help='Skip downloading the file; just print the job result')
47
49
  def report(chariot, title, client_name, status_filter, risk_keys,
48
- target, start_date, end_date, report_date, draft,
50
+ target, start_date, end_date, report_date, draft, retest,
49
51
  report_version, export_format, group_by, shared,
50
52
  executive_summary, narratives, appendix, output,
51
53
  timeout, no_download):
@@ -80,6 +82,7 @@ def report(chariot, title, client_name, status_filter, risk_keys,
80
82
  end_date=end_date,
81
83
  report_date=report_date,
82
84
  draft=draft,
85
+ retest=retest,
83
86
  version=report_version,
84
87
  export_format=export_format,
85
88
  group_by=group_by,
@@ -99,7 +99,12 @@ def integrations(chariot, filter, details, offset, page):
99
99
 
100
100
  @list.command()
101
101
  @list_params('group of the job asset')
102
- def jobs(chariot, filter, details, offset, page):
102
+ @click.option('-c', '--capability', default=None, help='Filter by capability name (e.g., nuclei, portscan, diana-agent)')
103
+ @click.option('-s', '--status', type=click.Choice(['JQ', 'JR', 'JP', 'JF']),
104
+ default=None, help='Filter by job status (JQ=queued, JR=running, JP=passed, JF=failed)')
105
+ @click.option('-t', '--target', default=None, help='Filter by target DNS or hostname')
106
+ @click.option('--summary', is_flag=True, default=False, help='Show job count summary by capability and status')
107
+ def jobs(chariot, capability, status, target, summary, filter, details, offset, page):
103
108
  """
104
109
  List jobs
105
110
 
@@ -110,10 +115,24 @@ def jobs(chariot, filter, details, offset, page):
110
115
  Example usages:
111
116
  - guard list jobs
112
117
  - guard list jobs --filter www.example.com
118
+ - guard list jobs --capability nuclei
119
+ - guard list jobs --capability diana-agent --details
120
+ - guard list jobs --status JR
121
+ - guard list jobs --target api.example.com
122
+ - guard list jobs --summary
113
123
  - guard list jobs --details
114
124
  - guard list jobs --page all
115
125
  """
116
- render_list_results(chariot.jobs.list(filter, offset, pagination_size(page)), details)
126
+ if summary:
127
+ print_json(chariot.jobs.summary(pagination_size(page)))
128
+ elif capability:
129
+ render_list_results(chariot.jobs.list_by_capability(capability, offset, pagination_size(page)), details)
130
+ elif status:
131
+ render_list_results(chariot.jobs.list_by_status(status, offset, pagination_size(page)), details)
132
+ elif target:
133
+ render_list_results(chariot.jobs.list_by_target(target, offset, pagination_size(page)), details)
134
+ else:
135
+ render_list_results(chariot.jobs.list(filter, offset, pagination_size(page)), details)
117
136
 
118
137
 
119
138
  @list.command()
@@ -175,26 +175,109 @@ def tool(sdk, tool_name, target, extra_config, credential, wait, use_agent, loca
175
175
 
176
176
  @run.command('list')
177
177
  @cli_handler
178
- def list_tools(sdk):
179
- """ List named agents and their descriptions
178
+ @click.option('-s', '--surface', default='', help='Filter by surface (external, internal, cloud, repository, application)')
179
+ @click.option('-e', '--executor', default='', help='Filter by executor (chariot, janus, aegis, aurelian, agent)')
180
+ @click.option('--json', 'as_json', is_flag=True, default=False, help='Output as JSON')
181
+ def list_tools(sdk, surface, executor, as_json):
182
+ """ List all available tools and capabilities
180
183
 
181
184
  \b
182
- Example usage:
185
+ Shows all Guard capabilities with descriptions, indicating which can
186
+ be installed locally. Use --surface or --executor to filter.
187
+
188
+ \b
189
+ Example usages:
183
190
  guard run list
191
+ guard run list --surface external
192
+ guard run list --executor agent
193
+ guard run list --json
184
194
  """
185
- click.echo(f'\n{"Agent":<16} {"Description"}')
186
- click.echo(f'{"─"*16} {"─"*50}')
187
- for name, desc in sorted(FRIENDLY_NAMES.items()):
188
- click.echo(f'{name:<16} {desc}')
189
- click.echo(f'\nUse "guard run capabilities" for the full list of backend capabilities.')
195
+ from praetorian_cli.runners.local import INSTALLABLE_TOOLS, list_installed
196
+
197
+ try:
198
+ result = sdk.capabilities.list()
199
+ caps = result[0] if isinstance(result, tuple) else result
200
+ if not isinstance(caps, list):
201
+ caps = []
202
+ except Exception:
203
+ caps = []
204
+
205
+ if not caps:
206
+ click.echo('Could not fetch capabilities from backend.', err=True)
207
+ click.echo(f'\nLocally installable tools:')
208
+ for name, info in sorted(INSTALLABLE_TOOLS.items()):
209
+ click.echo(f' {name:<18} {info["description"]}')
210
+ return
211
+
212
+ if as_json:
213
+ print_json(caps)
214
+ return
215
+
216
+ installed = list_installed()
217
+
218
+ # Apply filters
219
+ if surface:
220
+ caps = [c for c in caps if c.get('surface', '') == surface]
221
+ if executor:
222
+ caps = [c for c in caps if c.get('executor', '') == executor]
223
+
224
+ if not caps:
225
+ click.echo('No capabilities match the filters.')
226
+ return
227
+
228
+ # Sort: installable first, then by name
229
+ def sort_key(c):
230
+ name = c.get('name', '')
231
+ is_installable = name in INSTALLABLE_TOOLS
232
+ return (not is_installable, name)
233
+
234
+ caps.sort(key=sort_key)
235
+
236
+ # Render table
237
+ click.echo(f'\n{"Name":<22} {"Type":<10} {"Surface":<12} {"Description"}')
238
+ click.echo(f'{"─"*22} {"─"*10} {"─"*12} {"─"*48}')
239
+
240
+ for c in caps:
241
+ name = c.get('name', '')
242
+ desc = c.get('description', '') or c.get('title', '')
243
+ cap_surface = c.get('surface', '')
244
+ cap_executor = c.get('executor', '')
245
+
246
+ # Determine type label
247
+ if name in INSTALLABLE_TOOLS:
248
+ if name in installed:
249
+ type_label = 'local ✓'
250
+ else:
251
+ type_label = 'local'
252
+ elif cap_executor == 'agent':
253
+ type_label = 'agent'
254
+ else:
255
+ type_label = 'remote'
256
+
257
+ # Truncate description
258
+ max_desc = 55
259
+ if len(desc) > max_desc:
260
+ desc = desc[:max_desc - 1] + '…'
261
+
262
+ click.echo(f'{name:<22} {type_label:<10} {cap_surface:<12} {desc}')
263
+
264
+ click.echo(f'\n{len(caps)} capabilities')
265
+ installable_count = sum(1 for c in caps if c.get('name', '') in INSTALLABLE_TOOLS)
266
+ installed_count = sum(1 for c in caps if c.get('name', '') in installed)
267
+ if installable_count:
268
+ click.echo(f'{installable_count} installable locally ({installed_count} installed) — use "guard run install <name>"')
190
269
 
191
270
 
192
271
  @run.command('capabilities')
193
272
  @cli_handler
194
273
  @click.option('-n', '--name', default='', help='Filter by capability name')
195
274
  @click.option('-t', '--target', default='', help='Filter by target type')
196
- def capabilities(sdk, name, target):
197
- """ List all available capabilities from the backend
275
+ @click.option('-e', '--executor', default='', help='Filter by executor')
276
+ def capabilities(sdk, name, target, executor):
277
+ """ List all capabilities as JSON (for scripting)
278
+
279
+ \b
280
+ Full machine-readable output. For a human-readable view, use "guard run list".
198
281
 
199
282
  \b
200
283
  Example usages:
@@ -202,8 +285,9 @@ def capabilities(sdk, name, target):
202
285
  guard run capabilities --name nuclei
203
286
  guard run capabilities --target asset
204
287
  """
205
- result = sdk.capabilities.list(name=name, target=target)
206
- print_json(result)
288
+ result = sdk.capabilities.list(name=name, target=target, executor=executor)
289
+ caps = result[0] if isinstance(result, tuple) else result
290
+ print_json(caps)
207
291
 
208
292
 
209
293
  @run.command('install')
@@ -1,5 +1,6 @@
1
1
  import click
2
2
 
3
+ import praetorian_cli.handlers.access
3
4
  import praetorian_cli.handlers.add
4
5
  import praetorian_cli.handlers.aegis
5
6
  import praetorian_cli.handlers.agent
@@ -71,7 +71,10 @@ def is_installed(tool_name: str) -> bool:
71
71
  def install_tool(tool_name: str, force=False) -> str:
72
72
  """Download and install a tool from GitHub releases. Returns binary path."""
73
73
  if tool_name not in INSTALLABLE_TOOLS:
74
- raise ValueError(f'Unknown tool: {tool_name}. Available: {", ".join(sorted(INSTALLABLE_TOOLS))}')
74
+ lines = [f'Unknown tool: {tool_name}. Installable tools:']
75
+ for name in sorted(INSTALLABLE_TOOLS):
76
+ lines.append(f' {name:<18} {INSTALLABLE_TOOLS[name]["description"]}')
77
+ raise ValueError('\n'.join(lines))
75
78
 
76
79
  if not force and is_installed(tool_name):
77
80
  return get_binary_path(tool_name)