praetorian-cli 2.4.0__tar.gz → 2.4.1__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 (140) hide show
  1. {praetorian_cli-2.4.0/praetorian_cli.egg-info → praetorian_cli-2.4.1}/PKG-INFO +1 -1
  2. {praetorian_cli-2.4.0 → praetorian_cli-2.4.1}/praetorian_cli/handlers/export.py +4 -1
  3. {praetorian_cli-2.4.0 → praetorian_cli-2.4.1}/praetorian_cli/handlers/run.py +96 -12
  4. {praetorian_cli-2.4.0 → praetorian_cli-2.4.1}/praetorian_cli/runners/local.py +4 -1
  5. {praetorian_cli-2.4.0 → praetorian_cli-2.4.1}/praetorian_cli/sdk/entities/reports.py +5 -1
  6. {praetorian_cli-2.4.0 → praetorian_cli-2.4.1}/praetorian_cli/sdk/test/test_report.py +3 -0
  7. {praetorian_cli-2.4.0 → praetorian_cli-2.4.1}/praetorian_cli/ui/console/commands/search.py +36 -15
  8. {praetorian_cli-2.4.0 → praetorian_cli-2.4.1}/praetorian_cli/ui/console/console.py +13 -4
  9. {praetorian_cli-2.4.0 → praetorian_cli-2.4.1}/praetorian_cli/ui/console/renderer.py +151 -6
  10. {praetorian_cli-2.4.0 → praetorian_cli-2.4.1/praetorian_cli.egg-info}/PKG-INFO +1 -1
  11. {praetorian_cli-2.4.0 → praetorian_cli-2.4.1}/setup.cfg +1 -1
  12. {praetorian_cli-2.4.0 → praetorian_cli-2.4.1}/LICENSE +0 -0
  13. {praetorian_cli-2.4.0 → praetorian_cli-2.4.1}/MANIFEST.in +0 -0
  14. {praetorian_cli-2.4.0 → praetorian_cli-2.4.1}/README.md +0 -0
  15. {praetorian_cli-2.4.0 → praetorian_cli-2.4.1}/praetorian_cli/__init__.py +0 -0
  16. {praetorian_cli-2.4.0 → praetorian_cli-2.4.1}/praetorian_cli/handlers/__init__.py +0 -0
  17. {praetorian_cli-2.4.0 → praetorian_cli-2.4.1}/praetorian_cli/handlers/add.py +0 -0
  18. {praetorian_cli-2.4.0 → praetorian_cli-2.4.1}/praetorian_cli/handlers/aegis.py +0 -0
  19. {praetorian_cli-2.4.0 → praetorian_cli-2.4.1}/praetorian_cli/handlers/agent.py +0 -0
  20. {praetorian_cli-2.4.0 → praetorian_cli-2.4.1}/praetorian_cli/handlers/chariot.py +0 -0
  21. {praetorian_cli-2.4.0 → praetorian_cli-2.4.1}/praetorian_cli/handlers/cli_decorators.py +0 -0
  22. {praetorian_cli-2.4.0 → praetorian_cli-2.4.1}/praetorian_cli/handlers/configure.py +0 -0
  23. {praetorian_cli-2.4.0 → praetorian_cli-2.4.1}/praetorian_cli/handlers/critfinder.py +0 -0
  24. {praetorian_cli-2.4.0 → praetorian_cli-2.4.1}/praetorian_cli/handlers/delete.py +0 -0
  25. {praetorian_cli-2.4.0 → praetorian_cli-2.4.1}/praetorian_cli/handlers/engagement.py +0 -0
  26. {praetorian_cli-2.4.0 → praetorian_cli-2.4.1}/praetorian_cli/handlers/enrich.py +0 -0
  27. {praetorian_cli-2.4.0 → praetorian_cli-2.4.1}/praetorian_cli/handlers/find.py +0 -0
  28. {praetorian_cli-2.4.0 → praetorian_cli-2.4.1}/praetorian_cli/handlers/get.py +0 -0
  29. {praetorian_cli-2.4.0 → praetorian_cli-2.4.1}/praetorian_cli/handlers/imports.py +0 -0
  30. {praetorian_cli-2.4.0 → praetorian_cli-2.4.1}/praetorian_cli/handlers/link.py +0 -0
  31. {praetorian_cli-2.4.0 → praetorian_cli-2.4.1}/praetorian_cli/handlers/list.py +0 -0
  32. {praetorian_cli-2.4.0 → praetorian_cli-2.4.1}/praetorian_cli/handlers/report.py +0 -0
  33. {praetorian_cli-2.4.0 → praetorian_cli-2.4.1}/praetorian_cli/handlers/script.py +0 -0
  34. {praetorian_cli-2.4.0 → praetorian_cli-2.4.1}/praetorian_cli/handlers/search.py +0 -0
  35. {praetorian_cli-2.4.0 → praetorian_cli-2.4.1}/praetorian_cli/handlers/ssh_utils.py +0 -0
  36. {praetorian_cli-2.4.0 → praetorian_cli-2.4.1}/praetorian_cli/handlers/test.py +0 -0
  37. {praetorian_cli-2.4.0 → praetorian_cli-2.4.1}/praetorian_cli/handlers/unlink.py +0 -0
  38. {praetorian_cli-2.4.0 → praetorian_cli-2.4.1}/praetorian_cli/handlers/update.py +0 -0
  39. {praetorian_cli-2.4.0 → praetorian_cli-2.4.1}/praetorian_cli/handlers/utils.py +0 -0
  40. {praetorian_cli-2.4.0 → praetorian_cli-2.4.1}/praetorian_cli/main.py +0 -0
  41. {praetorian_cli-2.4.0 → praetorian_cli-2.4.1}/praetorian_cli/runners/__init__.py +0 -0
  42. {praetorian_cli-2.4.0 → praetorian_cli-2.4.1}/praetorian_cli/scripts/__init__.py +0 -0
  43. {praetorian_cli-2.4.0 → praetorian_cli-2.4.1}/praetorian_cli/scripts/commands/__init__.py +0 -0
  44. {praetorian_cli-2.4.0 → praetorian_cli-2.4.1}/praetorian_cli/scripts/commands/nmap-example.py +0 -0
  45. {praetorian_cli-2.4.0 → praetorian_cli-2.4.1}/praetorian_cli/scripts/utils.py +0 -0
  46. {praetorian_cli-2.4.0 → praetorian_cli-2.4.1}/praetorian_cli/sdk/__init__.py +0 -0
  47. {praetorian_cli-2.4.0 → praetorian_cli-2.4.1}/praetorian_cli/sdk/chariot.py +0 -0
  48. {praetorian_cli-2.4.0 → praetorian_cli-2.4.1}/praetorian_cli/sdk/entities/__init__.py +0 -0
  49. {praetorian_cli-2.4.0 → praetorian_cli-2.4.1}/praetorian_cli/sdk/entities/accounts.py +0 -0
  50. {praetorian_cli-2.4.0 → praetorian_cli-2.4.1}/praetorian_cli/sdk/entities/ad.py +0 -0
  51. {praetorian_cli-2.4.0 → praetorian_cli-2.4.1}/praetorian_cli/sdk/entities/aegis.py +0 -0
  52. {praetorian_cli-2.4.0 → praetorian_cli-2.4.1}/praetorian_cli/sdk/entities/agents.py +0 -0
  53. {praetorian_cli-2.4.0 → praetorian_cli-2.4.1}/praetorian_cli/sdk/entities/assets.py +0 -0
  54. {praetorian_cli-2.4.0 → praetorian_cli-2.4.1}/praetorian_cli/sdk/entities/attributes.py +0 -0
  55. {praetorian_cli-2.4.0 → praetorian_cli-2.4.1}/praetorian_cli/sdk/entities/capabilities.py +0 -0
  56. {praetorian_cli-2.4.0 → praetorian_cli-2.4.1}/praetorian_cli/sdk/entities/configurations.py +0 -0
  57. {praetorian_cli-2.4.0 → praetorian_cli-2.4.1}/praetorian_cli/sdk/entities/credentials.py +0 -0
  58. {praetorian_cli-2.4.0 → praetorian_cli-2.4.1}/praetorian_cli/sdk/entities/definitions.py +0 -0
  59. {praetorian_cli-2.4.0 → praetorian_cli-2.4.1}/praetorian_cli/sdk/entities/files.py +0 -0
  60. {praetorian_cli-2.4.0 → praetorian_cli-2.4.1}/praetorian_cli/sdk/entities/integrations.py +0 -0
  61. {praetorian_cli-2.4.0 → praetorian_cli-2.4.1}/praetorian_cli/sdk/entities/jobs.py +0 -0
  62. {praetorian_cli-2.4.0 → praetorian_cli-2.4.1}/praetorian_cli/sdk/entities/keys.py +0 -0
  63. {praetorian_cli-2.4.0 → praetorian_cli-2.4.1}/praetorian_cli/sdk/entities/preseeds.py +0 -0
  64. {praetorian_cli-2.4.0 → praetorian_cli-2.4.1}/praetorian_cli/sdk/entities/risks.py +0 -0
  65. {praetorian_cli-2.4.0 → praetorian_cli-2.4.1}/praetorian_cli/sdk/entities/scanners.py +0 -0
  66. {praetorian_cli-2.4.0 → praetorian_cli-2.4.1}/praetorian_cli/sdk/entities/schedules.py +0 -0
  67. {praetorian_cli-2.4.0 → praetorian_cli-2.4.1}/praetorian_cli/sdk/entities/schema.py +0 -0
  68. {praetorian_cli-2.4.0 → praetorian_cli-2.4.1}/praetorian_cli/sdk/entities/search.py +0 -0
  69. {praetorian_cli-2.4.0 → praetorian_cli-2.4.1}/praetorian_cli/sdk/entities/seeds.py +0 -0
  70. {praetorian_cli-2.4.0 → praetorian_cli-2.4.1}/praetorian_cli/sdk/entities/settings.py +0 -0
  71. {praetorian_cli-2.4.0 → praetorian_cli-2.4.1}/praetorian_cli/sdk/entities/statistics.py +0 -0
  72. {praetorian_cli-2.4.0 → praetorian_cli-2.4.1}/praetorian_cli/sdk/entities/webhook.py +0 -0
  73. {praetorian_cli-2.4.0 → praetorian_cli-2.4.1}/praetorian_cli/sdk/entities/webpage.py +0 -0
  74. {praetorian_cli-2.4.0 → praetorian_cli-2.4.1}/praetorian_cli/sdk/guard.py +0 -0
  75. {praetorian_cli-2.4.0 → praetorian_cli-2.4.1}/praetorian_cli/sdk/keychain.py +0 -0
  76. {praetorian_cli-2.4.0 → praetorian_cli-2.4.1}/praetorian_cli/sdk/mcp_server.py +0 -0
  77. {praetorian_cli-2.4.0 → praetorian_cli-2.4.1}/praetorian_cli/sdk/model/__init__.py +0 -0
  78. {praetorian_cli-2.4.0 → praetorian_cli-2.4.1}/praetorian_cli/sdk/model/aegis.py +0 -0
  79. {praetorian_cli-2.4.0 → praetorian_cli-2.4.1}/praetorian_cli/sdk/model/globals.py +0 -0
  80. {praetorian_cli-2.4.0 → praetorian_cli-2.4.1}/praetorian_cli/sdk/model/query.py +0 -0
  81. {praetorian_cli-2.4.0 → praetorian_cli-2.4.1}/praetorian_cli/sdk/model/utils.py +0 -0
  82. {praetorian_cli-2.4.0 → praetorian_cli-2.4.1}/praetorian_cli/sdk/test/__init__.py +0 -0
  83. {praetorian_cli-2.4.0 → praetorian_cli-2.4.1}/praetorian_cli/sdk/test/pytest.ini +0 -0
  84. {praetorian_cli-2.4.0 → praetorian_cli-2.4.1}/praetorian_cli/sdk/test/test_account.py +0 -0
  85. {praetorian_cli-2.4.0 → praetorian_cli-2.4.1}/praetorian_cli/sdk/test/test_agent.py +0 -0
  86. {praetorian_cli-2.4.0 → praetorian_cli-2.4.1}/praetorian_cli/sdk/test/test_asset.py +0 -0
  87. {praetorian_cli-2.4.0 → praetorian_cli-2.4.1}/praetorian_cli/sdk/test/test_attribute.py +0 -0
  88. {praetorian_cli-2.4.0 → praetorian_cli-2.4.1}/praetorian_cli/sdk/test/test_capabilities.py +0 -0
  89. {praetorian_cli-2.4.0 → praetorian_cli-2.4.1}/praetorian_cli/sdk/test/test_configuration.py +0 -0
  90. {praetorian_cli-2.4.0 → praetorian_cli-2.4.1}/praetorian_cli/sdk/test/test_conversation.py +0 -0
  91. {praetorian_cli-2.4.0 → praetorian_cli-2.4.1}/praetorian_cli/sdk/test/test_definition.py +0 -0
  92. {praetorian_cli-2.4.0 → praetorian_cli-2.4.1}/praetorian_cli/sdk/test/test_extend.py +0 -0
  93. {praetorian_cli-2.4.0 → praetorian_cli-2.4.1}/praetorian_cli/sdk/test/test_file.py +0 -0
  94. {praetorian_cli-2.4.0 → praetorian_cli-2.4.1}/praetorian_cli/sdk/test/test_job.py +0 -0
  95. {praetorian_cli-2.4.0 → praetorian_cli-2.4.1}/praetorian_cli/sdk/test/test_key.py +0 -0
  96. {praetorian_cli-2.4.0 → praetorian_cli-2.4.1}/praetorian_cli/sdk/test/test_mcp.py +0 -0
  97. {praetorian_cli-2.4.0 → praetorian_cli-2.4.1}/praetorian_cli/sdk/test/test_preseed.py +0 -0
  98. {praetorian_cli-2.4.0 → praetorian_cli-2.4.1}/praetorian_cli/sdk/test/test_risk.py +0 -0
  99. {praetorian_cli-2.4.0 → praetorian_cli-2.4.1}/praetorian_cli/sdk/test/test_search.py +0 -0
  100. {praetorian_cli-2.4.0 → praetorian_cli-2.4.1}/praetorian_cli/sdk/test/test_seed.py +0 -0
  101. {praetorian_cli-2.4.0 → praetorian_cli-2.4.1}/praetorian_cli/sdk/test/test_setting.py +0 -0
  102. {praetorian_cli-2.4.0 → praetorian_cli-2.4.1}/praetorian_cli/sdk/test/test_webhook.py +0 -0
  103. {praetorian_cli-2.4.0 → praetorian_cli-2.4.1}/praetorian_cli/sdk/test/test_webpage.py +0 -0
  104. {praetorian_cli-2.4.0 → praetorian_cli-2.4.1}/praetorian_cli/sdk/test/test_z_cli.py +0 -0
  105. {praetorian_cli-2.4.0 → praetorian_cli-2.4.1}/praetorian_cli/sdk/test/ui_mocks.py +0 -0
  106. {praetorian_cli-2.4.0 → praetorian_cli-2.4.1}/praetorian_cli/sdk/test/utils.py +0 -0
  107. {praetorian_cli-2.4.0 → praetorian_cli-2.4.1}/praetorian_cli/ui/__init__.py +0 -0
  108. {praetorian_cli-2.4.0 → praetorian_cli-2.4.1}/praetorian_cli/ui/aegis/__init__.py +0 -0
  109. {praetorian_cli-2.4.0 → praetorian_cli-2.4.1}/praetorian_cli/ui/aegis/commands/__init__.py +0 -0
  110. {praetorian_cli-2.4.0 → praetorian_cli-2.4.1}/praetorian_cli/ui/aegis/commands/cp.py +0 -0
  111. {praetorian_cli-2.4.0 → praetorian_cli-2.4.1}/praetorian_cli/ui/aegis/commands/help.py +0 -0
  112. {praetorian_cli-2.4.0 → praetorian_cli-2.4.1}/praetorian_cli/ui/aegis/commands/info.py +0 -0
  113. {praetorian_cli-2.4.0 → praetorian_cli-2.4.1}/praetorian_cli/ui/aegis/commands/job.py +0 -0
  114. {praetorian_cli-2.4.0 → praetorian_cli-2.4.1}/praetorian_cli/ui/aegis/commands/job_helpers.py +0 -0
  115. {praetorian_cli-2.4.0 → praetorian_cli-2.4.1}/praetorian_cli/ui/aegis/commands/list.py +0 -0
  116. {praetorian_cli-2.4.0 → praetorian_cli-2.4.1}/praetorian_cli/ui/aegis/commands/proxy.py +0 -0
  117. {praetorian_cli-2.4.0 → praetorian_cli-2.4.1}/praetorian_cli/ui/aegis/commands/schedule.py +0 -0
  118. {praetorian_cli-2.4.0 → praetorian_cli-2.4.1}/praetorian_cli/ui/aegis/commands/schedule_helpers.py +0 -0
  119. {praetorian_cli-2.4.0 → praetorian_cli-2.4.1}/praetorian_cli/ui/aegis/commands/set.py +0 -0
  120. {praetorian_cli-2.4.0 → praetorian_cli-2.4.1}/praetorian_cli/ui/aegis/commands/ssh.py +0 -0
  121. {praetorian_cli-2.4.0 → praetorian_cli-2.4.1}/praetorian_cli/ui/aegis/constants.py +0 -0
  122. {praetorian_cli-2.4.0 → praetorian_cli-2.4.1}/praetorian_cli/ui/aegis/menu.py +0 -0
  123. {praetorian_cli-2.4.0 → praetorian_cli-2.4.1}/praetorian_cli/ui/aegis/theme.py +0 -0
  124. {praetorian_cli-2.4.0 → praetorian_cli-2.4.1}/praetorian_cli/ui/aegis/utils.py +0 -0
  125. {praetorian_cli-2.4.0 → praetorian_cli-2.4.1}/praetorian_cli/ui/console/__init__.py +0 -0
  126. {praetorian_cli-2.4.0 → praetorian_cli-2.4.1}/praetorian_cli/ui/console/commands/__init__.py +0 -0
  127. {praetorian_cli-2.4.0 → praetorian_cli-2.4.1}/praetorian_cli/ui/console/commands/accounts.py +0 -0
  128. {praetorian_cli-2.4.0 → praetorian_cli-2.4.1}/praetorian_cli/ui/console/commands/context.py +0 -0
  129. {praetorian_cli-2.4.0 → praetorian_cli-2.4.1}/praetorian_cli/ui/console/commands/marcus.py +0 -0
  130. {praetorian_cli-2.4.0 → praetorian_cli-2.4.1}/praetorian_cli/ui/console/commands/reporting.py +0 -0
  131. {praetorian_cli-2.4.0 → praetorian_cli-2.4.1}/praetorian_cli/ui/console/commands/tools.py +0 -0
  132. {praetorian_cli-2.4.0 → praetorian_cli-2.4.1}/praetorian_cli/ui/console/context.py +0 -0
  133. {praetorian_cli-2.4.0 → praetorian_cli-2.4.1}/praetorian_cli/ui/conversation/__init__.py +0 -0
  134. {praetorian_cli-2.4.0 → praetorian_cli-2.4.1}/praetorian_cli/ui/conversation/textual_chat.py +0 -0
  135. {praetorian_cli-2.4.0 → praetorian_cli-2.4.1}/praetorian_cli.egg-info/SOURCES.txt +0 -0
  136. {praetorian_cli-2.4.0 → praetorian_cli-2.4.1}/praetorian_cli.egg-info/dependency_links.txt +0 -0
  137. {praetorian_cli-2.4.0 → praetorian_cli-2.4.1}/praetorian_cli.egg-info/entry_points.txt +0 -0
  138. {praetorian_cli-2.4.0 → praetorian_cli-2.4.1}/praetorian_cli.egg-info/requires.txt +0 -0
  139. {praetorian_cli-2.4.0 → praetorian_cli-2.4.1}/praetorian_cli.egg-info/top_level.txt +0 -0
  140. {praetorian_cli-2.4.0 → praetorian_cli-2.4.1}/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.1
