diff --git a/prowler/CHANGELOG.md b/prowler/CHANGELOG.md index fc703fca53..12468afeb6 100644 --- a/prowler/CHANGELOG.md +++ b/prowler/CHANGELOG.md @@ -7,6 +7,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) + ### Changed ### Fixed diff --git a/prowler/providers/azure/services/vm/vm_linux_enforce_ssh_authentication/__init__.py b/prowler/providers/azure/services/vm/vm_linux_enforce_ssh_authentication/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/prowler/providers/azure/services/vm/vm_linux_enforce_ssh_authentication/vm_linux_enforce_ssh_authentication.metadata.json b/prowler/providers/azure/services/vm/vm_linux_enforce_ssh_authentication/vm_linux_enforce_ssh_authentication.metadata.json new file mode 100644 index 0000000000..3067ca5736 --- /dev/null +++ b/prowler/providers/azure/services/vm/vm_linux_enforce_ssh_authentication/vm_linux_enforce_ssh_authentication.metadata.json @@ -0,0 +1,30 @@ +{ + "Provider": "azure", + "CheckID": "vm_linux_enforce_ssh_authentication", + "CheckTitle": "Ensure SSH key authentication is enforced on Linux-based Virtual Machines", + "CheckType": [], + "ServiceName": "vm", + "SubServiceName": "", + "ResourceIdTemplate": "/subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/providers/Microsoft.Compute/virtualMachines/{vmName}", + "Severity": "high", + "ResourceType": "Microsoft.Compute/virtualMachines", + "Description": "Ensure that Azure Linux-based virtual machines are configured to use SSH keys by disabling password authentication.", + "Risk": "Allowing password-based SSH authentication increases the risk of brute-force attacks and unauthorized access. Enforcing SSH key authentication ensures only users with the private key can access the VM.", + "RelatedUrl": "https://docs.microsoft.com/en-us/azure/virtual-machines/linux/create-ssh-keys-detailed", + "Remediation": { + "Code": { + "CLI": "", + "NativeIaC": "", + "Other": "https://www.trendmicro.com/cloudoneconformity/knowledge-base/azure/VirtualMachines/ssh-authentication-type.html", + "Terraform": "" + }, + "Recommendation": { + "Text": "Recreate Linux VMs with SSH key authentication enabled and password authentication disabled.", + "Url": "https://docs.microsoft.com/en-us/azure/virtual-machines/linux/create-ssh-keys-detailed" + } + }, + "Categories": [], + "DependsOn": [], + "RelatedTo": [], + "Notes": "" +} diff --git a/prowler/providers/azure/services/vm/vm_linux_enforce_ssh_authentication/vm_linux_enforce_ssh_authentication.py b/prowler/providers/azure/services/vm/vm_linux_enforce_ssh_authentication/vm_linux_enforce_ssh_authentication.py new file mode 100644 index 0000000000..c31b2c9a18 --- /dev/null +++ b/prowler/providers/azure/services/vm/vm_linux_enforce_ssh_authentication/vm_linux_enforce_ssh_authentication.py @@ -0,0 +1,29 @@ +from prowler.lib.check.models import Check, Check_Report_Azure +from prowler.providers.azure.services.vm.vm_client import vm_client + + +class vm_linux_enforce_ssh_authentication(Check): + """ + Ensure that Azure Linux-based virtual machines are configured to use SSH keys (password authentication is disabled). + + This check evaluates whether disablePasswordAuthentication is set to True for Linux VMs to ensure only SSH key authentication is allowed. + - PASS: VM has password authentication disabled (SSH key authentication enforced). + - FAIL: VM has password authentication enabled (password-based SSH allowed). + """ + + def execute(self) -> list[Check_Report_Azure]: + findings = [] + for subscription_name, vms in vm_client.virtual_machines.items(): + for vm in vms.values(): + if vm.linux_configuration: + report = Check_Report_Azure(metadata=self.metadata(), resource=vm) + report.subscription = subscription_name + + if vm.linux_configuration.disable_password_authentication: + report.status = "PASS" + report.status_extended = f"VM {vm.resource_name} in subscription {subscription_name} has password authentication disabled (SSH key authentication enforced)." + else: + report.status = "FAIL" + report.status_extended = f"VM {vm.resource_name} in subscription {subscription_name} has password authentication enabled (password-based SSH allowed)." + 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 a30379d9bc..ffa80f8e07 100644 --- a/prowler/providers/azure/services/vm/vm_service.py +++ b/prowler/providers/azure/services/vm/vm_service.py @@ -62,6 +62,18 @@ class VirtualMachines(AzureService): if extension ] + # Collect LinuxConfiguration.disablePasswordAuthentication if available + linux_configuration = None + os_profile = getattr(vm, "os_profile", None) + if os_profile: + linux_conf = getattr(os_profile, "linux_configuration", None) + if linux_conf: + linux_configuration = LinuxConfiguration( + disable_password_authentication=getattr( + linux_conf, "disable_password_authentication", False + ) + ) + virtual_machines[subscription_name].update( { vm.id: VirtualMachine( @@ -92,6 +104,7 @@ class VirtualMachines(AzureService): location=vm.location, security_profile=getattr(vm, "security_profile", None), extensions=extensions, + linux_configuration=linux_configuration, ) } ) @@ -180,6 +193,10 @@ class VirtualMachineExtension(BaseModel): id: str +class LinuxConfiguration(BaseModel): + disable_password_authentication: bool + + class VirtualMachine(BaseModel): resource_id: str resource_name: str @@ -187,6 +204,7 @@ class VirtualMachine(BaseModel): security_profile: Optional[SecurityProfile] extensions: list[VirtualMachineExtension] storage_profile: Optional[StorageProfile] = None + linux_configuration: Optional[LinuxConfiguration] = None class Disk(BaseModel): diff --git a/tests/providers/azure/services/vm/vm_ensure_using_managed_disks/vm_ensure_using_managed_disks_test.py b/tests/providers/azure/services/vm/vm_ensure_using_managed_disks/vm_ensure_using_managed_disks_test.py index b73075337e..3c494d861c 100644 --- a/tests/providers/azure/services/vm/vm_ensure_using_managed_disks/vm_ensure_using_managed_disks_test.py +++ b/tests/providers/azure/services/vm/vm_ensure_using_managed_disks/vm_ensure_using_managed_disks_test.py @@ -86,6 +86,7 @@ class Test_vm_ensure_using_managed_disks: ), data_disks=[], ), + linux_configuration=None, ), } } @@ -142,6 +143,7 @@ class Test_vm_ensure_using_managed_disks: ), data_disks=[], ), + linux_configuration=None, ) } } @@ -200,6 +202,7 @@ class Test_vm_ensure_using_managed_disks: DataDisk(lun=0, name="data_disk_1", managed_disk=None) ], ), + linux_configuration=None, ) } } diff --git a/tests/providers/azure/services/vm/vm_linux_enforce_ssh_authentication/vm_linux_enforce_ssh_authentication_test.py b/tests/providers/azure/services/vm/vm_linux_enforce_ssh_authentication/vm_linux_enforce_ssh_authentication_test.py new file mode 100644 index 0000000000..5d400ac7bf --- /dev/null +++ b/tests/providers/azure/services/vm/vm_linux_enforce_ssh_authentication/vm_linux_enforce_ssh_authentication_test.py @@ -0,0 +1,173 @@ +from unittest import mock +from uuid import uuid4 + +from prowler.providers.azure.services.vm.vm_service import ( + LinuxConfiguration, + VirtualMachine, +) +from tests.providers.azure.azure_fixtures import ( + AZURE_SUBSCRIPTION_ID, + set_mocked_azure_provider, +) + + +class Test_vm_linux_enforce_ssh_authentication: + 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_linux_enforce_ssh_authentication.vm_linux_enforce_ssh_authentication.vm_client", + new=vm_client, + ), + ): + from prowler.providers.azure.services.vm.vm_linux_enforce_ssh_authentication.vm_linux_enforce_ssh_authentication import ( + vm_linux_enforce_ssh_authentication, + ) + + check = vm_linux_enforce_ssh_authentication() + result = check.execute() + assert len(result) == 0 + + def test_empty_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_linux_enforce_ssh_authentication.vm_linux_enforce_ssh_authentication.vm_client", + new=vm_client, + ), + ): + from prowler.providers.azure.services.vm.vm_linux_enforce_ssh_authentication.vm_linux_enforce_ssh_authentication import ( + vm_linux_enforce_ssh_authentication, + ) + + check = vm_linux_enforce_ssh_authentication() + result = check.execute() + assert len(result) == 0 + + def test_linux_vm_password_auth_disabled(self): + vm_id = str(uuid4()) + vm_client = mock.MagicMock + vm_client.virtual_machines = { + AZURE_SUBSCRIPTION_ID: { + vm_id: VirtualMachine( + resource_id=vm_id, + resource_name="LinuxVM", + location="westeurope", + security_profile=None, + extensions=[], + storage_profile=None, + linux_configuration=LinuxConfiguration( + disable_password_authentication=True + ), + ) + } + } + 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_linux_enforce_ssh_authentication.vm_linux_enforce_ssh_authentication.vm_client", + new=vm_client, + ), + ): + from prowler.providers.azure.services.vm.vm_linux_enforce_ssh_authentication.vm_linux_enforce_ssh_authentication import ( + vm_linux_enforce_ssh_authentication, + ) + + check = vm_linux_enforce_ssh_authentication() + result = check.execute() + assert len(result) == 1 + assert result[0].status == "PASS" + assert result[0].subscription == AZURE_SUBSCRIPTION_ID + assert result[0].resource_name == "LinuxVM" + assert result[0].resource_id == vm_id + assert "password authentication disabled" in result[0].status_extended + + def test_linux_vm_password_auth_enabled(self): + vm_id = str(uuid4()) + vm_client = mock.MagicMock + vm_client.virtual_machines = { + AZURE_SUBSCRIPTION_ID: { + vm_id: VirtualMachine( + resource_id=vm_id, + resource_name="LinuxVM", + location="westeurope", + security_profile=None, + extensions=[], + storage_profile=None, + linux_configuration=LinuxConfiguration( + disable_password_authentication=False + ), + ) + } + } + 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_linux_enforce_ssh_authentication.vm_linux_enforce_ssh_authentication.vm_client", + new=vm_client, + ), + ): + from prowler.providers.azure.services.vm.vm_linux_enforce_ssh_authentication.vm_linux_enforce_ssh_authentication import ( + vm_linux_enforce_ssh_authentication, + ) + + check = vm_linux_enforce_ssh_authentication() + result = check.execute() + assert len(result) == 1 + assert result[0].status == "FAIL" + assert result[0].subscription == AZURE_SUBSCRIPTION_ID + assert result[0].resource_name == "LinuxVM" + assert result[0].resource_id == vm_id + assert "password authentication enabled" in result[0].status_extended + + def test_non_linux_vm(self): + vm_id = str(uuid4()) + vm_client = mock.MagicMock + vm_client.virtual_machines = { + AZURE_SUBSCRIPTION_ID: { + vm_id: VirtualMachine( + resource_id=vm_id, + resource_name="WindowsVM", + location="westeurope", + security_profile=None, + extensions=[], + storage_profile=None, + linux_configuration=None, # Not a Linux 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_linux_enforce_ssh_authentication.vm_linux_enforce_ssh_authentication.vm_client", + new=vm_client, + ), + ): + from prowler.providers.azure.services.vm.vm_linux_enforce_ssh_authentication.vm_linux_enforce_ssh_authentication import ( + vm_linux_enforce_ssh_authentication, + ) + + check = vm_linux_enforce_ssh_authentication() + result = check.execute() + assert len(result) == 0 diff --git a/tests/providers/azure/services/vm/vm_service_test.py b/tests/providers/azure/services/vm/vm_service_test.py index 78aaccda2a..6a3144a5c9 100644 --- a/tests/providers/azure/services/vm/vm_service_test.py +++ b/tests/providers/azure/services/vm/vm_service_test.py @@ -2,6 +2,7 @@ from unittest.mock import patch from prowler.providers.azure.services.vm.vm_service import ( Disk, + LinuxConfiguration, ManagedDiskParameters, OperatingSystemType, OSDisk, @@ -40,6 +41,7 @@ def mock_vm_get_virtual_machines(_): ), data_disks=[], ), + linux_configuration=None, ) } } @@ -55,6 +57,7 @@ def mock_vm_get_virtual_machines_with_none(_): security_profile=None, extensions=[], storage_profile=None, + linux_configuration=None, ), "vm_id-2": VirtualMachine( resource_id="/subscriptions/resource_id2", @@ -66,6 +69,7 @@ def mock_vm_get_virtual_machines_with_none(_): os_disk=None, data_disks=[], ), + linux_configuration=None, ), } } @@ -85,6 +89,24 @@ def mock_vm_get_disks(_): } +def mock_vm_get_virtual_machines_with_linux(_): + return { + AZURE_SUBSCRIPTION_ID: { + "vm_id-linux": VirtualMachine( + resource_id="/subscriptions/resource_id_linux", + resource_name="LinuxVM", + location="location", + security_profile=None, + extensions=[], + storage_profile=None, + linux_configuration=LinuxConfiguration( + disable_password_authentication=True + ), + ) + } + } + + @patch( "prowler.providers.azure.services.vm.vm_service.VirtualMachines._get_virtual_machines", new=mock_vm_get_virtual_machines, @@ -186,3 +208,14 @@ class Test_VirtualMachines_NoneCases: assert vm_2.storage_profile.os_disk is None assert vm_2.storage_profile.data_disks == [] assert vm_2.resource_name == "VMWithPartialNone" + + +@patch( + "prowler.providers.azure.services.vm.vm_service.VirtualMachines._get_virtual_machines", + new=mock_vm_get_virtual_machines_with_linux, +) +def test_virtual_machine_with_linux_configuration(): + virtual_machines = VirtualMachines(set_mocked_azure_provider()) + vm = virtual_machines.virtual_machines[AZURE_SUBSCRIPTION_ID]["vm_id-linux"] + assert vm.linux_configuration is not None + assert vm.linux_configuration.disable_password_authentication is True