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.
- {praetorian_cli-2.3.3/praetorian_cli.egg-info → praetorian_cli-2.3.5}/PKG-INFO +1 -1
- {praetorian_cli-2.3.3 → praetorian_cli-2.3.5}/praetorian_cli/handlers/add.py +4 -2
- praetorian_cli-2.3.5/praetorian_cli/handlers/export.py +102 -0
- {praetorian_cli-2.3.3 → praetorian_cli-2.3.5}/praetorian_cli/handlers/link.py +8 -6
- {praetorian_cli-2.3.3 → praetorian_cli-2.3.5}/praetorian_cli/handlers/update.py +4 -2
- {praetorian_cli-2.3.3 → praetorian_cli-2.3.5}/praetorian_cli/main.py +1 -0
- {praetorian_cli-2.3.3 → praetorian_cli-2.3.5}/praetorian_cli/sdk/chariot.py +7 -2
- {praetorian_cli-2.3.3 → praetorian_cli-2.3.5}/praetorian_cli/sdk/entities/accounts.py +4 -2
- praetorian_cli-2.3.5/praetorian_cli/sdk/entities/reports.py +194 -0
- {praetorian_cli-2.3.3 → praetorian_cli-2.3.5}/praetorian_cli/sdk/entities/risks.py +10 -2
- {praetorian_cli-2.3.3 → praetorian_cli-2.3.5}/praetorian_cli/sdk/entities/webhook.py +1 -1
- {praetorian_cli-2.3.3 → praetorian_cli-2.3.5}/praetorian_cli/sdk/test/test_job.py +8 -0
- praetorian_cli-2.3.5/praetorian_cli/sdk/test/test_report.py +242 -0
- {praetorian_cli-2.3.3 → praetorian_cli-2.3.5}/praetorian_cli/sdk/test/test_risk.py +14 -0
- {praetorian_cli-2.3.3 → praetorian_cli-2.3.5}/praetorian_cli/sdk/test/test_z_cli.py +47 -6
- {praetorian_cli-2.3.3 → praetorian_cli-2.3.5/praetorian_cli.egg-info}/PKG-INFO +1 -1
- {praetorian_cli-2.3.3 → praetorian_cli-2.3.5}/praetorian_cli.egg-info/SOURCES.txt +3 -0
- {praetorian_cli-2.3.3 → praetorian_cli-2.3.5}/setup.cfg +1 -1
- {praetorian_cli-2.3.3 → praetorian_cli-2.3.5}/LICENSE +0 -0
- {praetorian_cli-2.3.3 → praetorian_cli-2.3.5}/MANIFEST.in +0 -0
- {praetorian_cli-2.3.3 → praetorian_cli-2.3.5}/README.md +0 -0
- {praetorian_cli-2.3.3 → praetorian_cli-2.3.5}/praetorian_cli/__init__.py +0 -0
- {praetorian_cli-2.3.3 → praetorian_cli-2.3.5}/praetorian_cli/handlers/__init__.py +0 -0
- {praetorian_cli-2.3.3 → praetorian_cli-2.3.5}/praetorian_cli/handlers/aegis.py +0 -0
- {praetorian_cli-2.3.3 → praetorian_cli-2.3.5}/praetorian_cli/handlers/agent.py +0 -0
- {praetorian_cli-2.3.3 → praetorian_cli-2.3.5}/praetorian_cli/handlers/chariot.py +0 -0
- {praetorian_cli-2.3.3 → praetorian_cli-2.3.5}/praetorian_cli/handlers/cli_decorators.py +0 -0
- {praetorian_cli-2.3.3 → praetorian_cli-2.3.5}/praetorian_cli/handlers/configure.py +0 -0
- {praetorian_cli-2.3.3 → praetorian_cli-2.3.5}/praetorian_cli/handlers/delete.py +0 -0
- {praetorian_cli-2.3.3 → praetorian_cli-2.3.5}/praetorian_cli/handlers/enrich.py +0 -0
- {praetorian_cli-2.3.3 → praetorian_cli-2.3.5}/praetorian_cli/handlers/get.py +0 -0
- {praetorian_cli-2.3.3 → praetorian_cli-2.3.5}/praetorian_cli/handlers/imports.py +0 -0
- {praetorian_cli-2.3.3 → praetorian_cli-2.3.5}/praetorian_cli/handlers/list.py +0 -0
- {praetorian_cli-2.3.3 → praetorian_cli-2.3.5}/praetorian_cli/handlers/script.py +0 -0
- {praetorian_cli-2.3.3 → praetorian_cli-2.3.5}/praetorian_cli/handlers/search.py +0 -0
- {praetorian_cli-2.3.3 → praetorian_cli-2.3.5}/praetorian_cli/handlers/ssh_utils.py +0 -0
- {praetorian_cli-2.3.3 → praetorian_cli-2.3.5}/praetorian_cli/handlers/test.py +0 -0
- {praetorian_cli-2.3.3 → praetorian_cli-2.3.5}/praetorian_cli/handlers/unlink.py +0 -0
- {praetorian_cli-2.3.3 → praetorian_cli-2.3.5}/praetorian_cli/handlers/utils.py +0 -0
- {praetorian_cli-2.3.3 → praetorian_cli-2.3.5}/praetorian_cli/scripts/__init__.py +0 -0
- {praetorian_cli-2.3.3 → praetorian_cli-2.3.5}/praetorian_cli/scripts/commands/__init__.py +0 -0
- {praetorian_cli-2.3.3 → praetorian_cli-2.3.5}/praetorian_cli/scripts/commands/nmap-example.py +0 -0
- {praetorian_cli-2.3.3 → praetorian_cli-2.3.5}/praetorian_cli/scripts/utils.py +0 -0
- {praetorian_cli-2.3.3 → praetorian_cli-2.3.5}/praetorian_cli/sdk/__init__.py +0 -0
- {praetorian_cli-2.3.3 → praetorian_cli-2.3.5}/praetorian_cli/sdk/entities/__init__.py +0 -0
- {praetorian_cli-2.3.3 → praetorian_cli-2.3.5}/praetorian_cli/sdk/entities/ad.py +0 -0
- {praetorian_cli-2.3.3 → praetorian_cli-2.3.5}/praetorian_cli/sdk/entities/aegis.py +0 -0
- {praetorian_cli-2.3.3 → praetorian_cli-2.3.5}/praetorian_cli/sdk/entities/agents.py +0 -0
- {praetorian_cli-2.3.3 → praetorian_cli-2.3.5}/praetorian_cli/sdk/entities/assets.py +0 -0
- {praetorian_cli-2.3.3 → praetorian_cli-2.3.5}/praetorian_cli/sdk/entities/attributes.py +0 -0
- {praetorian_cli-2.3.3 → praetorian_cli-2.3.5}/praetorian_cli/sdk/entities/capabilities.py +0 -0
- {praetorian_cli-2.3.3 → praetorian_cli-2.3.5}/praetorian_cli/sdk/entities/configurations.py +0 -0
- {praetorian_cli-2.3.3 → praetorian_cli-2.3.5}/praetorian_cli/sdk/entities/credentials.py +0 -0
- {praetorian_cli-2.3.3 → praetorian_cli-2.3.5}/praetorian_cli/sdk/entities/definitions.py +0 -0
- {praetorian_cli-2.3.3 → praetorian_cli-2.3.5}/praetorian_cli/sdk/entities/files.py +0 -0
- {praetorian_cli-2.3.3 → praetorian_cli-2.3.5}/praetorian_cli/sdk/entities/integrations.py +0 -0
- {praetorian_cli-2.3.3 → praetorian_cli-2.3.5}/praetorian_cli/sdk/entities/jobs.py +0 -0
- {praetorian_cli-2.3.3 → praetorian_cli-2.3.5}/praetorian_cli/sdk/entities/keys.py +0 -0
- {praetorian_cli-2.3.3 → praetorian_cli-2.3.5}/praetorian_cli/sdk/entities/preseeds.py +0 -0
- {praetorian_cli-2.3.3 → praetorian_cli-2.3.5}/praetorian_cli/sdk/entities/scanners.py +0 -0
- {praetorian_cli-2.3.3 → praetorian_cli-2.3.5}/praetorian_cli/sdk/entities/schedules.py +0 -0
- {praetorian_cli-2.3.3 → praetorian_cli-2.3.5}/praetorian_cli/sdk/entities/schema.py +0 -0
- {praetorian_cli-2.3.3 → praetorian_cli-2.3.5}/praetorian_cli/sdk/entities/search.py +0 -0
- {praetorian_cli-2.3.3 → praetorian_cli-2.3.5}/praetorian_cli/sdk/entities/seeds.py +0 -0
- {praetorian_cli-2.3.3 → praetorian_cli-2.3.5}/praetorian_cli/sdk/entities/settings.py +0 -0
- {praetorian_cli-2.3.3 → praetorian_cli-2.3.5}/praetorian_cli/sdk/entities/statistics.py +0 -0
- {praetorian_cli-2.3.3 → praetorian_cli-2.3.5}/praetorian_cli/sdk/entities/webpage.py +0 -0
- {praetorian_cli-2.3.3 → praetorian_cli-2.3.5}/praetorian_cli/sdk/guard.py +0 -0
- {praetorian_cli-2.3.3 → praetorian_cli-2.3.5}/praetorian_cli/sdk/keychain.py +0 -0
- {praetorian_cli-2.3.3 → praetorian_cli-2.3.5}/praetorian_cli/sdk/mcp_server.py +0 -0
- {praetorian_cli-2.3.3 → praetorian_cli-2.3.5}/praetorian_cli/sdk/model/__init__.py +0 -0
- {praetorian_cli-2.3.3 → praetorian_cli-2.3.5}/praetorian_cli/sdk/model/aegis.py +0 -0
- {praetorian_cli-2.3.3 → praetorian_cli-2.3.5}/praetorian_cli/sdk/model/globals.py +0 -0
- {praetorian_cli-2.3.3 → praetorian_cli-2.3.5}/praetorian_cli/sdk/model/query.py +0 -0
- {praetorian_cli-2.3.3 → praetorian_cli-2.3.5}/praetorian_cli/sdk/model/utils.py +0 -0
- {praetorian_cli-2.3.3 → praetorian_cli-2.3.5}/praetorian_cli/sdk/test/__init__.py +0 -0
- {praetorian_cli-2.3.3 → praetorian_cli-2.3.5}/praetorian_cli/sdk/test/pytest.ini +0 -0
- {praetorian_cli-2.3.3 → praetorian_cli-2.3.5}/praetorian_cli/sdk/test/test_account.py +0 -0
- {praetorian_cli-2.3.3 → praetorian_cli-2.3.5}/praetorian_cli/sdk/test/test_agent.py +0 -0
- {praetorian_cli-2.3.3 → praetorian_cli-2.3.5}/praetorian_cli/sdk/test/test_asset.py +0 -0
- {praetorian_cli-2.3.3 → praetorian_cli-2.3.5}/praetorian_cli/sdk/test/test_attribute.py +0 -0
- {praetorian_cli-2.3.3 → praetorian_cli-2.3.5}/praetorian_cli/sdk/test/test_capabilities.py +0 -0
- {praetorian_cli-2.3.3 → praetorian_cli-2.3.5}/praetorian_cli/sdk/test/test_configuration.py +0 -0
- {praetorian_cli-2.3.3 → praetorian_cli-2.3.5}/praetorian_cli/sdk/test/test_conversation.py +0 -0
- {praetorian_cli-2.3.3 → praetorian_cli-2.3.5}/praetorian_cli/sdk/test/test_definition.py +0 -0
- {praetorian_cli-2.3.3 → praetorian_cli-2.3.5}/praetorian_cli/sdk/test/test_extend.py +0 -0
- {praetorian_cli-2.3.3 → praetorian_cli-2.3.5}/praetorian_cli/sdk/test/test_file.py +0 -0
- {praetorian_cli-2.3.3 → praetorian_cli-2.3.5}/praetorian_cli/sdk/test/test_key.py +0 -0
- {praetorian_cli-2.3.3 → praetorian_cli-2.3.5}/praetorian_cli/sdk/test/test_mcp.py +0 -0
- {praetorian_cli-2.3.3 → praetorian_cli-2.3.5}/praetorian_cli/sdk/test/test_preseed.py +0 -0
- {praetorian_cli-2.3.3 → praetorian_cli-2.3.5}/praetorian_cli/sdk/test/test_search.py +0 -0
- {praetorian_cli-2.3.3 → praetorian_cli-2.3.5}/praetorian_cli/sdk/test/test_seed.py +0 -0
- {praetorian_cli-2.3.3 → praetorian_cli-2.3.5}/praetorian_cli/sdk/test/test_setting.py +0 -0
- {praetorian_cli-2.3.3 → praetorian_cli-2.3.5}/praetorian_cli/sdk/test/test_webhook.py +0 -0
- {praetorian_cli-2.3.3 → praetorian_cli-2.3.5}/praetorian_cli/sdk/test/test_webpage.py +0 -0
- {praetorian_cli-2.3.3 → praetorian_cli-2.3.5}/praetorian_cli/sdk/test/ui_mocks.py +0 -0
- {praetorian_cli-2.3.3 → praetorian_cli-2.3.5}/praetorian_cli/sdk/test/utils.py +0 -0
- {praetorian_cli-2.3.3 → praetorian_cli-2.3.5}/praetorian_cli/ui/__init__.py +0 -0
- {praetorian_cli-2.3.3 → praetorian_cli-2.3.5}/praetorian_cli/ui/aegis/__init__.py +0 -0
- {praetorian_cli-2.3.3 → praetorian_cli-2.3.5}/praetorian_cli/ui/aegis/commands/__init__.py +0 -0
- {praetorian_cli-2.3.3 → praetorian_cli-2.3.5}/praetorian_cli/ui/aegis/commands/cp.py +0 -0
- {praetorian_cli-2.3.3 → praetorian_cli-2.3.5}/praetorian_cli/ui/aegis/commands/help.py +0 -0
- {praetorian_cli-2.3.3 → praetorian_cli-2.3.5}/praetorian_cli/ui/aegis/commands/info.py +0 -0
- {praetorian_cli-2.3.3 → praetorian_cli-2.3.5}/praetorian_cli/ui/aegis/commands/job.py +0 -0
- {praetorian_cli-2.3.3 → praetorian_cli-2.3.5}/praetorian_cli/ui/aegis/commands/job_helpers.py +0 -0
- {praetorian_cli-2.3.3 → praetorian_cli-2.3.5}/praetorian_cli/ui/aegis/commands/list.py +0 -0
- {praetorian_cli-2.3.3 → praetorian_cli-2.3.5}/praetorian_cli/ui/aegis/commands/proxy.py +0 -0
- {praetorian_cli-2.3.3 → praetorian_cli-2.3.5}/praetorian_cli/ui/aegis/commands/schedule.py +0 -0
- {praetorian_cli-2.3.3 → praetorian_cli-2.3.5}/praetorian_cli/ui/aegis/commands/schedule_helpers.py +0 -0
- {praetorian_cli-2.3.3 → praetorian_cli-2.3.5}/praetorian_cli/ui/aegis/commands/set.py +0 -0
- {praetorian_cli-2.3.3 → praetorian_cli-2.3.5}/praetorian_cli/ui/aegis/commands/ssh.py +0 -0
- {praetorian_cli-2.3.3 → praetorian_cli-2.3.5}/praetorian_cli/ui/aegis/constants.py +0 -0
- {praetorian_cli-2.3.3 → praetorian_cli-2.3.5}/praetorian_cli/ui/aegis/menu.py +0 -0
- {praetorian_cli-2.3.3 → praetorian_cli-2.3.5}/praetorian_cli/ui/aegis/theme.py +0 -0
- {praetorian_cli-2.3.3 → praetorian_cli-2.3.5}/praetorian_cli/ui/aegis/utils.py +0 -0
- {praetorian_cli-2.3.3 → praetorian_cli-2.3.5}/praetorian_cli/ui/conversation/__init__.py +0 -0
- {praetorian_cli-2.3.3 → praetorian_cli-2.3.5}/praetorian_cli/ui/conversation/textual_chat.py +0 -0
- {praetorian_cli-2.3.3 → praetorian_cli-2.3.5}/praetorian_cli.egg-info/dependency_links.txt +0 -0
- {praetorian_cli-2.3.3 → praetorian_cli-2.3.5}/praetorian_cli.egg-info/entry_points.txt +0 -0
- {praetorian_cli-2.3.3 → praetorian_cli-2.3.5}/praetorian_cli.egg-info/requires.txt +0 -0
- {praetorian_cli-2.3.3 → praetorian_cli-2.3.5}/praetorian_cli.egg-info/top_level.txt +0 -0
- {praetorian_cli-2.3.3 → praetorian_cli-2.3.5}/pyproject.toml +0 -0
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
-
|
|
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
|
-
|
|
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
|
-
|
|
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)
|
|
@@ -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')
|