praetorian-cli 2.3.3__tar.gz → 2.3.5__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 (122) hide show
  1. {praetorian_cli-2.3.3/praetorian_cli.egg-info → praetorian_cli-2.3.5}/PKG-INFO +1 -1
  2. {praetorian_cli-2.3.3 → praetorian_cli-2.3.5}/praetorian_cli/handlers/add.py +4 -2
  3. praetorian_cli-2.3.5/praetorian_cli/handlers/export.py +102 -0
  4. {praetorian_cli-2.3.3 → praetorian_cli-2.3.5}/praetorian_cli/handlers/link.py +8 -6
  5. {praetorian_cli-2.3.3 → praetorian_cli-2.3.5}/praetorian_cli/handlers/update.py +4 -2
  6. {praetorian_cli-2.3.3 → praetorian_cli-2.3.5}/praetorian_cli/main.py +1 -0
  7. {praetorian_cli-2.3.3 → praetorian_cli-2.3.5}/praetorian_cli/sdk/chariot.py +7 -2
  8. {praetorian_cli-2.3.3 → praetorian_cli-2.3.5}/praetorian_cli/sdk/entities/accounts.py +4 -2
  9. praetorian_cli-2.3.5/praetorian_cli/sdk/entities/reports.py +194 -0
  10. {praetorian_cli-2.3.3 → praetorian_cli-2.3.5}/praetorian_cli/sdk/entities/risks.py +10 -2
  11. {praetorian_cli-2.3.3 → praetorian_cli-2.3.5}/praetorian_cli/sdk/entities/webhook.py +1 -1
  12. {praetorian_cli-2.3.3 → praetorian_cli-2.3.5}/praetorian_cli/sdk/test/test_job.py +8 -0
  13. praetorian_cli-2.3.5/praetorian_cli/sdk/test/test_report.py +242 -0
  14. {praetorian_cli-2.3.3 → praetorian_cli-2.3.5}/praetorian_cli/sdk/test/test_risk.py +14 -0
  15. {praetorian_cli-2.3.3 → praetorian_cli-2.3.5}/praetorian_cli/sdk/test/test_z_cli.py +47 -6
  16. {praetorian_cli-2.3.3 → praetorian_cli-2.3.5/praetorian_cli.egg-info}/PKG-INFO +1 -1
  17. {praetorian_cli-2.3.3 → praetorian_cli-2.3.5}/praetorian_cli.egg-info/SOURCES.txt +3 -0
  18. {praetorian_cli-2.3.3 → praetorian_cli-2.3.5}/setup.cfg +1 -1
  19. {praetorian_cli-2.3.3 → praetorian_cli-2.3.5}/LICENSE +0 -0
  20. {praetorian_cli-2.3.3 → praetorian_cli-2.3.5}/MANIFEST.in +0 -0
  21. {praetorian_cli-2.3.3 → praetorian_cli-2.3.5}/README.md +0 -0
  22. {praetorian_cli-2.3.3 → praetorian_cli-2.3.5}/praetorian_cli/__init__.py +0 -0
  23. {praetorian_cli-2.3.3 → praetorian_cli-2.3.5}/praetorian_cli/handlers/__init__.py +0 -0
  24. {praetorian_cli-2.3.3 → praetorian_cli-2.3.5}/praetorian_cli/handlers/aegis.py +0 -0
  25. {praetorian_cli-2.3.3 → praetorian_cli-2.3.5}/praetorian_cli/handlers/agent.py +0 -0
  26. {praetorian_cli-2.3.3 → praetorian_cli-2.3.5}/praetorian_cli/handlers/chariot.py +0 -0
  27. {praetorian_cli-2.3.3 → praetorian_cli-2.3.5}/praetorian_cli/handlers/cli_decorators.py +0 -0
  28. {praetorian_cli-2.3.3 → praetorian_cli-2.3.5}/praetorian_cli/handlers/configure.py +0 -0
  29. {praetorian_cli-2.3.3 → praetorian_cli-2.3.5}/praetorian_cli/handlers/delete.py +0 -0
  30. {praetorian_cli-2.3.3 → praetorian_cli-2.3.5}/praetorian_cli/handlers/enrich.py +0 -0
  31. {praetorian_cli-2.3.3 → praetorian_cli-2.3.5}/praetorian_cli/handlers/get.py +0 -0
  32. {praetorian_cli-2.3.3 → praetorian_cli-2.3.5}/praetorian_cli/handlers/imports.py +0 -0
  33. {praetorian_cli-2.3.3 → praetorian_cli-2.3.5}/praetorian_cli/handlers/list.py +0 -0
  34. {praetorian_cli-2.3.3 → praetorian_cli-2.3.5}/praetorian_cli/handlers/script.py +0 -0
  35. {praetorian_cli-2.3.3 → praetorian_cli-2.3.5}/praetorian_cli/handlers/search.py +0 -0
  36. {praetorian_cli-2.3.3 → praetorian_cli-2.3.5}/praetorian_cli/handlers/ssh_utils.py +0 -0
  37. {praetorian_cli-2.3.3 → praetorian_cli-2.3.5}/praetorian_cli/handlers/test.py +0 -0
  38. {praetorian_cli-2.3.3 → praetorian_cli-2.3.5}/praetorian_cli/handlers/unlink.py +0 -0
  39. {praetorian_cli-2.3.3 → praetorian_cli-2.3.5}/praetorian_cli/handlers/utils.py +0 -0
  40. {praetorian_cli-2.3.3 → praetorian_cli-2.3.5}/praetorian_cli/scripts/__init__.py +0 -0
  41. {praetorian_cli-2.3.3 → praetorian_cli-2.3.5}/praetorian_cli/scripts/commands/__init__.py +0 -0
  42. {praetorian_cli-2.3.3 → praetorian_cli-2.3.5}/praetorian_cli/scripts/commands/nmap-example.py +0 -0
  43. {praetorian_cli-2.3.3 → praetorian_cli-2.3.5}/praetorian_cli/scripts/utils.py +0 -0
  44. {praetorian_cli-2.3.3 → praetorian_cli-2.3.5}/praetorian_cli/sdk/__init__.py +0 -0
  45. {praetorian_cli-2.3.3 → praetorian_cli-2.3.5}/praetorian_cli/sdk/entities/__init__.py +0 -0
  46. {praetorian_cli-2.3.3 → praetorian_cli-2.3.5}/praetorian_cli/sdk/entities/ad.py +0 -0
  47. {praetorian_cli-2.3.3 → praetorian_cli-2.3.5}/praetorian_cli/sdk/entities/aegis.py +0 -0
  48. {praetorian_cli-2.3.3 → praetorian_cli-2.3.5}/praetorian_cli/sdk/entities/agents.py +0 -0
  49. {praetorian_cli-2.3.3 → praetorian_cli-2.3.5}/praetorian_cli/sdk/entities/assets.py +0 -0
  50. {praetorian_cli-2.3.3 → praetorian_cli-2.3.5}/praetorian_cli/sdk/entities/attributes.py +0 -0
  51. {praetorian_cli-2.3.3 → praetorian_cli-2.3.5}/praetorian_cli/sdk/entities/capabilities.py +0 -0
  52. {praetorian_cli-2.3.3 → praetorian_cli-2.3.5}/praetorian_cli/sdk/entities/configurations.py +0 -0
  53. {praetorian_cli-2.3.3 → praetorian_cli-2.3.5}/praetorian_cli/sdk/entities/credentials.py +0 -0
  54. {praetorian_cli-2.3.3 → praetorian_cli-2.3.5}/praetorian_cli/sdk/entities/definitions.py +0 -0
  55. {praetorian_cli-2.3.3 → praetorian_cli-2.3.5}/praetorian_cli/sdk/entities/files.py +0 -0
  56. {praetorian_cli-2.3.3 → praetorian_cli-2.3.5}/praetorian_cli/sdk/entities/integrations.py +0 -0
  57. {praetorian_cli-2.3.3 → praetorian_cli-2.3.5}/praetorian_cli/sdk/entities/jobs.py +0 -0
  58. {praetorian_cli-2.3.3 → praetorian_cli-2.3.5}/praetorian_cli/sdk/entities/keys.py +0 -0
  59. {praetorian_cli-2.3.3 → praetorian_cli-2.3.5}/praetorian_cli/sdk/entities/preseeds.py +0 -0
  60. {praetorian_cli-2.3.3 → praetorian_cli-2.3.5}/praetorian_cli/sdk/entities/scanners.py +0 -0
  61. {praetorian_cli-2.3.3 → praetorian_cli-2.3.5}/praetorian_cli/sdk/entities/schedules.py +0 -0
  62. {praetorian_cli-2.3.3 → praetorian_cli-2.3.5}/praetorian_cli/sdk/entities/schema.py +0 -0
  63. {praetorian_cli-2.3.3 → praetorian_cli-2.3.5}/praetorian_cli/sdk/entities/search.py +0 -0
  64. {praetorian_cli-2.3.3 → praetorian_cli-2.3.5}/praetorian_cli/sdk/entities/seeds.py +0 -0
  65. {praetorian_cli-2.3.3 → praetorian_cli-2.3.5}/praetorian_cli/sdk/entities/settings.py +0 -0
  66. {praetorian_cli-2.3.3 → praetorian_cli-2.3.5}/praetorian_cli/sdk/entities/statistics.py +0 -0
  67. {praetorian_cli-2.3.3 → praetorian_cli-2.3.5}/praetorian_cli/sdk/entities/webpage.py +0 -0
  68. {praetorian_cli-2.3.3 → praetorian_cli-2.3.5}/praetorian_cli/sdk/guard.py +0 -0
  69. {praetorian_cli-2.3.3 → praetorian_cli-2.3.5}/praetorian_cli/sdk/keychain.py +0 -0
  70. {praetorian_cli-2.3.3 → praetorian_cli-2.3.5}/praetorian_cli/sdk/mcp_server.py +0 -0
  71. {praetorian_cli-2.3.3 → praetorian_cli-2.3.5}/praetorian_cli/sdk/model/__init__.py +0 -0
  72. {praetorian_cli-2.3.3 → praetorian_cli-2.3.5}/praetorian_cli/sdk/model/aegis.py +0 -0
  73. {praetorian_cli-2.3.3 → praetorian_cli-2.3.5}/praetorian_cli/sdk/model/globals.py +0 -0
  74. {praetorian_cli-2.3.3 → praetorian_cli-2.3.5}/praetorian_cli/sdk/model/query.py +0 -0
  75. {praetorian_cli-2.3.3 → praetorian_cli-2.3.5}/praetorian_cli/sdk/model/utils.py +0 -0
  76. {praetorian_cli-2.3.3 → praetorian_cli-2.3.5}/praetorian_cli/sdk/test/__init__.py +0 -0
  77. {praetorian_cli-2.3.3 → praetorian_cli-2.3.5}/praetorian_cli/sdk/test/pytest.ini +0 -0
  78. {praetorian_cli-2.3.3 → praetorian_cli-2.3.5}/praetorian_cli/sdk/test/test_account.py +0 -0
  79. {praetorian_cli-2.3.3 → praetorian_cli-2.3.5}/praetorian_cli/sdk/test/test_agent.py +0 -0
  80. {praetorian_cli-2.3.3 → praetorian_cli-2.3.5}/praetorian_cli/sdk/test/test_asset.py +0 -0
  81. {praetorian_cli-2.3.3 → praetorian_cli-2.3.5}/praetorian_cli/sdk/test/test_attribute.py +0 -0
  82. {praetorian_cli-2.3.3 → praetorian_cli-2.3.5}/praetorian_cli/sdk/test/test_capabilities.py +0 -0
  83. {praetorian_cli-2.3.3 → praetorian_cli-2.3.5}/praetorian_cli/sdk/test/test_configuration.py +0 -0
  84. {praetorian_cli-2.3.3 → praetorian_cli-2.3.5}/praetorian_cli/sdk/test/test_conversation.py +0 -0
  85. {praetorian_cli-2.3.3 → praetorian_cli-2.3.5}/praetorian_cli/sdk/test/test_definition.py +0 -0
  86. {praetorian_cli-2.3.3 → praetorian_cli-2.3.5}/praetorian_cli/sdk/test/test_extend.py +0 -0
  87. {praetorian_cli-2.3.3 → praetorian_cli-2.3.5}/praetorian_cli/sdk/test/test_file.py +0 -0
  88. {praetorian_cli-2.3.3 → praetorian_cli-2.3.5}/praetorian_cli/sdk/test/test_key.py +0 -0
  89. {praetorian_cli-2.3.3 → praetorian_cli-2.3.5}/praetorian_cli/sdk/test/test_mcp.py +0 -0
  90. {praetorian_cli-2.3.3 → praetorian_cli-2.3.5}/praetorian_cli/sdk/test/test_preseed.py +0 -0
  91. {praetorian_cli-2.3.3 → praetorian_cli-2.3.5}/praetorian_cli/sdk/test/test_search.py +0 -0
  92. {praetorian_cli-2.3.3 → praetorian_cli-2.3.5}/praetorian_cli/sdk/test/test_seed.py +0 -0
  93. {praetorian_cli-2.3.3 → praetorian_cli-2.3.5}/praetorian_cli/sdk/test/test_setting.py +0 -0
  94. {praetorian_cli-2.3.3 → praetorian_cli-2.3.5}/praetorian_cli/sdk/test/test_webhook.py +0 -0
  95. {praetorian_cli-2.3.3 → praetorian_cli-2.3.5}/praetorian_cli/sdk/test/test_webpage.py +0 -0
  96. {praetorian_cli-2.3.3 → praetorian_cli-2.3.5}/praetorian_cli/sdk/test/ui_mocks.py +0 -0
  97. {praetorian_cli-2.3.3 → praetorian_cli-2.3.5}/praetorian_cli/sdk/test/utils.py +0 -0
  98. {praetorian_cli-2.3.3 → praetorian_cli-2.3.5}/praetorian_cli/ui/__init__.py +0 -0
  99. {praetorian_cli-2.3.3 → praetorian_cli-2.3.5}/praetorian_cli/ui/aegis/__init__.py +0 -0
  100. {praetorian_cli-2.3.3 → praetorian_cli-2.3.5}/praetorian_cli/ui/aegis/commands/__init__.py +0 -0
  101. {praetorian_cli-2.3.3 → praetorian_cli-2.3.5}/praetorian_cli/ui/aegis/commands/cp.py +0 -0
  102. {praetorian_cli-2.3.3 → praetorian_cli-2.3.5}/praetorian_cli/ui/aegis/commands/help.py +0 -0
  103. {praetorian_cli-2.3.3 → praetorian_cli-2.3.5}/praetorian_cli/ui/aegis/commands/info.py +0 -0
  104. {praetorian_cli-2.3.3 → praetorian_cli-2.3.5}/praetorian_cli/ui/aegis/commands/job.py +0 -0
  105. {praetorian_cli-2.3.3 → praetorian_cli-2.3.5}/praetorian_cli/ui/aegis/commands/job_helpers.py +0 -0
  106. {praetorian_cli-2.3.3 → praetorian_cli-2.3.5}/praetorian_cli/ui/aegis/commands/list.py +0 -0
  107. {praetorian_cli-2.3.3 → praetorian_cli-2.3.5}/praetorian_cli/ui/aegis/commands/proxy.py +0 -0
  108. {praetorian_cli-2.3.3 → praetorian_cli-2.3.5}/praetorian_cli/ui/aegis/commands/schedule.py +0 -0
  109. {praetorian_cli-2.3.3 → praetorian_cli-2.3.5}/praetorian_cli/ui/aegis/commands/schedule_helpers.py +0 -0
  110. {praetorian_cli-2.3.3 → praetorian_cli-2.3.5}/praetorian_cli/ui/aegis/commands/set.py +0 -0
  111. {praetorian_cli-2.3.3 → praetorian_cli-2.3.5}/praetorian_cli/ui/aegis/commands/ssh.py +0 -0
  112. {praetorian_cli-2.3.3 → praetorian_cli-2.3.5}/praetorian_cli/ui/aegis/constants.py +0 -0
  113. {praetorian_cli-2.3.3 → praetorian_cli-2.3.5}/praetorian_cli/ui/aegis/menu.py +0 -0
  114. {praetorian_cli-2.3.3 → praetorian_cli-2.3.5}/praetorian_cli/ui/aegis/theme.py +0 -0
  115. {praetorian_cli-2.3.3 → praetorian_cli-2.3.5}/praetorian_cli/ui/aegis/utils.py +0 -0
  116. {praetorian_cli-2.3.3 → praetorian_cli-2.3.5}/praetorian_cli/ui/conversation/__init__.py +0 -0
  117. {praetorian_cli-2.3.3 → praetorian_cli-2.3.5}/praetorian_cli/ui/conversation/textual_chat.py +0 -0
  118. {praetorian_cli-2.3.3 → praetorian_cli-2.3.5}/praetorian_cli.egg-info/dependency_links.txt +0 -0
  119. {praetorian_cli-2.3.3 → praetorian_cli-2.3.5}/praetorian_cli.egg-info/entry_points.txt +0 -0
  120. {praetorian_cli-2.3.3 → praetorian_cli-2.3.5}/praetorian_cli.egg-info/requires.txt +0 -0
  121. {praetorian_cli-2.3.3 → praetorian_cli-2.3.5}/praetorian_cli.egg-info/top_level.txt +0 -0
  122. {praetorian_cli-2.3.3 → praetorian_cli-2.3.5}/pyproject.toml +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: praetorian-cli
