diff --git a/gvm/protocols/gmp/_gmpnext.py b/gvm/protocols/gmp/_gmpnext.py index 4b3e4894..7e21c966 100644 --- a/gvm/protocols/gmp/_gmpnext.py +++ b/gvm/protocols/gmp/_gmpnext.py @@ -449,6 +449,46 @@ def modify_active_directory_credential( ) ) + def verify_active_directory_credential( + self, + *, + credential_id: EntityID | None = None, + host: str | None = None, + bind_dn: str | None = None, + bind_password: str | None = None, + base_dn: str | None = None, + tls_mode: str | None = None, + port: int | None = None, + ca_certificate: str | None = None, + ) -> T: + """Verify a saved AD credential or supplied AD connection settings. + + Requires AD integration to be enabled in gvmd. Provide either + credential_id alone or the required inline connection fields. + + Args: + credential_id: UUID of a saved AD credential. + host: AD server hostname or IP address. + bind_dn: Distinguished name used to bind to the directory. + bind_password: Password used for the bind. + base_dn: Base distinguished name for directory searches. + tls_mode: Either "ldaps" or "starttls". + port: Optional server port, from 1 to 65535. + ca_certificate: Optional CA certificate in PEM format. + """ + return self._send_request_and_transform_response( + Credentials.verify_active_directory_credential( + credential_id=credential_id, + host=host, + bind_dn=bind_dn, + bind_password=bind_password, + base_dn=base_dn, + tls_mode=tls_mode, + port=port, + ca_certificate=ca_certificate, + ) + ) + def create_credential_store_credential( self, name: str, diff --git a/gvm/protocols/gmp/requests/next/_credentials.py b/gvm/protocols/gmp/requests/next/_credentials.py index 17192161..a24bf5d1 100644 --- a/gvm/protocols/gmp/requests/next/_credentials.py +++ b/gvm/protocols/gmp/requests/next/_credentials.py @@ -249,6 +249,77 @@ def modify_active_directory_credential( cmd.add_element("ca_certificate", str(ca_certificate)) return cmd + @classmethod + def verify_active_directory_credential( + cls, + *, + credential_id: EntityID | None = None, + host: str | None = None, + bind_dn: str | None = None, + bind_password: str | None = None, + base_dn: str | None = None, + tls_mode: str | None = None, + port: int | None = None, + ca_certificate: str | None = None, + ) -> Request: + """Verify a saved AD credential or supplied AD connection settings.""" + function = cls.verify_active_directory_credential.__name__ + + has_inline_fields = ( + host is not None + or bind_dn is not None + or bind_password is not None + or base_dn is not None + or tls_mode is not None + or port is not None + or ca_certificate is not None + ) + + if credential_id is not None: + if not credential_id: + raise RequiredArgument( + function=function, argument="credential_id" + ) + if has_inline_fields: + raise InvalidArgument( + function=function, argument="credential_id" + ) + + cmd = XmlCommand("verify_active_directory_credential") + cmd.set_attribute("credential_id", str(credential_id)) + return cmd + + if not host: + raise RequiredArgument(function=function, argument="host") + if not bind_dn: + raise RequiredArgument(function=function, argument="bind_dn") + if not bind_password: + raise RequiredArgument(function=function, argument="bind_password") + if not base_dn: + raise RequiredArgument(function=function, argument="base_dn") + if not tls_mode: + raise RequiredArgument(function=function, argument="tls_mode") + + if tls_mode not in ("ldaps", "starttls"): + raise InvalidArgument(function=function, argument="tls_mode") + + if port is not None and ( + not isinstance(port, int) or not 1 <= port <= 65535 + ): + raise InvalidArgument(function=function, argument="port") + + cmd = XmlCommand("verify_active_directory_credential") + cmd.add_element("host", host) + if port is not None: + cmd.add_element("port", str(port)) + cmd.add_element("bind_dn", bind_dn) + cmd.add_element("bind_password", bind_password) + cmd.add_element("base_dn", base_dn) + cmd.add_element("tls_mode", tls_mode) + if ca_certificate is not None: + cmd.add_element("ca_certificate", ca_certificate) + return cmd + @classmethod def create_credential_store_credential( cls, diff --git a/tests/protocols/gmpnext/entities/credentials/__init__.py b/tests/protocols/gmpnext/entities/credentials/__init__.py index e1425d24..a57a05ae 100644 --- a/tests/protocols/gmpnext/entities/credentials/__init__.py +++ b/tests/protocols/gmpnext/entities/credentials/__init__.py @@ -14,10 +14,14 @@ from .test_modify_credential_store_credential import ( GmpModifyCredentialStoreCredentialTestMixin, ) +from .test_verify_active_directory_credential import ( + GmpVerifyActiveDirectoryCredentialTestMixin, +) __all__ = ( "GmpCreateActiveDirectoryCredentialTestMixin", "GmpCreateCredentialStoreCredentialTestMixin", "GmpModifyActiveDirectoryCredentialTestMixin", "GmpModifyCredentialStoreCredentialTestMixin", + "GmpVerifyActiveDirectoryCredentialTestMixin", ) diff --git a/tests/protocols/gmpnext/entities/credentials/test_verify_active_directory_credential.py b/tests/protocols/gmpnext/entities/credentials/test_verify_active_directory_credential.py new file mode 100644 index 00000000..bfeed659 --- /dev/null +++ b/tests/protocols/gmpnext/entities/credentials/test_verify_active_directory_credential.py @@ -0,0 +1,112 @@ +# SPDX-FileCopyrightText: 2026 Greenbone AG +# +# SPDX-License-Identifier: GPL-3.0-or-later +# + +from gvm.errors import InvalidArgument, RequiredArgument + + +class GmpVerifyActiveDirectoryCredentialTestMixin: + def test_verify_saved_ad_credential(self): + self.gmp.verify_active_directory_credential( + credential_id="credential-id" + ) + + self.connection.send.has_been_called_with( + b'' + ) + + def test_verify_inline_ad_credential(self): + self.gmp.verify_active_directory_credential( + host="dc.example.org", + bind_dn="CN=Scanner,DC=example,DC=org", + bind_password="secret", + base_dn="DC=example,DC=org", + tls_mode="ldaps", + ) + + self.connection.send.has_been_called_with( + b"" + b"dc.example.org" + b"CN=Scanner,DC=example,DC=org" + b"secret" + b"DC=example,DC=org" + b"ldaps" + b"" + ) + + def test_verify_inline_ad_credential_with_optional_fields(self): + self.gmp.verify_active_directory_credential( + host="dc.example.org", + bind_dn="CN=Scanner,DC=example,DC=org", + bind_password="secret<&", + base_dn="DC=example,DC=org", + tls_mode="starttls", + port=389, + ca_certificate="certificate<&", + ) + + self.connection.send.has_been_called_with( + b"" + b"dc.example.org" + b"389" + b"CN=Scanner,DC=example,DC=org" + b"secret<&" + b"DC=example,DC=org" + b"starttls" + b"certificate<&" + b"" + ) + + def test_verify_without_saved_id_or_inline_fields(self): + with self.assertRaises(RequiredArgument): + self.gmp.verify_active_directory_credential() + + with self.assertRaises(RequiredArgument): + self.gmp.verify_active_directory_credential(credential_id="") + + def test_verify_saved_id_with_inline_fields(self): + for field in ("host", "port", "ca_certificate"): + with self.subTest(field=field): + with self.assertRaises(InvalidArgument): + self.gmp.verify_active_directory_credential( + credential_id="credential-id", **{field: "value"} + ) + + def test_verify_inline_ad_credential_missing_required_field(self): + fields = { + "host": "dc.example.org", + "bind_dn": "CN=Scanner,DC=example,DC=org", + "bind_password": "secret", + "base_dn": "DC=example,DC=org", + "tls_mode": "ldaps", + } + for field in fields: + with self.subTest(field=field): + with self.assertRaises(RequiredArgument): + self.gmp.verify_active_directory_credential( + **{**fields, field: ""} + ) + + def test_verify_inline_ad_credential_invalid_tls_mode(self): + with self.assertRaises(InvalidArgument): + self.gmp.verify_active_directory_credential( + host="dc.example.org", + bind_dn="CN=Scanner,DC=example,DC=org", + bind_password="secret", + base_dn="DC=example,DC=org", + tls_mode="ldap", + ) + + def test_verify_inline_ad_credential_invalid_port(self): + for port in (0, 65536, "389"): + with self.subTest(port=port): + with self.assertRaises(InvalidArgument): + self.gmp.verify_active_directory_credential( + host="dc.example.org", + bind_dn="CN=Scanner,DC=example,DC=org", + bind_password="secret", + base_dn="DC=example,DC=org", + tls_mode="ldaps", + port=port, + ) diff --git a/tests/protocols/gmpnext/entities/test_credentials.py b/tests/protocols/gmpnext/entities/test_credentials.py index 27724bee..f952f5a3 100644 --- a/tests/protocols/gmpnext/entities/test_credentials.py +++ b/tests/protocols/gmpnext/entities/test_credentials.py @@ -17,6 +17,7 @@ GmpCreateCredentialStoreCredentialTestMixin, GmpModifyActiveDirectoryCredentialTestMixin, GmpModifyCredentialStoreCredentialTestMixin, + GmpVerifyActiveDirectoryCredentialTestMixin, ) @@ -52,3 +53,9 @@ class GMPModifyCredentialTestCase( GMPTestCase, ): pass + + +class GMPVerifyActiveDirectoryCredentialTestCase( + GmpVerifyActiveDirectoryCredentialTestMixin, GMPTestCase +): + pass