diff --git a/gvm/protocols/gmp/_gmpnext.py b/gvm/protocols/gmp/_gmpnext.py index fae99d99..3ec405eb 100644 --- a/gvm/protocols/gmp/_gmpnext.py +++ b/gvm/protocols/gmp/_gmpnext.py @@ -2,7 +2,7 @@ # # SPDX-License-Identifier: GPL-3.0-or-later -from typing import Any, Mapping, Optional, Sequence +from typing import Any, Mapping, Optional, Sequence, Union from gvm.protocols.gmp.requests import EntityID @@ -13,6 +13,9 @@ AgentGroups, AgentInstallers, Agents, + Credentials, + CredentialStoreCredentialType, + CredentialStores, OCIImageTargets, Tasks, ) @@ -297,6 +300,174 @@ def clone_agent_group( AgentGroups.clone_agent_group(agent_group_id) ) + def create_credential_store_credential( + self, + name: str, + credential_type: Union[CredentialStoreCredentialType, str], + *, + comment: Optional[str] = None, + credential_store_id: Optional[EntityID] = None, + vault_id: Optional[str] = None, + host_identifier: Optional[str] = None, + ) -> T: + """Create a new credential store type credential + + Args: + name: Name of the credential + credential_type: Type of the credential + comment: Optional comment for the credential object + credential_store_id: Optional credential store id to fetch the credential from + vault_id: Vault id used to fetch the credential from credential store + host_identifier: Host identifier used to fetch the credential from credential store + """ + return self._send_request_and_transform_response( + Credentials.create_credential_store_credential( + name=name, + credential_type=credential_type, + comment=comment, + credential_store_id=credential_store_id, + vault_id=vault_id, + host_identifier=host_identifier, + ) + ) + + def modify_credential_store_credential( + self, + credential_id: EntityID, + *, + name: Optional[str] = None, + comment: Optional[str] = None, + credential_store_id: Optional[EntityID] = None, + vault_id: Optional[str] = None, + host_identifier: Optional[str] = None, + ) -> T: + """Modify an existing credential stored in a credential store + + Args: + credential_id: UUID of the credential to modify + name: Name of the credential + comment: Optional comment for the credential object + credential_store_id: Optional credential store id to fetch the credential from + vault_id: Vault id used to fetch the credential from credential store + host_identifier: Host identifier used to fetch the credential from credential store + """ + return self._send_request_and_transform_response( + Credentials.modify_credential_store_credential( + credential_id=credential_id, + name=name, + comment=comment, + credential_store_id=credential_store_id, + vault_id=vault_id, + host_identifier=host_identifier, + ) + ) + + def get_credential_store( + self, + credential_store_id: EntityID, + *, + details: Optional[bool] = None, + ) -> T: + """Request a credential store + + Args: + credential_store_id: ID of credential store to fetch + details: True to request all details + """ + return self._send_request_and_transform_response( + CredentialStores.get_credential_store( + credential_store_id=credential_store_id, + details=details, + ) + ) + + def get_credential_stores( + self, + *, + filter_string: Optional[str] = None, + filter_id: Optional[EntityID] = None, + details: Optional[bool] = None, + ) -> T: + """Request a list of credential stores + + Args: + filter_string: Filter term to use for the query + filter_id: UUID of an existing filter to use for the query + details: True to request all details + """ + return self._send_request_and_transform_response( + CredentialStores.get_credential_stores( + filter_string=filter_string, + filter_id=filter_id, + details=details, + ) + ) + + def modify_credential_store( + self, + credential_store_id: EntityID, + *, + active: Optional[bool] = None, + host: Optional[str] = None, + port: Optional[int] = None, + path: Optional[str] = None, + app_id: Optional[str] = None, + client_cert: Optional[str] = None, + client_key: Optional[str] = None, + client_pkcs12_file: Optional[str] = None, + passphrase: Optional[str] = None, + server_ca_cert: Optional[str] = None, + comment: Optional[str] = None, + ) -> T: + """Modify an existing credential store + + Args: + credential_store_id: ID of credential store to fetch + active: Whether the credential store is active + host: The host to use for reaching the credential store + port: The port to use for reaching the credential store + path: The URI path the credential store is using + app_id: Depends on the credential store used. Usually called the same in the credential store + client_cert: The client certificate to use for authorization, as a plain string + client_key: The client key to use for authorization, as a plain string + client_pkcs12_file: The pkcs12 file contents to use for authorization, as a plain string + (alternative to using client_cert and client_key) + passphrase: The passphrase to use to decrypt client_pkcs12_file or client_key file + server_ca_cert: The server certificate, so the credential store can be trusted + comment: An optional comment to store alongside the credential store + """ + return self._send_request_and_transform_response( + CredentialStores.modify_credential_store( + credential_store_id=credential_store_id, + active=active, + host=host, + port=port, + path=path, + app_id=app_id, + client_cert=client_cert, + client_key=client_key, + client_pkcs12_file=client_pkcs12_file, + passphrase=passphrase, + server_ca_cert=server_ca_cert, + comment=comment, + ) + ) + + def verify_credential_store( + self, + credential_store_id: EntityID, + ) -> T: + """Verify that the connection to an existing credential store works + + Args: + credential_store_id: The uuid of the credential store to verify + """ + return self._send_request_and_transform_response( + CredentialStores.verify_credential_store( + credential_store_id=credential_store_id, + ) + ) + def create_oci_image_target( self, name: str, diff --git a/gvm/protocols/gmp/requests/next/__init__.py b/gvm/protocols/gmp/requests/next/__init__.py index 109199a8..b6541fcf 100644 --- a/gvm/protocols/gmp/requests/next/__init__.py +++ b/gvm/protocols/gmp/requests/next/__init__.py @@ -5,6 +5,11 @@ 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._credential_stores import CredentialStores +from gvm.protocols.gmp.requests.next._credentials import ( + Credentials, + CredentialStoreCredentialType, +) from gvm.protocols.gmp.requests.next._oci_image_targets import OCIImageTargets from gvm.protocols.gmp.requests.next._tasks import Tasks @@ -24,7 +29,6 @@ CertBundAdvisories, Cpes, CredentialFormat, - Credentials, CredentialType, Cves, DfnCertAdvisories, @@ -98,6 +102,8 @@ "Credentials", "CredentialFormat", "CredentialType", + "CredentialStoreCredentialType", + "CredentialStores", "Cves", "DfnCertAdvisories", "EntityID", diff --git a/gvm/protocols/gmp/requests/next/_credential_stores.py b/gvm/protocols/gmp/requests/next/_credential_stores.py new file mode 100644 index 00000000..67ae0db6 --- /dev/null +++ b/gvm/protocols/gmp/requests/next/_credential_stores.py @@ -0,0 +1,171 @@ +# SPDX-FileCopyrightText: 2025 Greenbone AG +# +# SPDX-License-Identifier: GPL-3.0-or-later + +from base64 import b64encode +from typing import Optional + +from gvm.errors import RequiredArgument +from gvm.protocols.core import Request +from gvm.utils import to_bool +from gvm.xml import XmlCommand + +from .._entity_id import EntityID + + +class CredentialStores: + @classmethod + def get_credential_store( + cls, + credential_store_id: EntityID, + *, + details: Optional[bool] = None, + ) -> Request: + """Request a credential store + + Args: + credential_store_id: ID of credential store to fetch + details: True to request all details + """ + + if not credential_store_id: + raise RequiredArgument( + function=cls.get_credential_store.__name__, + argument="credential_store_id", + ) + + cmd = XmlCommand("get_credential_stores") + cmd.add_element("credential_store_id", str(credential_store_id)) + + if details is not None: + cmd.set_attribute("details", to_bool(details)) + + return cmd + + @classmethod + def get_credential_stores( + cls, + *, + filter_string: Optional[str] = None, + filter_id: Optional[EntityID] = None, + details: Optional[bool] = None, + ) -> Request: + """Request a list of credential stores + + Args: + filter_string: Filter term to use for the query + filter_id: UUID of an existing filter to use for the query + details: True to request all details + """ + + cmd = XmlCommand("get_credential_stores") + cmd.add_filter(filter_string, filter_id) + + if details is not None: + cmd.set_attribute("details", to_bool(details)) + + return cmd + + @classmethod + def modify_credential_store( + cls, + credential_store_id: EntityID, + *, + active: Optional[bool] = None, + host: Optional[str] = None, + port: Optional[int] = None, + path: Optional[str] = None, + app_id: Optional[str] = None, + client_cert: Optional[str] = None, + client_key: Optional[str] = None, + client_pkcs12_file: Optional[str] = None, + passphrase: Optional[str] = None, + server_ca_cert: Optional[str] = None, + comment: Optional[str] = None, + ) -> Request: + """Modify a credential store + + Args: + credential_store_id: ID of credential store to fetch + active: Whether the credential store is active + host: The host to use for reaching the credential store + port: The port to use for reaching the credential store + path: The URI path the credential store is using + app_id: Depends on the credential store used. Usually called the same in the credential store + client_cert: The client certificate to use for authorization, as a plain string + client_key: The client key to use for authorization, as a plain string + client_pkcs12_file: The pkcs12 file contents to use for authorization, as a plain string + (alternative to using client_cert and client_key) + passphrase: The passphrase to use to decrypt client_pkcs12_file or client_key file + server_ca_cert: The server certificate, so the credential store can be trusted + comment: An optional comment to store alongside the credential store + """ + + if not credential_store_id: + raise RequiredArgument( + function=cls.verify_credential_store.__name__, + argument="credential_store_id", + ) + + cmd = XmlCommand("modify_credential_store") + cmd.set_attribute("credential_store_id", str(credential_store_id)) + + if active is not None: + cmd.add_element("active", to_bool(active)) + if host: + cmd.add_element("host", host) + if port: + cmd.add_element("port", str(port)) + if path: + cmd.add_element("path", path) + if comment: + cmd.add_element("comment", comment) + + preferences = cmd.add_element("preferences") + + if app_id: + preferences.add_element("app_id", app_id) + if client_cert: + preferences.add_element( + "client_cert", + b64encode(client_cert.encode("ascii")).decode("ascii"), + ) + if client_key: + preferences.add_element( + "client_key", + b64encode(client_key.encode("ascii")).decode("ascii"), + ) + if client_pkcs12_file: + preferences.add_element( + "client_pkcs12_file", + b64encode(client_pkcs12_file.encode("ascii")).decode("ascii"), + ) + if passphrase: + preferences.add_element("passphrase", passphrase) + if server_ca_cert: + preferences.add_element( + "server_ca_cert", + b64encode(server_ca_cert.encode("ascii")).decode("ascii"), + ) + + return cmd + + @classmethod + def verify_credential_store( + cls, + credential_store_id: EntityID, + ) -> Request: + """Verify that the connection to a credential store works + + Args: + credential_store_id: The uuid of the credential store to verify + """ + if not credential_store_id: + raise RequiredArgument( + function=cls.verify_credential_store.__name__, + argument="credential_store_id", + ) + + cmd = XmlCommand("verify_credential_store") + cmd.set_attribute("credential_store_id", str(credential_store_id)) + return cmd diff --git a/gvm/protocols/gmp/requests/next/_credentials.py b/gvm/protocols/gmp/requests/next/_credentials.py new file mode 100644 index 00000000..a8e9d454 --- /dev/null +++ b/gvm/protocols/gmp/requests/next/_credentials.py @@ -0,0 +1,179 @@ +# SPDX-FileCopyrightText: 2025 Greenbone AG +# +# SPDX-License-Identifier: GPL-3.0-or-later + +from typing import Optional, Union + +from gvm._enum import Enum +from gvm.errors import InvalidArgument, RequiredArgument +from gvm.protocols.core import Request +from gvm.xml import XmlCommand + +from .._entity_id import EntityID +from ..v224._credentials import ( + Credentials as CredentialsV224, +) + + +class CredentialStoreCredentialType(Enum): + """Enum for credential store credential types""" + + CLIENT_CERTIFICATE = "cs_cc" + SNMP = "cs_snmp" + USERNAME_PASSWORD = "cs_up" + USERNAME_SSH_KEY = "cs_usk" + SMIME_CERTIFICATE = "cs_smime" + PGP_ENCRYPTION_KEY = "cs_pgp" + PASSWORD_ONLY = "cs_pw" + + +class Credentials(CredentialsV224): + @classmethod + def create_credential_store_credential( + cls, + name: str, + credential_type: Union[CredentialStoreCredentialType, str], + *, + comment: Optional[str] = None, + credential_store_id: Optional[EntityID] = None, + vault_id: Optional[str] = None, + host_identifier: Optional[str] = None, + ) -> Request: + """Create a new credential that is fetched from a credential store + + Create a new credential e.g. to be used in the method of an alert. + + Currently the following credential types are supported: + + - Username + Password + - Username + SSH-Key + - Client Certificates + - SNMPv1 or SNMPv2c protocol + - S/MIME Certificate + - OpenPGP Key + - Password only + + Arguments: + name: Name of the new credential + credential_type: The credential type. + comment: Comment for the credential + credential_store_id: Optional id of the credential store to use + (gvmd will pick default one if none is provided) + vault_id: Vault-ID used to access the secret in credential store + host_identifier: Host-Identifier used to access the secret in credential store + + Examples: + Creating a Password-Only credential stored in a Credential Store + + .. code-block:: python + + request = Credentials.create_credential( + name='Credential-Store Password-Only Credential', + credential_type=CredentialType.CREDENTIAL_STOREPASSWORD_ONLY, + vault_id='a5f84dd4-da18-447c-a9fb-b77b5df49076', + host_identifier='/My/Secret', + ) + """ + if not name: + raise RequiredArgument( + function=cls.create_credential.__name__, argument="name" + ) + + if not credential_type: + raise RequiredArgument( + function=cls.create_credential.__name__, + argument="credential_type", + ) + + if not isinstance(credential_type, CredentialStoreCredentialType): + credential_type = CredentialStoreCredentialType(credential_type) + + cmd = XmlCommand("create_credential") + cmd.add_element("name", name) + + cmd.add_element("type", credential_type.value) + + if comment: + cmd.add_element("comment", comment) + + if ( + credential_type != CredentialStoreCredentialType.CLIENT_CERTIFICATE + and credential_type != CredentialStoreCredentialType.SNMP + and credential_type + != CredentialStoreCredentialType.USERNAME_PASSWORD + and credential_type + != CredentialStoreCredentialType.USERNAME_SSH_KEY + and credential_type + != CredentialStoreCredentialType.SMIME_CERTIFICATE + and credential_type + != CredentialStoreCredentialType.PGP_ENCRYPTION_KEY + and credential_type != CredentialStoreCredentialType.PASSWORD_ONLY + ): + raise InvalidArgument( + function=cls.create_credential.__name__, + argument="credential_type", + ) + + if not vault_id: + raise RequiredArgument( + function=cls.create_credential.__name__, + argument="vault_id", + ) + if not host_identifier: + raise RequiredArgument( + function=cls.create_credential.__name__, + argument="host_identifier", + ) + + if credential_store_id: + cmd.add_element("credential_store_id", str(credential_store_id)) + + cmd.add_element("vault_id", vault_id) + cmd.add_element("host_identifier", host_identifier) + + return cmd + + @classmethod + def modify_credential_store_credential( + cls, + credential_id: EntityID, + *, + name: Optional[str] = None, + comment: Optional[str] = None, + credential_store_id: Optional[EntityID] = None, + vault_id: Optional[str] = None, + host_identifier: Optional[str] = None, + ) -> Request: + """Modifies an existing credential. + + Arguments: + credential_id: UUID of the credential + name: Name of the credential + comment: Comment for the credential + credential_store_id: Optional id of the credential store to use + (gvmd will pick default one if none is provided) + vault_id: Vault-ID used to access the secret in credential store + host_identifier: Host-Identifier used to access the secret in credential store + """ + if not credential_id: + raise RequiredArgument( + function=cls.modify_credential.__name__, + argument="credential_id", + ) + + cmd = XmlCommand("modify_credential") + cmd.set_attribute("credential_id", str(credential_id)) + + if name: + cmd.add_element("name", name) + if comment: + cmd.add_element("comment", comment) + + if credential_store_id: + cmd.add_element("credential_store_id", str(credential_store_id)) + if vault_id: + cmd.add_element("vault_id", vault_id) + if host_identifier: + cmd.add_element("host_identifier", host_identifier) + + return cmd diff --git a/tests/protocols/gmpnext/entities/credential_stores/__init__.py b/tests/protocols/gmpnext/entities/credential_stores/__init__.py new file mode 100644 index 00000000..ca5fe222 --- /dev/null +++ b/tests/protocols/gmpnext/entities/credential_stores/__init__.py @@ -0,0 +1,13 @@ +# SPDX-FileCopyrightText: 2025 Greenbone AG +# +# SPDX-License-Identifier: GPL-3.0-or-later + +from .test_get_credential_stores import GmpGetCredentialStoresTestMixin +from .test_modify_credential_stores import GmpModifyCredentialStoreTestMixin +from .test_verify_credential_stores import GmpVerifyCredentialStoreTestMixin + +__all__ = ( + "GmpGetCredentialStoresTestMixin", + "GmpModifyCredentialStoreTestMixin", + "GmpVerifyCredentialStoreTestMixin", +) diff --git a/tests/protocols/gmpnext/entities/credential_stores/test_get_credential_stores.py b/tests/protocols/gmpnext/entities/credential_stores/test_get_credential_stores.py new file mode 100644 index 00000000..6d460585 --- /dev/null +++ b/tests/protocols/gmpnext/entities/credential_stores/test_get_credential_stores.py @@ -0,0 +1,73 @@ +# SPDX-FileCopyrightText: 2025 Greenbone AG +# +# SPDX-License-Identifier: GPL-3.0-or-later +# + +from gvm.errors import RequiredArgument + + +class GmpGetCredentialStoresTestMixin: + def test_get_credential_store(self): + self.gmp.get_credential_store(credential_store_id="cs1") + + self.connection.send.has_been_called_with( + b"" + b"cs1" + b"" + ) + + def test_get_credential_store_with_details(self): + self.gmp.get_credential_store(credential_store_id="cs1", details=True) + + self.connection.send.has_been_called_with( + b'' + b"cs1" + b"" + ) + + self.gmp.get_credential_store(credential_store_id="cs1", details=False) + + self.connection.send.has_been_called_with( + b'' + b"cs1" + b"" + ) + + def test_get_credential_store_without_id(self): + with self.assertRaises(RequiredArgument): + self.gmp.get_credential_store(credential_store_id=None) + + with self.assertRaises(RequiredArgument): + self.gmp.get_credential_store(credential_store_id="") + + def test_get_credential_stores(self): + self.gmp.get_credential_stores() + + self.connection.send.has_been_called_with(b"") + + def test_get_credential_stores_with_filter_string(self): + self.gmp.get_credential_stores(filter_string="foo=bar") + + self.connection.send.has_been_called_with( + b'' + ) + + def test_get_credential_stores_with_filter_id(self): + self.gmp.get_credential_stores(filter_id="f1") + + self.connection.send.has_been_called_with( + b'' + ) + + def test_get_credential_stores_with_details(self): + self.gmp.get_credential_stores(details=True) + + self.connection.send.has_been_called_with( + b'' + ) + + self.gmp.get_credential_stores(details=False) + + self.connection.send.has_been_called_with( + b'' + ) diff --git a/tests/protocols/gmpnext/entities/credential_stores/test_modify_credential_stores.py b/tests/protocols/gmpnext/entities/credential_stores/test_modify_credential_stores.py new file mode 100644 index 00000000..0210b505 --- /dev/null +++ b/tests/protocols/gmpnext/entities/credential_stores/test_modify_credential_stores.py @@ -0,0 +1,219 @@ +# SPDX-FileCopyrightText: 2025 Greenbone AG +# +# SPDX-License-Identifier: GPL-3.0-or-later +# + +from gvm.errors import RequiredArgument + + +class GmpModifyCredentialStoreTestMixin: + def test_modify_credential_store(self): + self.gmp.modify_credential_store(credential_store_id="cs1") + + self.connection.send.has_been_called_with( + b'' + b"" + b"" + ) + + def test_modify_credential_store_without_id(self): + with self.assertRaises(RequiredArgument): + self.gmp.modify_credential_store(credential_store_id=None) + + with self.assertRaises(RequiredArgument): + self.gmp.modify_credential_store(credential_store_id="") + + def test_modify_credential_store_with_active(self): + self.gmp.modify_credential_store( + credential_store_id="cs1", + active=True, + ) + + self.connection.send.has_been_called_with( + b'' + b"1" + b"" + b"" + ) + + self.gmp.modify_credential_store( + credential_store_id="cs1", + active=False, + ) + + self.connection.send.has_been_called_with( + b'' + b"0" + b"" + b"" + ) + + def test_modify_credential_store_with_host(self): + self.gmp.modify_credential_store( + credential_store_id="cs1", + host="foo", + ) + + self.connection.send.has_been_called_with( + b'' + b"foo" + b"" + b"" + ) + + def test_modify_credential_store_with_port(self): + self.gmp.modify_credential_store( + credential_store_id="cs1", + port=1234, + ) + + self.connection.send.has_been_called_with( + b'' + b"1234" + b"" + b"" + ) + + def test_modify_credential_store_with_path(self): + self.gmp.modify_credential_store( + credential_store_id="cs1", + path="/foo/bar", + ) + + self.connection.send.has_been_called_with( + b'' + b"/foo/bar" + b"" + b"" + ) + + def test_modify_credential_store_with_comment(self): + self.gmp.modify_credential_store( + credential_store_id="cs1", + comment="ahoi", + ) + + self.connection.send.has_been_called_with( + b'' + b"ahoi" + b"" + b"" + ) + + def test_modify_credential_store_with_app_id(self): + self.gmp.modify_credential_store( + credential_store_id="cs1", + app_id="foo", + ) + + self.connection.send.has_been_called_with( + b'' + b"" + b"foo" + b"" + b"" + ) + + def test_modify_credential_store_with_client_cert(self): + self.gmp.modify_credential_store( + credential_store_id="cs1", + client_cert="foo", + ) + + self.connection.send.has_been_called_with( + b'' + b"" + b"Zm9v" + b"" + b"" + ) + + def test_modify_credential_store_with_client_key(self): + self.gmp.modify_credential_store( + credential_store_id="cs1", + client_key="foo", + ) + + self.connection.send.has_been_called_with( + b'' + b"" + b"Zm9v" + b"" + b"" + ) + + def test_modify_credential_store_with_client_pkcs12_file(self): + self.gmp.modify_credential_store( + credential_store_id="cs1", + client_pkcs12_file="foo", + ) + + self.connection.send.has_been_called_with( + b'' + b"" + b"Zm9v" + b"" + b"" + ) + + def test_modify_credential_store_with_passphrase(self): + self.gmp.modify_credential_store( + credential_store_id="cs1", + passphrase="foo", + ) + + self.connection.send.has_been_called_with( + b'' + b"" + b"foo" + b"" + b"" + ) + + def test_modify_credential_store_with_server_ca_cert(self): + self.gmp.modify_credential_store( + credential_store_id="cs1", + server_ca_cert="foo", + ) + + self.connection.send.has_been_called_with( + b'' + b"" + b"Zm9v" + b"" + b"" + ) + + def test_modify_credential_store_with_all(self): + self.gmp.modify_credential_store( + credential_store_id="cs1", + active=False, + host="localhost", + port="80", + path="/api", + comment="why was 6 afraid of 7? because 7 8 9", + app_id="appId", + client_cert="clientCert", + client_key="clientKey", + client_pkcs12_file="clientPkcs12File", + passphrase="secret", + server_ca_cert="serverCaCert", + ) + + self.connection.send.has_been_called_with( + b'' + b"0" + b"localhost" + b"80" + b"/api" + b"why was 6 afraid of 7? because 7 8 9" + b"" + b"appId" + b"Y2xpZW50Q2VydA==" + b"Y2xpZW50S2V5" + b"Y2xpZW50UGtjczEyRmlsZQ==" + b"secret" + b"c2VydmVyQ2FDZXJ0" + b"" + b"" + ) diff --git a/tests/protocols/gmpnext/entities/credential_stores/test_verify_credential_stores.py b/tests/protocols/gmpnext/entities/credential_stores/test_verify_credential_stores.py new file mode 100644 index 00000000..d1c3d298 --- /dev/null +++ b/tests/protocols/gmpnext/entities/credential_stores/test_verify_credential_stores.py @@ -0,0 +1,22 @@ +# SPDX-FileCopyrightText: 2025 Greenbone AG +# +# SPDX-License-Identifier: GPL-3.0-or-later +# + +from gvm.errors import RequiredArgument + + +class GmpVerifyCredentialStoreTestMixin: + def test_verify_credential_store(self): + self.gmp.verify_credential_store(credential_store_id="cs1") + + self.connection.send.has_been_called_with( + b'' + ) + + def test_verify_credential_store_without_id(self): + with self.assertRaises(RequiredArgument): + self.gmp.verify_credential_store(credential_store_id=None) + + with self.assertRaises(RequiredArgument): + self.gmp.verify_credential_store(credential_store_id="") diff --git a/tests/protocols/gmpnext/entities/credentials/__init__.py b/tests/protocols/gmpnext/entities/credentials/__init__.py new file mode 100644 index 00000000..4a49c76e --- /dev/null +++ b/tests/protocols/gmpnext/entities/credentials/__init__.py @@ -0,0 +1,15 @@ +# SPDX-FileCopyrightText: 2025 Greenbone AG +# +# SPDX-License-Identifier: GPL-3.0-or-later + +from .test_create_credential_store_credential import ( + GmpCreateCredentialStoreCredentialTestMixin, +) +from .test_modify_credential_store_credential import ( + GmpModifyCredentialStoreCredentialTestMixin, +) + +__all__ = ( + "GmpCreateCredentialStoreCredentialTestMixin", + "GmpModifyCredentialStoreCredentialTestMixin", +) diff --git a/tests/protocols/gmpnext/entities/credentials/test_create_credential_store_credential.py b/tests/protocols/gmpnext/entities/credentials/test_create_credential_store_credential.py new file mode 100644 index 00000000..0a17c452 --- /dev/null +++ b/tests/protocols/gmpnext/entities/credentials/test_create_credential_store_credential.py @@ -0,0 +1,201 @@ +# SPDX-FileCopyrightText: 2025 Greenbone AG +# +# SPDX-License-Identifier: GPL-3.0-or-later +# + +from gvm.errors import InvalidArgument, RequiredArgument +from gvm.protocols.gmp.requests.next import CredentialStoreCredentialType + + +class GmpCreateCredentialStoreCredentialTestMixin: + def test_create_cs_up_credential_missing_name(self): + with self.assertRaises(RequiredArgument): + self.gmp.create_credential_store_credential( + name="", + credential_type=CredentialStoreCredentialType.USERNAME_PASSWORD, + vault_id="foo", + host_identifier="bar", + ) + + with self.assertRaises(RequiredArgument): + self.gmp.create_credential_store_credential( + name=None, + credential_type=CredentialStoreCredentialType.USERNAME_PASSWORD, + vault_id="foo", + host_identifier="bar", + ) + + def test_create_cs_up_credential(self): + self.gmp.create_credential_store_credential( + name="foo", + credential_type=CredentialStoreCredentialType.USERNAME_PASSWORD, + comment="bar", + vault_id="123", + host_identifier="456", + ) + + self.connection.send.has_been_called_with( + b"" + b"foo" + b"cs_up" + b"bar" + b"123" + b"456" + b"" + ) + + def test_create_cs_up_credential_with_credential_store_id(self): + self.gmp.create_credential_store_credential( + name="foo", + credential_type=CredentialStoreCredentialType.USERNAME_PASSWORD, + comment="bar", + credential_store_id="abc", + vault_id="123", + host_identifier="456", + ) + + self.connection.send.has_been_called_with( + b"" + b"foo" + b"cs_up" + b"bar" + b"abc" + b"123" + b"456" + b"" + ) + + def test_create_cs_up_credential_with_missing_vault_id(self): + with self.assertRaises(RequiredArgument): + self.gmp.create_credential_store_credential( + name="foo", + credential_type=CredentialStoreCredentialType.USERNAME_PASSWORD, + comment="bar", + host_identifier="456", + ) + + def test_create_cs_up_credential_with_missing_host_identifier(self): + with self.assertRaises(RequiredArgument): + self.gmp.create_credential_store_credential( + name="foo", + credential_type=CredentialStoreCredentialType.USERNAME_PASSWORD, + comment="bar", + vault_id="123", + ) + + def test_create_cs_cc_credential(self): + self.gmp.create_credential_store_credential( + name="foo", + credential_type=CredentialStoreCredentialType.CLIENT_CERTIFICATE, + vault_id="123", + host_identifier="456", + ) + + self.connection.send.has_been_called_with( + b"" + b"foo" + b"cs_cc" + b"123" + b"456" + b"" + ) + + def test_create_cs_usk_credential(self): + self.gmp.create_credential_store_credential( + name="foo", + credential_type=CredentialStoreCredentialType.USERNAME_SSH_KEY, + vault_id="123", + host_identifier="456", + ) + + self.connection.send.has_been_called_with( + b"" + b"foo" + b"cs_usk" + b"123" + b"456" + b"" + ) + + def test_create_cs_snmp_credential(self): + self.gmp.create_credential_store_credential( + name="foo", + credential_type=CredentialStoreCredentialType.SNMP, + vault_id="123", + host_identifier="456", + ) + + self.connection.send.has_been_called_with( + b"" + b"foo" + b"cs_snmp" + b"123" + b"456" + b"" + ) + + def test_create_cs_smime_credential(self): + self.gmp.create_credential_store_credential( + name="foo", + credential_type=CredentialStoreCredentialType.SMIME_CERTIFICATE, + vault_id="123", + host_identifier="456", + ) + + self.connection.send.has_been_called_with( + b"" + b"foo" + b"cs_smime" + b"123" + b"456" + b"" + ) + + def test_create_cs_pgp_credential(self): + self.gmp.create_credential_store_credential( + name="foo", + credential_type=CredentialStoreCredentialType.PGP_ENCRYPTION_KEY, + vault_id="123", + host_identifier="456", + ) + + self.connection.send.has_been_called_with( + b"" + b"foo" + b"cs_pgp" + b"123" + b"456" + b"" + ) + + def test_create_cs_credential_invalid_credential_type(self): + with self.assertRaises(RequiredArgument): + self.gmp.create_credential_store_credential( + name="foo", credential_type=None + ) + + with self.assertRaises(RequiredArgument): + self.gmp.create_credential_store_credential( + name="foo", credential_type="" + ) + + with self.assertRaises(InvalidArgument): + self.gmp.create_credential_store_credential( + name="foo", credential_type="bar" + ) + + def test_create_cs_pw_credential(self): + self.gmp.create_credential_store_credential( + name="foo", + credential_type=CredentialStoreCredentialType.PASSWORD_ONLY, + vault_id="123", + host_identifier="456", + ) + self.connection.send.has_been_called_with( + b"" + b"foo" + b"cs_pw" + b"123" + b"456" + b"" + ) diff --git a/tests/protocols/gmpnext/entities/credentials/test_modify_credential_store_credential.py b/tests/protocols/gmpnext/entities/credentials/test_modify_credential_store_credential.py new file mode 100644 index 00000000..b8dbc860 --- /dev/null +++ b/tests/protocols/gmpnext/entities/credentials/test_modify_credential_store_credential.py @@ -0,0 +1,100 @@ +# SPDX-FileCopyrightText: 2025 Greenbone AG +# +# SPDX-License-Identifier: GPL-3.0-or-later +# + +from gvm.errors import RequiredArgument + + +class GmpModifyCredentialStoreCredentialTestMixin: + def test_modify_cs_credential(self): + self.gmp.modify_credential_store_credential(credential_id="c1") + + self.connection.send.has_been_called_with( + b'' + ) + + def test_modify_cs_credential_missing_credential_id(self): + with self.assertRaises(RequiredArgument): + self.gmp.modify_credential_store_credential(None) + + with self.assertRaises(RequiredArgument): + self.gmp.modify_credential_store_credential("") + + with self.assertRaises(RequiredArgument): + self.gmp.modify_credential_store_credential(credential_id="") + + def test_modify_cs_credential_with_name(self): + self.gmp.modify_credential_store_credential( + credential_id="c1", name="foo" + ) + + self.connection.send.has_been_called_with( + b'' + b"foo" + b"" + ) + + def test_modify_cs_credential_with_comment(self): + self.gmp.modify_credential_store_credential( + credential_id="c1", comment="foo" + ) + + self.connection.send.has_been_called_with( + b'' + b"foo" + b"" + ) + + def test_modify_cs_credential_with_credential_store_id(self): + self.gmp.modify_credential_store_credential( + credential_id="c1", credential_store_id="foo" + ) + + self.connection.send.has_been_called_with( + b'' + b"foo" + b"" + ) + + def test_modify_cs_credential_with_vault_id(self): + self.gmp.modify_credential_store_credential( + credential_id="c1", vault_id="foo" + ) + + self.connection.send.has_been_called_with( + b'' + b"foo" + b"" + ) + + def test_modify_cs_credential_with_host_identifier(self): + self.gmp.modify_credential_store_credential( + credential_id="c1", host_identifier="foo" + ) + + self.connection.send.has_been_called_with( + b'' + b"foo" + b"" + ) + + def test_modify_cs_credential_with_all(self): + self.gmp.modify_credential_store_credential( + credential_id="c1", + name="foo_name", + comment="foo_comment", + credential_store_id="foo_csid", + vault_id="foo_vid", + host_identifier="foo_hid", + ) + + self.connection.send.has_been_called_with( + b'' + b"foo_name" + b"foo_comment" + b"foo_csid" + b"foo_vid" + b"foo_hid" + b"" + ) diff --git a/tests/protocols/gmpnext/entities/test_credential_stores.py b/tests/protocols/gmpnext/entities/test_credential_stores.py new file mode 100644 index 00000000..1c3628e6 --- /dev/null +++ b/tests/protocols/gmpnext/entities/test_credential_stores.py @@ -0,0 +1,27 @@ +# SPDX-FileCopyrightText: 2025 Greenbone AG +# +# SPDX-License-Identifier: GPL-3.0-or-later +# + +from ...gmpnext import GMPTestCase +from ..entities.credential_stores import ( + GmpGetCredentialStoresTestMixin, + GmpModifyCredentialStoreTestMixin, + GmpVerifyCredentialStoreTestMixin, +) + + +class GMPGetCredentialStoresTest(GmpGetCredentialStoresTestMixin, GMPTestCase): + pass + + +class GMPModifyCredentialStoreTest( + GmpModifyCredentialStoreTestMixin, GMPTestCase +): + pass + + +class GMPVerifyCredentialStoreTest( + GmpVerifyCredentialStoreTestMixin, GMPTestCase +): + pass diff --git a/tests/protocols/gmpnext/entities/test_credentials.py b/tests/protocols/gmpnext/entities/test_credentials.py index 5843ed24..6fd67390 100644 --- a/tests/protocols/gmpnext/entities/test_credentials.py +++ b/tests/protocols/gmpnext/entities/test_credentials.py @@ -3,6 +3,7 @@ # SPDX-License-Identifier: GPL-3.0-or-later # +from ...gmpnext import GMPTestCase from ...gmpv224.entities.credentials import ( GmpCloneCredentialTestMixin, GmpCreateCredentialTestMixin, @@ -11,14 +12,21 @@ GmpGetCredentialTestMixin, GmpModifyCredentialTestMixin, ) -from ...gmpv227 import GMPTestCase +from ..entities.credentials import ( + GmpCreateCredentialStoreCredentialTestMixin, + GmpModifyCredentialStoreCredentialTestMixin, +) class GMPCloneCredentialTestCase(GmpCloneCredentialTestMixin, GMPTestCase): pass -class GMPCreateCredentialTestCase(GmpCreateCredentialTestMixin, GMPTestCase): +class GMPCreateCredentialTestCase( + GmpCreateCredentialTestMixin, + GmpCreateCredentialStoreCredentialTestMixin, + GMPTestCase, +): pass @@ -34,5 +42,9 @@ class GMPGetCredentialsTestCase(GmpGetCredentialsTestMixin, GMPTestCase): pass -class GMPModifyCredentialTestCase(GmpModifyCredentialTestMixin, GMPTestCase): +class GMPModifyCredentialTestCase( + GmpModifyCredentialTestMixin, + GmpModifyCredentialStoreCredentialTestMixin, + GMPTestCase, +): pass