diff --git a/prowler/providers/kubernetes/exceptions/exceptions.py b/prowler/providers/kubernetes/exceptions/exceptions.py index 57cfda39c6..a8138d64cf 100644 --- a/prowler/providers/kubernetes/exceptions/exceptions.py +++ b/prowler/providers/kubernetes/exceptions/exceptions.py @@ -25,7 +25,11 @@ class KubernetesBaseException(ProwlerException): "message": "An error occurred in the Kubernetes provider.", "remediation": "Check the provider code and configuration to identify the issue. For more information on troubleshooting Kubernetes providers, refer to the Kubernetes documentation: https://kubernetes.io/docs/reference/", }, - (1936, "KubernetesInvalidKubeConfigFileError"): { + (1936, "KubernetesInvalidProviderIdError"): { + "message": "The provider ID is invalid.", + "remediation": "Check the provider ID and ensure it is correctly formatted. Refer to the Kubernetes documentation for guidance on provider IDs: https://kubernetes.io/docs/reference/access-authn-authz/rbac/", + }, + (1937, "KubernetesInvalidKubeConfigFileError"): { "message": "The provided kube-config is invalid.", "remediation": "Review the kube-config and the attached error to get more details. Please, refer to the Kubernetes config documentation: https://kubernetes.io/docs/reference/config-api/kubeconfig.v1/#Config", }, @@ -76,6 +80,11 @@ class KubernetesTimeoutError(KubernetesBaseException): super().__init__(1934, file, original_exception, message) -class KubernetesInvalidKubeConfigFileError(KubernetesBaseException): +class KubernetesInvalidProviderIdError(KubernetesBaseException): def __init__(self, file=None, original_exception=None, message=None): super().__init__(1936, file, original_exception, message) + + +class KubernetesInvalidKubeConfigFileError(KubernetesBaseException): + def __init__(self, file=None, original_exception=None, message=None): + super().__init__(1937, file, original_exception, message) diff --git a/prowler/providers/kubernetes/kubernetes_provider.py b/prowler/providers/kubernetes/kubernetes_provider.py index 38997a62de..6c4dd26e57 100644 --- a/prowler/providers/kubernetes/kubernetes_provider.py +++ b/prowler/providers/kubernetes/kubernetes_provider.py @@ -4,7 +4,7 @@ from colorama import Fore, Style from kubernetes.client.exceptions import ApiException from kubernetes.config.config_exception import ConfigException from requests.exceptions import Timeout -from yaml import safe_load +from yaml import parser, safe_load from kubernetes import client, config from prowler.config.config import get_default_mute_file_path @@ -17,6 +17,7 @@ from prowler.providers.kubernetes.exceptions.exceptions import ( KubernetesCloudResourceManagerAPINotUsedError, KubernetesError, KubernetesInvalidKubeConfigFileError, + KubernetesInvalidProviderIdError, KubernetesSetUpSessionError, KubernetesTimeoutError, ) @@ -146,7 +147,7 @@ class KubernetesProvider(Provider): def setup_session( kubeconfig_file: str = None, kubeconfig_content: dict = None, - input_context: str = None, + context: str = None, ) -> KubernetesSession: """ Sets up the Kubernetes session. @@ -154,7 +155,7 @@ class KubernetesProvider(Provider): Args: kubeconfig_file (str): Path to the kubeconfig file. kubeconfig_content (dict): Content of the kubeconfig file. - input_context (str): Context name. + context (str): Context name. Returns: Tuple: A tuple containing the API client and the context. @@ -163,8 +164,9 @@ class KubernetesProvider(Provider): try: if kubeconfig_content: config.load_kube_config_from_dict( - kubeconfig_content, context=input_context + safe_load(kubeconfig_content), context=context ) + else: try: config.load_kube_config( @@ -173,7 +175,7 @@ class KubernetesProvider(Provider): if kubeconfig_file != "~/.kube/config" else os.path.expanduser(kubeconfig_file) ), - context=input_context, + context=context, ) except ConfigException: # If the kubeconfig file is not found, try to use the in-cluster config @@ -189,22 +191,36 @@ class KubernetesProvider(Provider): return KubernetesSession( api_client=client.ApiClient(), context=context ) - if input_context: + if context: contexts = config.list_kube_config_contexts()[0] for context_item in contexts: - if context_item["name"] == input_context: + if context_item["name"] == context: context = context_item else: context = config.list_kube_config_contexts()[1] return KubernetesSession(api_client=client.ApiClient(), context=context) + except parser.ParserError as parser_error: + logger.critical( + f"{parser_error.__class__.__name__}[{parser_error.__traceback__.tb_lineno}]: {parser_error}" + ) + raise KubernetesInvalidKubeConfigFileError( + original_exception=parser_error, file=os.path.abspath(__file__) + ) except ConfigException as config_error: logger.critical( f"{config_error.__class__.__name__}[{config_error.__traceback__.tb_lineno}]: {config_error}" ) - raise KubernetesInvalidKubeConfigFileError( - original_exception=config_error, file=os.path.abspath(__file__) - ) + if f"Expected object with name {context} in kube-config/contexts" in str( + config_error + ): + raise KubernetesInvalidProviderIdError( + original_exception=config_error, file=os.path.abspath(__file__) + ) + else: + raise KubernetesInvalidKubeConfigFileError( + original_exception=config_error, file=os.path.abspath(__file__) + ) except Exception as error: logger.critical( f"{error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}" @@ -218,7 +234,7 @@ class KubernetesProvider(Provider): kubeconfig_file: str = "~/.kube/config", kubeconfig_content: dict = None, namespace: str = None, - context: str = None, + provider_id: str = None, raise_on_exception: bool = True, ) -> Connection: """ @@ -228,15 +244,16 @@ class KubernetesProvider(Provider): kubeconfig_file (str): Path to the kubeconfig file. kubeconfig_content (dict): Content of the kubeconfig file. namespace (str): Namespace name. - context (str): Context name. + provider_id (str): Provider ID to use, in this case, the Kubernetes context. raise_on_exception (bool): Whether to raise an exception on error. Returns: Connection: A Connection object. """ try: - kubeconfig_content = safe_load(kubeconfig_content) KubernetesProvider.setup_session( - kubeconfig_file, kubeconfig_content, context + kubeconfig_file=kubeconfig_file, + kubeconfig_content=kubeconfig_content, + context=provider_id, ) if namespace: client.CoreV1Api().list_namespaced_pod( @@ -245,6 +262,20 @@ class KubernetesProvider(Provider): else: client.CoreV1Api().list_namespace(timeout_seconds=2, _request_timeout=2) return Connection(is_connected=True) + except KubernetesInvalidKubeConfigFileError as invalid_kubeconfig_error: + logger.critical( + f"KubernetesInvalidKubeConfigFileError[{invalid_kubeconfig_error.__traceback__.tb_lineno}]: {invalid_kubeconfig_error}" + ) + if raise_on_exception: + raise invalid_kubeconfig_error + return Connection(error=invalid_kubeconfig_error) + except KubernetesInvalidProviderIdError as invalid_provider_id_error: + logger.critical( + f"KubernetesInvalidProviderIdError[{invalid_provider_id_error.__traceback__.tb_lineno}]: {invalid_provider_id_error}" + ) + if raise_on_exception: + raise invalid_provider_id_error + return Connection(error=invalid_provider_id_error) except KubernetesSetUpSessionError as setup_session_error: logger.critical( f"KubernetesSetUpSessionError[{setup_session_error.__traceback__.tb_lineno}]: {setup_session_error}" diff --git a/tests/providers/kubernetes/kubernetes_provider_test.py b/tests/providers/kubernetes/kubernetes_provider_test.py index 990ccf6748..b41021e77f 100644 --- a/tests/providers/kubernetes/kubernetes_provider_test.py +++ b/tests/providers/kubernetes/kubernetes_provider_test.py @@ -7,6 +7,9 @@ from prowler.config.config import ( default_fixer_config_file_path, load_and_validate_config_file, ) +from prowler.providers.kubernetes.exceptions.exceptions import ( + KubernetesSetUpSessionError, +) from prowler.providers.kubernetes.kubernetes_provider import KubernetesProvider from prowler.providers.kubernetes.models import ( KubernetesIdentityInfo, @@ -111,7 +114,7 @@ class TestKubernetesProvider: connection = KubernetesProvider.test_connection( kubeconfig_file=None, kubeconfig_content=kubeconfig_content, - context="example-context", + provider_id="example-context", raise_on_exception=False, ) @@ -146,7 +149,7 @@ class TestKubernetesProvider: connection = KubernetesProvider.test_connection( kubeconfig_file="dummy_kubeconfig_path", kubeconfig_content="", - context="test-context", + provider_id="test-context", raise_on_exception=False, ) @@ -182,10 +185,121 @@ class TestKubernetesProvider: ] connection = KubernetesProvider.test_connection( - kubeconfig_file="dummy_kubeconfig_path", + kubeconfig_file="", kubeconfig_content="", namespace="test-namespace", - context="test-context", + provider_id="test-context", + raise_on_exception=False, + ) + + assert connection.is_connected + assert connection.error is None + + @patch( + "prowler.providers.kubernetes.kubernetes_provider.client.CoreV1Api.list_namespace" + ) + @patch("kubernetes.config.list_kube_config_contexts") + @patch("kubernetes.config.load_kube_config_from_dict") + def test_kubernetes_test_connection_with_kubeconfig_content_invalid_provider_id( + self, + mock_load_kube_config_from_dict, + mock_list_kube_config_contexts, + mock_list_namespace, + ): + mock_load_kube_config_from_dict.return_value = None + mock_list_kube_config_contexts.return_value = ( + [ + { + "name": "example-context", + "context": { + "cluster": "example-cluster", + "user": "example-user", + }, + } + ], + None, + ) + mock_list_namespace.return_value.items = [ + client.V1Namespace(metadata=client.V1ObjectMeta(name="namespace-1")), + ] + + kubeconfig_content = { + "apiVersion": "v1", + "clusters": [ + { + "cluster": { + "server": "https://kubernetes.example.com", + }, + "name": "example-cluster", + } + ], + "contexts": [ + { + "context": { + "cluster": "example-cluster", + "user": "example-user", + }, + "name": "example-context", + } + ], + "current-context": "example-context", + "kind": "Config", + "preferences": {}, + "users": [ + { + "name": "example-user", + "user": { + "token": "EXAMPLE_TOKEN", + }, + } + ], + } + + connection = KubernetesProvider.test_connection( + kubeconfig_file=None, + kubeconfig_content=kubeconfig_content, + provider_id="example-context-invalid", + raise_on_exception=False, + ) + + assert not connection.is_connected + assert connection.error is not None + assert isinstance(connection.error, KubernetesSetUpSessionError) + + @patch( + "prowler.providers.kubernetes.kubernetes_provider.client.CoreV1Api.list_namespace" + ) + @patch("kubernetes.config.list_kube_config_contexts") + @patch("kubernetes.config.load_kube_config_from_dict") + def test_kubernetes_test_connection_with_kubeconfig_content_valid_provider_id( + self, + mock_load_kube_config_from_dict, + mock_list_kube_config_contexts, + mock_list_namespace, + ): + mock_load_kube_config_from_dict.return_value = None + mock_list_kube_config_contexts.return_value = ( + [ + { + "name": "example-context", + "context": { + "cluster": "example-cluster", + "user": "example-user", + }, + } + ], + None, + ) + mock_list_namespace.return_value.items = [ + client.V1Namespace(metadata=client.V1ObjectMeta(name="namespace-1")), + ] + + kubeconfig_content = '{"apiVersion": "v1", "clusters": [{"cluster": {"server": "https://kubernetes.example.com"}, "name": "example-cluster"}], "contexts": [{"context": {"cluster": "example-cluster", "user": "example-user"}, "name": "example-context"}], "current-context": "example-context", "kind": "Config", "preferences": {}, "users": [{"name": "example-user", "user": {"token": "EXAMPLE_TOKEN"}}]}' + + connection = KubernetesProvider.test_connection( + kubeconfig_file=None, + kubeconfig_content=kubeconfig_content, + provider_id="example-context", raise_on_exception=False, )