diff --git a/gvm/protocols/gmp/_gmpnext.py b/gvm/protocols/gmp/_gmpnext.py index 631bf321..9a9a87f7 100644 --- a/gvm/protocols/gmp/_gmpnext.py +++ b/gvm/protocols/gmp/_gmpnext.py @@ -30,6 +30,7 @@ ReportTlsCertificates, ReportVulnerabilities, Tasks, + WebApplicationTargets, ) from .requests.v224 import HostsOrdering @@ -1245,3 +1246,139 @@ def get_report_vulnerabilities( details=details, ) ) + + def create_web_application_target( + self, + name: str, + urls: list[str], + *, + comment: str | None = None, + exclude_urls: list[str] | None = None, + credential_id: EntityID | None = None, + ) -> T: + """Create a new web application target. + + Args: + name: Name of the web application target. + urls: List of URLs to scan. + comment: Comment for the target. + exclude_urls: List of URLs to exclude from the scan. + credential_id: UUID of a credential to use on target. + """ + return self._send_request_and_transform_response( + WebApplicationTargets.create_web_application_target( + name=name, + urls=urls, + comment=comment, + exclude_urls=exclude_urls, + credential_id=credential_id, + ) + ) + + def modify_web_application_target( + self, + web_application_target_id: EntityID, + *, + name: str | None = None, + comment: str | None = None, + urls: list[str] | None = None, + exclude_urls: list[str] | None = None, + credential_id: EntityID | None = None, + ) -> T: + """Modify an existing web application target. + + Args: + web_application_target_id: UUID of target to modify. + name: Name of target. + comment: Comment on target. + urls: List of URLs to scan. + exclude_urls: List of URLs to exclude from the scan. + credential_id: UUID of credential to use on target. + """ + return self._send_request_and_transform_response( + WebApplicationTargets.modify_web_application_target( + web_application_target_id, + name=name, + comment=comment, + urls=urls, + exclude_urls=exclude_urls, + credential_id=credential_id, + ) + ) + + def clone_web_application_target( + self, web_application_target_id: EntityID + ) -> T: + """Clone an existing web application target. + + Args: + web_application_target_id: UUID of an existing web application target to clone. + """ + return self._send_request_and_transform_response( + WebApplicationTargets.clone_web_application_target( + web_application_target_id + ) + ) + + def delete_web_application_target( + self, + web_application_target_id: EntityID, + *, + ultimate: bool | None = False, + ) -> T: + """Delete an existing web application target. + + Args: + web_application_target_id: UUID of an existing web application target to delete. + ultimate: Whether to remove entirely or to the trashcan. + """ + return self._send_request_and_transform_response( + WebApplicationTargets.delete_web_application_target( + web_application_target_id, + ultimate=ultimate, + ) + ) + + def get_web_application_target( + self, + web_application_target_id: EntityID, + *, + tasks: bool | None = None, + ) -> T: + """Request a single web application target. + + Args: + web_application_target_id: UUID of the web application target to request. + tasks: Whether to include list of tasks that use the target. + """ + return self._send_request_and_transform_response( + WebApplicationTargets.get_web_application_target( + web_application_target_id, + tasks=tasks, + ) + ) + + def get_web_application_targets( + self, + *, + filter_string: str | None = None, + filter_id: EntityID | None = None, + trash: bool | None = None, + tasks: bool | None = None, + ) -> T: + """Request a list of web application targets. + + Args: + filter_string: Filter term to use for the query. + filter_id: UUID of an existing filter to use for the query. + trash: Whether to include targets in the trashcan. + tasks: Whether to include list of tasks that use the target. + """ + return self._send_request_and_transform_response( + WebApplicationTargets.get_web_application_targets( + filter_string=filter_string, + filter_id=filter_id, + trash=trash, + tasks=tasks, + ) + ) diff --git a/gvm/protocols/gmp/requests/next/__init__.py b/gvm/protocols/gmp/requests/next/__init__.py index 593f37e0..66453693 100644 --- a/gvm/protocols/gmp/requests/next/__init__.py +++ b/gvm/protocols/gmp/requests/next/__init__.py @@ -45,6 +45,9 @@ ReportVulnerabilities, ) from gvm.protocols.gmp.requests.next._tasks import Tasks +from gvm.protocols.gmp.requests.next._web_application_targets import ( + WebApplicationTargets, +) from .._entity_id import EntityID from .._version import Version @@ -203,4 +206,5 @@ "Users", "Version", "Vulnerabilities", + "WebApplicationTargets", ) diff --git a/gvm/protocols/gmp/requests/next/_web_application_targets.py b/gvm/protocols/gmp/requests/next/_web_application_targets.py new file mode 100644 index 00000000..3d6c31cd --- /dev/null +++ b/gvm/protocols/gmp/requests/next/_web_application_targets.py @@ -0,0 +1,209 @@ +# SPDX-FileCopyrightText: 2026 Greenbone AG +# +# SPDX-License-Identifier: GPL-3.0-or-later +# + +from gvm.errors import RequiredArgument +from gvm.protocols.core import Request +from gvm.protocols.gmp.requests._entity_id import EntityID +from gvm.utils import to_bool, to_comma_list +from gvm.xml import XmlCommand + + +class WebApplicationTargets: + @classmethod + def create_web_application_target( + cls, + name: str, + urls: list[str], + *, + comment: str | None = None, + exclude_urls: list[str] | None = None, + credential_id: EntityID | None = None, + ) -> Request: + """Create a new web application target. + + Args: + name: Name of the target. + urls: List of URLs to scan. + comment: Comment for the target. + exclude_urls: List of URLs to exclude from the scan. + credential_id: UUID of a credential to use on target. + """ + if not name: + raise RequiredArgument( + function=cls.create_web_application_target.__name__, + argument="name", + ) + + if not urls: + raise RequiredArgument( + function=cls.create_web_application_target.__name__, + argument="urls", + ) + + cmd = XmlCommand("create_web_application_target") + cmd.add_element("name", name) + cmd.add_element("urls", to_comma_list(urls)) + + if comment: + cmd.add_element("comment", comment) + + if exclude_urls: + cmd.add_element("exclude_urls", to_comma_list(exclude_urls)) + + if credential_id: + cmd.add_element("credential", attrs={"id": str(credential_id)}) + + return cmd + + @classmethod + def modify_web_application_target( + cls, + web_application_target_id: EntityID, + *, + name: str | None = None, + comment: str | None = None, + urls: list[str] | None = None, + exclude_urls: list[str] | None = None, + credential_id: EntityID | None = None, + ) -> Request: + """Modify an existing web application target. + + Args: + web_application_target_id: UUID of target to modify. + name: Name of target. + comment: Comment on target. + urls: List of URLs to scan. + exclude_urls: List of URLs to exclude from the scan. + credential_id: UUID of credential to use on target. + """ + if not web_application_target_id: + raise RequiredArgument( + function=cls.modify_web_application_target.__name__, + argument="web_application_target_id", + ) + + cmd = XmlCommand("modify_web_application_target") + cmd.set_attribute( + "web_application_target_id", str(web_application_target_id) + ) + + if comment: + cmd.add_element("comment", comment) + + if name: + cmd.add_element("name", name) + + if urls: + cmd.add_element("urls", to_comma_list(urls)) + + if exclude_urls: + cmd.add_element("exclude_urls", to_comma_list(exclude_urls)) + + if credential_id: + cmd.add_element("credential", attrs={"id": str(credential_id)}) + + return cmd + + @classmethod + def clone_web_application_target( + cls, web_application_target_id: EntityID + ) -> Request: + """Clone an existing web application target. + + Args: + web_application_target_id: UUID of an existing target to clone. + """ + if not web_application_target_id: + raise RequiredArgument( + function=cls.clone_web_application_target.__name__, + argument="web_application_target_id", + ) + + cmd = XmlCommand("create_web_application_target") + cmd.add_element("copy", str(web_application_target_id)) + return cmd + + @classmethod + def delete_web_application_target( + cls, + web_application_target_id: EntityID, + *, + ultimate: bool | None = False, + ) -> Request: + """Delete an existing web application target. + + Args: + web_application_target_id: UUID of an existing target to delete. + ultimate: Whether to remove entirely or move to the trashcan. + """ + if not web_application_target_id: + raise RequiredArgument( + function=cls.delete_web_application_target.__name__, + argument="web_application_target_id", + ) + + cmd = XmlCommand("delete_web_application_target") + cmd.set_attribute( + "web_application_target_id", str(web_application_target_id) + ) + cmd.set_attribute("ultimate", to_bool(ultimate)) + return cmd + + @classmethod + def get_web_application_target( + cls, + web_application_target_id: EntityID, + *, + tasks: bool | None = None, + ) -> Request: + """Request a single web application target. + + Args: + web_application_target_id: UUID of the target to request. + tasks: Whether to include list of tasks that use the target. + """ + if not web_application_target_id: + raise RequiredArgument( + function=cls.get_web_application_target.__name__, + argument="web_application_target_id", + ) + + cmd = XmlCommand("get_web_application_targets") + cmd.set_attribute( + "web_application_target_id", str(web_application_target_id) + ) + + if tasks is not None: + cmd.set_attribute("tasks", to_bool(tasks)) + + return cmd + + @classmethod + def get_web_application_targets( + cls, + *, + filter_string: str | None = None, + filter_id: EntityID | None = None, + trash: bool | None = None, + tasks: bool | None = None, + ) -> Request: + """Request a list of web application targets. + + Args: + filter_string: Filter term to use for the query. + filter_id: UUID of an existing filter to use for the query. + trash: Whether to include targets in the trashcan. + tasks: Whether to include list of tasks that use the target. + """ + cmd = XmlCommand("get_web_application_targets") + cmd.add_filter(filter_string, filter_id) + + if trash is not None: + cmd.set_attribute("trash", to_bool(trash)) + + if tasks is not None: + cmd.set_attribute("tasks", to_bool(tasks)) + + return cmd diff --git a/tests/protocols/gmpnext/entities/test_web_application_targets.py b/tests/protocols/gmpnext/entities/test_web_application_targets.py new file mode 100644 index 00000000..dd0c8677 --- /dev/null +++ b/tests/protocols/gmpnext/entities/test_web_application_targets.py @@ -0,0 +1,50 @@ +# SPDX-FileCopyrightText: 2026 Greenbone AG +# +# SPDX-License-Identifier: GPL-3.0-or-later +# + +from ...gmpnext import GMPTestCase +from .web_application_targets import ( + GmpCloneWebApplicationTargetTestMixin, + GmpCreateWebApplicationTargetTestMixin, + GmpDeleteWebApplicationTargetTestMixin, + GmpGetWebApplicationTargetsTestMixin, + GmpGetWebApplicationTargetTestMixin, + GmpModifyWebApplicationTargetTestMixin, +) + + +class GmpCloneWebApplicationTargetTestCase( + GmpCloneWebApplicationTargetTestMixin, GMPTestCase +): + pass + + +class GmpCreateWebApplicationTargetTestCase( + GmpCreateWebApplicationTargetTestMixin, GMPTestCase +): + pass + + +class GmpDeleteWebApplicationTargetTestCase( + GmpDeleteWebApplicationTargetTestMixin, GMPTestCase +): + pass + + +class GmpGetWebApplicationTargetTestCase( + GmpGetWebApplicationTargetTestMixin, GMPTestCase +): + pass + + +class GmpGetWebApplicationTargetsTestCase( + GmpGetWebApplicationTargetsTestMixin, GMPTestCase +): + pass + + +class GmpModifyWebApplicationTargetTestCase( + GmpModifyWebApplicationTargetTestMixin, GMPTestCase +): + pass diff --git a/tests/protocols/gmpnext/entities/web_application_targets/__init__.py b/tests/protocols/gmpnext/entities/web_application_targets/__init__.py new file mode 100644 index 00000000..81c5a5c8 --- /dev/null +++ b/tests/protocols/gmpnext/entities/web_application_targets/__init__.py @@ -0,0 +1,32 @@ +# SPDX-FileCopyrightText: 2026 Greenbone AG +# +# SPDX-License-Identifier: GPL-3.0-or-later +# + +from .test_clone_web_application_target import ( + GmpCloneWebApplicationTargetTestMixin, +) +from .test_create_web_application_target import ( + GmpCreateWebApplicationTargetTestMixin, +) +from .test_delete_web_application_target import ( + GmpDeleteWebApplicationTargetTestMixin, +) +from .test_get_web_application_target import ( + GmpGetWebApplicationTargetTestMixin, +) +from .test_get_web_application_targets import ( + GmpGetWebApplicationTargetsTestMixin, +) +from .test_modify_web_application_target import ( + GmpModifyWebApplicationTargetTestMixin, +) + +__all__ = ( + "GmpCloneWebApplicationTargetTestMixin", + "GmpCreateWebApplicationTargetTestMixin", + "GmpDeleteWebApplicationTargetTestMixin", + "GmpGetWebApplicationTargetTestMixin", + "GmpGetWebApplicationTargetsTestMixin", + "GmpModifyWebApplicationTargetTestMixin", +) diff --git a/tests/protocols/gmpnext/entities/web_application_targets/test_clone_web_application_target.py b/tests/protocols/gmpnext/entities/web_application_targets/test_clone_web_application_target.py new file mode 100644 index 00000000..dfe74eaa --- /dev/null +++ b/tests/protocols/gmpnext/entities/web_application_targets/test_clone_web_application_target.py @@ -0,0 +1,26 @@ +# SPDX-FileCopyrightText: 2026 Greenbone AG +# +# SPDX-License-Identifier: GPL-3.0-or-later +# + +from gvm.errors import RequiredArgument + + +class GmpCloneWebApplicationTargetTestMixin: + TARGET_ID = "00000000-0000-0000-0000-000000000000" + + def test_clone(self): + self.gmp.clone_web_application_target(self.TARGET_ID) + + self.connection.send.has_been_called_with( + "" + f"{self.TARGET_ID}" + "".encode() + ) + + def test_missing_id(self): + with self.assertRaises(RequiredArgument): + self.gmp.clone_web_application_target("") + + with self.assertRaises(RequiredArgument): + self.gmp.clone_web_application_target(None) diff --git a/tests/protocols/gmpnext/entities/web_application_targets/test_create_web_application_target.py b/tests/protocols/gmpnext/entities/web_application_targets/test_create_web_application_target.py new file mode 100644 index 00000000..77f659bf --- /dev/null +++ b/tests/protocols/gmpnext/entities/web_application_targets/test_create_web_application_target.py @@ -0,0 +1,85 @@ +# SPDX-FileCopyrightText: 2026 Greenbone AG +# +# SPDX-License-Identifier: GPL-3.0-or-later +# + +from gvm.errors import RequiredArgument + + +class GmpCreateWebApplicationTargetTestMixin: + def test_create_target_missing_name(self): + with self.assertRaises(RequiredArgument): + self.gmp.create_web_application_target( + None, urls=["https://example.com"] + ) + + with self.assertRaises(RequiredArgument): + self.gmp.create_web_application_target( + name=None, urls=["https://example.com"] + ) + + with self.assertRaises(RequiredArgument): + self.gmp.create_web_application_target( + "", urls=["https://example.com"] + ) + + def test_create_target_missing_urls(self): + with self.assertRaises(RequiredArgument): + self.gmp.create_web_application_target(name="foo", urls=None) + + with self.assertRaises(RequiredArgument): + self.gmp.create_web_application_target(name="foo", urls=[]) + + def test_create_target_with_comment(self): + self.gmp.create_web_application_target( + "foo", urls=["https://example.com"], comment="bar" + ) + + self.connection.send.has_been_called_with( + b"" + b"foo" + b"https://example.com" + b"bar" + b"" + ) + + def test_create_target_with_exclude_urls(self): + self.gmp.create_web_application_target( + "foo", + urls=["https://example.com"], + exclude_urls=["https://example.com/logout"], + ) + + self.connection.send.has_been_called_with( + b"" + b"foo" + b"https://example.com" + b"https://example.com/logout" + b"" + ) + + def test_create_target_with_multiple_urls(self): + self.gmp.create_web_application_target( + "foo", + urls=["https://example.com", "https://example.com/app"], + ) + + self.connection.send.has_been_called_with( + b"" + b"foo" + b"https://example.com,https://example.com/app" + b"" + ) + + def test_create_target_with_credential_id(self): + self.gmp.create_web_application_target( + "foo", urls=["https://example.com"], credential_id="c1" + ) + + self.connection.send.has_been_called_with( + b"" + b"foo" + b"https://example.com" + b'' + b"" + ) diff --git a/tests/protocols/gmpnext/entities/web_application_targets/test_delete_web_application_target.py b/tests/protocols/gmpnext/entities/web_application_targets/test_delete_web_application_target.py new file mode 100644 index 00000000..79f960a9 --- /dev/null +++ b/tests/protocols/gmpnext/entities/web_application_targets/test_delete_web_application_target.py @@ -0,0 +1,31 @@ +# SPDX-FileCopyrightText: 2026 Greenbone AG +# +# SPDX-License-Identifier: GPL-3.0-or-later +# + +from gvm.errors import GvmError + + +class GmpDeleteWebApplicationTargetTestMixin: + def test_delete(self): + self.gmp.delete_web_application_target("a1") + + self.connection.send.has_been_called_with( + b"' + ) + + def test_delete_ultimate(self): + self.gmp.delete_web_application_target("a1", ultimate=True) + + self.connection.send.has_been_called_with( + b"' + ) + + def test_missing_id(self): + with self.assertRaises(GvmError): + self.gmp.delete_web_application_target(None) + + with self.assertRaises(GvmError): + self.gmp.delete_web_application_target("") diff --git a/tests/protocols/gmpnext/entities/web_application_targets/test_get_web_application_target.py b/tests/protocols/gmpnext/entities/web_application_targets/test_get_web_application_target.py new file mode 100644 index 00000000..79c4aabb --- /dev/null +++ b/tests/protocols/gmpnext/entities/web_application_targets/test_get_web_application_target.py @@ -0,0 +1,45 @@ +# SPDX-FileCopyrightText: 2026 Greenbone AG +# +# SPDX-License-Identifier: GPL-3.0-or-later +# + +from gvm.errors import RequiredArgument + + +class GmpGetWebApplicationTargetTestMixin: + def test_get_target(self): + self.gmp.get_web_application_target("t1") + + self.connection.send.has_been_called_with( + b'' + ) + + self.gmp.get_web_application_target(web_application_target_id="t1") + + self.connection.send.has_been_called_with( + b'' + ) + + def test_get_target_missing_target_id(self): + with self.assertRaises(RequiredArgument): + self.gmp.get_web_application_target(web_application_target_id=None) + + with self.assertRaises(RequiredArgument): + self.gmp.get_web_application_target("") + + def test_get_target_with_tasks(self): + self.gmp.get_web_application_target( + web_application_target_id="t1", tasks=True + ) + + self.connection.send.has_been_called_with( + b'' + ) + + self.gmp.get_web_application_target( + web_application_target_id="t1", tasks=False + ) + + self.connection.send.has_been_called_with( + b'' + ) diff --git a/tests/protocols/gmpnext/entities/web_application_targets/test_get_web_application_targets.py b/tests/protocols/gmpnext/entities/web_application_targets/test_get_web_application_targets.py new file mode 100644 index 00000000..e287d96b --- /dev/null +++ b/tests/protocols/gmpnext/entities/web_application_targets/test_get_web_application_targets.py @@ -0,0 +1,53 @@ +# SPDX-FileCopyrightText: 2026 Greenbone AG +# +# SPDX-License-Identifier: GPL-3.0-or-later +# + + +class GmpGetWebApplicationTargetsTestMixin: + def test_get_targets(self): + self.gmp.get_web_application_targets() + + self.connection.send.has_been_called_with( + b"" + ) + + def test_get_targets_with_filter_string(self): + self.gmp.get_web_application_targets(filter_string="foo=bar") + + self.connection.send.has_been_called_with( + b'' + ) + + def test_get_targets_with_filter_id(self): + self.gmp.get_web_application_targets(filter_id="f1") + + self.connection.send.has_been_called_with( + b'' + ) + + def test_get_targets_with_trash(self): + self.gmp.get_web_application_targets(trash=True) + + self.connection.send.has_been_called_with( + b'' + ) + + self.gmp.get_web_application_targets(trash=False) + + self.connection.send.has_been_called_with( + b'' + ) + + def test_get_targets_with_tasks(self): + self.gmp.get_web_application_targets(tasks=True) + + self.connection.send.has_been_called_with( + b'' + ) + + self.gmp.get_web_application_targets(tasks=False) + + self.connection.send.has_been_called_with( + b'' + ) diff --git a/tests/protocols/gmpnext/entities/web_application_targets/test_modify_web_application_target.py b/tests/protocols/gmpnext/entities/web_application_targets/test_modify_web_application_target.py new file mode 100644 index 00000000..47ba177f --- /dev/null +++ b/tests/protocols/gmpnext/entities/web_application_targets/test_modify_web_application_target.py @@ -0,0 +1,91 @@ +# SPDX-FileCopyrightText: 2026 Greenbone AG +# +# SPDX-License-Identifier: GPL-3.0-or-later +# + +from gvm.errors import RequiredArgument + + +class GmpModifyWebApplicationTargetTestMixin: + def test_modify_web_application_target(self): + self.gmp.modify_web_application_target(web_application_target_id="t1") + + self.connection.send.has_been_called_with( + b'' + ) + + def test_modify_target_missing_target_id(self): + with self.assertRaises(RequiredArgument): + self.gmp.modify_web_application_target( + web_application_target_id=None + ) + + with self.assertRaises(RequiredArgument): + self.gmp.modify_web_application_target(web_application_target_id="") + + def test_modify_target_with_comment(self): + self.gmp.modify_web_application_target( + web_application_target_id="t1", comment="foo" + ) + + self.connection.send.has_been_called_with( + b'' + b"foo" + b"" + ) + + def test_modify_target_with_urls(self): + self.gmp.modify_web_application_target( + web_application_target_id="t1", urls=["https://example.com"] + ) + + self.connection.send.has_been_called_with( + b'' + b"https://example.com" + b"" + ) + + self.gmp.modify_web_application_target( + web_application_target_id="t1", + urls=["https://example.com", "https://example.com/app"], + ) + + self.connection.send.has_been_called_with( + b'' + b"https://example.com,https://example.com/app" + b"" + ) + + def test_modify_target_with_exclude_urls(self): + self.gmp.modify_web_application_target( + web_application_target_id="t1", + exclude_urls=["https://example.com/logout"], + ) + + self.connection.send.has_been_called_with( + b'' + b"https://example.com/logout" + b"" + ) + + def test_modify_target_with_name(self): + self.gmp.modify_web_application_target( + web_application_target_id="t1", name="foo" + ) + + self.connection.send.has_been_called_with( + b'' + b"foo" + b"" + ) + + def test_modify_target_with_credential_id(self): + self.gmp.modify_web_application_target( + web_application_target_id="t1", credential_id="c1" + ) + + self.connection.send.has_been_called_with( + b'' + b'' + b"" + )