4
4
  Summary: For interacting with the Guard API
5
5
  Home-page: https://github.com/praetorian-inc/praetorian-cli
6
6
  Author: Praetorian
@@ -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,
@@ -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')
@@ -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)
@@ -35,7 +35,8 @@ class Reports:
35
35
  def build_export_body(self, title, client_name, customer_email,
36
36
  status_filter=('O', 'T'), risk_keys=(),
37
37
  target='', start_date='', end_date='',
38
- report_date='', draft=False, version='1.0',
38
+ report_date='', draft=False, retest=False,
39
+ version='1.0',
39
40
  export_format='pdf', group_by='attack_surface',
40
41
  shared_output=False, executive_summary_path='',
41
42
  narratives_path='', appendix_path=''):
@@ -62,6 +63,8 @@ class Reports:
62
63
  :type report_date: str
63
64
  :param draft: Whether to add DRAFT watermark
64
65
  :type draft: bool
66
+ :param retest: Whether to include retest status badges and sections
67
+ :type retest: bool
65
68
  :param version: Report version string
66
69
  :type version: str
67
70
  :param export_format: Output format ('pdf' or 'zip')
@@ -89,6 +92,7 @@ class Reports:
89
92
  'client_name': client_name,
90
93
  'report_date': report_date,
91
94
  'draft': draft,
