From f9814c0c1a92336d3db444d7bf1343b9aac18ba5 Mon Sep 17 00:00:00 2001 From: ozgen Date: Thu, 6 Aug 2026 14:35:19 +0200 Subject: [PATCH 1/2] add: add export_scan_report request support --- gvm/protocols/gmp/_gmpnext.py | 50 +++++++ .../gmp/requests/next/_scan_report.py | 73 +++++++++ .../gmpnext/entities/scan_reports/__init__.py | 8 +- .../scan_reports/test_export_scan_report.py | 138 ++++++++++++++++++ .../gmpnext/entities/test_scan_reports.py | 7 +- uv.lock | 2 +- 6 files changed, 275 insertions(+), 3 deletions(-) create mode 100644 tests/protocols/gmpnext/entities/scan_reports/test_export_scan_report.py 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..428c84d9 100644 --- a/gvm/protocols/gmp/requests/next/_scan_report.py +++ b/gvm/protocols/gmp/requests/next/_scan_report.py @@ -37,3 +37,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", + "1" if ignore_pagination else "0", + ) + cmd.set_attribute("lean", "1" if lean else "0") + cmd.set_attribute( + "notes_details", + "1" if notes_details else "0", + ) + cmd.set_attribute( + "overrides_details", + "1" if overrides_details else "0", + ) + cmd.set_attribute( + "result_tags", + "1" if result_tags else "0", + ) + + 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 diff --git a/uv.lock b/uv.lock index 7c18a4e5..fae3e98b 100644 --- a/uv.lock +++ b/uv.lock @@ -1267,7 +1267,7 @@ wheels = [ [[package]] name = "python-gvm" -version = "27.5.1.dev1" +version = "27.6.1.dev1" source = { editable = "." } dependencies = [ { name = "httpx", extra = ["http2"] }, From 9d0becde8f95a377e8dc5c4e400ae12737dffdfa Mon Sep 17 00:00:00 2001 From: ozgen Date: Thu, 6 Aug 2026 14:48:47 +0200 Subject: [PATCH 2/2] use to_bool for parsing boolean values --- gvm/protocols/gmp/requests/next/_scan_report.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/gvm/protocols/gmp/requests/next/_scan_report.py b/gvm/protocols/gmp/requests/next/_scan_report.py index 428c84d9..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 @@ -93,20 +94,20 @@ def export_scan_report( cmd.set_attribute( "ignore_pagination", - "1" if ignore_pagination else "0", + to_bool(ignore_pagination), ) - cmd.set_attribute("lean", "1" if lean else "0") + cmd.set_attribute("lean", to_bool(lean)) cmd.set_attribute( "notes_details", - "1" if notes_details else "0", + to_bool(notes_details), ) cmd.set_attribute( "overrides_details", - "1" if overrides_details else "0", + to_bool(overrides_details), ) cmd.set_attribute( "result_tags", - "1" if result_tags else "0", + to_bool(result_tags), ) return cmd