Files
prowler/prowler/providers/azure/azure_provider.py
T

1123 lines
51 KiB
Python

import asyncio
import os
import re
from argparse import ArgumentTypeError
from os import getenv
from uuid import UUID
import requests
from azure.core.exceptions import ClientAuthenticationError, HttpResponseError
from azure.identity import (
ClientSecretCredential,
CredentialUnavailableError,
DefaultAzureCredential,
InteractiveBrowserCredential,
)
from azure.mgmt.subscription import SubscriptionClient
from colorama import Fore, Style
from msgraph import GraphServiceClient
from prowler.config.config import (
default_config_file_path,
get_default_mute_file_path,
load_and_validate_config_file,
)
from prowler.lib.logger import logger
from prowler.lib.utils.utils import print_boxes
from prowler.providers.azure.exceptions.exceptions import (
AzureArgumentTypeValidationError,
AzureBrowserAuthNoTenantIDError,
AzureClientAuthenticationError,
AzureClientIdAndClientSecretNotBelongingToTenantIdError,
AzureConfigCredentialsError,
AzureCredentialsUnavailableError,
AzureDefaultAzureCredentialError,
AzureEnvironmentVariableError,
AzureGetTokenIdentityError,
AzureHTTPResponseError,
AzureInteractiveBrowserCredentialError,
AzureInvalidProviderIdError,
AzureNoAuthenticationMethodError,
AzureNoSubscriptionsError,
AzureNotTenantIdButClientIdAndClienSecretError,
AzureNotValidClientIdError,
AzureNotValidClientSecretError,
AzureNotValidTenantIdError,
AzureSetUpIdentityError,
AzureSetUpRegionConfigError,
AzureSetUpSessionError,
AzureTenantIdAndClientIdNotBelongingToClientSecretError,
AzureTenantIdAndClientSecretNotBelongingToClientIdError,
AzureTenantIDNoBrowserAuthError,
)
from prowler.providers.azure.lib.arguments.arguments import validate_azure_region
from prowler.providers.azure.lib.mutelist.mutelist import AzureMutelist
from prowler.providers.azure.lib.regions.regions import get_regions_config
from prowler.providers.azure.models import AzureIdentityInfo, AzureRegionConfig
from prowler.providers.common.models import Audit_Metadata, Connection
from prowler.providers.common.provider import Provider
class AzureProvider(Provider):
"""
Represents an Azure provider.
This class provides functionality to interact with the Azure cloud provider.
It handles authentication, region configuration, and provides access to various properties and methods
related to the Azure provider.
Attributes:
_type (str): The type of the provider, which is set to "azure".
_session (DefaultAzureCredential): The session object associated with the Azure provider.
_identity (AzureIdentityInfo): The identity information for the Azure provider.
_audit_config (dict): The audit configuration for the Azure provider.
_region_config (AzureRegionConfig): The region configuration for the Azure provider.
_locations (dict): A dictionary containing the available locations for the Azure provider.
_mutelist (AzureMutelist): The mutelist object associated with the Azure provider.
audit_metadata (Audit_Metadata): The audit metadata for the Azure provider.
Methods:
__init__ -> Initializes the Azure provider.
identity(self): Returns the identity of the Azure provider.
type(self): Returns the type of the Azure provider.
session(self): Returns the session object associated with the Azure provider.
region_config(self): Returns the region configuration for the Azure provider.
locations(self): Returns a list of available locations for the Azure provider.
audit_config(self): Returns the audit configuration for the Azure provider.
fixer_config(self): Returns the fixer configuration.
output_options(self, options: tuple): Sets the output options for the Azure provider.
mutelist(self) -> AzureMutelist: Returns the mutelist object associated with the Azure provider.
get_output_mapping(self): Returns a dictionary that maps output keys to their corresponding values.
validate_arguments(cls, az_cli_auth, sp_env_auth, browser_auth, managed_identity_auth, tenant_id): Validates the authentication arguments for the Azure provider.
setup_region_config(cls, region): Sets up the region configuration for the Azure provider.
print_credentials(self): Prints the Azure credentials information.
setup_session(cls, az_cli_auth, sp_env_auth, browser_auth, managed_identity_auth, tenant_id, region_config): Set up the Azure session with the specified authentication method.
"""
_type: str = "azure"
_session: DefaultAzureCredential
_identity: AzureIdentityInfo
_audit_config: dict
_region_config: AzureRegionConfig
_locations: dict
_mutelist: AzureMutelist
# TODO: this is not optional, enforce for all providers
audit_metadata: Audit_Metadata
def __init__(
self,
az_cli_auth: bool = False,
sp_env_auth: bool = False,
browser_auth: bool = False,
managed_identity_auth: bool = False,
tenant_id: str = None,
region: str = "AzureCloud",
subscription_ids: list = [],
config_path: str = None,
config_content: dict = None,
fixer_config: dict = {},
mutelist_path: str = None,
mutelist_content: dict = None,
client_id: str = None,
client_secret: str = None,
):
"""
Initializes the Azure provider.
Args:
az_cli_auth (bool): Flag indicating whether to use Azure CLI authentication.
sp_env_auth (bool): Flag indicating whether to use Service Principal environment authentication.
browser_auth (bool): Flag indicating whether to use interactive browser authentication.
managed_identity_auth (bool): Flag indicating whether to use managed identity authentication.
tenant_id (str): The Azure Active Directory tenant ID.
region (str): The Azure region.
subscription_ids (list): List of subscription IDs.
config_path (str): The path to the configuration file.
config_content (dict): The configuration content.
fixer_config (dict): The fixer configuration.
mutelist_path (str): The path to the mutelist file.
mutelist_content (dict): The mutelist content.
client_id (str): The Azure client ID.
client_secret (str): The Azure client secret.
Returns:
None
Raises:
AzureArgumentTypeValidationError: If there is an error in the argument type validation.
AzureSetUpRegionConfigError: If there is an error in setting up the region configuration.
AzureDefaultAzureCredentialError: If there is an error in retrieving the Azure credentials.
AzureInteractiveBrowserCredentialError: If there is an error in retrieving the Azure credentials using browser authentication.
AzureConfigCredentialsError: If there is an error in configuring the Azure credentials from a dictionary.
AzureGetTokenIdentityError: If there is an error in getting the token from the Azure identity.
AzureHTTPResponseError: If there is an HTTP response error.
"""
logger.info("Setting Azure provider ...")
logger.info("Checking if any credentials mode is set ...")
# Validate the authentication arguments
self.validate_arguments(
az_cli_auth,
sp_env_auth,
browser_auth,
managed_identity_auth,
tenant_id,
client_id,
client_secret,
)
logger.info("Checking if region is different than default one")
self._region_config = self.setup_region_config(region)
# Get the dict from the static credentials
azure_credentials = None
if tenant_id and client_id and client_secret:
azure_credentials = self.validate_static_credentials(
tenant_id=tenant_id, client_id=client_id, client_secret=client_secret
)
# Set up the Azure session
self._session = self.setup_session(
az_cli_auth,
sp_env_auth,
browser_auth,
managed_identity_auth,
tenant_id,
azure_credentials,
self._region_config,
)
# Set up the identity
self._identity = self.setup_identity(
az_cli_auth,
sp_env_auth,
browser_auth,
managed_identity_auth,
subscription_ids,
client_id,
)
# TODO: should we keep this here or within the identity?
self._locations = self.get_locations(self.session)
# Audit Config
if config_content:
self._audit_config = config_content
else:
if not config_path:
config_path = default_config_file_path
self._audit_config = load_and_validate_config_file(self._type, config_path)
# Fixer Config
self._fixer_config = fixer_config
# Mutelist
if mutelist_content:
self._mutelist = AzureMutelist(
mutelist_content=mutelist_content,
)
else:
if not mutelist_path:
mutelist_path = get_default_mute_file_path(self.type)
self._mutelist = AzureMutelist(
mutelist_path=mutelist_path,
)
Provider.set_global_provider(self)
@property
def identity(self):
"""Returns the identity of the Azure provider."""
return self._identity
@property
def type(self):
"""Returns the type of the Azure provider."""
return self._type
@property
def session(self):
"""Returns the session object associated with the Azure provider."""
return self._session
@property
def region_config(self):
"""Returns the region configuration for the Azure provider."""
return self._region_config
@property
def locations(self):
"""Returns a list of available locations for the Azure provider."""
return self._locations
@property
def audit_config(self):
"""Returns the audit configuration for the Azure provider."""
return self._audit_config
@property
def fixer_config(self):
"""Returns the fixer configuration."""
return self._fixer_config
@property
def mutelist(self) -> AzureMutelist:
"""Mutelist object associated with this Azure provider."""
return self._mutelist
@property
def get_output_mapping(self):
"""Dictionary that maps output keys to their corresponding values."""
return {
# identity_type: identity_id
# "auth_method": "identity.profile",
"provider": "type",
# "account_uid": "identity.account",
# TODO: store subscription_name + id pairs
# "account_name": "organizations_metadata.account_details_name",
# "account_email": "organizations_metadata.account_details_email",
# TODO: check the tenant_ids
# TODO: we have to get the account organization, the tenant is not that
"account_organization_uid": "identity.tenant_ids",
"account_organization_name": "identity.tenant_domain",
# TODO: pending to get the subscription tags
# "account_tags": "organizations_metadata.account_details_tags",
"partition": "region_config.name",
}
# TODO: this should be moved to the argparse, if not we need to enforce it from the Provider
# previously was using the AzureException
@staticmethod
def validate_arguments(
az_cli_auth: bool,
sp_env_auth: bool,
browser_auth: bool,
managed_identity_auth: bool,
tenant_id: str,
client_id: str,
client_secret: str,
):
"""
Validates the authentication arguments for the Azure provider.
Args:
az_cli_auth (bool): Flag indicating whether AZ CLI authentication is enabled.
sp_env_auth (bool): Flag indicating whether Service Principal environment authentication is enabled.
browser_auth (bool): Flag indicating whether browser authentication is enabled.
managed_identity_auth (bool): Flag indicating whether managed identity authentication is enabled.
tenant_id (str): The Azure Tenant ID.
client_id (str): The Azure Client ID.
client_secret (str): The Azure Client Secret.
Raises:
AzureBrowserAuthNoTenantIDError: If browser authentication is enabled but the tenant ID is not found.
"""
if not client_id and not client_secret:
if not browser_auth and tenant_id:
raise AzureTenantIDNoBrowserAuthError(
file=os.path.basename(__file__),
message="Azure Tenant ID (--tenant-id) is required for browser authentication mode",
)
elif (
not az_cli_auth
and not sp_env_auth
and not browser_auth
and not managed_identity_auth
):
raise AzureNoAuthenticationMethodError(
file=os.path.basename(__file__),
message="Azure provider requires at least one authentication method set: [--az-cli-auth | --sp-env-auth | --browser-auth | --managed-identity-auth]",
)
elif browser_auth and not tenant_id:
raise AzureBrowserAuthNoTenantIDError(
file=os.path.basename(__file__),
message="Azure Tenant ID (--tenant-id) is required for browser authentication mode",
)
else:
if not tenant_id:
raise AzureNotTenantIdButClientIdAndClienSecretError(
file=os.path.basename(__file__),
message="Tenant Id is required for Azure static credentials. Make sure you are using the correct credentials.",
)
@staticmethod
def setup_region_config(region):
"""
Sets up the region configuration for the Azure provider.
Args:
region (str): The name of the region.
Returns:
AzureRegionConfig: The region configuration object.
"""
try:
validate_azure_region(region)
config = get_regions_config(region)
return AzureRegionConfig(
name=region,
authority=config["authority"],
base_url=config["base_url"],
credential_scopes=config["credential_scopes"],
)
except ArgumentTypeError as validation_error:
logger.error(
f"{validation_error.__class__.__name__}[{validation_error.__traceback__.tb_lineno}]: {validation_error}"
)
raise AzureArgumentTypeValidationError(
file=os.path.basename(__file__),
original_exception=validation_error,
)
except Exception as error:
logger.error(
f"{error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}"
)
raise AzureSetUpRegionConfigError(
file=os.path.basename(__file__),
original_exception=error,
)
def print_credentials(self):
"""Azure credentials information.
This method prints the Azure Tenant Domain, Azure Tenant ID, Azure Region,
Azure Subscriptions, Azure Identity Type, and Azure Identity ID.
Args:
None
Returns:
None
"""
printed_subscriptions = []
for key, value in self._identity.subscriptions.items():
intermediate = key + ": " + value
printed_subscriptions.append(intermediate)
report_lines = [
f"Azure Tenant Domain: {Fore.YELLOW}{self._identity.tenant_domain}{Style.RESET_ALL} Azure Tenant ID: {Fore.YELLOW}{self._identity.tenant_ids[0]}{Style.RESET_ALL}",
f"Azure Region: {Fore.YELLOW}{self.region_config.name}{Style.RESET_ALL}",
f"Azure Subscriptions: {Fore.YELLOW}{printed_subscriptions}{Style.RESET_ALL}",
f"Azure Identity Type: {Fore.YELLOW}{self._identity.identity_type}{Style.RESET_ALL} Azure Identity ID: {Fore.YELLOW}{self._identity.identity_id}{Style.RESET_ALL}",
]
report_title = (
f"{Style.BRIGHT}Using the Azure credentials below:{Style.RESET_ALL}"
)
print_boxes(report_lines, report_title)
# TODO: setup_session or setup_credentials?
# This should be setup_credentials, since it is setting up the credentials for the provider
@staticmethod
def setup_session(
az_cli_auth: bool,
sp_env_auth: bool,
browser_auth: bool,
managed_identity_auth: bool,
tenant_id: str,
azure_credentials: dict,
region_config: AzureRegionConfig,
):
"""Returns the Azure credentials object.
Set up the Azure session with the specified authentication method.
Args:
az_cli_auth (bool): Flag indicating whether to use Azure CLI authentication.
sp_env_auth (bool): Flag indicating whether to use Service Principal authentication with environment variables.
browser_auth (bool): Flag indicating whether to use interactive browser authentication.
managed_identity_auth (bool): Flag indicating whether to use managed identity authentication.
tenant_id (str): The Azure Active Directory tenant ID.
azure_credentials (dict): The Azure configuration object. It contains the following keys:
- tenant_id: The Azure Active Directory tenant ID.
- client_id: The Azure client ID.
- client_secret: The Azure client secret
region_config (AzureRegionConfig): The region configuration object.
Returns:
credentials: The Azure credentials object.
Raises:
Exception: If failed to retrieve Azure credentials.
"""
# Browser auth creds cannot be set with DefaultAzureCredentials()
if not browser_auth:
if sp_env_auth:
try:
AzureProvider.check_service_principal_creds_env_vars()
except AzureEnvironmentVariableError as environment_credentials_error:
logger.critical(
f"{environment_credentials_error.__class__.__name__}[{environment_credentials_error.__traceback__.tb_lineno}] -- {environment_credentials_error}"
)
raise environment_credentials_error
try:
if azure_credentials:
try:
credentials = ClientSecretCredential(
tenant_id=azure_credentials["tenant_id"],
client_id=azure_credentials["client_id"],
client_secret=azure_credentials["client_secret"],
)
return credentials
except ClientAuthenticationError as error:
logger.error(
f"{error.__class__.__name__}[{error.__traceback__.tb_lineno}] -- {error}"
)
raise AzureClientAuthenticationError(
file=os.path.basename(__file__), original_exception=error
)
except CredentialUnavailableError as error:
logger.error(
f"{error.__class__.__name__}[{error.__traceback__.tb_lineno}] -- {error}"
)
raise AzureCredentialsUnavailableError(
file=os.path.basename(__file__), original_exception=error
)
except Exception as error:
logger.error(
f"{error.__class__.__name__}[{error.__traceback__.tb_lineno}] -- {error}"
)
raise AzureConfigCredentialsError(
file=os.path.basename(__file__), original_exception=error
)
else:
# Since the authentication method to be used will come as True, we have to negate it since
# DefaultAzureCredential sets just one authentication method, excluding the others
try:
credentials = DefaultAzureCredential(
exclude_environment_credential=not sp_env_auth,
exclude_cli_credential=not az_cli_auth,
exclude_managed_identity_credential=not managed_identity_auth,
# Azure Auth using Visual Studio is not supported
exclude_visual_studio_code_credential=True,
# Azure Auth using Shared Token Cache is not supported
exclude_shared_token_cache_credential=True,
# Azure Auth using PowerShell is not supported
exclude_powershell_credential=True,
# set Authority of a Microsoft Entra endpoint
authority=region_config.authority,
)
except ClientAuthenticationError as error:
logger.error(
f"{error.__class__.__name__}[{error.__traceback__.tb_lineno}] -- {error}"
)
raise AzureClientAuthenticationError(
file=os.path.basename(__file__), original_exception=error
)
except CredentialUnavailableError as error:
logger.error(
f"{error.__class__.__name__}[{error.__traceback__.tb_lineno}] -- {error}"
)
raise AzureCredentialsUnavailableError(
file=os.path.basename(__file__), original_exception=error
)
except Exception as error:
logger.critical("Failed to retrieve azure credentials")
logger.critical(
f"{error.__class__.__name__}[{error.__traceback__.tb_lineno}] -- {error}"
)
raise AzureDefaultAzureCredentialError(
file=os.path.basename(__file__), original_exception=error
)
except Exception as error:
logger.critical("Failed to retrieve azure credentials")
logger.critical(
f"{error.__class__.__name__}[{error.__traceback__.tb_lineno}] -- {error}"
)
raise AzureSetUpSessionError(
file=os.path.basename(__file__), original_exception=error
)
else:
try:
credentials = InteractiveBrowserCredential(tenant_id=tenant_id)
except Exception as error:
logger.critical(
"Failed to retrieve azure credentials using browser authentication"
)
logger.critical(
f"{error.__class__.__name__}[{error.__traceback__.tb_lineno}] -- {error}"
)
raise AzureInteractiveBrowserCredentialError(
file=os.path.basename(__file__), original_exception=error
)
return credentials
@staticmethod
def test_connection(
az_cli_auth=False,
sp_env_auth=False,
browser_auth=False,
managed_identity_auth=False,
tenant_id=None,
region="AzureCloud",
raise_on_exception=True,
client_id=None,
client_secret=None,
provider_id=None,
) -> Connection:
"""Test connection to Azure subscription.
Test the connection to an Azure subscription using the provided credentials.
Args:
az_cli_auth (bool): Flag indicating if Azure CLI authentication is used.
sp_env_auth (bool): Flag indicating if Service Principal environment authentication is used.
browser_auth (bool): Flag indicating if browser authentication is used.
managed_identity_auth (bool): Flag indicating if managed entity authentication is used.
tenant_id (str): The Azure Active Directory tenant ID.
region (str): The Azure region.
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.
Raises:
Exception: If failed to test the connection to Azure subscription.
AzureArgumentTypeValidationError: If there is an error in the argument type validation.
AzureSetUpRegionConfigError: If there is an error in setting up the region configuration.
AzureDefaultAzureCredentialError: If there is an error in retrieving the Azure credentials.
AzureInteractiveBrowserCredentialError: If there is an error in retrieving the Azure credentials using browser authentication.
AzureHTTPResponseError: If there is an HTTP response error.
AzureConfigCredentialsError: If there is an error in configuring the Azure credentials from a dictionary.
Examples:
>>> AzureProvider.test_connection(az_cli_auth=True)
True
>>> AzureProvider.test_connection(sp_env_auth=False, browser_auth=True, tenant_id=None)
False, ArgumentTypeError: Azure Tenant ID is required only for browser authentication mode
>>> AzureProvider.test_connection(tenant_id="XXXXXXXXXX", client_id="XXXXXXXXXX", client_secret="XXXXXXXXXX")
True
"""
try:
AzureProvider.validate_arguments(
az_cli_auth,
sp_env_auth,
browser_auth,
managed_identity_auth,
tenant_id,
client_id,
client_secret,
)
region_config = AzureProvider.setup_region_config(region)
# Get the dict from the static credentials
azure_credentials = None
if tenant_id and client_id and client_secret:
azure_credentials = AzureProvider.validate_static_credentials(
tenant_id=tenant_id,
client_id=client_id,
client_secret=client_secret,
)
# Set up the Azure session
credentials = AzureProvider.setup_session(
az_cli_auth,
sp_env_auth,
browser_auth,
managed_identity_auth,
tenant_id,
azure_credentials,
region_config,
)
# Create a SubscriptionClient
subscription_client = SubscriptionClient(credentials)
# Get info from the subscriptions
available_subscriptions = []
for subscription in subscription_client.subscriptions.list():
available_subscriptions.append(subscription)
if provider_id and provider_id not in [
sub.subscription_id for sub in available_subscriptions
]:
raise AzureInvalidProviderIdError(
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
except AzureNoAuthenticationMethodError as no_auth_method_error:
logger.error(
f"{no_auth_method_error.__class__.__name__}[{no_auth_method_error.__traceback__.tb_lineno}]: {no_auth_method_error}"
)
if raise_on_exception:
raise no_auth_method_error
return Connection(error=no_auth_method_error)
except AzureBrowserAuthNoTenantIDError as browser_no_tenant_error:
logger.error(
f"{browser_no_tenant_error.__class__.__name__}[{browser_no_tenant_error.__traceback__.tb_lineno}]: {browser_no_tenant_error}"
)
if raise_on_exception:
raise browser_no_tenant_error
return Connection(error=browser_no_tenant_error)
except AzureTenantIDNoBrowserAuthError as tenant_no_browser_error:
logger.error(
f"{tenant_no_browser_error.__class__.__name__}[{tenant_no_browser_error.__traceback__.tb_lineno}]: {tenant_no_browser_error}"
)
# Exceptions from setup_region_config
except AzureArgumentTypeValidationError as type_validation_error:
logger.error(
f"{type_validation_error.__class__.__name__}[{type_validation_error.__traceback__.tb_lineno}]: {type_validation_error}"
)
if raise_on_exception:
raise type_validation_error
return Connection(error=type_validation_error)
except AzureSetUpRegionConfigError as region_config_error:
logger.error(
f"{region_config_error.__class__.__name__}[{region_config_error.__traceback__.tb_lineno}]: {region_config_error}"
)
if raise_on_exception:
raise region_config_error
return Connection(error=region_config_error)
# Exceptions from setup_session
except AzureEnvironmentVariableError as environment_credentials_error:
logger.error(
f"{environment_credentials_error.__class__.__name__}[{environment_credentials_error.__traceback__.tb_lineno}]: {environment_credentials_error}"
)
if raise_on_exception:
raise environment_credentials_error
return Connection(error=environment_credentials_error)
except AzureDefaultAzureCredentialError as default_credentials_error:
logger.error(
f"{default_credentials_error.__class__.__name__}[{default_credentials_error.__traceback__.tb_lineno}]: {default_credentials_error}"
)
if raise_on_exception:
raise default_credentials_error
return Connection(error=default_credentials_error)
except AzureInteractiveBrowserCredentialError as interactive_browser_error:
logger.error(
f"{interactive_browser_error.__class__.__name__}[{interactive_browser_error.__traceback__.tb_lineno}]: {interactive_browser_error}"
)
if raise_on_exception:
raise interactive_browser_error
return Connection(error=interactive_browser_error)
except AzureConfigCredentialsError as config_credentials_error:
logger.error(
f"{config_credentials_error.__class__.__name__}[{config_credentials_error.__traceback__.tb_lineno}]: {config_credentials_error}"
)
if raise_on_exception:
raise config_credentials_error
return Connection(error=config_credentials_error)
except AzureClientAuthenticationError as client_auth_error:
logger.error(
f"{client_auth_error.__class__.__name__}[{client_auth_error.__traceback__.tb_lineno}]: {client_auth_error}"
)
if raise_on_exception:
raise client_auth_error
return Connection(error=client_auth_error)
except AzureCredentialsUnavailableError as credential_unavailable_error:
logger.error(
f"{credential_unavailable_error.__class__.__name__}[{credential_unavailable_error.__traceback__.tb_lineno}]: {credential_unavailable_error}"
)
if raise_on_exception:
raise credential_unavailable_error
return Connection(error=credential_unavailable_error)
except AzureDefaultAzureCredentialError as default_credentials_error:
logger.error(
f"{default_credentials_error.__class__.__name__}[{default_credentials_error.__traceback__.tb_lineno}]: {default_credentials_error}"
)
if raise_on_exception:
raise default_credentials_error
return Connection(error=default_credentials_error)
except (
AzureClientIdAndClientSecretNotBelongingToTenantIdError
) as tenant_id_error:
logger.error(
f"{tenant_id_error.__class__.__name__}[{tenant_id_error.__traceback__.tb_lineno}]: {tenant_id_error}"
)
if raise_on_exception:
raise tenant_id_error
return Connection(error=tenant_id_error)
except (
AzureTenantIdAndClientSecretNotBelongingToClientIdError
) as client_id_error:
logger.error(
f"{client_id_error.__class__.__name__}[{client_id_error.__traceback__.tb_lineno}]: {client_id_error}"
)
if raise_on_exception:
raise client_id_error
return Connection(error=client_id_error)
except (
AzureTenantIdAndClientIdNotBelongingToClientSecretError
) as client_secret_error:
logger.error(
f"{client_secret_error.__class__.__name__}[{client_secret_error.__traceback__.tb_lineno}]: {client_secret_error}"
)
if raise_on_exception:
raise client_secret_error
return Connection(error=client_secret_error)
# Exceptions from provider_id validation
except AzureInvalidProviderIdError as invalid_credentials_error:
logger.error(
f"{invalid_credentials_error.__class__.__name__}[{invalid_credentials_error.__traceback__.tb_lineno}]: {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(
f"{http_response_error.__class__.__name__}[{http_response_error.__traceback__.tb_lineno}]: {http_response_error}"
)
if raise_on_exception:
raise AzureHTTPResponseError(
file=os.path.basename(__file__),
original_exception=http_response_error,
)
return Connection(error=http_response_error)
except Exception as error:
logger.critical(
f"{error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}"
)
if raise_on_exception:
# Raise directly the exception
raise error
return Connection(error=error)
@staticmethod
def check_service_principal_creds_env_vars():
"""
Checks the presence of required environment variables for service principal authentication against Azure.
This method checks for the presence of the following environment variables:
- AZURE_CLIENT_ID: Azure client ID
- AZURE_TENANT_ID: Azure tenant ID
- AZURE_CLIENT_SECRET: Azure client secret
If any of the environment variables is missing, it logs a critical error and exits the program.
"""
logger.info(
"Azure provider: checking service principal environment variables ..."
)
for env_var in ["AZURE_CLIENT_ID", "AZURE_TENANT_ID", "AZURE_CLIENT_SECRET"]:
if not getenv(env_var):
logger.critical(
f"Azure provider: Missing environment variable {env_var} needed to authenticate against Azure"
)
raise AzureEnvironmentVariableError(
file=os.path.basename(__file__),
message=f"Missing environment variable {env_var} required to authenticate.",
)
def setup_identity(
self,
az_cli_auth,
sp_env_auth,
browser_auth,
managed_identity_auth,
subscription_ids,
client_id,
):
"""
Sets up the identity for the Azure provider.
Args:
az_cli_auth (bool): Flag indicating if Azure CLI authentication is used.
sp_env_auth (bool): Flag indicating if Service Principal environment authentication is used.
browser_auth (bool): Flag indicating if browser authentication is used.
managed_identity_auth (bool): Flag indicating if managed entity authentication is used.
subscription_ids (list): List of subscription IDs.
Returns:
AzureIdentityInfo: An instance of AzureIdentityInfo containing the identity information.
"""
credentials = self.session
# TODO: fill this object with real values not default and set to none
identity = AzureIdentityInfo()
# If credentials comes from service principal or browser, if the required permissions are assigned
# the identity can access AAD and retrieve the tenant domain name.
# With cli also should be possible but right now it does not work, azure python package issue is coming
# At the time of writting this with az cli creds is not working, despite that is included
if sp_env_auth or browser_auth or az_cli_auth or client_id:
async def get_azure_identity():
# Trying to recover tenant domain info
try:
logger.info(
"Trying to retrieve tenant domain from AAD to populate identity structure ..."
)
client = GraphServiceClient(credentials=credentials)
domain_result = await client.domains.get()
if getattr(domain_result, "value"):
if getattr(domain_result.value[0], "id"):
identity.tenant_domain = domain_result.value[0].id
except HttpResponseError as error:
logger.error(
f"{error.__class__.__name__}[{error.__traceback__.tb_lineno}] -- {error}"
)
raise AzureHTTPResponseError(
file=os.path.basename(__file__),
original_exception=error,
)
except ClientAuthenticationError as error:
logger.error(
f"{error.__class__.__name__}[{error.__traceback__.tb_lineno}] -- {error}"
)
raise AzureGetTokenIdentityError(
file=os.path.basename(__file__),
original_exception=error,
)
except Exception as error:
logger.error(
f"{error.__class__.__name__}[{error.__traceback__.tb_lineno}] -- {error}"
)
# since that exception is not considered as critical, we keep filling another identity fields
if sp_env_auth or client_id:
# The id of the sp can be retrieved from environment variables
identity.identity_id = getenv("AZURE_CLIENT_ID")
identity.identity_type = "Service Principal"
# Same here, if user can access AAD, some fields are retrieved if not, default value, for az cli
# should work but it doesn't, pending issue
else:
identity.identity_id = "Unknown user id (Missing AAD permissions)"
identity.identity_type = "User"
try:
logger.info(
"Trying to retrieve user information from AAD to populate identity structure ..."
)
client = GraphServiceClient(credentials=credentials)
me = await client.me.get()
if me:
if getattr(me, "user_principal_name"):
identity.identity_id = me.user_principal_name
except Exception as error:
logger.error(
f"{error.__class__.__name__}[{error.__traceback__.tb_lineno}] -- {error}"
)
asyncio.get_event_loop().run_until_complete(get_azure_identity())
# Managed identities only can be assigned resource, resource group and subscription scope permissions
elif managed_identity_auth:
identity.identity_id = "Default Managed Identity ID"
identity.identity_type = "Managed Identity"
# Pending extracting info from managed identity
# once we have populated the id, type, and domain fields, time to retrieve the subscriptions and finally the tenants
try:
logger.info(
"Trying to subscriptions and tenant ids to populate identity structure ..."
)
subscriptions_client = SubscriptionClient(
credential=credentials,
base_url=self.region_config.base_url,
credential_scopes=self.region_config.credential_scopes,
)
if not subscription_ids:
logger.info("Scanning all the Azure subscriptions...")
for subscription in subscriptions_client.subscriptions.list():
# TODO: get tags or labels
# TODO: fill with AzureSubscription
identity.subscriptions.update(
{subscription.display_name: subscription.subscription_id}
)
else:
logger.info("Scanning the subscriptions passed as argument ...")
for id in subscription_ids:
subscription = subscriptions_client.subscriptions.get(
subscription_id=id
)
identity.subscriptions.update({subscription.display_name: id})
# If there are no subscriptions listed -> checks are not going to be run against any resource
if not identity.subscriptions:
logger.critical(
"It was not possible to retrieve any subscriptions, please check your permission assignments"
)
raise AzureNoSubscriptionsError(
file=os.path.basename(__file__),
message="No subscriptions were found, please check your permission assignments.",
)
tenants = subscriptions_client.tenants.list()
for tenant in tenants:
identity.tenant_ids.append(tenant.tenant_id)
# This error is critical, since it implies something is wrong with the credentials provided
except Exception as error:
logger.critical(
"Error with credentials provided getting subscriptions and tenants to scan"
)
logger.critical(
f"{error.__class__.__name__}[{error.__traceback__.tb_lineno}] -- {error}"
)
raise AzureSetUpIdentityError(
file=os.path.basename(__file__),
original_exception=error,
)
return identity
def get_locations(self, credentials) -> dict[str, list[str]]:
"""
Retrieves the locations available for each subscription using the provided credentials.
Args:
credentials: The credentials object used to authenticate the request.
Returns:
A dictionary containing the locations available for each subscription. The dictionary
has subscription display names as keys and lists of location names as values.
"""
locations = None
if credentials:
locations = {}
token = credentials.get_token("https://management.azure.com/.default").token
for display_name, subscription_id in self._identity.subscriptions.items():
locations.update({display_name: []})
url = f"https://management.azure.com/subscriptions/{subscription_id}/locations?api-version=2022-12-01"
headers = {
"Authorization": f"Bearer {token}",
"Content-Type": "application/json",
}
response = requests.get(url, headers=headers)
if response.status_code == 200:
data = response.json()
for location in data["value"]:
locations[display_name].append(location["name"])
return locations
@staticmethod
def validate_static_credentials(
tenant_id: str = None, client_id: str = None, client_secret: str = None
) -> dict:
"""
Validates the static credentials for the Azure provider.
Args:
tenant_id (str): The Azure Active Directory tenant ID.
client_id (str): The Azure client ID.
client_secret (str): The Azure client secret.
Raises:
AzureNotValidTenantIdError: If the provided Azure Tenant ID is not valid.
AzureNotValidClientIdError: If the provided Azure Client ID is not valid.
AzureNotValidClientSecretError: If the provided Azure Client Secret is not valid.
AzureClientIdAndClientSecretNotBelongingToTenantIdError: If the provided Azure Client ID and Client Secret do not belong to the specified Tenant ID.
AzureTenantIdAndClientSecretNotBelongingToClientIdError: If the provided Azure Tenant ID and Client Secret do not belong to the specified Client ID.
AzureTenantIdAndClientIdNotBelongingToClientSecretError: If the provided Azure Tenant ID and Client ID do not belong to the specified Client Secret.
Returns:
dict: A dictionary containing the validated static credentials.
"""
# Validate the Tenant ID
try:
UUID(tenant_id)
except ValueError:
raise AzureNotValidTenantIdError(
file=os.path.basename(__file__),
message="The provided Azure Tenant ID is not valid.",
)
# Validate the Client ID
try:
UUID(client_id)
except ValueError:
raise AzureNotValidClientIdError(
file=os.path.basename(__file__),
message="The provided Azure Client ID is not valid.",
)
# Validate the Client Secret
if not re.match("^[a-zA-Z0-9._~-]+$", client_secret):
raise AzureNotValidClientSecretError(
file=os.path.basename(__file__),
message="The provided Azure Client Secret is not valid.",
)
try:
AzureProvider.verify_client(tenant_id, client_id, client_secret)
return {
"tenant_id": tenant_id,
"client_id": client_id,
"client_secret": client_secret,
}
except AzureNotValidTenantIdError as tenant_id_error:
logger.error(
f"{tenant_id_error.__class__.__name__}[{tenant_id_error.__traceback__.tb_lineno}]: {tenant_id_error}"
)
raise AzureClientIdAndClientSecretNotBelongingToTenantIdError(
file=os.path.basename(__file__),
message="The provided Azure Client ID and Client Secret do not belong to the specified Tenant ID.",
)
except AzureNotValidClientIdError as client_id_error:
logger.error(
f"{client_id_error.__class__.__name__}[{client_id_error.__traceback__.tb_lineno}]: {client_id_error}"
)
raise AzureTenantIdAndClientSecretNotBelongingToClientIdError(
file=os.path.basename(__file__),
message="The provided Azure Tenant ID and Client Secret do not belong to the specified Client ID.",
)
except AzureNotValidClientSecretError as client_secret_error:
logger.error(
f"{client_secret_error.__class__.__name__}[{client_secret_error.__traceback__.tb_lineno}]: {client_secret_error}"
)
raise AzureTenantIdAndClientIdNotBelongingToClientSecretError(
file=os.path.basename(__file__),
message="The provided Azure Tenant ID and Client ID do not belong to the specified Client Secret.",
)
@staticmethod
def verify_client(tenant_id, client_id, client_secret) -> None:
"""
Verifies the Azure client credentials using the specified tenant ID, client ID, and client secret.
Args:
tenant_id (str): The Azure Active Directory tenant ID.
client_id (str): The Azure client ID.
client_secret (str): The Azure client secret.
Raises:
AzureNotValidTenantIdError: If the provided Azure Tenant ID is not valid.
AzureNotValidClientIdError: If the provided Azure Client ID is not valid.
AzureNotValidClientSecretError: If the provided Azure Client Secret is not valid.
Returns:
None
"""
url = f"https://login.microsoftonline.com/{tenant_id}/oauth2/v2.0/token"
headers = {
"Content-Type": "application/x-www-form-urlencoded",
"Accept": "application/json",
}
data = {
"grant_type": "client_credentials",
"client_id": client_id,
"client_secret": client_secret,
"scope": "https://graph.microsoft.com/.default",
}
response = requests.post(url, headers=headers, data=data).json()
if "access_token" not in response.keys() and "error_codes" in response.keys():
if f"Tenant '{tenant_id}'" in response["error_description"]:
raise AzureNotValidTenantIdError(
file=os.path.basename(__file__),
message="The provided Azure Tenant ID is not valid for the specified Client ID and Client Secret.",
)
if (
f"Application with identifier '{client_id}'"
in response["error_description"]
):
raise AzureNotValidClientIdError(
file=os.path.basename(__file__),
message="The provided Azure Client ID is not valid for the specified Tenant ID and Client Secret.",
)
if "Invalid client secret provided" in response["error_description"]:
raise AzureNotValidClientSecretError(
file=os.path.basename(__file__),
message="The provided Azure Client Secret is not valid for the specified Tenant ID and Client ID.",
)