From be19ec53bdacc18c22013b450225bdf890ae23bf Mon Sep 17 00:00:00 2001 From: Hugo966 <148140670+Hugo966@users.noreply.github.com> Date: Tue, 2 Apr 2024 13:00:42 +0200 Subject: [PATCH] feat(azure): Check related with roles and vm access with MFA (#3638) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Hugo Gálvez Ureña Co-authored-by: Sergio --- prowler/providers/azure/config.py | 12 + .../__init__.py | 0 ...min_in_less_than_five_users.metadata.json} | 2 +- ...a_global_admin_in_less_than_five_users.py} | 2 +- .../entra_non_privileged_user_has_mfa.py | 6 +- .../entra_privileged_user_has_mfa.py | 4 +- .../__init__.py | 0 ..._user_with_vm_access_has_mfa.metadata.json | 30 +++ .../entra_user_with_vm_access_has_mfa.py | 53 +++++ .../azure/services/iam/iam_service.py | 36 +++ ...bal_admin_in_less_than_five_users_test.py} | 48 ++-- .../entra_non_privileged_user_has_mfa_test.py | 7 +- .../entra_privileged_user_has_mfa_test.py | 4 +- .../entra_user_with_vm_access_has_mfa_test.py | 221 ++++++++++++++++++ 14 files changed, 391 insertions(+), 34 deletions(-) create mode 100644 prowler/providers/azure/config.py rename prowler/providers/azure/services/entra/{entra_users_less_than_five_global_admins => entra_global_admin_in_less_than_five_users}/__init__.py (100%) rename prowler/providers/azure/services/entra/{entra_users_less_than_five_global_admins/entra_users_less_than_five_global_admins.metadata.json => entra_global_admin_in_less_than_five_users/entra_global_admin_in_less_than_five_users.metadata.json} (97%) rename prowler/providers/azure/services/entra/{entra_users_less_than_five_global_admins/entra_users_less_than_five_global_admins.py => entra_global_admin_in_less_than_five_users/entra_global_admin_in_less_than_five_users.py} (95%) create mode 100644 prowler/providers/azure/services/entra/entra_user_with_vm_access_has_mfa/__init__.py create mode 100644 prowler/providers/azure/services/entra/entra_user_with_vm_access_has_mfa/entra_user_with_vm_access_has_mfa.metadata.json create mode 100644 prowler/providers/azure/services/entra/entra_user_with_vm_access_has_mfa/entra_user_with_vm_access_has_mfa.py rename tests/providers/azure/services/entra/{entra_users_less_than_five_global_admins/entra_users_less_than_five_global_admins_test.py => entra_global_admin_in_less_than_five_users/entra_global_admin_in_less_than_five_users_test.py} (70%) create mode 100644 tests/providers/azure/services/entra/entra_user_with_vm_access_has_mfa/entra_user_with_vm_access_has_mfa_test.py diff --git a/prowler/providers/azure/config.py b/prowler/providers/azure/config.py new file mode 100644 index 0000000000..9f4218a312 --- /dev/null +++ b/prowler/providers/azure/config.py @@ -0,0 +1,12 @@ +# General built-in roles +CONTRIBUTOR_ROLE_ID = "b24988ac-6180-42a0-ab88-20f7382dd24c" +OWNER_ROLE_ID = "8e3af657-a8ff-443c-a75c-2fe8c4bcb635" + +# Compute roles +VIRTUAL_MACHINE_CONTRIBUTOR_ROLE_ID = "9980e02c-c2be-4d73-94e8-173b1dc7cf3c" +VIRTUAL_MACHINE_ADMINISTRATOR_LOGIN_ROLE_ID = "1c0163c0-47e6-4577-8991-ea5c82e286e4" +VIRTUAL_MACHINE_USER_LOGIN_ROLE_ID = "fb879df8-f326-4884-b1cf-06f3ad86be52" +VIRTUAL_MACHINE_LOCAL_USER_LOGIN_ROLE_ID = "602da2ba-a5c2-41da-b01d-5360126ab525" +WINDOWS_ADMIN_CENTER_ADMINISTRATOR_LOGIN_ROLE_ID = ( + "a6333a3e-0164-44c3-b281-7a577aff287f" +) diff --git a/prowler/providers/azure/services/entra/entra_users_less_than_five_global_admins/__init__.py b/prowler/providers/azure/services/entra/entra_global_admin_in_less_than_five_users/__init__.py similarity index 100% rename from prowler/providers/azure/services/entra/entra_users_less_than_five_global_admins/__init__.py rename to prowler/providers/azure/services/entra/entra_global_admin_in_less_than_five_users/__init__.py diff --git a/prowler/providers/azure/services/entra/entra_users_less_than_five_global_admins/entra_users_less_than_five_global_admins.metadata.json b/prowler/providers/azure/services/entra/entra_global_admin_in_less_than_five_users/entra_global_admin_in_less_than_five_users.metadata.json similarity index 97% rename from prowler/providers/azure/services/entra/entra_users_less_than_five_global_admins/entra_users_less_than_five_global_admins.metadata.json rename to prowler/providers/azure/services/entra/entra_global_admin_in_less_than_five_users/entra_global_admin_in_less_than_five_users.metadata.json index b966c120c2..4941e3121b 100644 --- a/prowler/providers/azure/services/entra/entra_users_less_than_five_global_admins/entra_users_less_than_five_global_admins.metadata.json +++ b/prowler/providers/azure/services/entra/entra_global_admin_in_less_than_five_users/entra_global_admin_in_less_than_five_users.metadata.json @@ -1,6 +1,6 @@ { "Provider": "azure", - "CheckID": "entra_users_less_than_five_global_admins", + "CheckID": "entra_global_admin_in_less_than_five_users", "CheckTitle": "Ensure fewer than 5 users have global administrator assignment", "CheckType": [], "ServiceName": "entra", diff --git a/prowler/providers/azure/services/entra/entra_users_less_than_five_global_admins/entra_users_less_than_five_global_admins.py b/prowler/providers/azure/services/entra/entra_global_admin_in_less_than_five_users/entra_global_admin_in_less_than_five_users.py similarity index 95% rename from prowler/providers/azure/services/entra/entra_users_less_than_five_global_admins/entra_users_less_than_five_global_admins.py rename to prowler/providers/azure/services/entra/entra_global_admin_in_less_than_five_users/entra_global_admin_in_less_than_five_users.py index 500cb1a9aa..8831b18c07 100644 --- a/prowler/providers/azure/services/entra/entra_users_less_than_five_global_admins/entra_users_less_than_five_global_admins.py +++ b/prowler/providers/azure/services/entra/entra_global_admin_in_less_than_five_users/entra_global_admin_in_less_than_five_users.py @@ -2,7 +2,7 @@ from prowler.lib.check.models import Check, Check_Report_Azure from prowler.providers.azure.services.entra.entra_client import entra_client -class entra_users_less_than_five_global_admins(Check): +class entra_global_admin_in_less_than_five_users(Check): def execute(self) -> Check_Report_Azure: findings = [] diff --git a/prowler/providers/azure/services/entra/entra_non_privileged_user_has_mfa/entra_non_privileged_user_has_mfa.py b/prowler/providers/azure/services/entra/entra_non_privileged_user_has_mfa/entra_non_privileged_user_has_mfa.py index 48c7a43c44..c2eadfffd6 100644 --- a/prowler/providers/azure/services/entra/entra_non_privileged_user_has_mfa/entra_non_privileged_user_has_mfa.py +++ b/prowler/providers/azure/services/entra/entra_non_privileged_user_has_mfa/entra_non_privileged_user_has_mfa.py @@ -20,12 +20,14 @@ class entra_non_privileged_user_has_mfa(Check): report.resource_name = user_domain_name report.resource_id = user.id report.status_extended = ( - f"User '{user.name}' does not have MFA enabled." + f"Non-privileged user {user.name} does not have MFA." ) if len(user.authentication_methods) > 1: report.status = "PASS" - report.status_extended = f"User '{user.name}' has MFA enabled." + report.status_extended = ( + f"Non-privileged user {user.name} has MFA." + ) findings.append(report) diff --git a/prowler/providers/azure/services/entra/entra_privileged_user_has_mfa/entra_privileged_user_has_mfa.py b/prowler/providers/azure/services/entra/entra_privileged_user_has_mfa/entra_privileged_user_has_mfa.py index cd1a69cd41..807e982747 100644 --- a/prowler/providers/azure/services/entra/entra_privileged_user_has_mfa/entra_privileged_user_has_mfa.py +++ b/prowler/providers/azure/services/entra/entra_privileged_user_has_mfa/entra_privileged_user_has_mfa.py @@ -20,12 +20,12 @@ class entra_privileged_user_has_mfa(Check): report.resource_name = user_domain_name report.resource_id = user.id report.status_extended = ( - f"User '{user.name}' does not have MFA enabled." + f"Privileged user {user.name} does not have MFA." ) if len(user.authentication_methods) > 1: report.status = "PASS" - report.status_extended = f"User '{user.name}' has MFA enabled." + report.status_extended = f"Privileged user {user.name} has MFA." findings.append(report) diff --git a/prowler/providers/azure/services/entra/entra_user_with_vm_access_has_mfa/__init__.py b/prowler/providers/azure/services/entra/entra_user_with_vm_access_has_mfa/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/prowler/providers/azure/services/entra/entra_user_with_vm_access_has_mfa/entra_user_with_vm_access_has_mfa.metadata.json b/prowler/providers/azure/services/entra/entra_user_with_vm_access_has_mfa/entra_user_with_vm_access_has_mfa.metadata.json new file mode 100644 index 0000000000..e1eb406681 --- /dev/null +++ b/prowler/providers/azure/services/entra/entra_user_with_vm_access_has_mfa/entra_user_with_vm_access_has_mfa.metadata.json @@ -0,0 +1,30 @@ +{ + "Provider": "azure", + "CheckID": "entra_user_with_vm_access_has_mfa", + "CheckTitle": "Ensure only MFA enabled identities can access privileged Virtual Machine", + "CheckType": [], + "ServiceName": "iam", + "SubServiceName": "", + "ResourceIdTemplate": "", + "Severity": "medium", + "ResourceType": "#microsoft.graph.users", + "Description": "Verify identities without MFA that can log in to a privileged virtual machine using separate login credentials. An adversary can leverage the access to move laterally and perform actions with the virtual machine's managed identity. Make sure the virtual machine only has necessary permissions, and revoke the admin-level permissions according to the least privileges principal", + "Risk": "Managed disks are by default encrypted on the underlying hardware, so no additional encryption is required for basic protection. It is available if additional encryption is required. Managed disks are by design more resilient that storage accounts. For ARM-deployed Virtual Machines, Azure Adviser will at some point recommend moving VHDs to managed disks both from a security and cost management perspective.", + "RelatedUrl": "", + "Remediation": { + "Code": { + "CLI": "", + "NativeIaC": "", + "Other": "", + "Terraform": "" + }, + "Recommendation": { + "Text": "1. Log in to the Azure portal. Reducing access of managed identities attached to virtual machines. 2. This can be remediated by enabling MFA for user, Removing user access or • Case I : Enable MFA for users having access on virtual machines. 1. Navigate to Azure AD from the left pane and select Users from the Manage section. 2. Click on Per-User MFA from the top menu options and select each user with MULTI-FACTOR AUTH STATUS as Disabled and can login to virtual machines:  From quick steps on the right side select enable.  Click on enable multi-factor auth and share the link with the user to setup MFA as required. • Case II : Removing user access on a virtual machine. 1. Select the Subscription, then click on Access control (IAM). 2. Select Role assignments and search for Virtual Machine Administrator Login or Virtual Machine User Login or any role that provides access to log into virtual machines. 3. Click on Role Name, Select Assignments, and remove identities with no MFA configured. • Case III : Reducing access of managed identities attached to virtual machines. 1. Select the Subscription, then click on Access control (IAM). 2. Select Role Assignments from the top menu and apply filters on Assignment type as Privileged administrator roles and Type as Virtual Machines. 3. Click on Role Name, Select Assignments, and remove identities access make sure this follows the least privileges principal.", + "Url": "" + } + }, + "Categories": [], + "DependsOn": [], + "RelatedTo": [], + "Notes": "This recommendation requires an Azure AD P2 License to implement. Ensure that identities that are provisioned to a virtual machine utilizes an RBAC/ABAC group and is allocated a role using Azure PIM, and the Role settings require MFA or use another PAM solution (like CyberArk) for accessing Virtual Machines." +} diff --git a/prowler/providers/azure/services/entra/entra_user_with_vm_access_has_mfa/entra_user_with_vm_access_has_mfa.py b/prowler/providers/azure/services/entra/entra_user_with_vm_access_has_mfa/entra_user_with_vm_access_has_mfa.py new file mode 100644 index 0000000000..a3fc52e22f --- /dev/null +++ b/prowler/providers/azure/services/entra/entra_user_with_vm_access_has_mfa/entra_user_with_vm_access_has_mfa.py @@ -0,0 +1,53 @@ +from prowler.lib.check.models import Check, Check_Report_Azure +from prowler.providers.azure.config import ( + CONTRIBUTOR_ROLE_ID, + OWNER_ROLE_ID, + VIRTUAL_MACHINE_ADMINISTRATOR_LOGIN_ROLE_ID, + VIRTUAL_MACHINE_CONTRIBUTOR_ROLE_ID, + VIRTUAL_MACHINE_LOCAL_USER_LOGIN_ROLE_ID, + VIRTUAL_MACHINE_USER_LOGIN_ROLE_ID, + WINDOWS_ADMIN_CENTER_ADMINISTRATOR_LOGIN_ROLE_ID, +) +from prowler.providers.azure.services.entra.entra_client import entra_client +from prowler.providers.azure.services.iam.iam_client import iam_client + + +class entra_user_with_vm_access_has_mfa(Check): + def execute(self) -> Check_Report_Azure: + findings = [] + + for users in entra_client.users.values(): + for user_domain_name, user in users.items(): + for ( + subscription_name, + role_assigns, + ) in iam_client.role_assignments.items(): + for assignment in role_assigns.values(): + if ( + assignment.agent_type == "User" + and assignment.role_id + in [ + CONTRIBUTOR_ROLE_ID, + OWNER_ROLE_ID, + VIRTUAL_MACHINE_CONTRIBUTOR_ROLE_ID, + VIRTUAL_MACHINE_ADMINISTRATOR_LOGIN_ROLE_ID, + VIRTUAL_MACHINE_USER_LOGIN_ROLE_ID, + VIRTUAL_MACHINE_LOCAL_USER_LOGIN_ROLE_ID, + WINDOWS_ADMIN_CENTER_ADMINISTRATOR_LOGIN_ROLE_ID, + ] + and assignment.agent_id == user.id + ): + report = Check_Report_Azure(self.metadata()) + report.status = "FAIL" + report.status_extended = f"User {user.name} without MFA can access VMs in subscription {subscription_name}" + report.subscription = subscription_name + report.resource_name = user_domain_name + report.resource_id = user.id + + if len(user.authentication_methods) > 1: + report.status = "PASS" + report.status_extended = f"User {user.name} can access VMs in subscription {subscription_name} but it has MFA." + + findings.append(report) + + return findings diff --git a/prowler/providers/azure/services/iam/iam_service.py b/prowler/providers/azure/services/iam/iam_service.py index 73c3c4a609..04115bc5e8 100644 --- a/prowler/providers/azure/services/iam/iam_service.py +++ b/prowler/providers/azure/services/iam/iam_service.py @@ -12,6 +12,7 @@ class IAM(AzureService): def __init__(self, audit_info): super().__init__(AuthorizationManagementClient, audit_info) self.roles, self.custom_roles = self.__get_roles__() + self.role_assignments = self.__get_role_assignments__() def __get_roles__(self): logger.info("IAM - Getting roles...") @@ -52,6 +53,34 @@ class IAM(AzureService): ) return builtin_roles, custom_roles + def __get_role_assignments__(self): + logger.info("IAM - Getting role assignments...") + role_assignments = {} + for subscription, client in self.clients.items(): + try: + role_assignments.update({subscription: {}}) + all_role_assignments = client.role_assignments.list_for_subscription( + filter="atScope()" + ) + for role_assignment in all_role_assignments: + role_assignments[subscription].update( + { + role_assignment.id: RoleAssignment( + agent_id=role_assignment.principal_id, + agent_type=role_assignment.principal_type, + role_id=role_assignment.role_definition_id.split("/")[ + -1 + ], + ) + } + ) + except Exception as error: + logger.error(f"Subscription name: {subscription}") + logger.error( + f"{error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}" + ) + return role_assignments + @dataclass class Role: @@ -60,3 +89,10 @@ class Role: type: str assignable_scopes: list[str] permissions: list[Permission] + + +@dataclass +class RoleAssignment: + agent_id: str + agent_type: str + role_id: str diff --git a/tests/providers/azure/services/entra/entra_users_less_than_five_global_admins/entra_users_less_than_five_global_admins_test.py b/tests/providers/azure/services/entra/entra_global_admin_in_less_than_five_users/entra_global_admin_in_less_than_five_users_test.py similarity index 70% rename from tests/providers/azure/services/entra/entra_users_less_than_five_global_admins/entra_users_less_than_five_global_admins_test.py rename to tests/providers/azure/services/entra/entra_global_admin_in_less_than_five_users/entra_global_admin_in_less_than_five_users_test.py index b777bb1122..c46d73f4a0 100644 --- a/tests/providers/azure/services/entra/entra_users_less_than_five_global_admins/entra_users_less_than_five_global_admins_test.py +++ b/tests/providers/azure/services/entra/entra_global_admin_in_less_than_five_users/entra_global_admin_in_less_than_five_users_test.py @@ -4,21 +4,21 @@ from uuid import uuid4 from tests.providers.azure.azure_fixtures import DOMAIN -class Test_entra_users_less_than_five_global_admins: +class Test_entra_global_admin_in_less_than_five_users: def test_entra_no_tenants(self): entra_client = mock.MagicMock with mock.patch( - "prowler.providers.azure.services.entra.entra_users_less_than_five_global_admins.entra_users_less_than_five_global_admins.entra_client", + "prowler.providers.azure.services.entra.entra_global_admin_in_less_than_five_users.entra_global_admin_in_less_than_five_users.entra_client", new=entra_client, ): - from prowler.providers.azure.services.entra.entra_users_less_than_five_global_admins.entra_users_less_than_five_global_admins import ( - entra_users_less_than_five_global_admins, + from prowler.providers.azure.services.entra.entra_global_admin_in_less_than_five_users.entra_global_admin_in_less_than_five_users import ( + entra_global_admin_in_less_than_five_users, ) entra_client.directory_roles = {} - check = entra_users_less_than_five_global_admins() + check = entra_global_admin_in_less_than_five_users() result = check.execute() assert len(result) == 0 @@ -26,16 +26,16 @@ class Test_entra_users_less_than_five_global_admins: entra_client = mock.MagicMock with mock.patch( - "prowler.providers.azure.services.entra.entra_users_less_than_five_global_admins.entra_users_less_than_five_global_admins.entra_client", + "prowler.providers.azure.services.entra.entra_global_admin_in_less_than_five_users.entra_global_admin_in_less_than_five_users.entra_client", new=entra_client, ): - from prowler.providers.azure.services.entra.entra_users_less_than_five_global_admins.entra_users_less_than_five_global_admins import ( - entra_users_less_than_five_global_admins, + from prowler.providers.azure.services.entra.entra_global_admin_in_less_than_five_users.entra_global_admin_in_less_than_five_users import ( + entra_global_admin_in_less_than_five_users, ) entra_client.directory_roles = {DOMAIN: {}} - check = entra_users_less_than_five_global_admins() + check = entra_global_admin_in_less_than_five_users() result = check.execute() assert len(result) == 0 @@ -43,16 +43,16 @@ class Test_entra_users_less_than_five_global_admins: entra_client = mock.MagicMock with mock.patch( - "prowler.providers.azure.services.entra.entra_users_less_than_five_global_admins.entra_users_less_than_five_global_admins.entra_client", + "prowler.providers.azure.services.entra.entra_global_admin_in_less_than_five_users.entra_global_admin_in_less_than_five_users.entra_client", new=entra_client, ): + from prowler.providers.azure.services.entra.entra_global_admin_in_less_than_five_users.entra_global_admin_in_less_than_five_users import ( + entra_global_admin_in_less_than_five_users, + ) from prowler.providers.azure.services.entra.entra_service import ( DirectoryRole, User, ) - from prowler.providers.azure.services.entra.entra_users_less_than_five_global_admins.entra_users_less_than_five_global_admins import ( - entra_users_less_than_five_global_admins, - ) id = str(uuid4()) id_user1 = str(uuid4()) @@ -70,7 +70,7 @@ class Test_entra_users_less_than_five_global_admins: } } - check = entra_users_less_than_five_global_admins() + check = entra_global_admin_in_less_than_five_users() result = check.execute() assert len(result) == 1 assert result[0].status == "PASS" @@ -83,16 +83,16 @@ class Test_entra_users_less_than_five_global_admins: entra_client = mock.MagicMock with mock.patch( - "prowler.providers.azure.services.entra.entra_users_less_than_five_global_admins.entra_users_less_than_five_global_admins.entra_client", + "prowler.providers.azure.services.entra.entra_global_admin_in_less_than_five_users.entra_global_admin_in_less_than_five_users.entra_client", new=entra_client, ): + from prowler.providers.azure.services.entra.entra_global_admin_in_less_than_five_users.entra_global_admin_in_less_than_five_users import ( + entra_global_admin_in_less_than_five_users, + ) from prowler.providers.azure.services.entra.entra_service import ( DirectoryRole, User, ) - from prowler.providers.azure.services.entra.entra_users_less_than_five_global_admins.entra_users_less_than_five_global_admins import ( - entra_users_less_than_five_global_admins, - ) id = str(uuid4()) id_user1 = str(uuid4()) @@ -118,7 +118,7 @@ class Test_entra_users_less_than_five_global_admins: } } - check = entra_users_less_than_five_global_admins() + check = entra_global_admin_in_less_than_five_users() result = check.execute() assert len(result) == 1 assert result[0].status == "FAIL" @@ -134,16 +134,16 @@ class Test_entra_users_less_than_five_global_admins: entra_client = mock.MagicMock with mock.patch( - "prowler.providers.azure.services.entra.entra_users_less_than_five_global_admins.entra_users_less_than_five_global_admins.entra_client", + "prowler.providers.azure.services.entra.entra_global_admin_in_less_than_five_users.entra_global_admin_in_less_than_five_users.entra_client", new=entra_client, ): + from prowler.providers.azure.services.entra.entra_global_admin_in_less_than_five_users.entra_global_admin_in_less_than_five_users import ( + entra_global_admin_in_less_than_five_users, + ) from prowler.providers.azure.services.entra.entra_service import ( DirectoryRole, User, ) - from prowler.providers.azure.services.entra.entra_users_less_than_five_global_admins.entra_users_less_than_five_global_admins import ( - entra_users_less_than_five_global_admins, - ) id = str(uuid4()) id_user1 = str(uuid4()) @@ -167,7 +167,7 @@ class Test_entra_users_less_than_five_global_admins: } } - check = entra_users_less_than_five_global_admins() + check = entra_global_admin_in_less_than_five_users() result = check.execute() assert len(result) == 1 assert result[0].status == "FAIL" diff --git a/tests/providers/azure/services/entra/entra_non_privileged_user_has_mfa/entra_non_privileged_user_has_mfa_test.py b/tests/providers/azure/services/entra/entra_non_privileged_user_has_mfa/entra_non_privileged_user_has_mfa_test.py index db1014b1a1..80fbfc3834 100644 --- a/tests/providers/azure/services/entra/entra_non_privileged_user_has_mfa/entra_non_privileged_user_has_mfa_test.py +++ b/tests/providers/azure/services/entra/entra_non_privileged_user_has_mfa/entra_non_privileged_user_has_mfa_test.py @@ -68,7 +68,10 @@ class Test_entra_non_privileged_user_has_mfa: result = check.execute() assert len(result) == 1 assert result[0].status == "FAIL" - assert result[0].status_extended == "User 'foo' does not have MFA enabled." + assert ( + result[0].status_extended + == "Non-privileged user foo does not have MFA." + ) assert result[0].resource_name == f"foo@{DOMAIN}" assert result[0].resource_id == user_id assert result[0].subscription == f"Tenant: {DOMAIN}" @@ -102,7 +105,7 @@ class Test_entra_non_privileged_user_has_mfa: result = check.execute() assert len(result) == 1 assert result[0].status == "PASS" - assert result[0].status_extended == "User 'foo' has MFA enabled." + assert result[0].status_extended == "Non-privileged user foo has MFA." assert result[0].resource_name == f"foo@{DOMAIN}" assert result[0].resource_id == user_id assert result[0].subscription == f"Tenant: {DOMAIN}" diff --git a/tests/providers/azure/services/entra/entra_privileged_user_has_mfa/entra_privileged_user_has_mfa_test.py b/tests/providers/azure/services/entra/entra_privileged_user_has_mfa/entra_privileged_user_has_mfa_test.py index 0273449036..0788a0c9c8 100644 --- a/tests/providers/azure/services/entra/entra_privileged_user_has_mfa/entra_privileged_user_has_mfa_test.py +++ b/tests/providers/azure/services/entra/entra_privileged_user_has_mfa/entra_privileged_user_has_mfa_test.py @@ -128,7 +128,7 @@ class Test_entra_privileged_user_has_mfa: result = check.execute() assert len(result) == 1 assert result[0].status == "FAIL" - assert result[0].status_extended == "User 'foo' does not have MFA enabled." + assert result[0].status_extended == "Privileged user foo does not have MFA." assert result[0].resource_name == f"foo@{DOMAIN}" assert result[0].resource_id == user_id assert result[0].subscription == f"Tenant: {DOMAIN}" @@ -164,7 +164,7 @@ class Test_entra_privileged_user_has_mfa: result = check.execute() assert len(result) == 1 assert result[0].status == "PASS" - assert result[0].status_extended == "User 'foo' has MFA enabled." + assert result[0].status_extended == "Privileged user foo has MFA." assert result[0].resource_name == f"foo@{DOMAIN}" assert result[0].resource_id == user_id assert result[0].subscription == f"Tenant: {DOMAIN}" diff --git a/tests/providers/azure/services/entra/entra_user_with_vm_access_has_mfa/entra_user_with_vm_access_has_mfa_test.py b/tests/providers/azure/services/entra/entra_user_with_vm_access_has_mfa/entra_user_with_vm_access_has_mfa_test.py new file mode 100644 index 0000000000..25d69b51d0 --- /dev/null +++ b/tests/providers/azure/services/entra/entra_user_with_vm_access_has_mfa/entra_user_with_vm_access_has_mfa_test.py @@ -0,0 +1,221 @@ +from unittest import mock +from uuid import uuid4 + +from prowler.providers.azure.config import VIRTUAL_MACHINE_ADMINISTRATOR_LOGIN_ROLE_ID +from tests.providers.azure.azure_fixtures import AZURE_SUBSCRIPTION, DOMAIN + + +class Test_iam_assignment_priviledge_access_vm_has_mfa: + def test_iam_no_roles(self): + iam_client = mock.MagicMock + entra_client = mock.MagicMock + + with mock.patch( + "prowler.providers.azure.services.entra.entra_user_with_vm_access_has_mfa.entra_user_with_vm_access_has_mfa.iam_client", + new=iam_client, + ): + from prowler.providers.azure.services.entra.entra_user_with_vm_access_has_mfa.entra_user_with_vm_access_has_mfa import ( + entra_user_with_vm_access_has_mfa, + ) + + iam_client.role_assignments = {} + entra_client.users = {} + + check = entra_user_with_vm_access_has_mfa() + result = check.execute() + assert len(result) == 0 + + def test_entra_user_with_vm_access_has_mfa(self): + iam_client = mock.MagicMock + role_assigment_id = str(uuid4()) + entra_client = mock.MagicMock + user_id = str(uuid4()) + + with mock.patch( + "prowler.providers.azure.services.entra.entra_user_with_vm_access_has_mfa.entra_user_with_vm_access_has_mfa.iam_client", + new=iam_client, + ): + with mock.patch( + "prowler.providers.azure.services.entra.entra_user_with_vm_access_has_mfa.entra_user_with_vm_access_has_mfa.entra_client", + new=entra_client, + ): + from prowler.providers.azure.services.entra.entra_service import User + from prowler.providers.azure.services.entra.entra_user_with_vm_access_has_mfa.entra_user_with_vm_access_has_mfa import ( + entra_user_with_vm_access_has_mfa, + ) + from prowler.providers.azure.services.iam.iam_service import ( + RoleAssignment, + ) + + iam_client.role_assignments = { + AZURE_SUBSCRIPTION: { + role_assigment_id: RoleAssignment( + role_id=VIRTUAL_MACHINE_ADMINISTRATOR_LOGIN_ROLE_ID, + agent_type="User", + agent_id=user_id, + ) + } + } + + entra_client.users = { + DOMAIN: { + f"test@{DOMAIN}": User( + id=user_id, + name="test", + authentication_methods=[ + "Password", + "MicrosoftAuthenticator", + ], + ) + } + } + + check = entra_user_with_vm_access_has_mfa() + result = check.execute() + assert len(result) == 1 + assert result[0].status == "PASS" + assert ( + result[0].status_extended + == f"User test can access VMs in subscription {AZURE_SUBSCRIPTION} but it has MFA." + ) + assert result[0].subscription == AZURE_SUBSCRIPTION + assert result[0].resource_name == f"test@{DOMAIN}" + assert result[0].resource_id == user_id + + def test_entra_user_with_vm_access_has_mfa_no_mfa(self): + iam_client = mock.MagicMock + role_assigment_id = str(uuid4()) + entra_client = mock.MagicMock + user_id = str(uuid4()) + + with mock.patch( + "prowler.providers.azure.services.entra.entra_user_with_vm_access_has_mfa.entra_user_with_vm_access_has_mfa.iam_client", + new=iam_client, + ): + with mock.patch( + "prowler.providers.azure.services.entra.entra_user_with_vm_access_has_mfa.entra_user_with_vm_access_has_mfa.entra_client", + new=entra_client, + ): + from prowler.providers.azure.services.entra.entra_service import User + from prowler.providers.azure.services.entra.entra_user_with_vm_access_has_mfa.entra_user_with_vm_access_has_mfa import ( + entra_user_with_vm_access_has_mfa, + ) + from prowler.providers.azure.services.iam.iam_service import ( + RoleAssignment, + ) + + iam_client.role_assignments = { + AZURE_SUBSCRIPTION: { + role_assigment_id: RoleAssignment( + role_id=VIRTUAL_MACHINE_ADMINISTRATOR_LOGIN_ROLE_ID, + agent_type="User", + agent_id=user_id, + ) + } + } + + entra_client.users = { + DOMAIN: { + f"test@{DOMAIN}": User( + id=user_id, name="test", authentication_methods=["Password"] + ) + } + } + + check = entra_user_with_vm_access_has_mfa() + result = check.execute() + assert len(result) == 1 + assert result[0].status == "FAIL" + assert ( + result[0].status_extended + == f"User test without MFA can access VMs in subscription {AZURE_SUBSCRIPTION}" + ) + assert result[0].subscription == AZURE_SUBSCRIPTION + assert result[0].resource_name == f"test@{DOMAIN}" + assert result[0].resource_id == user_id + + def test_entra_user_with_vm_access_has_mfa_no_user(self): + iam_client = mock.MagicMock + role_assigment_id = str(uuid4()) + entra_client = mock.MagicMock + user_id = str(uuid4()) + + with mock.patch( + "prowler.providers.azure.services.entra.entra_user_with_vm_access_has_mfa.entra_user_with_vm_access_has_mfa.iam_client", + new=iam_client, + ): + with mock.patch( + "prowler.providers.azure.services.entra.entra_user_with_vm_access_has_mfa.entra_user_with_vm_access_has_mfa.entra_client", + new=entra_client, + ): + from prowler.providers.azure.services.entra.entra_user_with_vm_access_has_mfa.entra_user_with_vm_access_has_mfa import ( + entra_user_with_vm_access_has_mfa, + ) + from prowler.providers.azure.services.iam.iam_service import ( + RoleAssignment, + ) + + iam_client.role_assignments = { + AZURE_SUBSCRIPTION: { + role_assigment_id: RoleAssignment( + role_id=VIRTUAL_MACHINE_ADMINISTRATOR_LOGIN_ROLE_ID, + agent_type="User", + agent_id=user_id, + ) + } + } + + entra_client.users = {DOMAIN: {}} + + check = entra_user_with_vm_access_has_mfa() + result = check.execute() + assert len(result) == 0 + + def test_entra_user_with_vm_access_has_mfa_no_role(self): + iam_client = mock.MagicMock + role_assigment_id = str(uuid4()) + entra_client = mock.MagicMock + user_id = str(uuid4()) + + with mock.patch( + "prowler.providers.azure.services.entra.entra_user_with_vm_access_has_mfa.entra_user_with_vm_access_has_mfa.iam_client", + new=iam_client, + ): + with mock.patch( + "prowler.providers.azure.services.entra.entra_user_with_vm_access_has_mfa.entra_user_with_vm_access_has_mfa.entra_client", + new=entra_client, + ): + from prowler.providers.azure.services.entra.entra_service import User + from prowler.providers.azure.services.entra.entra_user_with_vm_access_has_mfa.entra_user_with_vm_access_has_mfa import ( + entra_user_with_vm_access_has_mfa, + ) + from prowler.providers.azure.services.iam.iam_service import ( + RoleAssignment, + ) + + iam_client.role_assignments = { + AZURE_SUBSCRIPTION: { + role_assigment_id: RoleAssignment( + role_id=str(uuid4()), + agent_type="User", + agent_id=user_id, + ) + } + } + + entra_client.users = { + DOMAIN: { + f"test@{DOMAIN}": User( + id=user_id, + name="test", + authentication_methods=[ + "Password", + "MicrosoftAuthenticator", + ], + ) + } + } + + check = entra_user_with_vm_access_has_mfa() + result = check.execute() + assert len(result) == 0