3
- Version: 2.3.3
3
+ Version: 2.3.5
4
4
  Summary: For interacting with the Guard API
5
5
  Home-page: https://github.com/praetorian-inc/praetorian-cli
6
6
  Author: Praetorian
@@ -145,7 +145,8 @@ def webhook(sdk):
145
145
  @click.option('-c', '--comment', default='', help='Comment for the risk')
146
146
  @click.option('-cap', '--capability', default='', help='Capability that discoverd the risk')
147
147
  @click.option('--title', '-t', default=None, help='Human-readable title for the risk')
148
- def risk(sdk, name, asset, status, comment, capability, title):
148
+ @click.option('-g', '--tag', 'tags', multiple=True, help='Tag for the risk (can be specified multiple times)')
149
+ def risk(sdk, name, asset, status, comment, capability, title, tags):
149
150
  """ Add a risk
150
151
 
151
152
  This command adds a risk to Guard. A risk must have an associated asset.
@@ -161,8 +162,9 @@ def risk(sdk, name, asset, status, comment, capability, title):
161
162
  - guard add risk CVE-2024-23049 --asset "#asset#example.com#1.2.3.4" --status TI
162
163
  - guard add risk CVE-2024-23049 --asset "#asset#example.com#1.2.3.4" --status TC
163
164
  - guard add risk CVE-2024-23049 --asset "#asset#example.com#1.2.3.4" --status TC --capability red-team
165
+ - guard add risk CVE-2024-23049 --asset "#asset#example.com#1.2.3.4" --status TI --tag critical --tag needs-review
164
166
  """