95
+ 'retest': retest,
92
96
  'version': version,
93
97
  },
94
98
  'shared_output': shared_output,
@@ -52,6 +52,7 @@ class TestBuildExportBody:
52
52
  assert body['group_by'] == 'attack_surface'
53
53
  assert body['shared_output'] is False
54
54
  assert body['config']['draft'] is False
55
+ assert body['config']['retest'] is False
55
56
  assert body['config']['version'] == '1.0'
56
57
  assert 'risk_keys' not in body
57
58
  assert 'target' not in body['config']
@@ -85,6 +86,7 @@ class TestBuildExportBody:
85
86
  end_date='2026-03-01',
86
87
  report_date='2026-03-15',
87
88
  draft=True,
89
+ retest=True,
88
90
  version='2.0',
89
91
  export_format='zip',
90
92
  group_by='tag',
@@ -99,6 +101,7 @@ class TestBuildExportBody:
99
101
  assert body['config']['start_date'] == '2026-01-01'
100
102
  assert body['config']['end_date'] == '2026-03-01'
101
103
  assert body['config']['draft'] is True
104
+ assert body['config']['retest'] is True
102
105
  assert body['config']['version'] == '2.0'
103
106
  assert body['export_format'] == 'zip'
104
107
  assert body['group_by'] == 'tag'
