diff --git a/prowler/changelog.d/azure-sovereign-cloud-defender-keyvault-hosts.fixed.md b/prowler/changelog.d/azure-sovereign-cloud-defender-keyvault-hosts.fixed.md new file mode 100644 index 0000000000..99ef3354e0 --- /dev/null +++ b/prowler/changelog.d/azure-sovereign-cloud-defender-keyvault-hosts.fixed.md @@ -0,0 +1 @@ +Azure Defender security contacts and Key Vault key rotation policies now use the endpoints of the selected cloud (`--azure-region`) instead of the hardcoded `management.azure.com` and `vault.azure.net` hosts, so both work on `AzureUSGovernment` and `AzureChinaCloud` diff --git a/prowler/providers/azure/lib/service/service.py b/prowler/providers/azure/lib/service/service.py index 9d63639e94..9647723173 100644 --- a/prowler/providers/azure/lib/service/service.py +++ b/prowler/providers/azure/lib/service/service.py @@ -27,6 +27,7 @@ class AzureService: ) self.subscriptions = provider.identity.subscriptions + self.region_config = provider.region_config self.resource_groups = provider.resource_groups self.locations = provider.locations self.audit_config = provider.audit_config diff --git a/prowler/providers/azure/models.py b/prowler/providers/azure/models.py index 62d03db365..5df80afab6 100644 --- a/prowler/providers/azure/models.py +++ b/prowler/providers/azure/models.py @@ -18,8 +18,8 @@ class AzureIdentityInfo(BaseModel): class AzureRegionConfig(BaseModel): name: str = "" authority: Optional[str] = None - base_url: str = "" - credential_scopes: list = [] + base_url: str = "https://management.azure.com" + credential_scopes: list = ["https://management.azure.com/.default"] graph_host: str = "https://graph.microsoft.com" graph_scope: str = "https://graph.microsoft.com/.default" logs_endpoint: str = "https://api.loganalytics.io" diff --git a/prowler/providers/azure/services/defender/defender_service.py b/prowler/providers/azure/services/defender/defender_service.py index d68d88dc22..17777843e4 100644 --- a/prowler/providers/azure/services/defender/defender_service.py +++ b/prowler/providers/azure/services/defender/defender_service.py @@ -12,7 +12,16 @@ from prowler.providers.azure.lib.service.service import AzureService class Defender(AzureService): + """Microsoft Defender for Cloud service: pricings, settings, assessments, + security contacts, IoT solutions and JIT policies per subscription.""" + def __init__(self, provider: AzureProvider): + """Collect the Defender configuration of every audited subscription. + + Args: + provider: Azure provider supplying the session, subscriptions and + the region config whose endpoints are used for every call. + """ super().__init__(SecurityCenter, provider) self.pricings = self._get_pricings() @@ -21,7 +30,7 @@ class Defender(AzureService): self.settings = self._get_settings() self.security_contact_configurations = self._get_security_contacts( token=provider.session.get_token( - "https://management.azure.com/.default" + *self.region_config.credential_scopes ).token ) self.iot_security_solutions = self._get_iot_security_solutions() @@ -168,7 +177,7 @@ class Defender(AzureService): security_contacts = {} for subscription_id, display_name in self.subscriptions.items(): try: - url = f"https://management.azure.com/subscriptions/{subscription_id}/providers/Microsoft.Security/securityContacts?api-version=2023-12-01-preview" + url = f"{self.region_config.base_url}/subscriptions/{subscription_id}/providers/Microsoft.Security/securityContacts?api-version=2023-12-01-preview" headers = { "Authorization": f"Bearer {token}", "Content-Type": "application/json", diff --git a/prowler/providers/azure/services/keyvault/keyvault_service.py b/prowler/providers/azure/services/keyvault/keyvault_service.py index e5b2e76427..5e79412f4b 100644 --- a/prowler/providers/azure/services/keyvault/keyvault_service.py +++ b/prowler/providers/azure/services/keyvault/keyvault_service.py @@ -83,6 +83,7 @@ class KeyVault(AzureService): subscription, resource_group, keyvault_name, + getattr(keyvault_properties, "vault_uri", ""), provider, ) secrets_future = executor.submit( @@ -150,7 +151,22 @@ class KeyVault(AzureService): ) return None - def _get_keys(self, subscription, resource_group, keyvault_name, provider): + def _get_keys( + self, subscription, resource_group, keyvault_name, vault_uri, provider + ): + """Get the keys of a Key Vault, enriched with their rotation policies. + + Args: + subscription: Subscription ID the vault belongs to. + resource_group: Resource group name of the vault. + keyvault_name: Vault name, used for the management API and logs. + vault_uri: Data-plane URI of the vault as returned by ARM, valid in + any Azure cloud. When empty, rotation policies are skipped. + provider: Azure provider whose session authenticates the KeyClient. + + Returns: + A list of Key objects; rotation_policy is set when it could be read. + """ logger.info(f"KeyVault - Getting keys for {keyvault_name}...") keys = [] keys_dict = {} @@ -179,10 +195,15 @@ class KeyVault(AzureService): f"Subscription ID: {subscription} -- {error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}" ) + if not vault_uri: + logger.warning( + f"KeyVault {keyvault_name} in {subscription} -- has no vault URI, skipping key rotation policies" + ) + return keys + try: key_client = KeyClient( - vault_url=f"https://{keyvault_name}.vault.azure.net/", - # TODO: review the following line + vault_url=vault_uri, credential=provider.session, ) properties = list(key_client.list_properties_of_keys()) diff --git a/tests/providers/azure/lib/service/azure_service_test.py b/tests/providers/azure/lib/service/azure_service_test.py index 9360be85ea..49042dc476 100644 --- a/tests/providers/azure/lib/service/azure_service_test.py +++ b/tests/providers/azure/lib/service/azure_service_test.py @@ -106,3 +106,19 @@ class TestAzureServiceSovereignClouds: service.__set_clients__(identity, session, logs_service, region_config) logs_service.assert_called_once_with(credential=session, endpoint=logs_endpoint) + + +class TestAzureServiceRegionConfig: + def test_init_keeps_provider_region_config(self): + region_config = AzureRegionConfig( + name="AzureUSGovernment", + base_url="https://management.usgovcloudapi.net", + credential_scopes=["https://management.usgovcloudapi.net/.default"], + ) + provider = MagicMock() + provider.region_config = region_config + + with patch.object(AzureService, "__set_clients__", return_value={}): + service = AzureService(MagicMock(), provider) + + assert service.region_config is region_config diff --git a/tests/providers/azure/services/defender/defender_service_test.py b/tests/providers/azure/services/defender/defender_service_test.py index b50ddd26c9..d9cdb149ab 100644 --- a/tests/providers/azure/services/defender/defender_service_test.py +++ b/tests/providers/azure/services/defender/defender_service_test.py @@ -1,6 +1,7 @@ from datetime import timedelta from unittest.mock import MagicMock, patch +from prowler.providers.azure.models import AzureRegionConfig from prowler.providers.azure.services.defender.defender_service import ( Assesment, AutoProvisioningSetting, @@ -618,3 +619,53 @@ class Test_Defender_get_jit_policies: mock_client.jit_network_access_policies.list_by_resource_group.assert_called_once_with( resource_group_name="RG" ) + + +US_GOV_REGION_CONFIG = AzureRegionConfig( + name="AzureUSGovernment", + base_url="https://management.usgovcloudapi.net", + credential_scopes=["https://management.usgovcloudapi.net/.default"], +) + + +class Test_Defender_get_security_contacts_sovereign_cloud: + def _defender(self, provider): + with ( + patch(DEFENDER_INIT_PATCHES[0], return_value={}), + patch(DEFENDER_INIT_PATCHES[1], return_value={}), + patch(DEFENDER_INIT_PATCHES[2], return_value={}), + patch(DEFENDER_INIT_PATCHES[3], return_value={}), + patch(DEFENDER_INIT_PATCHES[4], return_value={}), + patch(DEFENDER_INIT_PATCHES[5], return_value={}), + patch(DEFENDER_INIT_PATCHES[6], return_value={}), + ): + return Defender(provider) + + def test_init_requests_token_for_cloud_scope(self): + provider = set_mocked_azure_provider(azure_region_config=US_GOV_REGION_CONFIG) + + self._defender(provider) + + provider.session.get_token.assert_called_once_with( + "https://management.usgovcloudapi.net/.default" + ) + + def test_get_security_contacts_uses_cloud_management_host(self): + provider = set_mocked_azure_provider(azure_region_config=US_GOV_REGION_CONFIG) + defender = self._defender(provider) + + response = MagicMock() + response.json.return_value = {"value": []} + with patch( + "prowler.providers.azure.services.defender.defender_service.requests.get", + return_value=response, + ) as mock_get: + result = defender._get_security_contacts(token="token") + + assert result == {AZURE_SUBSCRIPTION_ID: {}} + mock_get.assert_called_once() + url = mock_get.call_args.args[0] + assert url.startswith( + f"https://management.usgovcloudapi.net/subscriptions/{AZURE_SUBSCRIPTION_ID}/" + ) + assert "management.azure.com" not in url diff --git a/tests/providers/azure/services/keyvault/keyvault_service_test.py b/tests/providers/azure/services/keyvault/keyvault_service_test.py index e43b7a9fff..d52d292af3 100644 --- a/tests/providers/azure/services/keyvault/keyvault_service_test.py +++ b/tests/providers/azure/services/keyvault/keyvault_service_test.py @@ -470,3 +470,87 @@ class Test_KeyVault_get_key_vaults: mock_client.vaults.list_by_resource_group.assert_called_once_with( resource_group_name="MyRG" ) + + +class Test_KeyVault_get_keys: + def test_get_keys_builds_key_client_from_vault_uri(self): + mock_client = MagicMock() + mock_client.keys.list.return_value = [] + + mock_provider = MagicMock() + mock_provider.identity = MagicMock() + with ( + patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=mock_provider, + ), + patch( + "prowler.providers.azure.services.monitor.monitor_service.Monitor", + new=MagicMock(), + ), + patch( + "prowler.providers.azure.services.keyvault.keyvault_service.KeyVault._get_key_vaults", + return_value={}, + ), + ): + from prowler.providers.azure.services.keyvault.keyvault_service import ( + KeyVault, + ) + + keyvault = KeyVault(set_mocked_azure_provider()) + + keyvault.clients = {AZURE_SUBSCRIPTION_ID: mock_client} + provider = set_mocked_azure_provider() + vault_uri = "https://my-vault.vault.usgovcloudapi.net/" + + with patch( + "prowler.providers.azure.services.keyvault.keyvault_service.KeyClient" + ) as mock_key_client_cls: + mock_key_client_cls.return_value.list_properties_of_keys.return_value = [] + keys = keyvault._get_keys( + AZURE_SUBSCRIPTION_ID, RESOURCE_GROUP, "my-vault", vault_uri, provider + ) + + assert keys == [] + mock_key_client_cls.assert_called_once_with( + vault_url=vault_uri, credential=provider.session + ) + + def test_get_keys_without_vault_uri_skips_rotation_policies(self): + mock_client = MagicMock() + mock_client.keys.list.return_value = [] + + mock_provider = MagicMock() + mock_provider.identity = MagicMock() + with ( + patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=mock_provider, + ), + patch( + "prowler.providers.azure.services.monitor.monitor_service.Monitor", + new=MagicMock(), + ), + patch( + "prowler.providers.azure.services.keyvault.keyvault_service.KeyVault._get_key_vaults", + return_value={}, + ), + ): + from prowler.providers.azure.services.keyvault.keyvault_service import ( + KeyVault, + ) + + keyvault = KeyVault(set_mocked_azure_provider()) + + keyvault.clients = {AZURE_SUBSCRIPTION_ID: mock_client} + provider = set_mocked_azure_provider() + + with patch( + "prowler.providers.azure.services.keyvault.keyvault_service.KeyClient" + ) as mock_key_client_cls: + keys = keyvault._get_keys( + AZURE_SUBSCRIPTION_ID, RESOURCE_GROUP, "my-vault", "", provider + ) + + assert keys == [] + mock_key_client_cls.assert_not_called()