diff --git a/gvm/protocols/gmp/_gmpnext.py b/gvm/protocols/gmp/_gmpnext.py index a77d649d..357c236e 100644 --- a/gvm/protocols/gmp/_gmpnext.py +++ b/gvm/protocols/gmp/_gmpnext.py @@ -1758,3 +1758,53 @@ def get_audit_report_legacy( ignore_pagination=ignore_pagination, details=details, ) + + def export_scan_report( + self, + report_id: EntityID, + format_id: EntityID, + *, + config_id: EntityID | None = None, + filter_string: str | None = None, + ignore_pagination: bool = False, + lean: bool = False, + notes_details: bool = False, + overrides_details: bool = False, + result_tags: bool = False, + ) -> T: + """Request an asynchronous export of a scan report. + + If an identical export is already pending or running, the existing + report export is returned instead of creating a duplicate. + + Args: + report_id: UUID of the scan report to export. + format_id: UUID of the report format to apply. + config_id: UUID of an optional report configuration. + filter_string: Filter term to apply while generating the report. + ignore_pagination: Whether pagination settings in the filter + should be ignored. + lean: Whether lean report data should be generated. + notes_details: Whether note details should be included. + overrides_details: Whether override details should be included. + result_tags: Whether result tags should be included. + + Returns: + A request for the export_scan_report GMP command. + + Raises: + RequiredArgument: If report_id or format_id is not provided. + """ + return self._send_request_and_transform_response( + ScanReports.export_scan_report( + report_id=report_id, + format_id=format_id, + config_id=config_id, + filter_string=filter_string, + ignore_pagination=ignore_pagination, + lean=lean, + notes_details=notes_details, + overrides_details=overrides_details, + result_tags=result_tags, + ) + ) diff --git a/gvm/protocols/gmp/requests/next/_scan_report.py b/gvm/protocols/gmp/requests/next/_scan_report.py index e62c432a..b7c13589 100644 --- a/gvm/protocols/gmp/requests/next/_scan_report.py +++ b/gvm/protocols/gmp/requests/next/_scan_report.py @@ -1,6 +1,7 @@ from gvm.errors import RequiredArgument from gvm.protocols.core import Request from gvm.protocols.gmp.requests import EntityID +from gvm.utils import to_bool from gvm.xml import XmlCommand @@ -37,3 +38,76 @@ def get_scan_report( cmd.add_filter(filter_string, filter_id) return cmd + + @classmethod + def export_scan_report( + cls, + report_id: EntityID, + format_id: EntityID, + *, + config_id: EntityID | None = None, + filter_string: str | None = None, + ignore_pagination: bool = False, + lean: bool = False, + notes_details: bool = False, + overrides_details: bool = False, + result_tags: bool = False, + ) -> Request: + """Request an asynchronous export of a scan report. + + If an identical export is already pending or running, the existing + report export is returned instead of creating a duplicate. + + Args: + report_id: UUID of the scan report to export. + format_id: UUID of the report format to apply. + config_id: UUID of an optional report configuration. + filter_string: Filter term to apply while generating the report. + ignore_pagination: Whether pagination settings in the filter + should be ignored. + lean: Whether lean report data should be generated. + notes_details: Whether note details should be included. + overrides_details: Whether override details should be included. + result_tags: Whether result tags should be included. + + Returns: + A request for the export_scan_report GMP command. + + Raises: + RequiredArgument: If report_id is not provided. + """ + if not report_id: + raise RequiredArgument( + function=cls.export_scan_report.__name__, + argument="report_id", + ) + + cmd = XmlCommand("export_scan_report") + cmd.set_attribute("report_id", str(report_id)) + cmd.set_attribute("format_id", str(format_id)) + + if config_id: + cmd.set_attribute("config_id", str(config_id)) + + if filter_string is not None: + cmd.set_attribute("filter", filter_string) + + cmd.set_attribute( + "ignore_pagination", + to_bool(ignore_pagination), + ) + cmd.set_attribute("lean", to_bool(lean)) + cmd.set_attribute( + "notes_details", + to_bool(notes_details), + ) + cmd.set_attribute( + "overrides_details", + to_bool(overrides_details), + ) + cmd.set_attribute( + "result_tags", + to_bool(result_tags), + ) + + return cmd diff --git a/tests/protocols/gmpnext/entities/scan_reports/__init__.py b/tests/protocols/gmpnext/entities/scan_reports/__init__.py index 585512c6..de5bb5e4 100644 --- a/tests/protocols/gmpnext/entities/scan_reports/__init__.py +++ b/tests/protocols/gmpnext/entities/scan_reports/__init__.py @@ -3,8 +3,14 @@ # SPDX-License-Identifier: GPL-3.0-or-later # +from .test_export_scan_report import ( + GmpExportScanReportTestMixin, +) from .test_get_scan_report import ( GmpGetScanReportTestMixin, ) -__all__ = ("GmpGetScanReportTestMixin",) +__all__ = ( + "GmpExportScanReportTestMixin", + "GmpGetScanReportTestMixin", +) diff --git a/tests/protocols/gmpnext/entities/scan_reports/test_export_scan_report.py b/tests/protocols/gmpnext/entities/scan_reports/test_export_scan_report.py new file mode 100644 index 00000000..5a5660fc --- /dev/null +++ b/tests/protocols/gmpnext/entities/scan_reports/test_export_scan_report.py @@ -0,0 +1,138 @@ +# SPDX-FileCopyrightText: 2026 Greenbone AG +# +# SPDX-License-Identifier: GPL-3.0-or-later +# + +from gvm.errors import RequiredArgument + + +class GmpExportScanReportTestMixin: + def test_export_scan_report_without_report_id(self): + with self.assertRaises(RequiredArgument): + self.gmp.export_scan_report(None, format_id="f1") + + with self.assertRaises(RequiredArgument): + self.gmp.export_scan_report("", format_id="f1") + + def test_export_scan_report(self): + self.gmp.export_scan_report( + report_id="r1", + format_id="f1", + ) + + self.connection.send.has_been_called_with( + b'' + ) + + def test_export_scan_report_with_config_id(self): + self.gmp.export_scan_report( + report_id="r1", + format_id="f1", + config_id="c1", + ) + + self.connection.send.has_been_called_with( + b'' + ) + + def test_export_scan_report_with_filter_string(self): + self.gmp.export_scan_report( + report_id="r1", + format_id="f1", + filter_string="levels=hml", + ) + + self.connection.send.has_been_called_with( + b'' + ) + + def test_export_scan_report_with_ignore_pagination(self): + self.gmp.export_scan_report( + report_id="r1", + format_id="f1", + ignore_pagination=True, + ) + + self.connection.send.has_been_called_with( + b'' + ) + + def test_export_scan_report_with_lean(self): + self.gmp.export_scan_report( + report_id="r1", + format_id="f1", + lean=True, + ) + + self.connection.send.has_been_called_with( + b'' + ) + + def test_export_scan_report_with_notes_details(self): + self.gmp.export_scan_report( + report_id="r1", + format_id="f1", + notes_details=True, + ) + + self.connection.send.has_been_called_with( + b'' + ) + + def test_export_scan_report_with_overrides_details(self): + self.gmp.export_scan_report( + report_id="r1", + format_id="f1", + overrides_details=True, + ) + + self.connection.send.has_been_called_with( + b'' + ) + + def test_export_scan_report_with_result_tags(self): + self.gmp.export_scan_report( + report_id="r1", + format_id="f1", + result_tags=True, + ) + + self.connection.send.has_been_called_with( + b'' + ) + + def test_export_scan_report_with_all_arguments(self): + self.gmp.export_scan_report( + report_id="r1", + format_id="f1", + config_id="c1", + filter_string="levels=hml", + ignore_pagination=True, + lean=True, + notes_details=True, + overrides_details=True, + result_tags=True, + ) + + self.connection.send.has_been_called_with( + b'' + ) diff --git a/tests/protocols/gmpnext/entities/test_scan_reports.py b/tests/protocols/gmpnext/entities/test_scan_reports.py index d4fb2caa..9a889296 100644 --- a/tests/protocols/gmpnext/entities/test_scan_reports.py +++ b/tests/protocols/gmpnext/entities/test_scan_reports.py @@ -4,10 +4,15 @@ # from ...gmpnext import GMPTestCase -from .scan_reports.test_get_scan_report import ( +from .scan_reports import ( + GmpExportScanReportTestMixin, GmpGetScanReportTestMixin, ) class GmpGetScanReportTestCase(GmpGetScanReportTestMixin, GMPTestCase): pass + + +class GmpExportScanReportTestCase(GmpExportScanReportTestMixin, GMPTestCase): + pass