165
- sdk.risks.add(asset, name, status, comment, capability, title)
167
+ sdk.risks.add(asset, name, status, comment, capability, title, tags)
166
168
 
167
169
 
168
170
  @add.command()
@@ -0,0 +1,102 @@
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
+ @chariot.group()
10
+ def export():
11
+ """ Export data from Guard """
12
+ pass
13
+
14
+
15
+ @export.command()
16
+ @cli_handler
17
+ @click.option('--title', required=True, help='Report title')
18
+ @click.option('--client-name', required=True, help='Client organization name')
19
+ @click.option('--status-filter', multiple=True, default=('O', 'T'),
20
+ help='Risk status filter (repeatable). Default: O T')
21
+ @click.option('--risk-keys', multiple=True, default=(),
22
+ help='Specific risk keys to include (repeatable). Default: all')
23
+ @click.option('--target', default='', help='Target/scope (e.g., example.com)')
24
+ @click.option('--start-date', default='', help='Engagement start date (ISO format)')
25
+ @click.option('--end-date', default='', help='Engagement end date (ISO format)')
26
+ @click.option('--report-date', default='', help='Report date (ISO format). Default: today')
27
+ @click.option('--draft/--no-draft', default=False, help='Add DRAFT watermark')
28
+ @click.option('--version', 'report_version', default='1.0', help='Report version string')
29
+ @click.option('--format', 'export_format', type=click.Choice(['pdf', 'zip']),
30
+ default='pdf', help='Export format')
31
+ @click.option('--group-by', type=click.Choice(['attack_surface', 'tag']),
32
+ default='attack_surface', help='Finding grouping strategy')
33
+ @click.option('--shared/--no-shared', default=False,
34
+ help='Copy report to customer shared files')
35
+ @click.option('--executive-summary', default='',
36
+ help='Path to executive summary .md file in Guard storage')
37
+ @click.option('--narratives', default='',
38
+ help='Path to narratives .md file in Guard storage')
39
+ @click.option('--appendix', default='',
40
+ help='Path to appendix .md file in Guard storage')
41
+ @click.option('--output', '-o', default=os.getcwd(),
42
+ help='Local directory to save the downloaded report')
43
+ @click.option('--timeout', default=300, type=int,
44
+ help='Max seconds to wait for report generation. Default: 300')
45
+ @click.option('--no-download', is_flag=True, default=False,
46
+ help='Skip downloading the file; just print the job result')
47
+ def report(chariot, title, client_name, status_filter, risk_keys,
48
+ target, start_date, end_date, report_date, draft,
49
+ report_version, export_format, group_by, shared,
50
+ executive_summary, narratives, appendix, output,
51
+ timeout, no_download):
52
+ """ Generate and download a PDF or ZIP report.
53
+
54
+ Requires Praetorian engineer access. Initiates report generation,
55
+ polls until the job completes, then downloads the file.
56
+
57
+ The customer email is derived automatically from --account (the customer
58
+ you are viewing) or the logged-in user's email.
59
+
60
+ \b
61
+ Example usages:
62
+ guard --account customer@acme.com export report \\
63
+ --title "Pentest Report" --client-name "Acme"
64
+
65
+ guard --account customer@acme.com export report \\
66
+ --title "Q1 Assessment" --client-name "Acme" \\
67
+ --format zip --draft \\
68
+ --status-filter O --status-filter T --status-filter R
69
+ """
70
+ customer_email = chariot.reports.customer_email()
71
+
72
+ body = chariot.reports.build_export_body(
73
+ title=title,
74
+ client_name=client_name,
75
+ customer_email=customer_email,
76
+ status_filter=status_filter,
77
+ risk_keys=risk_keys,
78
+ target=target,
79
+ start_date=start_date,
80
+ end_date=end_date,
81
+ report_date=report_date,
82
+ draft=draft,
83
+ version=report_version,
84
+ export_format=export_format,
85
+ group_by=group_by,
86
+ shared_output=shared,
87
+ executive_summary_path=executive_summary,
88
+ narratives_path=narratives,
89
+ appendix_path=appendix,
90
+ )
91
+
92
+ click.echo(f'Starting {export_format.upper()} report generation for {customer_email}...')
93
+ job = chariot.reports.export(body, timeout=timeout)
94
+ click.echo('Report generation complete.')
95
+
96
+ if no_download:
97
+ click.echo(f'Output path: {chariot.reports.output_path(job)}')
98
+ return
99
+
100
+ click.echo(f'Downloading {chariot.reports.output_path(job)}...')
101
+ local_path = chariot.reports.download(job, output)
102
+ click.echo(f'Saved to {local_path}')
@@ -13,7 +13,10 @@ def link():
13
13
  @link.command()
