diff --git a/prowler/providers/azure/azure_provider.py b/prowler/providers/azure/azure_provider.py index 49d2dee189..40272f3bda 100644 --- a/prowler/providers/azure/azure_provider.py +++ b/prowler/providers/azure/azure_provider.py @@ -32,6 +32,7 @@ from prowler.providers.azure.exceptions.exceptions import ( AzureGetTokenIdentityError, AzureHTTPResponseError, AzureInteractiveBrowserCredentialError, + AzureInvalidAccountCredentialsError, AzureNoAuthenticationMethodError, AzureNoSubscriptionsError, AzureNotTenantIdButClientIdAndClienSecret, @@ -539,6 +540,7 @@ class AzureProvider(Provider): raise_on_exception=True, client_id=None, client_secret=None, + provider_id=None, ) -> Connection: """Test connection to Azure subscription. @@ -554,6 +556,7 @@ class AzureProvider(Provider): raise_on_exception (bool): Flag indicating whether to raise an exception if the connection fails. client_id (str): The Azure client ID. client_secret (str): The Azure client secret. + provider_id (str): The provider ID, in this case it's the Azure subscription ID. Returns: bool: True if the connection is successful, False otherwise. @@ -610,10 +613,20 @@ class AzureProvider(Provider): # Create a SubscriptionClient subscription_client = SubscriptionClient(credentials) - # Get info from the first subscription - subscription = next(subscription_client.subscriptions.list()) + # Get info from the subscriptions + available_subscriptions = [] + for subscription in subscription_client.subscriptions.list(): + available_subscriptions.append(subscription) - logger.info(f"Connected to Azure subscription: {subscription.display_name}") + if provider_id and provider_id not in [ + sub.subscription_id for sub in available_subscriptions + ]: + raise AzureInvalidAccountCredentialsError( + file=os.path.basename(__file__), + message="The provided credentials are not valid for the specified Azure subscription.", + ) + + logger.info("Azure provider: Connection to Azure subscription successful") return Connection(is_connected=True) # Exceptions from validate_arguments @@ -697,6 +710,12 @@ class AzureProvider(Provider): if raise_on_exception: raise client_secret_error return Connection(error=client_secret_error) + # Exceptions from provider_id validation + except AzureInvalidAccountCredentialsError as invalid_credentials_error: + logger.error(str(invalid_credentials_error)) + if raise_on_exception: + raise invalid_credentials_error + return Connection(error=invalid_credentials_error) # Exceptions from SubscriptionClient except HttpResponseError as http_response_error: logger.error( diff --git a/prowler/providers/azure/exceptions/exceptions.py b/prowler/providers/azure/exceptions/exceptions.py index 2aee21db32..163fbb424d 100644 --- a/prowler/providers/azure/exceptions/exceptions.py +++ b/prowler/providers/azure/exceptions/exceptions.py @@ -97,6 +97,10 @@ class AzureBaseException(ProwlerException): "message": "The provided tenant ID and client ID do not belong to the provided client secret", "remediation": "Check the tenant ID and client ID and ensure they belong to the provided client secret.", }, + (1937, "AzureInvalidAccountCredentialsError"): { + "message": "The provided provider_id does not match with the available subscriptions", + "remediation": "Check the provider_id and ensure it is a valid subscription for the given credentials.", + }, } def __init__(self, code, file=None, original_exception=None, message=None): @@ -279,3 +283,10 @@ class AzureTenantIdAndClientIdNotBelongingToClientSecretError(AzureCredentialsEr super().__init__( 1936, file=file, original_exception=original_exception, message=message ) + + +class AzureInvalidAccountCredentialsError(AzureBaseException): + def __init__(self, file=None, original_exception=None, message=None): + super().__init__( + 1937, file=file, original_exception=original_exception, message=message + ) diff --git a/tests/providers/azure/azure_provider_test.py b/tests/providers/azure/azure_provider_test.py index 99762502f9..cfe24049be 100644 --- a/tests/providers/azure/azure_provider_test.py +++ b/tests/providers/azure/azure_provider_test.py @@ -15,6 +15,7 @@ from prowler.providers.azure.azure_provider import AzureProvider from prowler.providers.azure.exceptions.exceptions import ( AzureBrowserAuthNoTenantIDError, AzureHTTPResponseError, + AzureInvalidAccountCredentialsError, AzureNoAuthenticationMethodError, AzureTenantIDNoBrowserAuthError, ) @@ -280,6 +281,103 @@ class TestAzureProvider: assert test_connection.is_connected assert test_connection.error is None + def test_test_connection_provider_validation(self): + with patch( + "prowler.providers.azure.azure_provider.DefaultAzureCredential" + ) as mock_default_credential, patch( + "prowler.providers.azure.azure_provider.AzureProvider.setup_session" + ) as mock_setup_session, patch( + "prowler.providers.azure.azure_provider.SubscriptionClient" + ) as mock_resource_client, patch( + "prowler.providers.azure.azure_provider.AzureProvider.validate_static_credentials" + ) as mock_validate_static_credentials: + + # Mock the return value of DefaultAzureCredential + mock_default_credential.return_value = { + "client_id": str(uuid4()), + "client_secret": str(uuid4()), + "tenant_id": str(uuid4()), + } + + # Mock setup_session to return a mocked session object + mock_session = MagicMock() + mock_setup_session.return_value = mock_session + + # Mock ValidateStaticCredentials to avoid real API calls + mock_validate_static_credentials.return_value = None + + # Mock ResourceManagementClient to avoid real API calls + mock_subscription = MagicMock() + mock_subscription.subscription_id = "test_provider_id" + mock_return_value = MagicMock() + mock_return_value.subscriptions.list.return_value = [mock_subscription] + mock_resource_client.return_value = mock_return_value + + test_connection = AzureProvider.test_connection( + browser_auth=False, + tenant_id=str(uuid4()), + region="AzureCloud", + raise_on_exception=False, + client_id=str(uuid4()), + client_secret=str(uuid4()), + provider_id="test_provider_id", + ) + + assert isinstance(test_connection, Connection) + assert test_connection.is_connected + assert test_connection.error is None + + def test_test_connection_provider_validation_error(self): + with patch( + "prowler.providers.azure.azure_provider.DefaultAzureCredential" + ) as mock_default_credential, patch( + "prowler.providers.azure.azure_provider.AzureProvider.setup_session" + ) as mock_setup_session, patch( + "prowler.providers.azure.azure_provider.SubscriptionClient" + ) as mock_resource_client, patch( + "prowler.providers.azure.azure_provider.AzureProvider.validate_static_credentials" + ) as mock_validate_static_credentials: + + # Mock the return value of DefaultAzureCredential + mock_default_credential.return_value = { + "client_id": str(uuid4()), + "client_secret": str(uuid4()), + "tenant_id": str(uuid4()), + } + + # Mock setup_session to return a mocked session object + mock_session = MagicMock() + mock_setup_session.return_value = mock_session + + # Mock ValidateStaticCredentials to avoid real API calls + mock_validate_static_credentials.return_value = None + + # Mock ResourceManagementClient to avoid real API calls + mock_subscription = MagicMock() + mock_subscription.subscription_id = "test_invalid_provider_id" + mock_return_value = MagicMock() + mock_return_value.subscriptions.list.return_value = [mock_subscription] + mock_resource_client.return_value = mock_return_value + + test_connection = AzureProvider.test_connection( + browser_auth=False, + tenant_id=str(uuid4()), + region="AzureCloud", + raise_on_exception=False, + client_id=str(uuid4()), + client_secret=str(uuid4()), + provider_id="test_provider_id", + ) + + assert test_connection.error is not None + assert isinstance( + test_connection.error, AzureInvalidAccountCredentialsError + ) + assert ( + "The provided credentials are not valid for the specified Azure subscription." + in test_connection.error.args[0] + ) + def test_test_connection_with_ClientAuthenticationError(self): with pytest.raises(AzureHTTPResponseError) as exception: tenant_id = str(uuid4())