feat(azure/vm): add new check vm_backup_enabled (#8182)

Co-authored-by: Sergio Garcia <hello@mistercloudsec.com>
This commit is contained in:
Rubén De la Torre Vico
2025-07-08 11:01:22 +02:00
committed by GitHub
parent c0c736bffe
commit 4c50f4d811
9 changed files with 529 additions and 3 deletions
Generated
+40 -2
View File
@@ -1,4 +1,4 @@
# This file is automatically @generated by Poetry 2.1.3 and should not be changed by hand.
# This file is automatically @generated by Poetry 2.1.2 and should not be changed by hand.
[[package]]
name = "about-time"
@@ -666,6 +666,42 @@ azure-common = ">=1.1,<2.0"
azure-mgmt-core = ">=1.3.0,<2.0.0"
msrest = ">=0.6.21"
[[package]]
name = "azure-mgmt-recoveryservices"
version = "3.1.0"
description = "Microsoft Azure Recovery Services Client Library for Python"
optional = false
python-versions = ">=3.9"
groups = ["main"]
files = [
{file = "azure_mgmt_recoveryservices-3.1.0-py3-none-any.whl", hash = "sha256:21c58afdf4ae66806783e95f8cd17e3bec31be7178c48784db21f0b05de7fa66"},
{file = "azure_mgmt_recoveryservices-3.1.0.tar.gz", hash = "sha256:7f2db98401708cf145322f50bc491caf7967bec4af3bf7b0984b9f07d3092687"},
]
[package.dependencies]
azure-common = ">=1.1"
azure-mgmt-core = ">=1.5.0"
isodate = ">=0.6.1"
typing-extensions = ">=4.6.0"
[[package]]
name = "azure-mgmt-recoveryservicesbackup"
version = "9.2.0"
description = "Microsoft Azure Recovery Services Backup Management Client Library for Python"
optional = false
python-versions = ">=3.8"
groups = ["main"]
files = [
{file = "azure_mgmt_recoveryservicesbackup-9.2.0-py3-none-any.whl", hash = "sha256:c0002858d0166b6a10189a1fd580a49c83dc31b111e98010a5b2ea0f767dfff1"},
{file = "azure_mgmt_recoveryservicesbackup-9.2.0.tar.gz", hash = "sha256:c402b3e22a6c3879df56bc37e0063142c3352c5102599ff102d19824f1b32b29"},
]
[package.dependencies]
azure-common = ">=1.1"
azure-mgmt-core = ">=1.3.2"
isodate = ">=0.6.1"
typing-extensions = ">=4.6.0"
[[package]]
name = "azure-mgmt-resource"
version = "23.3.0"
@@ -2678,6 +2714,8 @@ python-versions = "*"
groups = ["dev"]
files = [
{file = "jsonpath-ng-1.7.0.tar.gz", hash = "sha256:f6f5f7fd4e5ff79c785f1573b394043b39849fb2bb47bcead935d12b00beab3c"},
{file = "jsonpath_ng-1.7.0-py2-none-any.whl", hash = "sha256:898c93fc173f0c336784a3fa63d7434297544b7198124a68f9a3ef9597b0ae6e"},
{file = "jsonpath_ng-1.7.0-py3-none-any.whl", hash = "sha256:f3d7f9e848cba1b6da28c55b1c26ff915dc9e0b1ba7e752a53d6da8d5cbd00b6"},
]
[package.dependencies]
@@ -6622,4 +6660,4 @@ type = ["pytest-mypy"]
[metadata]
lock-version = "2.1"
python-versions = ">3.9.1,<3.13"
content-hash = "c442552635c8e904d1c7a50f4787c8e90ec90787960ee1867f2235a7aa2205f0"
content-hash = "a0c3e917dcedf073426ae47c942c1db1e04e14ea1ab1a81d7fb91f2873daf1cb"
+1 -1
View File
@@ -6,9 +6,9 @@ All notable changes to the **Prowler SDK** are documented in this file.
### Added
- `storage_smb_channel_encryption_with_secure_algorithm` check for Azure provider [(#8123)](https://github.com/prowler-cloud/prowler/pull/8123)
- `vm_backup_enabled` check for Azure provider [(#8182)](https://github.com/prowler-cloud/prowler/pull/8182)
- `vm_linux_enforce_ssh_authentication` check for Azure provider [(#8149)](https://github.com/prowler-cloud/prowler/pull/8149)
- `vm_ensure_using_approved_images` check for Azure provider [(#8168)](https://github.com/prowler-cloud/prowler/pull/8168)
- `vm_scaleset_associated_load_balancer` check for Azure provider [(#8181)](https://github.com/prowler-cloud/prowler/pull/8181)
### Changed
@@ -0,0 +1,4 @@
from prowler.providers.azure.services.recovery.recovery_service import Recovery
from prowler.providers.common.provider import Provider
recovery_client = Recovery(Provider.get_global_provider())
@@ -0,0 +1,101 @@
from typing import Optional
from azure.mgmt.recoveryservices import RecoveryServicesClient
from azure.mgmt.recoveryservicesbackup import RecoveryServicesBackupClient
from azure.mgmt.recoveryservicesbackup.activestamp.models import DataSourceType
from pydantic import BaseModel, Field
from prowler.lib.logger import logger
from prowler.providers.azure.azure_provider import AzureProvider
from prowler.providers.azure.lib.service.service import AzureService
class BackupItem(BaseModel):
"""Minimal BackupItem: only essential identifying and descriptive fields."""
id: str
name: str
workload_type: Optional[DataSourceType]
class BackupVault(BaseModel):
"""Minimal BackupVault: only essential identifying fields and its backup items."""
id: str
name: str
location: str
backup_protected_items: dict[str, BackupItem] = Field(default_factory=dict)
class Recovery(AzureService):
def __init__(self, provider: AzureProvider):
super().__init__(RecoveryServicesClient, provider)
self.vaults: dict[str, dict[str, BackupVault]] = self._get_vaults()
RecoveryBackup(provider, self.vaults)
def _get_vaults(self) -> dict[str, dict[str, BackupVault]]:
"""
Retrieve all Recovery Services vaults for each subscription.
Returns:
Nested dictionary of vaults by subscription.
"""
logger.info("Recovery - Getting Recovery Services vaults...")
vaults_dict: dict[str, dict[str, BackupVault]] = {}
try:
vaults_dict: dict[str, dict[str, BackupVault]] = {}
for subscription_name, client in self.clients.items():
vaults = client.vaults.list_by_subscription_id()
vaults_dict[subscription_name] = {}
for vault in vaults:
vault_obj = BackupVault(
id=vault.id,
name=vault.name,
location=vault.location,
)
vaults_dict[subscription_name][vault_obj.id] = vault_obj
except Exception as error:
logger.error(
f"Subscription name: {subscription_name} -- {error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}"
)
return vaults_dict
class RecoveryBackup(AzureService):
def __init__(
self, provider: AzureProvider, vaults: dict[str, dict[str, BackupVault]]
):
super().__init__(RecoveryServicesBackupClient, provider)
for subscription_name, vaults in vaults.items():
for vault in vaults.values():
vault.backup_protected_items = self._get_backup_protected_items(
subscription_name=subscription_name, vault=vault
)
def _get_backup_protected_items(
self, subscription_name: str, vault: BackupVault
) -> dict[str, BackupItem]:
"""
Retrieve all backup protected items for a given vault.
"""
logger.info("Recovery - Getting backup protected items...")
backup_protected_items_dict: dict[str, BackupItem] = {}
try:
backup_protected_items = self.clients[
subscription_name
].backup_protected_items.list(
vault_name=vault.name,
resource_group_name=vault.id.split("/")[4],
)
for item in backup_protected_items:
item_properties = getattr(item, "properties", None)
backup_protected_items_dict[item.id] = BackupItem(
id=item.id,
name=item.name,
workload_type=(
item_properties.workload_type if item_properties else None
),
)
except Exception as e:
logger.error(f"Recovery - Error getting backup protected items: {e}")
return backup_protected_items_dict
@@ -0,0 +1,30 @@
{
"Provider": "azure",
"CheckID": "vm_backup_enabled",
"CheckTitle": "Ensure Backups are enabled for Azure Virtual Machines",
"CheckType": [],
"ServiceName": "vm",
"SubServiceName": "",
"ResourceIdTemplate": "",
"Severity": "high",
"ResourceType": "Microsoft.Compute/virtualMachines",
"Description": "Ensure that Microsoft Azure Backup service is in use for your Azure virtual machines (VMs) to protect against accidental deletion or corruption.",
"Risk": "Without Azure Backup enabled, VMs are at risk of data loss due to accidental deletion, corruption, or other failures, and recovery options are limited.",
"RelatedUrl": "https://docs.microsoft.com/en-us/azure/backup/backup-overview",
"Remediation": {
"Code": {
"CLI": "az backup protection enable-for-vm --resource-group <resource-group> --vm <vm-name> --vault-name <vault-name> --policy-name DefaultPolicy",
"NativeIaC": "",
"Other": "https://learn.microsoft.com/en-us/azure/backup/quick-backup-vm-portal",
"Terraform": ""
},
"Recommendation": {
"Text": "Enable Azure Backup for each VM by associating it with a Recovery Services vault and a backup policy.",
"Url": "https://docs.microsoft.com/en-us/azure/backup/quick-backup-vm-portal"
}
},
"Categories": [],
"DependsOn": [],
"RelatedTo": [],
"Notes": ""
}
@@ -0,0 +1,50 @@
from azure.mgmt.recoveryservicesbackup.activestamp.models import DataSourceType
from prowler.lib.check.models import Check, Check_Report_Azure
from prowler.providers.azure.services.recovery.recovery_client import recovery_client
from prowler.providers.azure.services.vm.vm_client import vm_client
class vm_backup_enabled(Check):
"""
Ensure that Microsoft Azure Backup service is in use for your Azure virtual machines (VMs).
This check evaluates whether each Azure VM in the subscription is protected by Azure Backup.
- PASS: The VM is protected by Azure Backup (present in a Recovery Services vault).
- FAIL: The VM is not protected by Azure Backup (not present in any Recovery Services vault).
"""
def execute(self) -> list[Check_Report_Azure]:
"""Execute Azure VM backup enabled check.
Returns:
A list of reports containing the result of the check.
"""
findings = []
for subscription_name, vms in vm_client.virtual_machines.items():
vaults = recovery_client.vaults.get(subscription_name, {})
for vm in vms.values():
found = False
found_vault_name = None
for vault in vaults.values():
for backup_item in vault.backup_protected_items.values():
if (
backup_item.workload_type == DataSourceType.VM
and backup_item.name.split(";")[-1] == vm.resource_name
):
found = True
found_vault_name = vault.name
break
if found:
break
report = Check_Report_Azure(metadata=self.metadata(), resource=vm)
report.subscription = subscription_name
if found:
report.status = "PASS"
report.status_extended = f"VM {vm.resource_name} in subscription {subscription_name} is protected by Azure Backup (vault: {found_vault_name})."
else:
report.status = "FAIL"
report.status_extended = f"VM {vm.resource_name} in subscription {subscription_name} is not protected by Azure Backup."
findings.append(report)
return findings
+2
View File
@@ -26,6 +26,8 @@ dependencies = [
"azure-mgmt-monitor==6.0.2",
"azure-mgmt-network==28.1.0",
"azure-mgmt-rdbms==10.1.0",
"azure-mgmt-recoveryservices==3.1.0",
"azure-mgmt-recoveryservicesbackup==9.2.0",
"azure-mgmt-resource==23.3.0",
"azure-mgmt-search==9.1.0",
"azure-mgmt-security==7.0.0",
@@ -0,0 +1,301 @@
from unittest import mock
from uuid import uuid4
from tests.providers.azure.azure_fixtures import (
AZURE_SUBSCRIPTION_ID,
set_mocked_azure_provider,
)
class Test_vm_backup_enabled:
def test_vm_backup_enabled_no_subscriptions(self):
vm_client = mock.MagicMock
recovery_client = mock.MagicMock
vm_client.virtual_machines = {}
recovery_client.vaults = {}
with (
mock.patch(
"prowler.providers.common.provider.Provider.get_global_provider",
return_value=set_mocked_azure_provider(),
),
mock.patch(
"prowler.providers.azure.services.vm.vm_backup_enabled.vm_backup_enabled.vm_client",
new=vm_client,
),
mock.patch(
"prowler.providers.azure.services.vm.vm_backup_enabled.vm_backup_enabled.recovery_client",
new=recovery_client,
),
):
from prowler.providers.azure.services.vm.vm_backup_enabled.vm_backup_enabled import (
vm_backup_enabled,
)
check = vm_backup_enabled()
result = check.execute()
assert len(result) == 0
def test_no_vms(self):
mock_vm_client = mock.MagicMock()
mock_vm_client.virtual_machines = {AZURE_SUBSCRIPTION_ID: {}}
mock_recovery_client = mock.MagicMock()
mock_recovery_client.vaults = {AZURE_SUBSCRIPTION_ID: {}}
with (
mock.patch(
"prowler.providers.common.provider.Provider.get_global_provider",
return_value=set_mocked_azure_provider(),
),
mock.patch(
"prowler.providers.azure.services.vm.vm_backup_enabled.vm_backup_enabled.vm_client",
new=mock_vm_client,
),
mock.patch(
"prowler.providers.azure.services.vm.vm_backup_enabled.vm_backup_enabled.recovery_client",
new=mock_recovery_client,
),
):
from prowler.providers.azure.services.vm.vm_backup_enabled.vm_backup_enabled import (
vm_backup_enabled,
)
check = vm_backup_enabled()
result = check.execute()
assert len(result) == 0
def test_vm_protected_by_backup(self):
vm_id = str(uuid4())
vm_name = "VMTest"
vault_id = str(uuid4())
vault_name = "vault1"
mock_vm_client = mock.MagicMock()
mock_recovery_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.vm.vm_backup_enabled.vm_backup_enabled.vm_client",
new=mock_vm_client,
),
mock.patch(
"prowler.providers.azure.services.vm.vm_backup_enabled.vm_backup_enabled.recovery_client",
new=mock_recovery_client,
),
):
from azure.mgmt.recoveryservicesbackup.activestamp.models import (
DataSourceType,
)
from prowler.providers.azure.services.recovery.recovery_service import (
BackupItem,
BackupVault,
)
from prowler.providers.azure.services.vm.vm_backup_enabled.vm_backup_enabled import (
vm_backup_enabled,
)
from prowler.providers.azure.services.vm.vm_service import (
ManagedDiskParameters,
OSDisk,
StorageProfile,
VirtualMachine,
)
vm = VirtualMachine(
resource_id=vm_id,
resource_name=vm_name,
location="eastus",
security_profile=None,
extensions=[],
storage_profile=StorageProfile(
os_disk=OSDisk(
name="os_disk_name",
operating_system_type="Linux",
managed_disk=ManagedDiskParameters(id="managed_disk_id"),
),
data_disks=[],
),
)
backup_item = BackupItem(
id=str(uuid4()),
name=f"someprefix;{vm_name}",
workload_type=DataSourceType.VM,
)
vault = BackupVault(
id=vault_id,
name=vault_name,
location="eastus",
backup_protected_items={backup_item.id: backup_item},
)
mock_vm_client.virtual_machines = {AZURE_SUBSCRIPTION_ID: {vm_id: vm}}
mock_recovery_client.vaults = {AZURE_SUBSCRIPTION_ID: {vault_id: vault}}
check = vm_backup_enabled()
result = check.execute()
assert len(result) == 1
assert result[0].status == "PASS"
assert result[0].subscription == AZURE_SUBSCRIPTION_ID
assert result[0].resource_name == vm_name
assert result[0].resource_id == vm_id
assert (
result[0].status_extended
== f"VM {vm_name} in subscription {AZURE_SUBSCRIPTION_ID} is protected by Azure Backup (vault: {vault_name})."
)
def test_vm_not_protected_by_backup(self):
vm_id = str(uuid4())
vm_name = "VMTest"
vault_id = str(uuid4())
vault_name = "vault1"
mock_vm_client = mock.MagicMock()
mock_recovery_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.vm.vm_backup_enabled.vm_backup_enabled.vm_client",
new=mock_vm_client,
),
mock.patch(
"prowler.providers.azure.services.vm.vm_backup_enabled.vm_backup_enabled.recovery_client",
new=mock_recovery_client,
),
):
from azure.mgmt.recoveryservicesbackup.activestamp.models import (
DataSourceType,
)
from prowler.providers.azure.services.recovery.recovery_service import (
BackupItem,
BackupVault,
)
from prowler.providers.azure.services.vm.vm_backup_enabled.vm_backup_enabled import (
vm_backup_enabled,
)
from prowler.providers.azure.services.vm.vm_service import (
ManagedDiskParameters,
OSDisk,
StorageProfile,
VirtualMachine,
)
vm = VirtualMachine(
resource_id=vm_id,
resource_name=vm_name,
location="eastus",
security_profile=None,
extensions=[],
storage_profile=StorageProfile(
os_disk=OSDisk(
name="os_disk_name",
operating_system_type="Linux",
managed_disk=ManagedDiskParameters(id="managed_disk_id"),
),
data_disks=[],
),
)
backup_item = BackupItem(
id=str(uuid4()),
name="someprefix;OtherVM",
workload_type=DataSourceType.VM,
)
vault = BackupVault(
id=vault_id,
name=vault_name,
location="eastus",
backup_protected_items={backup_item.id: backup_item},
)
mock_vm_client.virtual_machines = {AZURE_SUBSCRIPTION_ID: {vm_id: vm}}
mock_recovery_client.vaults = {AZURE_SUBSCRIPTION_ID: {vault_id: vault}}
check = vm_backup_enabled()
result = check.execute()
assert len(result) == 1
assert result[0].status == "FAIL"
assert result[0].subscription == AZURE_SUBSCRIPTION_ID
assert result[0].resource_name == vm_name
assert result[0].resource_id == vm_id
assert (
result[0].status_extended
== f"VM {vm_name} in subscription {AZURE_SUBSCRIPTION_ID} is not protected by Azure Backup."
)
def test_vm_protected_by_backup_non_vm_workload(self):
vm_id = str(uuid4())
vm_name = "VMTest"
vault_id = str(uuid4())
vault_name = "vault1"
mock_vm_client = mock.MagicMock()
mock_recovery_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.vm.vm_backup_enabled.vm_backup_enabled.vm_client",
new=mock_vm_client,
),
mock.patch(
"prowler.providers.azure.services.vm.vm_backup_enabled.vm_backup_enabled.recovery_client",
new=mock_recovery_client,
),
):
from azure.mgmt.recoveryservicesbackup.activestamp.models import (
DataSourceType,
)
from prowler.providers.azure.services.recovery.recovery_service import (
BackupItem,
BackupVault,
)
from prowler.providers.azure.services.vm.vm_backup_enabled.vm_backup_enabled import (
vm_backup_enabled,
)
from prowler.providers.azure.services.vm.vm_service import (
ManagedDiskParameters,
OSDisk,
StorageProfile,
VirtualMachine,
)
vm = VirtualMachine(
resource_id=vm_id,
resource_name=vm_name,
location="eastus",
security_profile=None,
extensions=[],
storage_profile=StorageProfile(
os_disk=OSDisk(
name="os_disk_name",
operating_system_type="Linux",
managed_disk=ManagedDiskParameters(id="managed_disk_id"),
),
data_disks=[],
),
)
backup_item = BackupItem(
id=str(uuid4()),
name=f"someprefix;{vm_name}",
workload_type=DataSourceType.FILE_FOLDER,
)
vault = BackupVault(
id=vault_id,
name=vault_name,
location="eastus",
backup_protected_items={backup_item.id: backup_item},
)
mock_vm_client.virtual_machines = {AZURE_SUBSCRIPTION_ID: {vm_id: vm}}
mock_recovery_client.vaults = {AZURE_SUBSCRIPTION_ID: {vault_id: vault}}
check = vm_backup_enabled()
result = check.execute()
assert len(result) == 1
assert result[0].status == "FAIL"
assert result[0].subscription == AZURE_SUBSCRIPTION_ID
assert result[0].resource_name == vm_name
assert result[0].resource_id == vm_id
assert (
result[0].status_extended
== f"VM {vm_name} in subscription {AZURE_SUBSCRIPTION_ID} is not protected by Azure Backup."
)