@@ -12,15 +12,29 @@ class SearchCommands:
12
12
  if not args:
13
13
  self.console.print('[dim]Usage: search <term> [--kind <type>][/dim]')
14
14
  return
15
- term = ' '.join(args)
16
- try:
17
- kind = None
18
- if '--kind' in args:
19
- idx = args.index('--kind')
20
- if idx + 1 < len(args):
21
- kind = args[idx + 1]
22
- term = ' '.join(args[:idx])
23
15
 
16
+ # Parse args
17
+ raw_args = list(args)
18
+ kind = None
19
+ if '--kind' in raw_args:
20
+ idx = raw_args.index('--kind')
21
+ if idx + 1 < len(raw_args):
22
+ kind = raw_args[idx + 1]
23
+ raw_args = raw_args[:idx] + raw_args[idx + 2:]
24
+ term = ' '.join(raw_args)
25
+
26
+ # Context-smart: if we have paged results, filter them locally first
27
+ if self._paged_results and not kind:
28
+ filtered = [
29
+ r for r in self._paged_results
30
+ if term.lower() in (r.get('key', '') + ' ' + r.get('name', '') + ' ' + r.get('dns', '') + ' ' + r.get('title', '')).lower()
31
+ ]
32
+ if filtered:
33
+ self._render_results(filtered, f'Search: "{term}" in {self._paged_title}')
34
+ return
35
+
36
+ # Fall through to API search
37
+ try:
24
38
  results, offset = self.sdk.search.by_term(term, kind)