14
14
  @cli_handler
15
15
  @click.argument('username')
16
- def account(chariot, username):
16
+ @click.option('--role', '-r', required=True,
17
+ type=click.Choice(['admin', 'analyst', 'readonly'], case_sensitive=False),
18
+ help='Role to assign to the collaborator.')
19
+ def account(chariot, username, role):
17
20
  """ Add a collaborator account to your account
18
21
 
19
22
  This allows them to assume access into your account
@@ -21,15 +24,14 @@ def account(chariot, username):
21
24
 
22
25
  \b
23
26
  Arguments:
24
- - NAME: their email address
25
-
26
-
27
+ - USERNAME: their email address
27
28
 
28
29
  \b
29
30
  Example usages:
30
- - guard link account john@praetorian.com
31
+ - guard link account john@praetorian.com --role admin
32
+ - guard link account analyst@example.com -r readonly
31
33
  """
32
- chariot.accounts.add_collaborator(username)
34
+ chariot.accounts.add_collaborator(username, role=role)
33
35
 
34
36
 
35
37
  @link.command('webpage-source')
@@ -38,7 +38,8 @@ def asset(chariot, key, status, surface):
38
38
  @click.option('-c', '--comment', default='', help='Comment for the risk')
39
39
  @click.option('-r', '--remove-comment', type=int, default=None, help='Remove comment at index (0, 1, ... or -1 for most recent)')
