From 3628e7b3e84171e5834308382832bb8baf68a930 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rub=C3=A9n=20De=20la=20Torre=20Vico?= Date: Tue, 8 Jul 2025 10:40:33 +0200 Subject: [PATCH] feat(azure/vm): add new check `vm_ensure_using_approved_images ` (#8168) --- prowler/CHANGELOG.md | 2 +- .../__init__.py | 0 ...ensure_using_approved_images.metadata.json | 30 ++++ .../vm_ensure_using_approved_images.py | 33 ++++ .../providers/azure/services/vm/vm_service.py | 6 + .../vm_ensure_using_approved_images_test.py | 165 ++++++++++++++++++ 6 files changed, 235 insertions(+), 1 deletion(-) create mode 100644 prowler/providers/azure/services/vm/vm_ensure_using_approved_images/__init__.py create mode 100644 prowler/providers/azure/services/vm/vm_ensure_using_approved_images/vm_ensure_using_approved_images.metadata.json create mode 100644 prowler/providers/azure/services/vm/vm_ensure_using_approved_images/vm_ensure_using_approved_images.py create mode 100644 tests/providers/azure/services/vm/vm_ensure_using_approved_images/vm_ensure_using_approved_images_test.py diff --git a/prowler/CHANGELOG.md b/prowler/CHANGELOG.md index 12468afeb6..432b31acfc 100644 --- a/prowler/CHANGELOG.md +++ b/prowler/CHANGELOG.md @@ -6,8 +6,8 @@ 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_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) ### Changed diff --git a/prowler/providers/azure/services/vm/vm_ensure_using_approved_images/__init__.py b/prowler/providers/azure/services/vm/vm_ensure_using_approved_images/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/prowler/providers/azure/services/vm/vm_ensure_using_approved_images/vm_ensure_using_approved_images.metadata.json b/prowler/providers/azure/services/vm/vm_ensure_using_approved_images/vm_ensure_using_approved_images.metadata.json new file mode 100644 index 0000000000..c1b8605612 --- /dev/null +++ b/prowler/providers/azure/services/vm/vm_ensure_using_approved_images/vm_ensure_using_approved_images.metadata.json @@ -0,0 +1,30 @@ +{ + "Provider": "azure", + "CheckID": "vm_ensure_using_approved_images", + "CheckTitle": "Ensure that Azure VMs are using an approved machine image.", + "CheckType": [], + "ServiceName": "vm", + "SubServiceName": "image", + "ResourceIdTemplate": "/subscriptions//resourceGroups//providers/Microsoft.Compute/images/", + "Severity": "medium", + "ResourceType": "Microsoft.Compute/images", + "Description": "Ensure that all your Azure virtual machine instances are launched from approved machine images only.", + "Risk": "An approved machine image is a custom virtual machine (VM) image that contains a pre-configured OS and a well-defined stack of server software approved by Azure, fully configured to run your application. Using approved (golden) machine images to launch new VM instances within your Azure cloud environment brings major benefits such as fast and stable application deployment and scaling, secure application stack upgrades, and versioning.", + "RelatedUrl": "https://learn.microsoft.com/en-us/azure/virtual-machines/windows/create-vm-generalized-managed", + "Remediation": { + "Code": { + "CLI": "", + "NativeIaC": "", + "Other": "https://www.trendmicro.com/cloudoneconformity/knowledge-base/azure/VirtualMachines/approved-machine-image.html", + "Terraform": "" + }, + "Recommendation": { + "Text": "Re-create the required VM instances using the approved (golden) machine image.", + "Url": "https://docs.microsoft.com/en-us/azure/virtual-machines/windows/create-vm-generalized-managed" + } + }, + "Categories": [], + "DependsOn": [], + "RelatedTo": [], + "Notes": "This check only validates if the VM was launched from a custom image. It does not validate the image content or security baseline." +} diff --git a/prowler/providers/azure/services/vm/vm_ensure_using_approved_images/vm_ensure_using_approved_images.py b/prowler/providers/azure/services/vm/vm_ensure_using_approved_images/vm_ensure_using_approved_images.py new file mode 100644 index 0000000000..4f6c378777 --- /dev/null +++ b/prowler/providers/azure/services/vm/vm_ensure_using_approved_images/vm_ensure_using_approved_images.py @@ -0,0 +1,33 @@ +from prowler.lib.check.models import Check, Check_Report_Azure +from prowler.providers.azure.services.vm.vm_client import vm_client + + +class vm_ensure_using_approved_images(Check): + """ + Ensure that Azure VMs are using an approved (custom) machine image. + + This check evaluates whether Azure Virtual Machines are launched from an approved (custom) machine image by checking the image reference ID format. + + - PASS: The Azure VM is using an approved custom machine image. + - FAIL: The Azure VM is not using an approved custom machine image. + """ + + def execute(self): + findings = [] + for subscription_name, vms in vm_client.virtual_machines.items(): + for vm in vms.values(): + report = Check_Report_Azure(metadata=self.metadata(), resource=vm) + report.subscription = subscription_name + image_id = getattr(vm, "image_reference", None) + if ( + image_id + and image_id.startswith("/subscriptions/") + and "/providers/Microsoft.Compute/images/" in image_id + ): + report.status = "PASS" + report.status_extended = f"VM {vm.resource_name} in subscription {subscription_name} is using an approved machine image: {image_id.split('/')[-1]}." + else: + report.status = "FAIL" + report.status_extended = f"VM {vm.resource_name} in subscription {subscription_name} is not using an approved machine image." + findings.append(report) + return findings diff --git a/prowler/providers/azure/services/vm/vm_service.py b/prowler/providers/azure/services/vm/vm_service.py index ffa80f8e07..7a2807b3da 100644 --- a/prowler/providers/azure/services/vm/vm_service.py +++ b/prowler/providers/azure/services/vm/vm_service.py @@ -104,6 +104,11 @@ class VirtualMachines(AzureService): location=vm.location, security_profile=getattr(vm, "security_profile", None), extensions=extensions, + image_reference=getattr( + getattr(storage_profile, "image_reference", None), + "id", + None, + ), linux_configuration=linux_configuration, ) } @@ -204,6 +209,7 @@ class VirtualMachine(BaseModel): security_profile: Optional[SecurityProfile] extensions: list[VirtualMachineExtension] storage_profile: Optional[StorageProfile] = None + image_reference: Optional[str] = None linux_configuration: Optional[LinuxConfiguration] = None diff --git a/tests/providers/azure/services/vm/vm_ensure_using_approved_images/vm_ensure_using_approved_images_test.py b/tests/providers/azure/services/vm/vm_ensure_using_approved_images/vm_ensure_using_approved_images_test.py new file mode 100644 index 0000000000..035ec5db3b --- /dev/null +++ b/tests/providers/azure/services/vm/vm_ensure_using_approved_images/vm_ensure_using_approved_images_test.py @@ -0,0 +1,165 @@ +from unittest import mock +from uuid import uuid4 + +from prowler.providers.azure.services.vm.vm_service import VirtualMachine +from tests.providers.azure.azure_fixtures import ( + AZURE_SUBSCRIPTION_ID, + set_mocked_azure_provider, +) + + +class Test_vm_ensure_using_approved_images: + def test_no_subscriptions(self): + vm_client = mock.MagicMock() + vm_client.virtual_machines = {} + 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_ensure_using_approved_images.vm_ensure_using_approved_images.vm_client", + new=vm_client, + ), + ): + from prowler.providers.azure.services.vm.vm_ensure_using_approved_images.vm_ensure_using_approved_images import ( + vm_ensure_using_approved_images, + ) + + check = vm_ensure_using_approved_images() + result = check.execute() + assert len(result) == 0 + + def test_empty_vms_in_subscription(self): + vm_client = mock.MagicMock() + vm_client.virtual_machines = {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_ensure_using_approved_images.vm_ensure_using_approved_images.vm_client", + new=vm_client, + ), + ): + from prowler.providers.azure.services.vm.vm_ensure_using_approved_images.vm_ensure_using_approved_images import ( + vm_ensure_using_approved_images, + ) + + check = vm_ensure_using_approved_images() + result = check.execute() + assert len(result) == 0 + + def test_vm_with_approved_image(self): + vm_id = str(uuid4()) + approved_image_id = f"/subscriptions/{AZURE_SUBSCRIPTION_ID}/resourceGroups/rg/providers/Microsoft.Compute/images/custom-image" + vm = VirtualMachine( + resource_id=vm_id, + resource_name="VMTestApproved", + location="westeurope", + security_profile=None, + extensions=[], + storage_profile=None, + image_reference=approved_image_id, + ) + vm_client = mock.MagicMock() + vm_client.virtual_machines = {AZURE_SUBSCRIPTION_ID: {vm_id: vm}} + 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_ensure_using_approved_images.vm_ensure_using_approved_images.vm_client", + new=vm_client, + ), + ): + from prowler.providers.azure.services.vm.vm_ensure_using_approved_images.vm_ensure_using_approved_images import ( + vm_ensure_using_approved_images, + ) + + check = vm_ensure_using_approved_images() + result = check.execute() + assert len(result) == 1 + assert result[0].status == "PASS" + assert result[0].resource_name == "VMTestApproved" + assert result[0].resource_id == vm_id + assert result[0].subscription == AZURE_SUBSCRIPTION_ID + expected_status_extended = f"VM VMTestApproved in subscription {AZURE_SUBSCRIPTION_ID} is using an approved machine image: custom-image." + assert result[0].status_extended == expected_status_extended + + def test_vm_with_not_approved_image(self): + vm_id = str(uuid4()) + not_approved_image_id = "/subscriptions/other/resourceGroups/rg/providers/Microsoft.Compute/otherResource/other-image" + vm = VirtualMachine( + resource_id=vm_id, + resource_name="VMTestNotApproved", + location="westeurope", + security_profile=None, + extensions=[], + storage_profile=None, + image_reference=not_approved_image_id, + ) + vm_client = mock.MagicMock() + vm_client.virtual_machines = {AZURE_SUBSCRIPTION_ID: {vm_id: vm}} + 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_ensure_using_approved_images.vm_ensure_using_approved_images.vm_client", + new=vm_client, + ), + ): + from prowler.providers.azure.services.vm.vm_ensure_using_approved_images.vm_ensure_using_approved_images import ( + vm_ensure_using_approved_images, + ) + + check = vm_ensure_using_approved_images() + result = check.execute() + assert len(result) == 1 + assert result[0].status == "FAIL" + assert result[0].resource_name == "VMTestNotApproved" + assert result[0].resource_id == vm_id + assert result[0].subscription == AZURE_SUBSCRIPTION_ID + expected_status_extended = f"VM VMTestNotApproved in subscription {AZURE_SUBSCRIPTION_ID} is not using an approved machine image." + assert result[0].status_extended == expected_status_extended + + def test_vm_with_missing_image_reference(self): + vm_id = str(uuid4()) + vm = VirtualMachine( + resource_id=vm_id, + resource_name="VMTestNoImageRef", + location="westeurope", + security_profile=None, + extensions=[], + storage_profile=None, + image_reference=None, + ) + vm_client = mock.MagicMock() + vm_client.virtual_machines = {AZURE_SUBSCRIPTION_ID: {vm_id: vm}} + 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_ensure_using_approved_images.vm_ensure_using_approved_images.vm_client", + new=vm_client, + ), + ): + from prowler.providers.azure.services.vm.vm_ensure_using_approved_images.vm_ensure_using_approved_images import ( + vm_ensure_using_approved_images, + ) + + check = vm_ensure_using_approved_images() + result = check.execute() + assert len(result) == 1 + assert result[0].status == "FAIL" + assert result[0].resource_name == "VMTestNoImageRef" + assert result[0].resource_id == vm_id + assert result[0].subscription == AZURE_SUBSCRIPTION_ID + expected_status_extended = f"VM VMTestNoImageRef in subscription {AZURE_SUBSCRIPTION_ID} is not using an approved machine image." + assert result[0].status_extended == expected_status_extended