diff --git a/gvm/protocols/gmp/_gmpnext.py b/gvm/protocols/gmp/_gmpnext.py index 7584d896..eb57fb99 100644 --- a/gvm/protocols/gmp/_gmpnext.py +++ b/gvm/protocols/gmp/_gmpnext.py @@ -8,7 +8,7 @@ from .._protocol import T from ._gmp227 import GMPv227 -from .requests.next import AgentGroups, AgentInstallers, Agents +from .requests.next import AgentGroups, AgentInstallers, Agents, OCIImageTargets class GMPNext(GMPv227[T]): @@ -274,3 +274,121 @@ def clone_agent_group( return self._send_request_and_transform_response( AgentGroups.clone_agent_group(agent_group_id) ) + + def create_oci_image_target( + self, + name: str, + image_references: list[str], + *, + comment: Optional[str] = None, + credential_id: Optional[EntityID] = None, + ) -> T: + """Create a new OCI image target + + Args: + name: Name of the OCI image target + image_references: List of OCI image URLs to scan + comment: Comment for the target + credential_id: UUID of a credential to use on target + """ + return self._send_request_and_transform_response( + OCIImageTargets.create_oci_image_target( + name=name, + image_references=image_references, + comment=comment, + credential_id=credential_id, + ) + ) + + def modify_oci_image_target( + self, + oci_image_target_id: EntityID, + *, + name: Optional[str] = None, + comment: Optional[str] = None, + image_references: Optional[list[str]] = None, + credential_id: Optional[EntityID] = None, + ) -> T: + """Modify an existing OCI image target. + + Args: + oci_image_target_id: UUID of target to modify. + comment: Comment on target. + name: Name of target. + image_references: List of OCI image URLs to scan. + credential_id: UUID of credential to use on target. + """ + return self._send_request_and_transform_response( + OCIImageTargets.modify_oci_image_target( + oci_image_target_id, + name=name, + comment=comment, + image_references=image_references, + credential_id=credential_id, + ) + ) + + def clone_oci_image_target(self, oci_image_target_id: EntityID) -> T: + """Clone an existing OCI image target. + + Args: + oci_image_target_id: UUID of an existing OCI image target to clone. + """ + return self._send_request_and_transform_response( + OCIImageTargets.clone_oci_image_target(oci_image_target_id) + ) + + def delete_oci_image_target( + self, oci_image_target_id: EntityID, *, ultimate: Optional[bool] = False + ) -> T: + """Delete an existing OCI image target. + + Args: + oci_image_target_id: UUID of an existing OCI image target to delete. + ultimate: Whether to remove entirely or to the trashcan. + """ + return self._send_request_and_transform_response( + OCIImageTargets.delete_oci_image_target( + oci_image_target_id, ultimate=ultimate + ) + ) + + def get_oci_image_target( + self, oci_image_target_id: EntityID, *, tasks: Optional[bool] = None + ) -> T: + """Request a single OCI image target. + + Args: + oci_image_target_id: UUID of the OCI image target to request. + tasks: Whether to include list of tasks that use the target + """ + return self._send_request_and_transform_response( + OCIImageTargets.get_oci_image_target( + oci_image_target_id, tasks=tasks + ) + ) + + def get_oci_image_targets( + self, + *, + filter_string: Optional[str] = None, + filter_id: Optional[EntityID] = None, + trash: Optional[bool] = None, + tasks: Optional[bool] = None, + ) -> T: + """Request a list of OCI image 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( + OCIImageTargets.get_oci_image_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 b42ae874..2d1e7184 100644 --- a/gvm/protocols/gmp/requests/next/__init__.py +++ b/gvm/protocols/gmp/requests/next/__init__.py @@ -5,6 +5,7 @@ from gvm.protocols.gmp.requests.next._agent_groups import AgentGroups from gvm.protocols.gmp.requests.next._agent_installers import AgentInstallers from gvm.protocols.gmp.requests.next._agents import Agents +from gvm.protocols.gmp.requests.next._oci_image_targets import OCIImageTargets from .._entity_id import EntityID from .._version import Version @@ -113,6 +114,7 @@ "InfoType", "Notes", "Nvts", + "OCIImageTargets", "OperatingSystems", "Overrides", "Permissions", diff --git a/gvm/protocols/gmp/requests/next/_oci_image_targets.py b/gvm/protocols/gmp/requests/next/_oci_image_targets.py new file mode 100644 index 00000000..9605e2b0 --- /dev/null +++ b/gvm/protocols/gmp/requests/next/_oci_image_targets.py @@ -0,0 +1,186 @@ +# SPDX-FileCopyrightText: 2025 Greenbone AG +# +# SPDX-License-Identifier: GPL-3.0-or-later +# + +from typing import Optional + +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 OCIImageTargets: + @classmethod + def create_oci_image_target( + cls, + name: str, + image_references: list[str], + *, + comment: Optional[str] = None, + credential_id: Optional[EntityID] = None, + ) -> Request: + """Create a new OCI image target + + Args: + name: Name of the target + image_references: List of OCI image URLs to scan + comment: Comment for the target + credential_id: UUID of a credential to use on target + """ + if not name: + raise RequiredArgument( + function=cls.create_oci_image_target.__name__, argument="name" + ) + + if not image_references: + raise RequiredArgument( + function=cls.create_oci_image_target.__name__, + argument="image_references", + ) + + cmd = XmlCommand("create_oci_image_target") + cmd.add_element("name", name) + cmd.add_element("image_references", to_comma_list(image_references)) + + if comment: + cmd.add_element("comment", comment) + + if credential_id: + cmd.add_element("credential", attrs={"id": str(credential_id)}) + + return cmd + + @classmethod + def modify_oci_image_target( + cls, + oci_image_target_id: EntityID, + *, + name: Optional[str] = None, + comment: Optional[str] = None, + image_references: Optional[list[str]] = None, + credential_id: Optional[EntityID] = None, + ) -> Request: + """Modify an existing target. + + Args: + oci_image_target_id: UUID of target to modify. + comment: Comment on target. + name: Name of target. + image_references: List of OCI image URLs. + credential_id: UUID of credential to use on target. + """ + if not oci_image_target_id: + raise RequiredArgument( + function=cls.modify_oci_image_target.__name__, + argument="oci_image_target_id", + ) + + cmd = XmlCommand("modify_oci_image_target") + cmd.set_attribute("oci_image_target_id", str(oci_image_target_id)) + + if comment: + cmd.add_element("comment", comment) + + if name: + cmd.add_element("name", name) + + if image_references: + cmd.add_element("image_references", to_comma_list(image_references)) + + if credential_id: + cmd.add_element("credential", attrs={"id": str(credential_id)}) + + return cmd + + @classmethod + def clone_oci_image_target(cls, oci_image_target_id: EntityID) -> Request: + """Clone an existing OCI image target. + + Args: + oci_image_target_id: UUID of an existing target to clone. + """ + if not oci_image_target_id: + raise RequiredArgument( + function=cls.clone_oci_image_target.__name__, + argument="oci_image_target_id", + ) + + cmd = XmlCommand("create_oci_image_target") + cmd.add_element("copy", str(oci_image_target_id)) + return cmd + + @classmethod + def delete_oci_image_target( + cls, oci_image_target_id: EntityID, *, ultimate: Optional[bool] = False + ) -> Request: + """Delete an existing OCI image target. + + Args: + oci_image_target_id: UUID of an existing target to delete. + ultimate: Whether to remove entirely or to the trashcan. + """ + if not oci_image_target_id: + raise RequiredArgument( + function=cls.delete_oci_image_target.__name__, + argument="oci_image_target_id", + ) + + cmd = XmlCommand("delete_oci_image_target") + cmd.set_attribute("oci_image_target_id", str(oci_image_target_id)) + cmd.set_attribute("ultimate", to_bool(ultimate)) + return cmd + + @classmethod + def get_oci_image_target( + cls, oci_image_target_id: EntityID, *, tasks: Optional[bool] = None + ) -> Request: + """Request a single OCI Image target. + + Args: + oci_image_target_id: UUID of the target to request. + tasks: Whether to include list of tasks that use the target + """ + if not oci_image_target_id: + raise RequiredArgument( + function=cls.get_oci_image_target.__name__, + argument="oci_image_target_id", + ) + + cmd = XmlCommand("get_oci_image_targets") + cmd.set_attribute("oci_image_target_id", str(oci_image_target_id)) + + if tasks is not None: + cmd.set_attribute("tasks", to_bool(tasks)) + + return cmd + + @classmethod + def get_oci_image_targets( + cls, + *, + filter_string: Optional[str] = None, + filter_id: Optional[EntityID] = None, + trash: Optional[bool] = None, + tasks: Optional[bool] = None, + ) -> Request: + """Request a list of OCI image 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_oci_image_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/oci_image_targets/__init__.py b/tests/protocols/gmpnext/entities/oci_image_targets/__init__.py new file mode 100644 index 00000000..4f8337bf --- /dev/null +++ b/tests/protocols/gmpnext/entities/oci_image_targets/__init__.py @@ -0,0 +1,20 @@ +# SPDX-FileCopyrightText: 2025 Greenbone AG +# +# SPDX-License-Identifier: GPL-3.0-or-later +# + +from .test_clone_oci_image_target import GmpCloneOCIImageTargetTestMixin +from .test_create_oci_image_target import GmpCreateOCIImageTargetTestMixin +from .test_delete_oci_image_target import GmpDeleteOCIImageTargetTestMixin +from .test_get_oci_image_target import GmpGetOCIImageTargetTestMixin +from .test_get_oci_image_targets import GmpGetOCIImageTargetsTestMixin +from .test_modify_oci_image_target import GmpModifyOCIImageTargetTestMixin + +__all__ = ( + "GmpCloneOCIImageTargetTestMixin", + "GmpCreateOCIImageTargetTestMixin", + "GmpDeleteOCIImageTargetTestMixin", + "GmpGetOCIImageTargetTestMixin", + "GmpGetOCIImageTargetsTestMixin", + "GmpModifyOCIImageTargetTestMixin", +) diff --git a/tests/protocols/gmpnext/entities/oci_image_targets/test_clone_oci_image_target.py b/tests/protocols/gmpnext/entities/oci_image_targets/test_clone_oci_image_target.py new file mode 100644 index 00000000..1ae8666e --- /dev/null +++ b/tests/protocols/gmpnext/entities/oci_image_targets/test_clone_oci_image_target.py @@ -0,0 +1,26 @@ +# SPDX-FileCopyrightText: 2025 Greenbone AG +# +# SPDX-License-Identifier: GPL-3.0-or-later +# + +from gvm.errors import RequiredArgument + + +class GmpCloneOCIImageTargetTestMixin: + TARGET_ID = "00000000-0000-0000-0000-000000000000" + + def test_clone(self): + self.gmp.clone_oci_image_target(self.TARGET_ID) + + self.connection.send.has_been_called_with( + "" + f"{self.TARGET_ID}" + "".encode("utf-8") + ) + + def test_missing_id(self): + with self.assertRaises(RequiredArgument): + self.gmp.clone_oci_image_target("") + + with self.assertRaises(RequiredArgument): + self.gmp.clone_oci_image_target(None) diff --git a/tests/protocols/gmpnext/entities/oci_image_targets/test_create_oci_image_target.py b/tests/protocols/gmpnext/entities/oci_image_targets/test_create_oci_image_target.py new file mode 100644 index 00000000..c22346de --- /dev/null +++ b/tests/protocols/gmpnext/entities/oci_image_targets/test_create_oci_image_target.py @@ -0,0 +1,54 @@ +# SPDX-FileCopyrightText: 2025 Greenbone AG +# +# SPDX-License-Identifier: GPL-3.0-or-later +# + +from gvm.errors import RequiredArgument + + +class GmpCreateOCIImageTargetTestMixin: + def test_create_target_missing_name(self): + with self.assertRaises(RequiredArgument): + self.gmp.create_oci_image_target( + None, image_references=["oci:foo/bar:latest"] + ) + + with self.assertRaises(RequiredArgument): + self.gmp.create_oci_image_target( + name=None, image_references=["oci:foo/bar:latest"] + ) + + with self.assertRaises(RequiredArgument): + self.gmp.create_oci_image_target( + "", image_references=["oci:foo/bar:latest"] + ) + + def test_create_target_missing_image_references(self): + with self.assertRaises(RequiredArgument): + self.gmp.create_oci_image_target(name="foo", image_references=None) + + def test_create_target_with_comment(self): + self.gmp.create_oci_image_target( + "foo", image_references=["oci:foo/bar:latest"], comment="bar" + ) + + self.connection.send.has_been_called_with( + b"" + b"foo" + b"oci:foo/bar:latest" + b"bar" + b"" + ) + + def test_create_target_with_smb_credential_id(self): + self.gmp.create_oci_image_target( + "foo", image_references=["oci:foo/bar:latest"], credential_id="c1" + ) + + self.connection.send.has_been_called_with( + b"" + b"foo" + b"oci:foo/bar:latest" + b'' + b"" + ) diff --git a/tests/protocols/gmpnext/entities/oci_image_targets/test_delete_oci_image_target.py b/tests/protocols/gmpnext/entities/oci_image_targets/test_delete_oci_image_target.py new file mode 100644 index 00000000..250e399b --- /dev/null +++ b/tests/protocols/gmpnext/entities/oci_image_targets/test_delete_oci_image_target.py @@ -0,0 +1,29 @@ +# SPDX-FileCopyrightText: 2025 Greenbone AG +# +# SPDX-License-Identifier: GPL-3.0-or-later +# + +from gvm.errors import GvmError + + +class GmpDeleteOCIImageTargetTestMixin: + def test_delete(self): + self.gmp.delete_oci_image_target("a1") + + self.connection.send.has_been_called_with( + b'' + ) + + def test_delete_ultimate(self): + self.gmp.delete_oci_image_target("a1", ultimate=True) + + self.connection.send.has_been_called_with( + b'' + ) + + def test_missing_id(self): + with self.assertRaises(GvmError): + self.gmp.delete_oci_image_target(None) + + with self.assertRaises(GvmError): + self.gmp.delete_oci_image_target("") diff --git a/tests/protocols/gmpnext/entities/oci_image_targets/test_get_oci_image_target.py b/tests/protocols/gmpnext/entities/oci_image_targets/test_get_oci_image_target.py new file mode 100644 index 00000000..766d1803 --- /dev/null +++ b/tests/protocols/gmpnext/entities/oci_image_targets/test_get_oci_image_target.py @@ -0,0 +1,41 @@ +# SPDX-FileCopyrightText: 2025 Greenbone AG +# +# SPDX-License-Identifier: GPL-3.0-or-later +# + +from gvm.errors import RequiredArgument + + +class GmpGetOCIImageTargetTestMixin: + def test_get_target(self): + self.gmp.get_oci_image_target("t1") + + self.connection.send.has_been_called_with( + b'' + ) + + self.gmp.get_oci_image_target(oci_image_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_oci_image_target(oci_image_target_id=None) + + with self.assertRaises(RequiredArgument): + self.gmp.get_oci_image_target("") + + def test_get_target_with_tasks(self): + self.gmp.get_oci_image_target(oci_image_target_id="t1", tasks=True) + + self.connection.send.has_been_called_with( + b'' + ) + + self.gmp.get_oci_image_target(oci_image_target_id="t1", tasks=False) + + self.connection.send.has_been_called_with( + b'' + ) diff --git a/tests/protocols/gmpnext/entities/oci_image_targets/test_get_oci_image_targets.py b/tests/protocols/gmpnext/entities/oci_image_targets/test_get_oci_image_targets.py new file mode 100644 index 00000000..4cf22336 --- /dev/null +++ b/tests/protocols/gmpnext/entities/oci_image_targets/test_get_oci_image_targets.py @@ -0,0 +1,51 @@ +# SPDX-FileCopyrightText: 2025 Greenbone AG +# +# SPDX-License-Identifier: GPL-3.0-or-later +# + + +class GmpGetOCIImageTargetsTestMixin: + def test_get_targets(self): + self.gmp.get_oci_image_targets() + + self.connection.send.has_been_called_with(b"") + + def test_get_targets_with_filter_string(self): + self.gmp.get_oci_image_targets(filter_string="foo=bar") + + self.connection.send.has_been_called_with( + b'' + ) + + def test_get_targets_with_filter_id(self): + self.gmp.get_oci_image_targets(filter_id="f1") + + self.connection.send.has_been_called_with( + b'' + ) + + def test_get_targets_with_trash(self): + self.gmp.get_oci_image_targets(trash=True) + + self.connection.send.has_been_called_with( + b'' + ) + + self.gmp.get_oci_image_targets(trash=False) + + self.connection.send.has_been_called_with( + b'' + ) + + def test_get_targets_with_tasks(self): + self.gmp.get_oci_image_targets(tasks=True) + + self.connection.send.has_been_called_with( + b'' + ) + + self.gmp.get_oci_image_targets(tasks=False) + + self.connection.send.has_been_called_with( + b'' + ) diff --git a/tests/protocols/gmpnext/entities/oci_image_targets/test_modify_oci_image_target.py b/tests/protocols/gmpnext/entities/oci_image_targets/test_modify_oci_image_target.py new file mode 100644 index 00000000..322e6abb --- /dev/null +++ b/tests/protocols/gmpnext/entities/oci_image_targets/test_modify_oci_image_target.py @@ -0,0 +1,75 @@ +# SPDX-FileCopyrightText: 2025 Greenbone AG +# +# SPDX-License-Identifier: GPL-3.0-or-later +# + +from gvm.errors import RequiredArgument + + +class GmpModifyOCIImageTargetTestMixin: + def test_modify_oci_image_target(self): + self.gmp.modify_oci_image_target(oci_image_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_oci_image_target(oci_image_target_id=None) + + with self.assertRaises(RequiredArgument): + self.gmp.modify_oci_image_target(oci_image_target_id="") + + def test_modify_target_with_comment(self): + self.gmp.modify_oci_image_target( + oci_image_target_id="t1", comment="foo" + ) + + self.connection.send.has_been_called_with( + b'' + b"foo" + b"" + ) + + def test_modify_target_with_image_references(self): + self.gmp.modify_oci_image_target( + oci_image_target_id="t1", image_references=["oci://foo/bar:latest"] + ) + + self.connection.send.has_been_called_with( + b'' + b"oci://foo/bar:latest" + b"" + ) + + self.gmp.modify_oci_image_target( + oci_image_target_id="t1", + image_references=["oci://foo/bar:latest", "oci://baz/qux:latest"], + ) + + self.connection.send.has_been_called_with( + b'' + b"oci://foo/bar:latest,oci://baz/qux:latest" + b"" + ) + + def test_modify_target_with_name(self): + self.gmp.modify_oci_image_target(oci_image_target_id="t1", name="foo") + + self.connection.send.has_been_called_with( + b'' + b"foo" + b"" + ) + + def test_modify_target_with_smb_credential_id(self): + self.gmp.modify_oci_image_target( + oci_image_target_id="t1", credential_id="c1" + ) + + self.connection.send.has_been_called_with( + b'' + b'' + b"" + ) diff --git a/tests/protocols/gmpnext/entities/test_oci_image_targets.py b/tests/protocols/gmpnext/entities/test_oci_image_targets.py new file mode 100644 index 00000000..3f15b57c --- /dev/null +++ b/tests/protocols/gmpnext/entities/test_oci_image_targets.py @@ -0,0 +1,46 @@ +# SPDX-FileCopyrightText: 2025 Greenbone AG +# +# SPDX-License-Identifier: GPL-3.0-or-later +# + +from ...gmpnext import GMPTestCase +from .oci_image_targets import ( + GmpCloneOCIImageTargetTestMixin, + GmpCreateOCIImageTargetTestMixin, + GmpDeleteOCIImageTargetTestMixin, + GmpGetOCIImageTargetsTestMixin, + GmpGetOCIImageTargetTestMixin, + GmpModifyOCIImageTargetTestMixin, +) + + +class GmpCloneOCIImageTargetTestCase( + GmpCloneOCIImageTargetTestMixin, GMPTestCase +): + pass + + +class GmpCreateOCIImageTargetTestCase( + GmpCreateOCIImageTargetTestMixin, GMPTestCase +): + pass + + +class GmpDeleteOCIImageTargetTestCase( + GmpDeleteOCIImageTargetTestMixin, GMPTestCase +): + pass + + +class GmpGetOCIImageTargetTestCase(GmpGetOCIImageTargetsTestMixin, GMPTestCase): + pass + + +class GmpGetOCIImageTargetsTestCase(GmpGetOCIImageTargetTestMixin, GMPTestCase): + pass + + +class GmpModifyOCIImageTargetTestCase( + GmpModifyOCIImageTargetTestMixin, GMPTestCase +): + pass