25
39
  self._render_results(results, f'Search: {term}')
26
40
  except Exception as e:
@@ -34,7 +48,7 @@ class SearchCommands:
34
48
  # Parse args
35
49
  term = []
36
50
  kind = None
37
- limit = 100
51
+ limit = 200
38
52
  i = 0
39
53
  while i < len(args):
40
54
  if args[i] == '--type' and i + 1 < len(args):
@@ -54,23 +68,30 @@ class SearchCommands:
54
68
  self.console.print(f'[error]{e}[/error]')
55
69
  return
56
70
 
57
- self._render_results(all_results, f'Find: {term}')
71
+ if not kind and all_results:
72
+ # Cross-type display: group by entity type and show type labels
73
+ self._render_typed_results(all_results, f'Find: {term}')
74
+ else:
75
+ self._render_results(all_results, f'Find: {term}')
76
+
58
77
  if len(all_results) >= limit:
59
- self.console.print(f'[warning]Showing {limit} results. Use --limit to increase.[/warning]')
78
+ self.console.print(f'[dim]Showing {limit} results. Use --limit to increase.[/dim]')
60
79
 
61
80
  def _cmd_assets(self, args):
62
81
  try:
63
- filter_text = self.context.scope or ''
82
+ filter_text = ' '.join(args) if args else (self.context.scope or '')
64
83
  results, _ = self.sdk.assets.list(filter_text, pages=1)