40
40
  @click.option('--title', '-t', default=None, help='Human-readable title for the risk')
41
- def risk(chariot, key, status, comment, remove_comment, title):
41
+ @click.option('-g', '--tag', 'tags', multiple=True, help='Tag for the risk (can be specified multiple times)')
42
+ def risk(chariot, key, status, comment, remove_comment, title, tags):
42
43
  """ Update the status and comment of a risk
43
44
 
44
45
  \b
@@ -51,11 +52,12 @@ def risk(chariot, key, status, comment, remove_comment, title):
51
52
  - guard update risk "#risk#www.example.com#open-ssh-port" --status RH --comment "John stopped sshd on the server"
52
53
  - guard update risk "#risk#www.example.com#CVE-2024-23049" --remove-comment 0
53
54
  - guard update risk "#risk#www.example.com#CVE-2024-23049" --remove-comment -1
55
+ - guard update risk "#risk#www.example.com#CVE-2024-23049" --tag resolved --tag verified
54
56
  """
55
57
  if comment and remove_comment is not None:
56
58
  raise click.UsageError("Cannot use --comment and --remove-comment together")
57
59
 
58
- chariot.risks.update(key, status, comment, remove_comment, title)
60
+ chariot.risks.update(key, status, comment, remove_comment, title, tags)
59
61
 
