praetorian-cli 2.3.4__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.4/praetorian_cli.egg-info → praetorian_cli-2.3.5}/PKG-INFO +1 -1
- praetorian_cli-2.3.5/praetorian_cli/handlers/export.py +102 -0
- {praetorian_cli-2.3.4 → praetorian_cli-2.3.5}/praetorian_cli/main.py +1 -0
- {praetorian_cli-2.3.4 → praetorian_cli-2.3.5}/praetorian_cli/sdk/chariot.py +2 -0
- praetorian_cli-2.3.5/praetorian_cli/sdk/entities/reports.py +194 -0
- {praetorian_cli-2.3.4 → praetorian_cli-2.3.5}/praetorian_cli/sdk/entities/webhook.py +1 -1
- praetorian_cli-2.3.5/praetorian_cli/sdk/test/test_report.py +242 -0
- {praetorian_cli-2.3.4 → praetorian_cli-2.3.5}/praetorian_cli/sdk/test/test_risk.py +2 -2
- {praetorian_cli-2.3.4 → praetorian_cli-2.3.5}/praetorian_cli/sdk/test/test_z_cli.py +11 -11
- {praetorian_cli-2.3.4 → praetorian_cli-2.3.5/praetorian_cli.egg-info}/PKG-INFO +1 -1
- {praetorian_cli-2.3.4 → praetorian_cli-2.3.5}/praetorian_cli.egg-info/SOURCES.txt +3 -0
- {praetorian_cli-2.3.4 → praetorian_cli-2.3.5}/setup.cfg +1 -1
- {praetorian_cli-2.3.4 → praetorian_cli-2.3.5}/LICENSE +0 -0
- {praetorian_cli-2.3.4 → praetorian_cli-2.3.5}/MANIFEST.in +0 -0
- {praetorian_cli-2.3.4 → praetorian_cli-2.3.5}/README.md +0 -0
- {praetorian_cli-2.3.4 → praetorian_cli-2.3.5}/praetorian_cli/__init__.py +0 -0
- {praetorian_cli-2.3.4 → praetorian_cli-2.3.5}/praetorian_cli/handlers/__init__.py +0 -0
- {praetorian_cli-2.3.4 → praetorian_cli-2.3.5}/praetorian_cli/handlers/add.py +0 -0
- {praetorian_cli-2.3.4 → praetorian_cli-2.3.5}/praetorian_cli/handlers/aegis.py +0 -0
- {praetorian_cli-2.3.4 → praetorian_cli-2.3.5}/praetorian_cli/handlers/agent.py +0 -0
- {praetorian_cli-2.3.4 → praetorian_cli-2.3.5}/praetorian_cli/handlers/chariot.py +0 -0
- {praetorian_cli-2.3.4 → praetorian_cli-2.3.5}/praetorian_cli/handlers/cli_decorators.py +0 -0
- {praetorian_cli-2.3.4 → praetorian_cli-2.3.5}/praetorian_cli/handlers/configure.py +0 -0
- {praetorian_cli-2.3.4 → praetorian_cli-2.3.5}/praetorian_cli/handlers/delete.py +0 -0
- {praetorian_cli-2.3.4 → praetorian_cli-2.3.5}/praetorian_cli/handlers/enrich.py +0 -0
- {praetorian_cli-2.3.4 → praetorian_cli-2.3.5}/praetorian_cli/handlers/get.py +0 -0
- {praetorian_cli-2.3.4 → praetorian_cli-2.3.5}/praetorian_cli/handlers/imports.py +0 -0
- {praetorian_cli-2.3.4 → praetorian_cli-2.3.5}/praetorian_cli/handlers/link.py +0 -0
- {praetorian_cli-2.3.4 → praetorian_cli-2.3.5}/praetorian_cli/handlers/list.py +0 -0
- {praetorian_cli-2.3.4 → praetorian_cli-2.3.5}/praetorian_cli/handlers/script.py +0 -0
- {praetorian_cli-2.3.4 → praetorian_cli-2.3.5}/praetorian_cli/handlers/search.py +0 -0
- {praetorian_cli-2.3.4 → praetorian_cli-2.3.5}/praetorian_cli/handlers/ssh_utils.py +0 -0
- {praetorian_cli-2.3.4 → praetorian_cli-2.3.5}/praetorian_cli/handlers/test.py +0 -0
- {praetorian_cli-2.3.4 → praetorian_cli-2.3.5}/praetorian_cli/handlers/unlink.py +0 -0
- {praetorian_cli-2.3.4 → praetorian_cli-2.3.5}/praetorian_cli/handlers/update.py +0 -0
- {praetorian_cli-2.3.4 → praetorian_cli-2.3.5}/praetorian_cli/handlers/utils.py +0 -0
- {praetorian_cli-2.3.4 → praetorian_cli-2.3.5}/praetorian_cli/scripts/__init__.py +0 -0
- {praetorian_cli-2.3.4 → praetorian_cli-2.3.5}/praetorian_cli/scripts/commands/__init__.py +0 -0
- {praetorian_cli-2.3.4 → praetorian_cli-2.3.5}/praetorian_cli/scripts/commands/nmap-example.py +0 -0
- {praetorian_cli-2.3.4 → praetorian_cli-2.3.5}/praetorian_cli/scripts/utils.py +0 -0
- {praetorian_cli-2.3.4 → praetorian_cli-2.3.5}/praetorian_cli/sdk/__init__.py +0 -0
- {praetorian_cli-2.3.4 → praetorian_cli-2.3.5}/praetorian_cli/sdk/entities/__init__.py +0 -0
- {praetorian_cli-2.3.4 → praetorian_cli-2.3.5}/praetorian_cli/sdk/entities/accounts.py +0 -0
- {praetorian_cli-2.3.4 → praetorian_cli-2.3.5}/praetorian_cli/sdk/entities/ad.py +0 -0
- {praetorian_cli-2.3.4 → praetorian_cli-2.3.5}/praetorian_cli/sdk/entities/aegis.py +0 -0
- {praetorian_cli-2.3.4 → praetorian_cli-2.3.5}/praetorian_cli/sdk/entities/agents.py +0 -0
- {praetorian_cli-2.3.4 → praetorian_cli-2.3.5}/praetorian_cli/sdk/entities/assets.py +0 -0
- {praetorian_cli-2.3.4 → praetorian_cli-2.3.5}/praetorian_cli/sdk/entities/attributes.py +0 -0
- {praetorian_cli-2.3.4 → praetorian_cli-2.3.5}/praetorian_cli/sdk/entities/capabilities.py +0 -0
- {praetorian_cli-2.3.4 → praetorian_cli-2.3.5}/praetorian_cli/sdk/entities/configurations.py +0 -0
- {praetorian_cli-2.3.4 → praetorian_cli-2.3.5}/praetorian_cli/sdk/entities/credentials.py +0 -0
- {praetorian_cli-2.3.4 → praetorian_cli-2.3.5}/praetorian_cli/sdk/entities/definitions.py +0 -0
- {praetorian_cli-2.3.4 → praetorian_cli-2.3.5}/praetorian_cli/sdk/entities/files.py +0 -0
- {praetorian_cli-2.3.4 → praetorian_cli-2.3.5}/praetorian_cli/sdk/entities/integrations.py +0 -0
- {praetorian_cli-2.3.4 → praetorian_cli-2.3.5}/praetorian_cli/sdk/entities/jobs.py +0 -0
- {praetorian_cli-2.3.4 → praetorian_cli-2.3.5}/praetorian_cli/sdk/entities/keys.py +0 -0
- {praetorian_cli-2.3.4 → praetorian_cli-2.3.5}/praetorian_cli/sdk/entities/preseeds.py +0 -0
- {praetorian_cli-2.3.4 → praetorian_cli-2.3.5}/praetorian_cli/sdk/entities/risks.py +0 -0
- {praetorian_cli-2.3.4 → praetorian_cli-2.3.5}/praetorian_cli/sdk/entities/scanners.py +0 -0
- {praetorian_cli-2.3.4 → praetorian_cli-2.3.5}/praetorian_cli/sdk/entities/schedules.py +0 -0
- {praetorian_cli-2.3.4 → praetorian_cli-2.3.5}/praetorian_cli/sdk/entities/schema.py +0 -0
- {praetorian_cli-2.3.4 → praetorian_cli-2.3.5}/praetorian_cli/sdk/entities/search.py +0 -0
- {praetorian_cli-2.3.4 → praetorian_cli-2.3.5}/praetorian_cli/sdk/entities/seeds.py +0 -0
- {praetorian_cli-2.3.4 → praetorian_cli-2.3.5}/praetorian_cli/sdk/entities/settings.py +0 -0
- {praetorian_cli-2.3.4 → praetorian_cli-2.3.5}/praetorian_cli/sdk/entities/statistics.py +0 -0
- {praetorian_cli-2.3.4 → praetorian_cli-2.3.5}/praetorian_cli/sdk/entities/webpage.py +0 -0
- {praetorian_cli-2.3.4 → praetorian_cli-2.3.5}/praetorian_cli/sdk/guard.py +0 -0
- {praetorian_cli-2.3.4 → praetorian_cli-2.3.5}/praetorian_cli/sdk/keychain.py +0 -0
- {praetorian_cli-2.3.4 → praetorian_cli-2.3.5}/praetorian_cli/sdk/mcp_server.py +0 -0
- {praetorian_cli-2.3.4 → praetorian_cli-2.3.5}/praetorian_cli/sdk/model/__init__.py +0 -0
- {praetorian_cli-2.3.4 → praetorian_cli-2.3.5}/praetorian_cli/sdk/model/aegis.py +0 -0
- {praetorian_cli-2.3.4 → praetorian_cli-2.3.5}/praetorian_cli/sdk/model/globals.py +0 -0
- {praetorian_cli-2.3.4 → praetorian_cli-2.3.5}/praetorian_cli/sdk/model/query.py +0 -0
- {praetorian_cli-2.3.4 → praetorian_cli-2.3.5}/praetorian_cli/sdk/model/utils.py +0 -0
- {praetorian_cli-2.3.4 → praetorian_cli-2.3.5}/praetorian_cli/sdk/test/__init__.py +0 -0
- {praetorian_cli-2.3.4 → praetorian_cli-2.3.5}/praetorian_cli/sdk/test/pytest.ini +0 -0
- {praetorian_cli-2.3.4 → praetorian_cli-2.3.5}/praetorian_cli/sdk/test/test_account.py +0 -0
- {praetorian_cli-2.3.4 → praetorian_cli-2.3.5}/praetorian_cli/sdk/test/test_agent.py +0 -0
- {praetorian_cli-2.3.4 → praetorian_cli-2.3.5}/praetorian_cli/sdk/test/test_asset.py +0 -0
- {praetorian_cli-2.3.4 → praetorian_cli-2.3.5}/praetorian_cli/sdk/test/test_attribute.py +0 -0
- {praetorian_cli-2.3.4 → praetorian_cli-2.3.5}/praetorian_cli/sdk/test/test_capabilities.py +0 -0
- {praetorian_cli-2.3.4 → praetorian_cli-2.3.5}/praetorian_cli/sdk/test/test_configuration.py +0 -0
- {praetorian_cli-2.3.4 → praetorian_cli-2.3.5}/praetorian_cli/sdk/test/test_conversation.py +0 -0
- {praetorian_cli-2.3.4 → praetorian_cli-2.3.5}/praetorian_cli/sdk/test/test_definition.py +0 -0
- {praetorian_cli-2.3.4 → praetorian_cli-2.3.5}/praetorian_cli/sdk/test/test_extend.py +0 -0
- {praetorian_cli-2.3.4 → praetorian_cli-2.3.5}/praetorian_cli/sdk/test/test_file.py +0 -0
- {praetorian_cli-2.3.4 → praetorian_cli-2.3.5}/praetorian_cli/sdk/test/test_job.py +0 -0
- {praetorian_cli-2.3.4 → praetorian_cli-2.3.5}/praetorian_cli/sdk/test/test_key.py +0 -0
- {praetorian_cli-2.3.4 → praetorian_cli-2.3.5}/praetorian_cli/sdk/test/test_mcp.py +0 -0
- {praetorian_cli-2.3.4 → praetorian_cli-2.3.5}/praetorian_cli/sdk/test/test_preseed.py +0 -0
- {praetorian_cli-2.3.4 → praetorian_cli-2.3.5}/praetorian_cli/sdk/test/test_search.py +0 -0
- {praetorian_cli-2.3.4 → praetorian_cli-2.3.5}/praetorian_cli/sdk/test/test_seed.py +0 -0
- {praetorian_cli-2.3.4 → praetorian_cli-2.3.5}/praetorian_cli/sdk/test/test_setting.py +0 -0
- {praetorian_cli-2.3.4 → praetorian_cli-2.3.5}/praetorian_cli/sdk/test/test_webhook.py +0 -0
- {praetorian_cli-2.3.4 → praetorian_cli-2.3.5}/praetorian_cli/sdk/test/test_webpage.py +0 -0
- {praetorian_cli-2.3.4 → praetorian_cli-2.3.5}/praetorian_cli/sdk/test/ui_mocks.py +0 -0
- {praetorian_cli-2.3.4 → praetorian_cli-2.3.5}/praetorian_cli/sdk/test/utils.py +0 -0
- {praetorian_cli-2.3.4 → praetorian_cli-2.3.5}/praetorian_cli/ui/__init__.py +0 -0
- {praetorian_cli-2.3.4 → praetorian_cli-2.3.5}/praetorian_cli/ui/aegis/__init__.py +0 -0
- {praetorian_cli-2.3.4 → praetorian_cli-2.3.5}/praetorian_cli/ui/aegis/commands/__init__.py +0 -0
- {praetorian_cli-2.3.4 → praetorian_cli-2.3.5}/praetorian_cli/ui/aegis/commands/cp.py +0 -0
- {praetorian_cli-2.3.4 → praetorian_cli-2.3.5}/praetorian_cli/ui/aegis/commands/help.py +0 -0
- {praetorian_cli-2.3.4 → praetorian_cli-2.3.5}/praetorian_cli/ui/aegis/commands/info.py +0 -0
- {praetorian_cli-2.3.4 → praetorian_cli-2.3.5}/praetorian_cli/ui/aegis/commands/job.py +0 -0
- {praetorian_cli-2.3.4 → praetorian_cli-2.3.5}/praetorian_cli/ui/aegis/commands/job_helpers.py +0 -0
- {praetorian_cli-2.3.4 → praetorian_cli-2.3.5}/praetorian_cli/ui/aegis/commands/list.py +0 -0
- {praetorian_cli-2.3.4 → praetorian_cli-2.3.5}/praetorian_cli/ui/aegis/commands/proxy.py +0 -0
- {praetorian_cli-2.3.4 → praetorian_cli-2.3.5}/praetorian_cli/ui/aegis/commands/schedule.py +0 -0
- {praetorian_cli-2.3.4 → praetorian_cli-2.3.5}/praetorian_cli/ui/aegis/commands/schedule_helpers.py +0 -0
- {praetorian_cli-2.3.4 → praetorian_cli-2.3.5}/praetorian_cli/ui/aegis/commands/set.py +0 -0
- {praetorian_cli-2.3.4 → praetorian_cli-2.3.5}/praetorian_cli/ui/aegis/commands/ssh.py +0 -0
- {praetorian_cli-2.3.4 → praetorian_cli-2.3.5}/praetorian_cli/ui/aegis/constants.py +0 -0
- {praetorian_cli-2.3.4 → praetorian_cli-2.3.5}/praetorian_cli/ui/aegis/menu.py +0 -0
- {praetorian_cli-2.3.4 → praetorian_cli-2.3.5}/praetorian_cli/ui/aegis/theme.py +0 -0
- {praetorian_cli-2.3.4 → praetorian_cli-2.3.5}/praetorian_cli/ui/aegis/utils.py +0 -0
- {praetorian_cli-2.3.4 → praetorian_cli-2.3.5}/praetorian_cli/ui/conversation/__init__.py +0 -0
- {praetorian_cli-2.3.4 → praetorian_cli-2.3.5}/praetorian_cli/ui/conversation/textual_chat.py +0 -0
- {praetorian_cli-2.3.4 → praetorian_cli-2.3.5}/praetorian_cli.egg-info/dependency_links.txt +0 -0
- {praetorian_cli-2.3.4 → praetorian_cli-2.3.5}/praetorian_cli.egg-info/entry_points.txt +0 -0
- {praetorian_cli-2.3.4 → praetorian_cli-2.3.5}/praetorian_cli.egg-info/requires.txt +0 -0
- {praetorian_cli-2.3.4 → praetorian_cli-2.3.5}/praetorian_cli.egg-info/top_level.txt +0 -0
- {praetorian_cli-2.3.4 → praetorian_cli-2.3.5}/pyproject.toml +0 -0
|
@@ -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}')
|
|
@@ -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)
|
|
@@ -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)
|
|
@@ -0,0 +1,242 @@
|
|
|
1
|
+
from datetime import date
|
|
2
|
+
from unittest.mock import MagicMock, patch
|
|
3
|
+
|
|
4
|
+
import pytest
|
|
5
|
+
|
|
6
|
+
from praetorian_cli.sdk.entities.reports import Reports
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def make_reports(account=None, username=None):
|
|
10
|
+
"""Create a Reports instance with a mocked API."""
|
|
11
|
+
api = MagicMock()
|
|
12
|
+
api.keychain.account = account
|
|
13
|
+
api.keychain.username.return_value = username
|
|
14
|
+
return Reports(api), api
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class TestCustomerEmail:
|
|
18
|
+
|
|
19
|
+
def test_uses_account_when_set(self):
|
|
20
|
+
reports, _ = make_reports(account='customer@acme.com', username='engineer@praetorian.com')
|
|
21
|
+
assert reports.customer_email() == 'customer@acme.com'
|
|
22
|
+
|
|
23
|
+
def test_falls_back_to_username(self):
|
|
24
|
+
reports, _ = make_reports(account=None, username='engineer@praetorian.com')
|
|
25
|
+
assert reports.customer_email() == 'engineer@praetorian.com'
|
|
26
|
+
|
|
27
|
+
def test_raises_when_neither_set(self):
|
|
28
|
+
reports, _ = make_reports(account=None, username=None)
|
|
29
|
+
with pytest.raises(Exception, match='Could not determine customer email'):
|
|
30
|
+
reports.customer_email()
|
|
31
|
+
|
|
32
|
+
def test_prefers_account_over_username(self):
|
|
33
|
+
reports, _ = make_reports(account='customer@acme.com', username='me@praetorian.com')
|
|
34
|
+
assert reports.customer_email() == 'customer@acme.com'
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class TestBuildExportBody:
|
|
38
|
+
|
|
39
|
+
def test_minimal_body(self):
|
|
40
|
+
reports, _ = make_reports()
|
|
41
|
+
body = reports.build_export_body(
|
|
42
|
+
title='Test Report',
|
|
43
|
+
client_name='Acme Corp',
|
|
44
|
+
customer_email='customer@acme.com',
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
assert body['config']['title'] == 'Test Report'
|
|
48
|
+
assert body['config']['client_name'] == 'Acme Corp'
|
|
49
|
+
assert body['customer_email'] == 'customer@acme.com'
|
|
50
|
+
assert body['status_filter'] == ['O', 'T']
|
|
51
|
+
assert body['export_format'] == 'pdf'
|
|
52
|
+
assert body['group_by'] == 'attack_surface'
|
|
53
|
+
assert body['shared_output'] is False
|
|
54
|
+
assert body['config']['draft'] is False
|
|
55
|
+
assert body['config']['version'] == '1.0'
|
|
56
|
+
assert 'risk_keys' not in body
|
|
57
|
+
assert 'target' not in body['config']
|
|
58
|
+
assert 'executive_summary_path' not in body
|
|
59
|
+
|
|
60
|
+
def test_defaults_report_date_to_today(self):
|
|
61
|
+
reports, _ = make_reports()
|
|
62
|
+
body = reports.build_export_body(
|
|
63
|
+
title='Test', client_name='Test', customer_email='test@test.com',
|
|
64
|
+
)
|
|
65
|
+
assert body['config']['report_date'] == date.today().isoformat()
|
|
66
|
+
|
|
67
|
+
def test_explicit_report_date(self):
|
|
68
|
+
reports, _ = make_reports()
|
|
69
|
+
body = reports.build_export_body(
|
|
70
|
+
title='Test', client_name='Test', customer_email='test@test.com',
|
|
71
|
+
report_date='2026-01-15',
|
|
72
|
+
)
|
|
73
|
+
assert body['config']['report_date'] == '2026-01-15'
|
|
74
|
+
|
|
75
|
+
def test_all_optional_fields(self):
|
|
76
|
+
reports, _ = make_reports()
|
|
77
|
+
body = reports.build_export_body(
|
|
78
|
+
title='Full Report',
|
|
79
|
+
client_name='Acme',
|
|
80
|
+
customer_email='customer@acme.com',
|
|
81
|
+
status_filter=['O', 'T', 'R'],
|
|
82
|
+
risk_keys=['#risk#example.com#sqli', '#risk#example.com#xss'],
|
|
83
|
+
target='example.com',
|
|
84
|
+
start_date='2026-01-01',
|
|
85
|
+
end_date='2026-03-01',
|
|
86
|
+
report_date='2026-03-15',
|
|
87
|
+
draft=True,
|
|
88
|
+
version='2.0',
|
|
89
|
+
export_format='zip',
|
|
90
|
+
group_by='tag',
|
|
91
|
+
shared_output=True,
|
|
92
|
+
executive_summary_path='home/exec-summary.md',
|
|
93
|
+
narratives_path='home/narratives.md',
|
|
94
|
+
appendix_path='home/appendix.md',
|
|
95
|
+
)
|
|
96
|
+
|
|
97
|
+
assert body['risk_keys'] == ['#risk#example.com#sqli', '#risk#example.com#xss']
|
|
98
|
+
assert body['config']['target'] == 'example.com'
|
|
99
|
+
assert body['config']['start_date'] == '2026-01-01'
|
|
100
|
+
assert body['config']['end_date'] == '2026-03-01'
|
|
101
|
+
assert body['config']['draft'] is True
|
|
102
|
+
assert body['config']['version'] == '2.0'
|
|
103
|
+
assert body['export_format'] == 'zip'
|
|
104
|
+
assert body['group_by'] == 'tag'
|
|
105
|
+
assert body['shared_output'] is True
|
|
106
|
+
assert body['executive_summary_path'] == 'home/exec-summary.md'
|
|
107
|
+
assert body['narratives_path'] == 'home/narratives.md'
|
|
108
|
+
assert body['appendix_path'] == 'home/appendix.md'
|
|
109
|
+
|
|
110
|
+
def test_status_filter_converts_tuple_to_list(self):
|
|
111
|
+
reports, _ = make_reports()
|
|
112
|
+
body = reports.build_export_body(
|
|
113
|
+
title='Test', client_name='Test', customer_email='test@test.com',
|
|
114
|
+
status_filter=('O', 'T', 'R'),
|
|
115
|
+
)
|
|
116
|
+
assert body['status_filter'] == ['O', 'T', 'R']
|
|
117
|
+
assert isinstance(body['status_filter'], list)
|
|
118
|
+
|
|
119
|
+
def test_empty_optional_strings_excluded(self):
|
|
120
|
+
reports, _ = make_reports()
|
|
121
|
+
body = reports.build_export_body(
|
|
122
|
+
title='Test', client_name='Test', customer_email='test@test.com',
|
|
123
|
+
target='', executive_summary_path='', narratives_path='', appendix_path='',
|
|
124
|
+
)
|
|
125
|
+
assert 'target' not in body['config']
|
|
126
|
+
assert 'executive_summary_path' not in body
|
|
127
|
+
assert 'narratives_path' not in body
|
|
128
|
+
assert 'appendix_path' not in body
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
class TestExport:
|
|
132
|
+
|
|
133
|
+
def test_success(self):
|
|
134
|
+
reports, api = make_reports()
|
|
135
|
+
api.post.return_value = {'key': '#job#report#123'}
|
|
136
|
+
api.jobs.get.return_value = {'status': 'JP', 'config': {'output': 'home/report.pdf'}}
|
|
137
|
+
api.jobs.is_passed.return_value = True
|
|
138
|
+
api.jobs.is_failed.return_value = False
|
|
139
|
+
|
|
140
|
+
job = reports.export({'customer_email': 'test@test.com'}, timeout=10, poll_interval=0)
|
|
141
|
+
|
|
142
|
+
api.post.assert_called_once_with('export/report', {'customer_email': 'test@test.com'})
|
|
143
|
+
assert job['config']['output'] == 'home/report.pdf'
|
|
144
|
+
|
|
145
|
+
def test_raises_on_missing_job_key(self):
|
|
146
|
+
reports, api = make_reports()
|
|
147
|
+
api.post.return_value = {}
|
|
148
|
+
|
|
149
|
+
with pytest.raises(Exception, match='No job key returned'):
|
|
150
|
+
reports.export({})
|
|
151
|
+
|
|
152
|
+
def test_raises_on_failure(self):
|
|
153
|
+
reports, api = make_reports()
|
|
154
|
+
api.post.return_value = {'key': '#job#report#123'}
|
|
155
|
+
api.jobs.get.return_value = {'status': 'JF', 'message': 'missing definitions'}
|
|
156
|
+
api.jobs.is_failed.return_value = True
|
|
157
|
+
api.jobs.is_passed.return_value = False
|
|
158
|
+
|
|
159
|
+
with pytest.raises(Exception, match='Report generation failed: missing definitions'):
|
|
160
|
+
reports.export({}, timeout=10, poll_interval=0)
|
|
161
|
+
|
|
162
|
+
@patch('praetorian_cli.sdk.entities.reports.time')
|
|
163
|
+
@patch('praetorian_cli.sdk.entities.reports.sleep')
|
|
164
|
+
def test_raises_on_timeout(self, mock_sleep, mock_time):
|
|
165
|
+
reports, api = make_reports()
|
|
166
|
+
api.post.return_value = {'key': '#job#report#123'}
|
|
167
|
+
api.jobs.get.return_value = {'status': 'JR'}
|
|
168
|
+
api.jobs.is_failed.return_value = False
|
|
169
|
+
api.jobs.is_passed.return_value = False
|
|
170
|
+
|
|
171
|
+
# Simulate time progressing past timeout
|
|
172
|
+
mock_time.side_effect = [0, 0, 100, 200, 301]
|
|
173
|
+
|
|
174
|
+
with pytest.raises(Exception, match='timed out after 300 seconds'):
|
|
175
|
+
reports.export({}, timeout=300, poll_interval=5)
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
class TestPollJob:
|
|
179
|
+
|
|
180
|
+
@patch('praetorian_cli.sdk.entities.reports.sleep')
|
|
181
|
+
def test_polls_until_passed(self, mock_sleep):
|
|
182
|
+
reports, api = make_reports()
|
|
183
|
+
|
|
184
|
+
# First call: running, second call: passed
|
|
185
|
+
api.jobs.get.side_effect = [
|
|
186
|
+
{'status': 'JR'},
|
|
187
|
+
{'status': 'JP', 'config': {'output': 'home/report.pdf'}},
|
|
188
|
+
]
|
|
189
|
+
api.jobs.is_failed.return_value = False
|
|
190
|
+
api.jobs.is_passed.side_effect = [False, True]
|
|
191
|
+
|
|
192
|
+
job = reports.poll_job('#job#123', timeout=60, poll_interval=0)
|
|
193
|
+
assert job['status'] == 'JP'
|
|
194
|
+
assert api.jobs.get.call_count == 2
|
|
195
|
+
|
|
196
|
+
@patch('praetorian_cli.sdk.entities.reports.sleep')
|
|
197
|
+
def test_raises_on_job_failure(self, mock_sleep):
|
|
198
|
+
reports, api = make_reports()
|
|
199
|
+
api.jobs.get.return_value = {'status': 'JF', 'message': 'orator crashed'}
|
|
200
|
+
api.jobs.is_failed.return_value = True
|
|
201
|
+
api.jobs.is_passed.return_value = False
|
|
202
|
+
|
|
203
|
+
with pytest.raises(Exception, match='orator crashed'):
|
|
204
|
+
reports.poll_job('#job#123', timeout=60, poll_interval=0)
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
class TestOutputPath:
|
|
208
|
+
|
|
209
|
+
def test_from_config_output(self):
|
|
210
|
+
reports, _ = make_reports()
|
|
211
|
+
job = {'config': {'output': 'home/my-report-2026.pdf'}, 'dns': 'fallback.pdf'}
|
|
212
|
+
assert reports.output_path(job) == 'home/my-report-2026.pdf'
|
|
213
|
+
|
|
214
|
+
def test_falls_back_to_dns(self):
|
|
215
|
+
reports, _ = make_reports()
|
|
216
|
+
job = {'config': {}, 'dns': 'home/fallback.pdf'}
|
|
217
|
+
assert reports.output_path(job) == 'home/fallback.pdf'
|
|
218
|
+
|
|
219
|
+
def test_raises_when_no_path(self):
|
|
220
|
+
reports, _ = make_reports()
|
|
221
|
+
job = {'config': {}, 'dns': ''}
|
|
222
|
+
with pytest.raises(Exception, match='Could not determine output file path'):
|
|
223
|
+
reports.output_path(job)
|
|
224
|
+
|
|
225
|
+
def test_raises_when_no_config(self):
|
|
226
|
+
reports, _ = make_reports()
|
|
227
|
+
job = {}
|
|
228
|
+
with pytest.raises(Exception, match='Could not determine output file path'):
|
|
229
|
+
reports.output_path(job)
|
|
230
|
+
|
|
231
|
+
|
|
232
|
+
class TestDownload:
|
|
233
|
+
|
|
234
|
+
def test_delegates_to_files_save(self):
|
|
235
|
+
reports, api = make_reports()
|
|
236
|
+
api.files.save.return_value = '/tmp/my-report.pdf'
|
|
237
|
+
|
|
238
|
+
job = {'config': {'output': 'home/my-report.pdf'}}
|
|
239
|
+
result = reports.download(job, '/tmp')
|
|
240
|
+
|
|
241
|
+
api.files.save.assert_called_once_with('home/my-report.pdf', '/tmp')
|
|
242
|
+
assert result == '/tmp/my-report.pdf'
|
|
@@ -94,7 +94,7 @@ class TestRisk:
|
|
|
94
94
|
tags = ('critical', 'needs-review')
|
|
95
95
|
r = self.sdk.risks.add(self.asset_key, self.risk_name, Risk.TRIAGE_HIGH.value, tags=tags)
|
|
96
96
|
risk = self.sdk.risks.get(r['key'])
|
|
97
|
-
assert risk['tags']
|
|
97
|
+
assert risk['tags'] == list(tags)
|
|
98
98
|
|
|
99
99
|
def test_update_risk_with_tags(self):
|
|
100
100
|
r = self.sdk.risks.add(self.asset_key, self.risk_name, Risk.TRIAGE_HIGH.value)
|
|
@@ -102,7 +102,7 @@ class TestRisk:
|
|
|
102
102
|
tags = ('resolved', 'verified')
|
|
103
103
|
self.sdk.risks.update(risk_key, tags=tags)
|
|
104
104
|
risk = self.sdk.risks.get(risk_key)
|
|
105
|
-
assert risk['tags']
|
|
105
|
+
assert risk['tags'] == list(tags)
|
|
106
106
|
|
|
107
107
|
def teardown_class(self):
|
|
108
108
|
clean_test_entities(self.sdk, self)
|
|
@@ -119,26 +119,26 @@ class TestZCli:
|
|
|
119
119
|
The substring checks in test_risk_cli cannot detect this structural issue.
|
|
120
120
|
"""
|
|
121
121
|
o = make_test_values(lambda: None)
|
|
122
|
-
self.verify(f'add asset -
|
|
122
|
+
self.verify(f'add asset -i {o.asset_name} -g {o.asset_dns}')
|
|
123
123
|
|
|
124
124
|
# Verify add risk with tags produces correct structure
|
|
125
125
|
self.verify(f'add risk {o.risk_name} -a "{o.asset_key}" -s {AddRisk.TRIAGE_HIGH.value} -g phase_web -g needs-triage',
|
|
126
126
|
ignore_stdout=True)
|
|
127
127
|
risk = self.run_json(f'get risk "{o.risk_key}"')
|
|
128
|
-
assert isinstance(risk['tags']
|
|
129
|
-
f'Expected tags
|
|
130
|
-
assert all(isinstance(t, str) for t in risk['tags']
|
|
131
|
-
f'Expected all tags to be strings, got: {risk["tags"]
|
|
132
|
-
assert set(risk['tags']
|
|
128
|
+
assert isinstance(risk['tags'], list), \
|
|
129
|
+
f'Expected tags to be a list, got {type(risk["tags"])}: {risk["tags"]}'
|
|
130
|
+
assert all(isinstance(t, str) for t in risk['tags']), \
|
|
131
|
+
f'Expected all tags to be strings, got: {risk["tags"]}'
|
|
132
|
+
assert set(risk['tags']) == {'phase_web', 'needs-triage'}
|
|
133
133
|
|
|
134
134
|
# Verify update risk with tags produces correct structure
|
|
135
135
|
self.verify(f'update risk "{o.risk_key}" -g resolved -g verified', ignore_stdout=True)
|
|
136
136
|
risk = self.run_json(f'get risk "{o.risk_key}"')
|
|
137
|
-
assert isinstance(risk['tags']
|
|
138
|
-
f'Expected tags
|
|
139
|
-
assert all(isinstance(t, str) for t in risk['tags']
|
|
140
|
-
f'Expected all tags to be strings after update, got: {risk["tags"]
|
|
141
|
-
assert set(risk['tags']
|
|
137
|
+
assert isinstance(risk['tags'], list), \
|
|
138
|
+
f'Expected tags to be a list after update, got {type(risk["tags"])}: {risk["tags"]}'
|
|
139
|
+
assert all(isinstance(t, str) for t in risk['tags']), \
|
|
140
|
+
f'Expected all tags to be strings after update, got: {risk["tags"]}'
|
|
141
|
+
assert set(risk['tags']) == {'resolved', 'verified'}
|
|
142
142
|
|
|
143
143
|
clean_test_entities(self.sdk, o)
|
|
144
144
|
|
|
@@ -20,6 +20,7 @@ praetorian_cli/handlers/cli_decorators.py
|
|
|
20
20
|
praetorian_cli/handlers/configure.py
|
|
21
21
|
praetorian_cli/handlers/delete.py
|
|
22
22
|
praetorian_cli/handlers/enrich.py
|
|
23
|
+
praetorian_cli/handlers/export.py
|
|
23
24
|
praetorian_cli/handlers/get.py
|
|
24
25
|
praetorian_cli/handlers/imports.py
|
|
25
26
|
praetorian_cli/handlers/link.py
|
|
@@ -56,6 +57,7 @@ praetorian_cli/sdk/entities/integrations.py
|
|
|
56
57
|
praetorian_cli/sdk/entities/jobs.py
|
|
57
58
|
praetorian_cli/sdk/entities/keys.py
|
|
58
59
|
praetorian_cli/sdk/entities/preseeds.py
|
|
60
|
+
praetorian_cli/sdk/entities/reports.py
|
|
59
61
|
praetorian_cli/sdk/entities/risks.py
|
|
60
62
|
praetorian_cli/sdk/entities/scanners.py
|
|
61
63
|
praetorian_cli/sdk/entities/schedules.py
|
|
@@ -87,6 +89,7 @@ praetorian_cli/sdk/test/test_job.py
|
|
|
87
89
|
praetorian_cli/sdk/test/test_key.py
|
|
88
90
|
praetorian_cli/sdk/test/test_mcp.py
|
|
89
91
|
praetorian_cli/sdk/test/test_preseed.py
|
|
92
|
+
praetorian_cli/sdk/test/test_report.py
|
|
90
93
|
praetorian_cli/sdk/test/test_risk.py
|
|
91
94
|
praetorian_cli/sdk/test/test_search.py
|
|
92
95
|
praetorian_cli/sdk/test/test_seed.py
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{praetorian_cli-2.3.4 → praetorian_cli-2.3.5}/praetorian_cli/scripts/commands/nmap-example.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{praetorian_cli-2.3.4 → praetorian_cli-2.3.5}/praetorian_cli/ui/aegis/commands/job_helpers.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{praetorian_cli-2.3.4 → praetorian_cli-2.3.5}/praetorian_cli/ui/aegis/commands/schedule_helpers.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{praetorian_cli-2.3.4 → praetorian_cli-2.3.5}/praetorian_cli/ui/conversation/textual_chat.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|