65
- self._render_results(results, 'Assets')
84
+ title = f'Assets: {filter_text}' if filter_text else 'Assets'
85
+ self._render_results(results, title)
66
86
  except Exception as e:
67
87
  self.console.print(f'[error]{e}[/error]')
68
88
 
69
89
  def _cmd_risks(self, args):
70
90
  try:
71
- filter_text = self.context.scope or ''
91
+ filter_text = ' '.join(args) if args else (self.context.scope or '')
72
92
  results, _ = self.sdk.risks.list(filter_text, pages=1)
73
- self._render_results(results, 'Risks')
93
+ title = f'Risks: {filter_text}' if filter_text else 'Risks'
94
+ self._render_results(results, title)
74
95
  except Exception as e:
75
96
  self.console.print(f'[error]{e}[/error]')
76
97
 
@@ -36,6 +36,7 @@ CONSOLE_COMMANDS = [
36
36
  'use', 'options', 'execute', 'exploit', 'back',
37
37
  'accounts', 'engagements', 'home', 'su',
38
38
  'search', 'find', 'assets', 'risks', 'jobs', 'info',
39
+ 'next', 'n', 'prev', 'p', 'page',
39
40
  'scan', 'tag',
40
41
  'run', 'status', 'download', 'install', 'installed',
41
42
  'asset-analyzer', 'brutus', 'julius', 'augustus', 'aurelius',
@@ -190,6 +191,11 @@ class GuardConsole(
190
191
  'risks': self._cmd_risks,
191
192
  'jobs': self._cmd_jobs,
192
193
  'info': self._cmd_info,
194
+ 'next': self._cmd_next,
195
+ 'n': self._cmd_next,
196
+ 'prev': self._cmd_prev,
197
+ 'p': self._cmd_prev,
198
+ 'page': self._cmd_page,
193
199
  'scan': self._cmd_scan,
194
200
  'tag': self._cmd_tag,
195
201
  'run': self._cmd_run,
@@ -289,11 +295,14 @@ class GuardConsole(
289
295
 
290
296
  help_table.add_row('', '')
291
297
  help_table.add_row('[section]Search & Recon[/section]', '')
292
- help_table.add_row('search <term>', 'Fast prefix search (DynamoDB)')
293
- help_table.add_row('find <term> [--type X]', 'Fulltext search (Neo4j)')
294
- help_table.add_row('assets', 'List assets (respects scope)')
295
- help_table.add_row('risks', 'List risks (respects scope)')
298
+ help_table.add_row('search <term>', 'Smart search (filters current results, or API search)')
299
+ help_table.add_row('find <term> [--type X]', 'Fulltext search across all entity types (Neo4j)')
300
+ help_table.add_row('assets [filter]', 'List assets (prefix filter or scope)')
301
+ help_table.add_row('risks [filter]', 'List risks (prefix filter or scope)')
296
302
  help_table.add_row('jobs [filter]', 'List jobs (filter by target/capability)')
303
+ help_table.add_row('next / n', 'Next page of results')
304
+ help_table.add_row('prev / p', 'Previous page of results')
305
+ help_table.add_row('page <#>', 'Jump to page number')
297
306
  help_table.add_row('status', 'Check status of last job')
298
307
  help_table.add_row('status <job_key>', 'Check status of specific job')
299
308
  help_table.add_row('download [proofs|agents|all]', 'Download job outputs to local dir')
@@ -9,22 +9,42 @@ from rich.text import Text
9
9
  class RendererMixin:
10
10
  """Rendering helpers for console output. Mixed into GuardConsole."""
11
11
 
12
- def _render_results(self, results: list, title: str):
12
+ # Pagination state
13
+ _paged_results = None # full result list
14
+ _paged_title = '' # title for the listing
15
+ _paged_page = 0 # current page (0-indexed)
16
+ _page_size = 50 # items per page
17
+
18
+ def _render_results(self, results: list, title: str, page: int = 0):
13
19
  if not results:
14
20
  self.console.print(f'[dim]No results for: {title}[/dim]')
15
21
  if self.context.scope:
16
22
  self.console.print(f'[dim]Current scope: {self.context.scope} -- use "unset scope" to broaden[/dim]')
17
23
  return
18
24
 
19
- table = Table(title=f'{title} ({len(results)} results)', border_style=self.colors['primary'])
20
- table.add_column('#', style=self.colors['dim'], width=4)
25
+ # Store for pagination
26
+ self._paged_results = results
27
+ self._paged_title = title
28
+ self._paged_page = page
29
+
30
+ total = len(results)
31
+ start = page * self._page_size
32
+ end = min(start + self._page_size, total)
33
+ page_items = results[start:end]
34
+ total_pages = (total + self._page_size - 1) // self._page_size
35
+
36
+ table = Table(
37
+ title=f'{title} ({total} results) — page {page + 1}/{total_pages}',
38
+ border_style=self.colors['primary'],
39
+ )
40
+ table.add_column('#', style=self.colors['dim'], width=6)
21
41
  table.add_column('Key', style=self.colors['primary'])
22
42
  table.add_column('Name', style='white')
23
43
  table.add_column('Status', style=self.colors['accent'])
24
44
 
25
45
  # Update _target_list so "set target <#>" works from any listing
26
46
  self._target_list = []
27
- for i, item in enumerate(results[:100], 1):
47
+ for i, item in enumerate(page_items, start + 1):
28
48
  key = item.get('key', '')
29
49
  name = item.get('name', item.get('dns', item.get('title', '')))
30
50
  status = item.get('status', '')
@@ -32,8 +52,133 @@ class RendererMixin:
32
52
  self._target_list.append(key)
33
53
 
34
54
  self.console.print(table)
35
- if len(results) > 100:
36
- self.console.print(f'[dim]Showing first 100 of {len(results)} results[/dim]')
55
+
56
+ # Pagination hint
57
+ hints = []
58
+ if page > 0:
59
+ hints.append('"prev" or "p"')
60
+ if end < total:
61
+ hints.append('"next" or "n"')
62
+ if total_pages > 2:
63
+ hints.append(f'"page <1-{total_pages}>"')
64
+ if hints:
65
+ self.console.print(f'[dim]{" | ".join(hints)}[/dim]')
66
+
67
+ def _cmd_next(self, args):
68
+ """Show next page of results."""
69
+ if not self._paged_results:
70
+ self.console.print('[dim]No results to page through.[/dim]')
71
+ return
72
+ total_pages = (len(self._paged_results) + self._page_size - 1) // self._page_size
73
+ if self._paged_page + 1 >= total_pages:
74
+ self.console.print('[dim]Already on the last page.[/dim]')
75
+ return
76
+ self._render_results(self._paged_results, self._paged_title, self._paged_page + 1)
77
+
78
+ def _cmd_prev(self, args):
79
+ """Show previous page of results."""
80
+ if not self._paged_results:
81
+ self.console.print('[dim]No results to page through.[/dim]')
82
+ return
83
+ if self._paged_page <= 0:
84
+ self.console.print('[dim]Already on the first page.[/dim]')
85
+ return
86
+ self._render_results(self._paged_results, self._paged_title, self._paged_page - 1)
87
+
88
+ def _cmd_page(self, args):
89
+ """Jump to a specific page: page <number>"""
90
+ if not self._paged_results:
91
+ self.console.print('[dim]No results to page through.[/dim]')
92
+ return
93
+ if not args:
94
+ self.console.print('[dim]Usage: page <number>[/dim]')
95
+ return
96
+ try:
97
+ page_num = int(args[0]) - 1 # 1-indexed input
98
+ except ValueError:
99
+ self.console.print('[dim]Usage: page <number>[/dim]')
100
+ return
101
+ total_pages = (len(self._paged_results) + self._page_size - 1) // self._page_size
102
+ if page_num < 0 or page_num >= total_pages:
103
+ self.console.print(f'[dim]Page must be between 1 and {total_pages}.[/dim]')
104
+ return
105
+ self._render_results(self._paged_results, self._paged_title, page_num)
106
+
107
+ def _render_typed_results(self, results: list, title: str):
108
+ """Render results with entity type labels — used for cross-type searches."""
109
+ if not results:
110
+ self.console.print(f'[dim]No results for: {title}[/dim]')
111
+ return
112
+
113
+ # Store for pagination
114
+ self._paged_results = results
115
+ self._paged_title = title
116
+ self._paged_page = 0
117
+
118
+ total = len(results)
119
+ page_items = results[:self._page_size]
120
+ total_pages = (total + self._page_size - 1) // self._page_size
121
+
122
+ table = Table(
123
+ title=f'{title} ({total} results) — page 1/{total_pages}',
124
+ border_style=self.colors['primary'],
125
+ )
126
+ table.add_column('#', style=self.colors['dim'], width=6)
127
+ table.add_column('Type', style=self.colors['accent'], width=10)
128
+ table.add_column('Key', style=self.colors['primary'])
129
+ table.add_column('Name', style='white')
130
+ table.add_column('Status', style=self.colors['accent'])
131
+
132
+ self._target_list = []
133
+ for i, item in enumerate(page_items, 1):
134
+ key = item.get('key', '')
135
+ name = item.get('name', item.get('dns', item.get('title', '')))
136
+ status = item.get('status', '')
137
+
138
+ # Infer type from key
139
+ entity_type = '?'
140
+ if key.startswith('#risk#'):
141
+ entity_type = 'risk'
142
+ elif key.startswith('#asset#'):
143
+ entity_type = 'asset'
144
+ elif key.startswith('#ad'):
145
+ # AD subtypes: aduser, adgroup, adcomputer, addomain, adenterpriseca, etc.
146
+ parts = key.split('#')
147
+ entity_type = parts[1] if len(parts) > 1 else 'ad'
148
+ elif key.startswith('#seed#'):
149
+ entity_type = 'seed'
150
+ elif key.startswith('#job#'):
151
+ entity_type = 'job'
152
+ elif key.startswith('#port#') or key.startswith('#attribute#'):
153
+ parts = key.split('#')
154
+ entity_type = parts[1] if len(parts) > 1 else 'other'
155
+ else:
156
+ parts = key.split('#')
157
+ entity_type = parts[1] if len(parts) > 2 else 'other'
158
+
159
+ table.add_row(str(i), entity_type, key, str(name), status)
160
+ self._target_list.append(key)
161
+
162
+ self.console.print(table)
163
+
164
+ # Type summary
165
+ from collections import Counter
166
+ type_counts = Counter()
167
+ for item in results:
168
+ key = item.get('key', '')
169
+ parts = key.split('#')
170
+ t = parts[1] if len(parts) > 2 else 'other'
171
+ type_counts[t] += 1
172
+ summary = ', '.join(f'{count} {t}' for t, count in type_counts.most_common())
173
+ self.console.print(f'[dim]{summary}[/dim]')
174
+
175
+ hints = []
176
+ if total_pages > 1:
177
+ hints.append('"next" or "n"')
178
+ if total_pages > 2:
179
+ hints.append(f'"page <1-{total_pages}>"')
180
+ if hints:
181
+ self.console.print(f'[dim]{" | ".join(hints)}[/dim]')
37
182
 
38
183
  def _render_evidence(self, hydrated: dict):
39
184
  risk = hydrated.get('risk', {})
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: praetorian-cli
3
- Version: 2.4.0
3
+ Version: 2.4.1
4
4
  Summary: For interacting with the Guard API
5
5
  Home-page: https://github.com/praetorian-inc/praetorian-cli
6
6
  Author: Praetorian
@@ -1,6 +1,6 @@
1
1
  [metadata]
2
2
  name = praetorian-cli
3
- version = 2.4.0
3
+ version = 2.4.1
4
4
  author = Praetorian
5
5
  author_email = support@praetorian.com
6
6
  description = For interacting with the Guard API
File without changes
File without changes