60
62
 
61
63
  @update.command()
@@ -5,6 +5,7 @@ import praetorian_cli.handlers.aegis
5
5
  import praetorian_cli.handlers.agent
6
6
  import praetorian_cli.handlers.delete
7
7
  import praetorian_cli.handlers.enrich
8
+ import praetorian_cli.handlers.export
8
9
  import praetorian_cli.handlers.get
9
10
  import praetorian_cli.handlers.imports
10
11
  import praetorian_cli.handlers.link
@@ -15,6 +15,7 @@ from praetorian_cli.sdk.entities.integrations import Integrations
15
15
  from praetorian_cli.sdk.entities.jobs import Jobs
16
16
  from praetorian_cli.sdk.entities.keys import Keys
17
17
  from praetorian_cli.sdk.entities.preseeds import Preseeds
18
+ from praetorian_cli.sdk.entities.reports import Reports
18
19
  from praetorian_cli.sdk.entities.risks import Risks
19
20
  from praetorian_cli.sdk.entities.scanners import Scanners
20
21
  from praetorian_cli.sdk.entities.schedules import Schedules
@@ -37,6 +38,7 @@ class Chariot:
37
38
  self.assets = Assets(self)
38
39
  self.seeds = Seeds(self)
39
40
  self.preseeds = Preseeds(self)
41
+ self.reports = Reports(self)
40
42
  self.risks = Risks(self)
41
43
  self.accounts = Accounts(self)
42
44
  self.integrations = Integrations(self)
@@ -183,8 +185,11 @@ class Chariot:
183
185
  def upsert(self, type: str, body: dict, params: dict = {}) -> dict:
184
186
  return self.put(type, body, params)
185
187
 
186
- def link_account(self, username: str, value: str = '', config: dict = {}) -> dict:
187
- resp = self.chariot_request('POST', self.url(f'/account/{username}'), json=dict(config=config, value=value))
188
+ def link_account(self, username: str, role: str = '', value: str = '', config: dict = {}) -> dict:
189
+ body = dict(config=config, value=value)
190
+ if role:
191
+ body['role'] = role
192
+ resp = self.chariot_request('POST', self.url(f'/account/{username}'), json=body)
188
193
  process_failure(resp)
189
194
  return resp.json()
190
195
 
@@ -43,16 +43,18 @@ class Accounts:
43
43
 
44
44
  return results, next_offset
45
45
 
46
- def add_collaborator(self, collaborator_email):
46
+ def add_collaborator(self, collaborator_email, role=''):
47
47
  """
48
48
  Add a collaborator to the account of the current principal.
49
49
 
50
50
  :param collaborator_email: Email address of the collaborator to add
51
51
  :type collaborator_email: str
52
+ :param role: Role to assign to the collaborator (admin, analyst, or readonly)
53
+ :type role: str
52
54
  :return: The created account entity with member information
53
55
  :rtype: dict
54
56
  """
55
- return self.api.link_account(collaborator_email)
57
+ return self.api.link_account(collaborator_email, role=role)
56
58
 
57
59
  def delete_collaborator(self, collaborator_email):
