feat(azure): add mysql_flexible_server_geo_redundant_backup_enabled check (#11041)

Co-authored-by: Daniel Barranquero <danielbo2001@gmail.com>
This commit is contained in:
s1ns3nz0
2026-06-18 19:41:04 +09:00
committed by GitHub
parent 2111d083df
commit 7dd08bc6bf
8 changed files with 206 additions and 3 deletions
+1
View File
@@ -20,6 +20,7 @@ All notable changes to the **Prowler SDK** are documented in this file.
- `databricks_workspace_public_network_access_disabled` check for Azure provider, verifying Databricks workspaces have public network access disabled so connectivity is restricted to Azure Private Link private endpoints [(#11035)](https://github.com/prowler-cloud/prowler/pull/11035)
- `databricks_workspace_no_public_ip_enabled` check for Azure provider, verifying Databricks workspaces use secure cluster connectivity (no public IP) so compute nodes are not assigned public IP addresses [(#11036)](https://github.com/prowler-cloud/prowler/pull/11036)
- `defender_ensure_defender_cspm_is_on` check for Azure provider, verifying Microsoft Defender Cloud Security Posture Management (CSPM) is enabled on the Standard tier [(#11037)](https://github.com/prowler-cloud/prowler/pull/11037)
- `mysql_flexible_server_geo_redundant_backup_enabled` check for Azure provider, verifying MySQL Flexible Servers have geo-redundant backup enabled so backups are replicated to the paired region [(#11041)](https://github.com/prowler-cloud/prowler/pull/11041)
- `aks_cluster_auto_upgrade_enabled` check for Azure provider [(#11027)](https://github.com/prowler-cloud/prowler/pull/11027)
- Public `Provider.get_class()` method that resolves a provider class by name for both built-in and external (entry-point) providers [(#11398)](https://github.com/prowler-cloud/prowler/pull/11398)
- Jira timeout preventing the calls from hanging indefinitely when the Jira endpoint is unreachable or slow [(#11602)](https://github.com/prowler-cloud/prowler/pull/11602)
+2 -1
View File
@@ -439,7 +439,8 @@
"keyvault_recoverable",
"sqlserver_auditing_retention_90_days",
"postgresql_flexible_server_log_retention_days_greater_3",
"cosmosdb_account_backup_policy_continuous"
"cosmosdb_account_backup_policy_continuous",
"mysql_flexible_server_geo_redundant_backup_enabled"
]
},
{
@@ -1267,7 +1267,8 @@
}
],
"Checks": [
"cosmosdb_account_backup_policy_continuous"
"cosmosdb_account_backup_policy_continuous",
"mysql_flexible_server_geo_redundant_backup_enabled"
]
},
{
@@ -1296,7 +1297,8 @@
"storage_secure_transfer_required_is_enabled",
"vm_ensure_using_managed_disks",
"vm_trusted_launch_enabled",
"cosmosdb_account_automatic_failover_enabled"
"cosmosdb_account_automatic_failover_enabled",
"mysql_flexible_server_geo_redundant_backup_enabled"
]
},
{
@@ -0,0 +1,37 @@
{
"Provider": "azure",
"CheckID": "mysql_flexible_server_geo_redundant_backup_enabled",
"CheckTitle": "MySQL flexible server has geo-redundant backup enabled",
"CheckType": [],
"ServiceName": "mysql",
"SubServiceName": "",
"ResourceIdTemplate": "",
"Severity": "medium",
"ResourceType": "microsoft.dbformysql/flexibleservers",
"ResourceGroup": "database",
"Description": "**Azure MySQL Flexible Server** is evaluated for **geo-redundant backup**. Geo-redundant backup stores backup copies in a paired Azure region, enabling restore and recovery from a full regional outage.",
"Risk": "Without **geo-redundant backup**, a regional disaster can cause **permanent data loss**. Locally redundant backups only protect against storage hardware failures within the same region and cannot be restored if the primary region becomes unavailable.",
"RelatedUrl": "",
"AdditionalURLs": [
"https://learn.microsoft.com/en-us/azure/mysql/flexible-server/concepts-backup-restore",
"https://learn.microsoft.com/en-us/azure/templates/microsoft.dbformysql/flexibleservers"
],
"Remediation": {
"Code": {
"CLI": "az mysql flexible-server create --name <server_name> --resource-group <resource_group_name> --location <region> --geo-redundant-backup Enabled",
"NativeIaC": "```bicep\n// Bicep: MySQL Flexible Server with geo-redundant backup (set at creation)\nresource mysql 'Microsoft.DBforMySQL/flexibleServers@2023-12-30' = {\n name: '<example_resource_name>'\n location: '<region>'\n properties: {\n backup: {\n geoRedundantBackup: 'Enabled' // CRITICAL: stores backups in the paired region\n }\n }\n}\n```",
"Other": "1. Geo-redundant backup must be configured when the server is created; it cannot be enabled on an existing server\n2. In the Azure portal, start creating a new Azure Database for MySQL flexible server\n3. On the Basics tab, under Compute + storage, open Configure server\n4. Set Geographically redundant backup to Enabled and save\n5. Finish creating the server and migrate workloads to it",
"Terraform": "```hcl\n# Terraform: MySQL Flexible Server with geo-redundant backup (set at creation)\nresource \"azurerm_mysql_flexible_server\" \"<example_resource_name>\" {\n name = \"<example_resource_name>\"\n resource_group_name = \"<example_resource_name>\"\n location = \"<region>\"\n geo_redundant_backup_enabled = true # CRITICAL: stores backups in the paired region\n}\n```"
},
"Recommendation": {
"Text": "Enable **geo-redundant backup** on MySQL Flexible Servers so backups are replicated to the paired Azure region and can be restored during a regional outage. Because this is set at server creation, plan a migration for existing servers, and pair it with an appropriate backup retention period and periodic restore testing.",
"Url": "https://hub.prowler.com/check/mysql_flexible_server_geo_redundant_backup_enabled"
}
},
"Categories": [
"resilience"
],
"DependsOn": [],
"RelatedTo": [],
"Notes": "Geo-redundant backup for Azure MySQL Flexible Server can only be configured at server creation time and cannot be changed afterwards."
}
@@ -0,0 +1,31 @@
from prowler.lib.check.models import Check, Check_Report_Azure
from prowler.providers.azure.services.mysql.mysql_client import mysql_client
class mysql_flexible_server_geo_redundant_backup_enabled(Check):
"""
Ensure Azure MySQL Flexible Servers have geo-redundant backup enabled.
This check evaluates whether each Azure MySQL Flexible Server stores backups in a paired Azure region, enabling recovery from a full regional outage.
- PASS: The server has geo-redundant backup enabled (geo_redundant_backup is "Enabled").
- FAIL: The server does not have geo-redundant backup enabled.
"""
def execute(self) -> Check_Report_Azure:
findings = []
for subscription_id, servers in mysql_client.flexible_servers.items():
subscription_name = mysql_client.subscriptions.get(
subscription_id, subscription_id
)
for server in servers.values():
report = Check_Report_Azure(metadata=self.metadata(), resource=server)
report.subscription = subscription_id
if server.geo_redundant_backup == "Enabled":
report.status = "PASS"
report.status_extended = f"Geo-redundant backup is enabled for server {server.name} in subscription {subscription_name} ({subscription_id})."
else:
report.status = "FAIL"
report.status_extended = f"Geo-redundant backup is disabled for server {server.name} in subscription {subscription_name} ({subscription_id})."
findings.append(report)
return findings
@@ -1,4 +1,5 @@
from dataclasses import dataclass
from typing import Optional
from azure.mgmt.rdbms.mysql_flexibleservers import MySQLManagementClient
@@ -21,6 +22,7 @@ class MySQL(AzureService):
servers_list = client.servers.list()
servers.update({subscription_id: {}})
for server in servers_list:
backup = getattr(server, "backup", None)
servers[subscription_id].update(
{
server.id: FlexibleServer(
@@ -31,6 +33,9 @@ class MySQL(AzureService):
configurations=self._get_configurations(
client, server.id.split("/")[4], server.name
),
geo_redundant_backup=getattr(
backup, "geo_redundant_backup", None
),
)
}
)
@@ -78,3 +83,4 @@ class FlexibleServer:
location: str
version: str
configurations: dict[Configuration]
geo_redundant_backup: Optional[str] = None
@@ -0,0 +1,125 @@
from unittest import mock
from uuid import uuid4
from prowler.providers.azure.services.mysql.mysql_service import FlexibleServer
from tests.providers.azure.azure_fixtures import (
AZURE_SUBSCRIPTION_DISPLAY,
AZURE_SUBSCRIPTION_ID,
AZURE_SUBSCRIPTION_NAME,
set_mocked_azure_provider,
)
class Test_mysql_flexible_server_geo_redundant_backup_enabled:
def test_mysql_no_subscriptions(self):
mysql_client = mock.MagicMock
mysql_client.subscriptions = {AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME}
mysql_client.flexible_servers = {}
with (
mock.patch(
"prowler.providers.common.provider.Provider.get_global_provider",
return_value=set_mocked_azure_provider(),
),
mock.patch(
"prowler.providers.azure.services.mysql.mysql_flexible_server_geo_redundant_backup_enabled.mysql_flexible_server_geo_redundant_backup_enabled.mysql_client",
new=mysql_client,
),
):
from prowler.providers.azure.services.mysql.mysql_flexible_server_geo_redundant_backup_enabled.mysql_flexible_server_geo_redundant_backup_enabled import (
mysql_flexible_server_geo_redundant_backup_enabled,
)
check = mysql_flexible_server_geo_redundant_backup_enabled()
result = check.execute()
assert len(result) == 0
def test_mysql_geo_redundant_backup_disabled(self):
server_id = str(uuid4())
server_name = "test-server"
mysql_client = mock.MagicMock
mysql_client.subscriptions = {AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME}
mysql_client.flexible_servers = {
AZURE_SUBSCRIPTION_ID: {
server_id: FlexibleServer(
resource_id=server_id,
name=server_name,
location="eastus",
version="8.0",
configurations={},
geo_redundant_backup="Disabled",
)
}
}
with (
mock.patch(
"prowler.providers.common.provider.Provider.get_global_provider",
return_value=set_mocked_azure_provider(),
),
mock.patch(
"prowler.providers.azure.services.mysql.mysql_flexible_server_geo_redundant_backup_enabled.mysql_flexible_server_geo_redundant_backup_enabled.mysql_client",
new=mysql_client,
),
):
from prowler.providers.azure.services.mysql.mysql_flexible_server_geo_redundant_backup_enabled.mysql_flexible_server_geo_redundant_backup_enabled import (
mysql_flexible_server_geo_redundant_backup_enabled,
)
check = mysql_flexible_server_geo_redundant_backup_enabled()
result = check.execute()
assert len(result) == 1
assert result[0].status == "FAIL"
assert (
result[0].status_extended
== f"Geo-redundant backup is disabled for server {server_name} in subscription {AZURE_SUBSCRIPTION_DISPLAY}."
)
assert result[0].subscription == AZURE_SUBSCRIPTION_ID
assert result[0].resource_name == server_name
assert result[0].resource_id == server_id
assert result[0].location == "eastus"
def test_mysql_geo_redundant_backup_enabled(self):
server_id = str(uuid4())
server_name = "test-server"
mysql_client = mock.MagicMock
mysql_client.subscriptions = {AZURE_SUBSCRIPTION_ID: AZURE_SUBSCRIPTION_NAME}
mysql_client.flexible_servers = {
AZURE_SUBSCRIPTION_ID: {
server_id: FlexibleServer(
resource_id=server_id,
name=server_name,
location="eastus",
version="8.0",
configurations={},
geo_redundant_backup="Enabled",
)
}
}
with (
mock.patch(
"prowler.providers.common.provider.Provider.get_global_provider",
return_value=set_mocked_azure_provider(),
),
mock.patch(
"prowler.providers.azure.services.mysql.mysql_flexible_server_geo_redundant_backup_enabled.mysql_flexible_server_geo_redundant_backup_enabled.mysql_client",
new=mysql_client,
),
):
from prowler.providers.azure.services.mysql.mysql_flexible_server_geo_redundant_backup_enabled.mysql_flexible_server_geo_redundant_backup_enabled import (
mysql_flexible_server_geo_redundant_backup_enabled,
)
check = mysql_flexible_server_geo_redundant_backup_enabled()
result = check.execute()
assert len(result) == 1
assert result[0].status == "PASS"
assert (
result[0].status_extended
== f"Geo-redundant backup is enabled for server {server_name} in subscription {AZURE_SUBSCRIPTION_DISPLAY}."
)
assert result[0].subscription == AZURE_SUBSCRIPTION_ID
assert result[0].resource_name == server_name
assert result[0].resource_id == server_id
assert result[0].location == "eastus"