From b5bb85c9564f6ca6a7f66c851bb56bde719205ee Mon Sep 17 00:00:00 2001 From: s1ns3nz0 Date: Tue, 16 Jun 2026 02:20:38 +0900 Subject: [PATCH] feat(azure): add cosmosdb_account_backup_policy_continuous check (#11032) Co-authored-by: Daniel Barranquero --- prowler/CHANGELOG.md | 1 + prowler/compliance/azure/hipaa_azure.json | 3 +- .../compliance/azure/iso27001_2022_azure.json | 4 +- prowler/compliance/csa_ccm_4.0.json | 3 +- .../__init__.py | 0 ...unt_backup_policy_continuous.metadata.json | 38 +++++ ...smosdb_account_backup_policy_continuous.py | 30 ++++ ...b_account_backup_policy_continuous_test.py | 157 ++++++++++++++++++ 8 files changed, 233 insertions(+), 3 deletions(-) create mode 100644 prowler/providers/azure/services/cosmosdb/cosmosdb_account_backup_policy_continuous/__init__.py create mode 100644 prowler/providers/azure/services/cosmosdb/cosmosdb_account_backup_policy_continuous/cosmosdb_account_backup_policy_continuous.metadata.json create mode 100644 prowler/providers/azure/services/cosmosdb/cosmosdb_account_backup_policy_continuous/cosmosdb_account_backup_policy_continuous.py create mode 100644 tests/providers/azure/services/cosmosdb/cosmosdb_account_backup_policy_continuous/cosmosdb_account_backup_policy_continuous_test.py diff --git a/prowler/CHANGELOG.md b/prowler/CHANGELOG.md index 3bac1d8e40..006e08082a 100644 --- a/prowler/CHANGELOG.md +++ b/prowler/CHANGELOG.md @@ -13,6 +13,7 @@ All notable changes to the **Prowler SDK** are documented in this file. - `cloudsql_instance_high_availability_enabled` check for GCP provider, verifying Cloud SQL primary instances use `REGIONAL` availability for automatic zone failover [(#11024)](https://github.com/prowler-cloud/prowler/pull/11024) - `identity_storage_service_level_admins_scoped` check for OCI provider CIS 3.1 control 1.15, ensuring storage service-level administrators exclude delete permissions [(#11523)](https://github.com/prowler-cloud/prowler/pull/11523) - `cosmosdb_account_automatic_failover_enabled` check for Azure provider [(#11031)](https://github.com/prowler-cloud/prowler/pull/11031) +- `cosmosdb_account_backup_policy_continuous` check for Azure provider [(#11032)](https://github.com/prowler-cloud/prowler/pull/11032) ### 🔄 Changed diff --git a/prowler/compliance/azure/hipaa_azure.json b/prowler/compliance/azure/hipaa_azure.json index 332d081590..3b1184e0e5 100644 --- a/prowler/compliance/azure/hipaa_azure.json +++ b/prowler/compliance/azure/hipaa_azure.json @@ -438,7 +438,8 @@ "storage_geo_redundant_enabled", "keyvault_recoverable", "sqlserver_auditing_retention_90_days", - "postgresql_flexible_server_log_retention_days_greater_3" + "postgresql_flexible_server_log_retention_days_greater_3", + "cosmosdb_account_backup_policy_continuous" ] }, { diff --git a/prowler/compliance/azure/iso27001_2022_azure.json b/prowler/compliance/azure/iso27001_2022_azure.json index 8e5458f526..635d55a854 100644 --- a/prowler/compliance/azure/iso27001_2022_azure.json +++ b/prowler/compliance/azure/iso27001_2022_azure.json @@ -1266,7 +1266,9 @@ "Check_Summary": "Backup copies of information, software and systems should be maintained and regularly tested in accordance with the agreed topic-specific policy on backup." } ], - "Checks": [] + "Checks": [ + "cosmosdb_account_backup_policy_continuous" + ] }, { "Id": "A.8.14", diff --git a/prowler/compliance/csa_ccm_4.0.json b/prowler/compliance/csa_ccm_4.0.json index e25ba23c6f..57f1a67758 100644 --- a/prowler/compliance/csa_ccm_4.0.json +++ b/prowler/compliance/csa_ccm_4.0.json @@ -1088,7 +1088,8 @@ "storage_geo_redundant_enabled", "vm_scaleset_associated_with_load_balancer", "vm_scaleset_not_empty", - "cosmosdb_account_automatic_failover_enabled" + "cosmosdb_account_automatic_failover_enabled", + "cosmosdb_account_backup_policy_continuous" ], "gcp": [ "compute_instance_automatic_restart_enabled", diff --git a/prowler/providers/azure/services/cosmosdb/cosmosdb_account_backup_policy_continuous/__init__.py b/prowler/providers/azure/services/cosmosdb/cosmosdb_account_backup_policy_continuous/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/prowler/providers/azure/services/cosmosdb/cosmosdb_account_backup_policy_continuous/cosmosdb_account_backup_policy_continuous.metadata.json b/prowler/providers/azure/services/cosmosdb/cosmosdb_account_backup_policy_continuous/cosmosdb_account_backup_policy_continuous.metadata.json new file mode 100644 index 0000000000..1631aa5b3a --- /dev/null +++ b/prowler/providers/azure/services/cosmosdb/cosmosdb_account_backup_policy_continuous/cosmosdb_account_backup_policy_continuous.metadata.json @@ -0,0 +1,38 @@ +{ + "Provider": "azure", + "CheckID": "cosmosdb_account_backup_policy_continuous", + "CheckTitle": "Cosmos DB account uses continuous backup policy", + "CheckType": [], + "ServiceName": "cosmosdb", + "SubServiceName": "", + "ResourceIdTemplate": "", + "Severity": "medium", + "ResourceType": "microsoft.documentdb/databaseaccounts", + "ResourceGroup": "database", + "Description": "**Azure Cosmos DB accounts** are evaluated for **continuous backup** policy. Continuous backup provides **point-in-time restore (PITR)** enabling recovery to any point within the retention window, unlike periodic backup which only supports full restores at fixed intervals.", + "Risk": "**Periodic backup** limits recovery to the last backup snapshot. Data changes between snapshots are lost during restore. **Continuous backup** enables **granular recovery** from accidental deletes, corruption, or ransomware.", + "RelatedUrl": "", + "AdditionalURLs": [ + "https://learn.microsoft.com/en-us/azure/cosmos-db/continuous-backup-restore-introduction", + "https://learn.microsoft.com/en-us/azure/cosmos-db/migrate-continuous-backup", + "https://learn.microsoft.com/en-us/azure/cosmos-db/restore-account-continuous-backup" + ], + "Remediation": { + "Code": { + "CLI": "az cosmosdb update --name --resource-group --backup-policy-type Continuous --continuous-tier Continuous30Days", + "NativeIaC": "```bicep\n// Bicep: Switch a Cosmos DB account to Continuous backup (irreversible)\nresource account 'Microsoft.DocumentDB/databaseAccounts@2025-10-15' = {\n name: ''\n location: resourceGroup().location\n kind: 'GlobalDocumentDB'\n properties: {\n databaseAccountOfferType: 'Standard'\n locations: [{ locationName: resourceGroup().location }]\n backupPolicy: {\n type: 'Continuous' // Critical: Enables point-in-time restore. Migration from Periodic is one-way.\n continuousModeProperties: {\n tier: 'Continuous30Days' // or 'Continuous7Days' for the lower-cost tier\n }\n }\n }\n}\n```", + "Other": "1. Sign in to the Azure portal and open your Cosmos DB account\n2. In the left menu, select Backup & Restore\n3. Click Switch to Continuous backup\n4. Select the retention tier (Continuous30Days or Continuous7Days)\n5. Acknowledge that the migration from Periodic to Continuous is irreversible\n6. Click Save", + "Terraform": "```hcl\n# Terraform: Configure Cosmos DB account with Continuous backup (irreversible)\nresource \"azurerm_cosmosdb_account\" \"\" {\n name = \"\"\n resource_group_name = \"\"\n location = \"\"\n offer_type = \"Standard\"\n kind = \"GlobalDocumentDB\"\n\n geo_location {\n location = \"\"\n failover_priority = 0\n }\n\n backup {\n type = \"Continuous\" # Critical: Enables point-in-time restore. One-way migration from Periodic.\n tier = \"Continuous30Days\" # or \"Continuous7Days\" for the lower-cost tier\n }\n}\n```" + }, + "Recommendation": { + "Text": "Use **Continuous backup** for Cosmos DB accounts that require **point-in-time restore (PITR)**. Pick the retention tier (`Continuous7Days` or `Continuous30Days`) based on recovery objectives, and validate restore procedures with periodic drills. Note that switching from **Periodic** to **Continuous** is a **one-way** migration; plan the change and review pricing before applying.", + "Url": "https://hub.prowler.com/check/cosmosdb_account_backup_policy_continuous" + } + }, + "Categories": [ + "forensics-ready" + ], + "DependsOn": [], + "RelatedTo": [], + "Notes": "" +} diff --git a/prowler/providers/azure/services/cosmosdb/cosmosdb_account_backup_policy_continuous/cosmosdb_account_backup_policy_continuous.py b/prowler/providers/azure/services/cosmosdb/cosmosdb_account_backup_policy_continuous/cosmosdb_account_backup_policy_continuous.py new file mode 100644 index 0000000000..f1a00fc2e8 --- /dev/null +++ b/prowler/providers/azure/services/cosmosdb/cosmosdb_account_backup_policy_continuous/cosmosdb_account_backup_policy_continuous.py @@ -0,0 +1,30 @@ +from prowler.lib.check.models import Check, Check_Report_Azure +from prowler.providers.azure.services.cosmosdb.cosmosdb_client import cosmosdb_client + + +class cosmosdb_account_backup_policy_continuous(Check): + """Ensure that Cosmos DB accounts use the continuous backup policy.""" + + def execute(self) -> Check_Report_Azure: + """Execute the Cosmos DB continuous-backup check. + + Iterates over every Cosmos DB account fetched by the service and reports + PASS when `backupPolicy.type` is `Continuous`, FAIL otherwise (including + when the property is missing). + + Returns: + A list of Check_Report_Azure with one report per Cosmos DB account. + """ + findings = [] + for subscription, accounts in cosmosdb_client.accounts.items(): + for account in accounts: + report = Check_Report_Azure(metadata=self.metadata(), resource=account) + report.subscription = subscription + report.status = "FAIL" + report.status_extended = f"CosmosDB account {account.name} from subscription {subscription} does not use continuous backup policy." + if account.backup_policy_type == "Continuous": + report.status = "PASS" + report.status_extended = f"CosmosDB account {account.name} from subscription {subscription} uses continuous backup policy." + findings.append(report) + + return findings diff --git a/tests/providers/azure/services/cosmosdb/cosmosdb_account_backup_policy_continuous/cosmosdb_account_backup_policy_continuous_test.py b/tests/providers/azure/services/cosmosdb/cosmosdb_account_backup_policy_continuous/cosmosdb_account_backup_policy_continuous_test.py new file mode 100644 index 0000000000..0cc282485b --- /dev/null +++ b/tests/providers/azure/services/cosmosdb/cosmosdb_account_backup_policy_continuous/cosmosdb_account_backup_policy_continuous_test.py @@ -0,0 +1,157 @@ +from unittest import mock + +from tests.providers.azure.azure_fixtures import ( + AZURE_SUBSCRIPTION_ID, + set_mocked_azure_provider, +) + + +class Test_cosmosdb_account_backup_policy_continuous: + def test_no_subscriptions(self): + cosmosdb_client = mock.MagicMock() + + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_azure_provider(), + ), + mock.patch( + "prowler.providers.azure.services.cosmosdb.cosmosdb_account_backup_policy_continuous.cosmosdb_account_backup_policy_continuous.cosmosdb_client", + new=cosmosdb_client, + ), + ): + from prowler.providers.azure.services.cosmosdb.cosmosdb_account_backup_policy_continuous.cosmosdb_account_backup_policy_continuous import ( + cosmosdb_account_backup_policy_continuous, + ) + + cosmosdb_client.accounts = {} + + check = cosmosdb_account_backup_policy_continuous() + result = check.execute() + assert len(result) == 0 + + def test_pass(self): + cosmosdb_client = mock.MagicMock() + + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_azure_provider(), + ), + mock.patch( + "prowler.providers.azure.services.cosmosdb.cosmosdb_account_backup_policy_continuous.cosmosdb_account_backup_policy_continuous.cosmosdb_client", + new=cosmosdb_client, + ), + ): + from prowler.providers.azure.services.cosmosdb.cosmosdb_account_backup_policy_continuous.cosmosdb_account_backup_policy_continuous import ( + cosmosdb_account_backup_policy_continuous, + ) + from prowler.providers.azure.services.cosmosdb.cosmosdb_service import ( + Account, + ) + + cosmosdb_client.accounts = { + AZURE_SUBSCRIPTION_ID: [ + Account( + id="/subscriptions/sub1/resourceGroups/rg1/providers/Microsoft.DocumentDB/databaseAccounts/test-account", + name="test-account", + kind="GlobalDocumentDB", + type="Microsoft.DocumentDB/databaseAccounts", + tags={}, + is_virtual_network_filter_enabled=False, + location="eastus", + private_endpoint_connections=[], + disable_local_auth=False, + backup_policy_type="Continuous", + ) + ] + } + + check = cosmosdb_account_backup_policy_continuous() + result = check.execute() + assert len(result) == 1 + assert result[0].status == "PASS" + + def test_fail_periodic(self): + cosmosdb_client = mock.MagicMock() + + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_azure_provider(), + ), + mock.patch( + "prowler.providers.azure.services.cosmosdb.cosmosdb_account_backup_policy_continuous.cosmosdb_account_backup_policy_continuous.cosmosdb_client", + new=cosmosdb_client, + ), + ): + from prowler.providers.azure.services.cosmosdb.cosmosdb_account_backup_policy_continuous.cosmosdb_account_backup_policy_continuous import ( + cosmosdb_account_backup_policy_continuous, + ) + from prowler.providers.azure.services.cosmosdb.cosmosdb_service import ( + Account, + ) + + cosmosdb_client.accounts = { + AZURE_SUBSCRIPTION_ID: [ + Account( + id="/subscriptions/sub1/resourceGroups/rg1/providers/Microsoft.DocumentDB/databaseAccounts/test-account", + name="test-account", + kind="GlobalDocumentDB", + type="Microsoft.DocumentDB/databaseAccounts", + tags={}, + is_virtual_network_filter_enabled=False, + location="eastus", + private_endpoint_connections=[], + disable_local_auth=False, + backup_policy_type="Periodic", + ) + ] + } + + check = cosmosdb_account_backup_policy_continuous() + result = check.execute() + assert len(result) == 1 + assert result[0].status == "FAIL" + + def test_fail_no_backup_policy(self): + cosmosdb_client = mock.MagicMock() + + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_azure_provider(), + ), + mock.patch( + "prowler.providers.azure.services.cosmosdb.cosmosdb_account_backup_policy_continuous.cosmosdb_account_backup_policy_continuous.cosmosdb_client", + new=cosmosdb_client, + ), + ): + from prowler.providers.azure.services.cosmosdb.cosmosdb_account_backup_policy_continuous.cosmosdb_account_backup_policy_continuous import ( + cosmosdb_account_backup_policy_continuous, + ) + from prowler.providers.azure.services.cosmosdb.cosmosdb_service import ( + Account, + ) + + cosmosdb_client.accounts = { + AZURE_SUBSCRIPTION_ID: [ + Account( + id="/subscriptions/sub1/resourceGroups/rg1/providers/Microsoft.DocumentDB/databaseAccounts/test-account", + name="test-account", + kind="GlobalDocumentDB", + type="Microsoft.DocumentDB/databaseAccounts", + tags={}, + is_virtual_network_filter_enabled=False, + location="eastus", + private_endpoint_connections=[], + disable_local_auth=False, + backup_policy_type=None, + ) + ] + } + + check = cosmosdb_account_backup_policy_continuous() + result = check.execute() + assert len(result) == 1 + assert result[0].status == "FAIL"