58
60
  """
@@ -0,0 +1,194 @@
1
+ from datetime import date
2
+ from time import sleep, time
3
+
4
+
5
+ class Reports:
6
+ """ The methods in this class are to be accessed from sdk.reports, where sdk
7
+ is an instance of Chariot. """
8
+
9
+ POLL_INTERVAL = 5
10
+ DEFAULT_TIMEOUT = 300
11
+
12
+ def __init__(self, api):
13
+ self.api = api
14
+
15
+ def customer_email(self):
16
+ """
17
+ Derive the customer email from the current SDK context.
18
+
19
+ Uses the --account (assumed account) if set, otherwise falls back
20
+ to the logged-in user's email. This mirrors the frontend logic of
21
+ ``friend || me``.
22
+
23
+ :return: The customer email address
24
+ :rtype: str
25
+ :raises Exception: If no customer email can be determined
26
+ """
27
+ email = self.api.keychain.account or self.api.keychain.username()
28
+ if not email:
29
+ raise Exception(
30
+ 'Could not determine customer email. '
31
+ 'Use --account or configure a username.'
32
+ )
33
+ return email
34
+
35
+ def build_export_body(self, title, client_name, customer_email,
36
+ status_filter=('O', 'T'), risk_keys=(),
37
+ target='', start_date='', end_date='',
38
+ report_date='', draft=False, version='1.0',
39
+ export_format='pdf', group_by='attack_surface',
40
+ shared_output=False, executive_summary_path='',
41
+ narratives_path='', appendix_path=''):
42
+ """
43
+ Build the request body for POST /export/report.
44
+
45
+ :param title: Report title
46
+ :type title: str
47
+ :param client_name: Client organization name
48
+ :type client_name: str
49
+ :param customer_email: Customer email address
50
+ :type customer_email: str
51
+ :param status_filter: Risk status codes to include
52
+ :type status_filter: tuple or list
53
+ :param risk_keys: Specific risk keys to include (empty for all)
54
+ :type risk_keys: tuple or list
55
+ :param target: Target/scope
56
+ :type target: str
57
+ :param start_date: Engagement start date (ISO format)
58
+ :type start_date: str
59
+ :param end_date: Engagement end date (ISO format)
60
+ :type end_date: str
61
+ :param report_date: Report date (ISO format). Defaults to today.
62
+ :type report_date: str
63
+ :param draft: Whether to add DRAFT watermark
64
+ :type draft: bool
65
+ :param version: Report version string
66
+ :type version: str
67
+ :param export_format: Output format ('pdf' or 'zip')
68
+ :type export_format: str
69
+ :param group_by: Finding grouping strategy ('attack_surface' or 'tag')
70
+ :type group_by: str
71
+ :param shared_output: Whether to copy to customer shared files
72
+ :type shared_output: bool
73
+ :param executive_summary_path: Path to executive summary .md in Guard storage
74
+ :type executive_summary_path: str
75
+ :param narratives_path: Path to narratives .md in Guard storage
76
+ :type narratives_path: str
77
+ :param appendix_path: Path to appendix .md in Guard storage
78
+ :type appendix_path: str
79
+ :return: Request body dict ready for POST /export/report
80
+ :rtype: dict
81
+ """
82
+ if not report_date:
83
+ report_date = date.today().isoformat()
84
+
85
+ body = {
86
+ 'status_filter': list(status_filter),
87
+ 'config': {
88
+ 'title': title,
89
+ 'client_name': client_name,
90
+ 'report_date': report_date,
91
+ 'draft': draft,
92
+ 'version': version,
93
+ },
94
+ 'shared_output': shared_output,
95
+ 'customer_email': customer_email,
96
+ 'export_format': export_format,
97
+ 'group_by': group_by,
98
+ }
99
+
100
+ if risk_keys:
101
+ body['risk_keys'] = list(risk_keys)
102
+ if target:
103
+ body['config']['target'] = target
104
+ if start_date:
105
+ body['config']['start_date'] = start_date
106
+ if end_date:
107
+ body['config']['end_date'] = end_date
108
+ if executive_summary_path:
109
+ body['executive_summary_path'] = executive_summary_path
110
+ if narratives_path:
111
+ body['narratives_path'] = narratives_path
112
+ if appendix_path:
113
+ body['appendix_path'] = appendix_path
114
+
115
+ return body
116
+
117
+ def export(self, body, timeout=DEFAULT_TIMEOUT, poll_interval=POLL_INTERVAL):
118
+ """
119
+ Start a report export job, poll until completion, and return the job.
120
+
121
+ :param body: Request body from build_export_body()
122
+ :type body: dict
123
+ :param timeout: Max seconds to wait for completion
124
+ :type timeout: int
125
+ :param poll_interval: Seconds between status polls
126
+ :type poll_interval: int
127
+ :return: The completed job dict
128
+ :rtype: dict
129
+ :raises Exception: If the job fails, times out, or no job key is returned
130
+ """
131
+ resp = self.api.post('export/report', body)
132
+
133
+ job_key = resp.get('key')
134
+ if not job_key:
135
+ raise Exception('No job key returned from export/report endpoint.')
136
+
137
+ return self.poll_job(job_key, timeout, poll_interval)
138
+
139
+ def poll_job(self, job_key, timeout=DEFAULT_TIMEOUT, poll_interval=POLL_INTERVAL):
140
+ """
141
+ Poll a report generation job until it passes, fails, or times out.
142
+
143
+ :param job_key: The job key to poll
144
+ :type job_key: str
145
+ :param timeout: Max seconds to wait
146
+ :type timeout: int
147
+ :param poll_interval: Seconds between polls
148
+ :type poll_interval: int
149
+ :return: The completed job dict
150
+ :rtype: dict
151
+ :raises Exception: If the job fails or times out
152
+ """
153
+ start_time = time()
154
+ job = None
155
+ while time() - start_time < timeout:
156
+ job = self.api.jobs.get(job_key)
157
+ if self.api.jobs.is_failed(job):
158
+ message = job.get('message', 'unknown error')
159
+ raise Exception(f'Report generation failed: {message}')
160
+ if self.api.jobs.is_passed(job):
161
+ return job
162
+ sleep(poll_interval)
163
+
164
+ raise Exception(f'Report generation timed out after {timeout} seconds.')
165
+
166
+ def output_path(self, job):
167
+ """
168
+ Extract the output file path from a completed report job.
169
+
170
+ :param job: The completed job dict
171
+ :type job: dict
172
+ :return: The output file path in Guard storage
173
+ :rtype: str
174
+ :raises Exception: If the output path cannot be determined
175
+ """
176
+ config = job.get('config', {})
177
+ path = config.get('output') or job.get('dns', '')
178
+ if not path:
179
+ raise Exception('Could not determine output file path from job.')
180
+ return path
181
+
182
+ def download(self, job, download_directory):
183
+ """
184
+ Download the report file from a completed job to a local directory.
185
+
186
+ :param job: The completed job dict
187
+ :type job: dict
188
+ :param download_directory: Local directory to save the file
189
+ :type download_directory: str
190
+ :return: The local file path where the report was saved
191
+ :rtype: str
192
+ """
193
+ path = self.output_path(job)
194
+ return self.api.files.save(path, download_directory)
@@ -9,7 +9,7 @@ class Risks:
9
9
  def __init__(self, api):
10
10
  self.api = api
11
11
 
12
- def add(self, asset_key, name, status, comment=None, capability='', title=None):
12
+ def add(self, asset_key, name, status, comment=None, capability='', title=None, tags=None):
13
13
  """
14
14
  Add a risk to an existing asset.
15
15
 
@@ -25,12 +25,16 @@ class Risks:
25
25
  :type capability: str
26
26
  :param title: Optional human-readable title for the risk
27
27
  :type title: str or None
28
+ :param tags: Optional tags for the risk
29
+ :type tags: tuple or list or None
28
30
  :return: The created risk object
29
31
  :rtype: dict
30
32
  """
31
33
  body = dict(key=asset_key, name=name, status=status, comment=comment, source=capability)
32
34
  if title is not None:
33
35
  body['title'] = title
36
+ if tags:
37
+ body['tags'] = list(tags)
34
38
  return self.api.upsert('risk', body)['risks'][0]
35
39
 
36
40
  def get(self, key, details=False):
@@ -49,7 +53,7 @@ class Risks:
49
53
  risk['affected_assets'] = self.affected_assets(key)
50
54
  return risk
51
55
 
52
- def update(self, key, status=None, comment=None, remove_comment=None, title=None):
56
+ def update(self, key, status=None, comment=None, remove_comment=None, title=None, tags=None):
53
57
  """
54
58
  Update a risk's status and/or comment, or remove a comment.
55
59
 
@@ -63,6 +67,8 @@ class Risks:
63
67
  :type remove_comment: int or None
64
68
  :param title: Optional human-readable title for the risk
65
69
  :type title: str or None
70
+ :param tags: Optional tags for the risk
71
+ :type tags: tuple or list or None
66
72
  :return: API response containing update results
67
73
  :rtype: dict
68
74
  """
@@ -73,6 +79,8 @@ class Risks:
73
79
  params = params | dict(comment=comment)
74
80
  if title is not None:
75
81
  params['title'] = title
82
+ if tags:
83
+ params['tags'] = list(tags)
76
84
  if remove_comment is not None:
77
85
  index = self.resolve_comment_entry_index(key, remove_comment)
78
86
  params = params | dict(remove=index)
@@ -48,7 +48,7 @@ class Webhook:
48
48
  ```
49
49
  """
50
50
  pin = str(uuid4())
51
- self.api.link_account('hook', pin)
51
+ self.api.link_account('hook', role='readonly', value=pin)
52
52
  return self.webhook_url(pin)
53
53
 
54
54
  def get_url(self):
@@ -12,6 +12,12 @@ class TestJob:
12
12
  self.sdk = setup_chariot()
13
13
  make_test_values(self)
14
14
 
15
+ self.was_frozen = False
16
+ frozen_setting = self.sdk.settings.get('#setting#frozen')
17
+ if frozen_setting and frozen_setting.get('value') == 'true':
18
+ self.was_frozen = True
19
+ self.sdk.settings.add('frozen', 'false')
20
+
15
21
  def test_add_job(self):
16
22
  result = self.sdk.assets.add(self.asset_dns, self.asset_dns)
17
23
  self.sdk.jobs.add(result['key'])
@@ -48,3 +54,5 @@ class TestJob:
48
54
 
49
55
  def teardown_class(self):
50
56
  clean_test_entities(self.sdk, self)
57
+ if self.was_frozen:
58
+ self.sdk.settings.add('frozen', 'true')