diff --git a/prowler/providers/azure/services/entra/entra_conditional_access_policy_require_mfa_for_management_api/__init__.py b/prowler/providers/azure/services/entra/entra_conditional_access_policy_require_mfa_for_management_api/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/prowler/providers/azure/services/entra/entra_conditional_access_policy_require_mfa_for_management_api/entra_conditional_access_policy_require_mfa_for_management_api.metadata.json b/prowler/providers/azure/services/entra/entra_conditional_access_policy_require_mfa_for_management_api/entra_conditional_access_policy_require_mfa_for_management_api.metadata.json new file mode 100644 index 0000000000..6becc5710b --- /dev/null +++ b/prowler/providers/azure/services/entra/entra_conditional_access_policy_require_mfa_for_management_api/entra_conditional_access_policy_require_mfa_for_management_api.metadata.json @@ -0,0 +1,30 @@ +{ + "Provider": "azure", + "CheckID": "entra_conditional_access_policy_require_mfa_for_management_api", + "CheckTitle": "Ensure Multifactor Authentication is Required for Windows Azure Service Management API", + "CheckType": [], + "ServiceName": "entra", + "SubServiceName": "", + "ResourceIdTemplate": "", + "Severity": "medium", + "ResourceType": "#microsoft.graph.conditionalAccess", + "Description": "This recommendation ensures that users accessing the Windows Azure Service Management API (i.e. Azure Powershell, Azure CLI, Azure Resource Manager API, etc.) are required to use multifactor authentication (MFA) credentials when accessing resources through the Windows Azure Service Management API.", + "Risk": "Administrative access to the Windows Azure Service Management API should be secured with a higher level of scrutiny to authenticating mechanisms. Enabling multifactor authentication is recommended to reduce the potential for abuse of Administrative actions, and to prevent intruders or compromised admin credentials from changing administrative settings.", + "RelatedUrl": "https://learn.microsoft.com/en-us/entra/identity/conditional-access/howto-conditional-access-policy-azure-management", + "Remediation": { + "Code": { + "CLI": "", + "NativeIaC": "", + "Other": "", + "Terraform": "" + }, + "Recommendation": { + "Text": "1. From the Azure Admin Portal dashboard, open Microsoft Entra ID. 2. Click Security in the Entra ID blade. 3. Click Conditional Access in the Security blade. 4. Click Policies in the Conditional Access blade. 5. Click + New policy. 6. Enter a name for the policy. 7. Click the blue text under Users. 8. Under Include, select All users. 9. Under Exclude, check Users and groups. 10. Select users or groups to be exempted from this policy (e.g. break-glass emergency accounts, and non-interactive service accounts) then click the Select button. 11. Click the blue text under Target Resources. 12. Under Include, click the Select apps radio button. 13. Click the blue text under Select. 14. Check the box next to Windows Azure Service Management APIs then click the Select button. 15. Click the blue text under Grant. 16. Under Grant access check the box for Require multifactor authentication then click the Select button. 17. Before creating, set Enable policy to Report-only. 18. Click Create. After testing the policy in report-only mode, update the Enable policy setting from Report-only to On.", + "Url": "https://learn.microsoft.com/en-us/entra/identity/conditional-access/concept-conditional-access-cloud-apps" + } + }, + "Categories": [], + "DependsOn": [], + "RelatedTo": [], + "Notes": "Conditional Access policies require Microsoft Entra ID P1 or P2 licenses. Similarly, they may require additional overhead to maintain if users lose access to their MFA. Any users or groups which are granted an exception to this policy should be carefully tracked, be granted only minimal necessary privileges, and conditional access exceptions should be regularly reviewed or investigated." +} diff --git a/prowler/providers/azure/services/entra/entra_conditional_access_policy_require_mfa_for_management_api/entra_conditional_access_policy_require_mfa_for_management_api.py b/prowler/providers/azure/services/entra/entra_conditional_access_policy_require_mfa_for_management_api/entra_conditional_access_policy_require_mfa_for_management_api.py new file mode 100644 index 0000000000..954fd7d52a --- /dev/null +++ b/prowler/providers/azure/services/entra/entra_conditional_access_policy_require_mfa_for_management_api/entra_conditional_access_policy_require_mfa_for_management_api.py @@ -0,0 +1,44 @@ +from prowler.lib.check.models import Check, Check_Report_Azure +from prowler.providers.azure.services.entra.entra_client import entra_client + + +class entra_conditional_access_policy_require_mfa_for_management_api(Check): + def execute(self) -> Check_Report_Azure: + findings = [] + WINDOWS_AZURE_SERVICE_MANAGEMENT_API = "797f4846-ba00-4fd7-ba43-dac1f8f63013" + + for ( + tenant_name, + conditional_access_policies, + ) in entra_client.conditional_access_policy.items(): + report = Check_Report_Azure(self.metadata()) + report.status = "FAIL" + report.subscription = f"Tenant: {tenant_name}" + report.resource_name = "Conditional Access Policy" + report.resource_id = "Conditional Access Policy" + report.status_extended = ( + "Conditional Access Policy does not require MFA for management API." + ) + + for policy_id, policy in conditional_access_policies.items(): + if ( + policy.state == "enabled" + and "All" in policy.users["include"] + and WINDOWS_AZURE_SERVICE_MANAGEMENT_API + in policy.target_resources["include"] + and any( + "mfa" in access_control.lower() + for access_control in policy.access_controls["grant"] + ) + ): + report.status = "PASS" + report.status_extended = ( + "Conditional Access Policy requires MFA for management API." + ) + report.resource_id = policy_id + report.resource_name = policy.name + break + + findings.append(report) + + return findings diff --git a/prowler/providers/azure/services/entra/entra_non_privileged_user_has_mfa/__init__.py b/prowler/providers/azure/services/entra/entra_non_privileged_user_has_mfa/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/prowler/providers/azure/services/entra/entra_non_privileged_user_has_mfa/entra_non_privileged_user_has_mfa.metadata.json b/prowler/providers/azure/services/entra/entra_non_privileged_user_has_mfa/entra_non_privileged_user_has_mfa.metadata.json new file mode 100644 index 0000000000..077d54654b --- /dev/null +++ b/prowler/providers/azure/services/entra/entra_non_privileged_user_has_mfa/entra_non_privileged_user_has_mfa.metadata.json @@ -0,0 +1,30 @@ +{ + "Provider": "azure", + "CheckID": "entra_non_privileged_user_has_mfa", + "CheckTitle": "Ensure that 'Multi-Factor Auth Status' is 'Enabled' for all Non-Privileged Users", + "CheckType": [], + "ServiceName": "entra", + "SubServiceName": "", + "ResourceIdTemplate": "", + "Severity": "high", + "ResourceType": "#microsoft.graph.users", + "Description": "Enable multi-factor authentication for all non-privileged users.", + "Risk": "Multi-factor authentication requires an individual to present a minimum of two separate forms of authentication before access is granted. Multi-factor authentication provides additional assurance that the individual attempting to gain access is who they claim to be. With multi-factor authentication, an attacker would need to compromise at least two different authentication mechanisms, increasing the difficulty of compromise and thus reducing the risk.", + "RelatedUrl": "https://learn.microsoft.com/en-us/entra/identity/authentication/concept-mfa-howitworks", + "Remediation": { + "Code": { + "CLI": "", + "NativeIaC": "", + "Other": "https://www.trendmicro.com/cloudoneconformity/knowledge-base/azure/ActiveDirectory/multi-factor-authentication-for-all-non-privileged-users.html#", + "Terraform": "" + }, + "Recommendation": { + "Text": "Activate one of the available multi-factor authentication methods for users in Microsoft Entra ID.", + "Url": "https://learn.microsoft.com/en-us/entra/identity/authentication/tutorial-enable-azure-mfa" + } + }, + "Categories": [], + "DependsOn": [], + "RelatedTo": [], + "Notes": "Users would require two forms of authentication before any access is granted. Also, this requires an overhead for managing dual forms of authentication." +} 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 new file mode 100644 index 0000000000..48c7a43c44 --- /dev/null +++ b/prowler/providers/azure/services/entra/entra_non_privileged_user_has_mfa/entra_non_privileged_user_has_mfa.py @@ -0,0 +1,32 @@ +from prowler.lib.check.models import Check, Check_Report_Azure +from prowler.providers.azure.services.entra.entra_client import entra_client +from prowler.providers.azure.services.entra.lib.user_privileges import ( + is_privileged_user, +) + + +class entra_non_privileged_user_has_mfa(Check): + def execute(self) -> Check_Report_Azure: + findings = [] + + for tenant_domain, users in entra_client.users.items(): + for user_domain_name, user in users.items(): + if not is_privileged_user( + user, entra_client.directory_roles[tenant_domain] + ): + report = Check_Report_Azure(self.metadata()) + report.status = "FAIL" + report.subscription = f"Tenant: {tenant_domain}" + report.resource_name = user_domain_name + report.resource_id = user.id + report.status_extended = ( + f"User '{user.name}' does not have MFA enabled." + ) + + if len(user.authentication_methods) > 1: + report.status = "PASS" + report.status_extended = f"User '{user.name}' has MFA enabled." + + findings.append(report) + + return findings diff --git a/prowler/providers/azure/services/entra/entra_privileged_user_has_mfa/__init__.py b/prowler/providers/azure/services/entra/entra_privileged_user_has_mfa/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/prowler/providers/azure/services/entra/entra_privileged_user_has_mfa/entra_privileged_user_has_mfa.metadata.json b/prowler/providers/azure/services/entra/entra_privileged_user_has_mfa/entra_privileged_user_has_mfa.metadata.json new file mode 100644 index 0000000000..f76b6b39dc --- /dev/null +++ b/prowler/providers/azure/services/entra/entra_privileged_user_has_mfa/entra_privileged_user_has_mfa.metadata.json @@ -0,0 +1,30 @@ +{ + "Provider": "azure", + "CheckID": "entra_privileged_user_has_mfa", + "CheckTitle": "Ensure that 'Multi-Factor Auth Status' is 'Enabled' for all Privileged Users", + "CheckType": [], + "ServiceName": "entra", + "SubServiceName": "", + "ResourceIdTemplate": "", + "Severity": "high", + "ResourceType": "#microsoft.graph.users", + "Description": "Enable multi-factor authentication for all roles, groups, and users that have write access or permissions to Azure resources. These include custom created objects or built-in roles such as; - Service Co-Administrators - Subscription Owners - Contributors", + "Risk": "Multi-factor authentication requires an individual to present a minimum of two separate forms of authentication before access is granted. Multi-factor authentication provides additional assurance that the individual attempting to gain access is who they claim to be. With multi-factor authentication, an attacker would need to compromise at least two different authentication mechanisms, increasing the difficulty of compromise and thus reducing the risk.", + "RelatedUrl": "https://learn.microsoft.com/en-us/entra/identity/authentication/concept-mfa-howitworks", + "Remediation": { + "Code": { + "CLI": "", + "NativeIaC": "", + "Other": "https://www.trendmicro.com/cloudoneconformity/knowledge-base/azure/ActiveDirectory/multi-factor-authentication-for-all-privileged-users.html#", + "Terraform": "" + }, + "Recommendation": { + "Text": "Activate one of the available multi-factor authentication methods for users in Microsoft Entra ID.", + "Url": "https://learn.microsoft.com/en-us/entra/identity/authentication/tutorial-enable-azure-mfa" + } + }, + "Categories": [], + "DependsOn": [], + "RelatedTo": [], + "Notes": "Users would require two forms of authentication before any access is granted. Additional administrative time will be required for managing dual forms of authentication when enabling multi-factor authentication." +} 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 new file mode 100644 index 0000000000..cd1a69cd41 --- /dev/null +++ b/prowler/providers/azure/services/entra/entra_privileged_user_has_mfa/entra_privileged_user_has_mfa.py @@ -0,0 +1,32 @@ +from prowler.lib.check.models import Check, Check_Report_Azure +from prowler.providers.azure.services.entra.entra_client import entra_client +from prowler.providers.azure.services.entra.lib.user_privileges import ( + is_privileged_user, +) + + +class entra_privileged_user_has_mfa(Check): + def execute(self) -> Check_Report_Azure: + findings = [] + + for tenant_domain, users in entra_client.users.items(): + for user_domain_name, user in users.items(): + if is_privileged_user( + user, entra_client.directory_roles[tenant_domain] + ): + report = Check_Report_Azure(self.metadata()) + report.status = "FAIL" + report.subscription = f"Tenant: {tenant_domain}" + report.resource_name = user_domain_name + report.resource_id = user.id + report.status_extended = ( + f"User '{user.name}' does not have MFA enabled." + ) + + if len(user.authentication_methods) > 1: + report.status = "PASS" + report.status_extended = f"User '{user.name}' has MFA enabled." + + findings.append(report) + + return findings diff --git a/prowler/providers/azure/services/entra/entra_service.py b/prowler/providers/azure/services/entra/entra_service.py index 8efc8841e0..a67654e995 100644 --- a/prowler/providers/azure/services/entra/entra_service.py +++ b/prowler/providers/azure/services/entra/entra_service.py @@ -1,6 +1,6 @@ import asyncio from dataclasses import dataclass -from typing import Optional +from typing import Any, List, Optional from uuid import UUID from msgraph import GraphServiceClient @@ -33,6 +33,9 @@ class Entra(AzureService): self.directory_roles = asyncio.get_event_loop().run_until_complete( self.__get_directory_roles__() ) + self.conditional_access_policy = asyncio.get_event_loop().run_until_complete( + self.__get_conditional_access_policy__() + ) async def __get_users__(self): users = {} @@ -44,7 +47,13 @@ class Entra(AzureService): users[tenant].update( { user.user_principal_name: User( - id=user.id, name=user.display_name + id=user.id, + name=user.display_name, + authentication_methods=( + await client.users.by_user_id( + user.id + ).authentication.methods.get() + ).value, ) } ) @@ -185,11 +194,11 @@ class Entra(AzureService): directory_role.display_name: DirectoryRole( id=directory_role.id, members=[ - User( - id=member.id, - name=member.display_name, - ) + self.users[tenant][member.user_principal_name] for member in directory_role_members.value + if self.users[tenant].get( + member.user_principal_name, None + ) ], ) } @@ -201,10 +210,88 @@ class Entra(AzureService): ) return directory_roles_with_members + async def __get_conditional_access_policy__(self): + conditional_access_policy = {} + try: + for tenant, client in self.clients.items(): + conditional_access_policies = ( + await client.identity.conditional_access.policies.get() + ) + conditional_access_policy.update({tenant: {}}) + for policy in getattr(conditional_access_policies, "value", []): + conditions = getattr(policy, "conditions", None) + + included_apps = [] + excluded_apps = [] + + if getattr(conditions, "applications", None): + if getattr(conditions.applications, "include_applications", []): + included_apps = conditions.applications.include_applications + elif getattr( + conditions.applications, "include_user_actions", [] + ): + included_apps = conditions.applications.include_user_actions + + if getattr(conditions.applications, "exclude_applications", []): + excluded_apps = conditions.applications.exclude_applications + elif getattr( + conditions.applications, "exclude_user_actions", [] + ): + excluded_apps = conditions.applications.exclude_user_actions + + grant_access_controls = [] + block_access_controls = [] + + for access_control in ( + getattr(policy.grant_controls, "built_in_controls") + if policy.grant_controls + else [] + ): + if "Grant" in str(access_control): + grant_access_controls.append(str(access_control)) + else: + block_access_controls.append(str(access_control)) + + conditional_access_policy[tenant].update( + { + policy.id: ConditionalAccessPolicy( + name=policy.display_name, + state=getattr(policy, "state", "None"), + users={ + "include": ( + getattr(conditions.users, "include_users", []) + if getattr(conditions, "users", None) + else [] + ), + "exclude": ( + getattr(conditions.users, "exclude_users", []) + if getattr(conditions, "users", None) + else [] + ), + }, + target_resources={ + "include": included_apps, + "exclude": excluded_apps, + }, + access_controls={ + "grant": grant_access_controls, + "block": block_access_controls, + }, + ) + } + ) + except Exception as error: + logger.error( + f"{error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}" + ) + + return conditional_access_policy + class User(BaseModel): id: str name: str + authentication_methods: List[Any] = [] @dataclass @@ -221,7 +308,7 @@ class AuthorizationPolicy: class GroupSetting: name: Optional[str] template_id: Optional[str] - settings: list[SettingValue] + settings: List[SettingValue] class SecurityDefault(BaseModel): @@ -232,10 +319,18 @@ class SecurityDefault(BaseModel): class NamedLocation(BaseModel): name: str - ip_ranges_addresses: list[str] + ip_ranges_addresses: List[str] is_trusted: bool class DirectoryRole(BaseModel): id: str - members: list[User] + members: List[User] + + +class ConditionalAccessPolicy(BaseModel): + name: str + state: str + users: dict[str, List[str]] + target_resources: dict[str, List[str]] + access_controls: dict[str, List[str]] diff --git a/prowler/providers/azure/services/entra/lib/__init__.py b/prowler/providers/azure/services/entra/lib/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/prowler/providers/azure/services/entra/lib/user_privileges.py b/prowler/providers/azure/services/entra/lib/user_privileges.py new file mode 100644 index 0000000000..47aa11a2c9 --- /dev/null +++ b/prowler/providers/azure/services/entra/lib/user_privileges.py @@ -0,0 +1,25 @@ +""" +This module contains functions with user privileges in Azure. +""" + + +def is_privileged_user(user, privileged_roles) -> bool: + """ + Checks if a user is a privileged user. + + Args: + user: An object representing the user to be checked. + privileged_roles: A dictionary containing privileged roles. + + Returns: + A boolean value indicating whether the user is a privileged user. + """ + + is_privileged = False + + for role in privileged_roles.values(): + if user in role.members: + is_privileged = True + break + + return is_privileged diff --git a/tests/providers/azure/services/entra/entra_conditional_access_policy_require_mfa_for_management_api/entra_conditional_access_policy_require_mfa_for_management_api_test.py b/tests/providers/azure/services/entra/entra_conditional_access_policy_require_mfa_for_management_api/entra_conditional_access_policy_require_mfa_for_management_api_test.py new file mode 100644 index 0000000000..318d67387f --- /dev/null +++ b/tests/providers/azure/services/entra/entra_conditional_access_policy_require_mfa_for_management_api/entra_conditional_access_policy_require_mfa_for_management_api_test.py @@ -0,0 +1,233 @@ +from unittest import mock +from uuid import uuid4 + +from tests.providers.azure.azure_fixtures import DOMAIN + + +class Test_entra_conditional_access_policy_require_mfa_for_management_api: + def test_entra_no_subscriptions(self): + entra_client = mock.MagicMock + + with mock.patch( + "prowler.providers.azure.services.entra.entra_conditional_access_policy_require_mfa_for_management_api.entra_conditional_access_policy_require_mfa_for_management_api.entra_client", + new=entra_client, + ): + from prowler.providers.azure.services.entra.entra_conditional_access_policy_require_mfa_for_management_api.entra_conditional_access_policy_require_mfa_for_management_api import ( + entra_conditional_access_policy_require_mfa_for_management_api, + ) + + entra_client.conditional_access_policy = {} + + check = entra_conditional_access_policy_require_mfa_for_management_api() + result = check.execute() + assert len(result) == 0 + + def test_entra_tenant_no_policies(self): + entra_client = mock.MagicMock + + with mock.patch( + "prowler.providers.azure.services.entra.entra_conditional_access_policy_require_mfa_for_management_api.entra_conditional_access_policy_require_mfa_for_management_api.entra_client", + new=entra_client, + ): + from prowler.providers.azure.services.entra.entra_conditional_access_policy_require_mfa_for_management_api.entra_conditional_access_policy_require_mfa_for_management_api import ( + entra_conditional_access_policy_require_mfa_for_management_api, + ) + + entra_client.conditional_access_policy = {DOMAIN: {}} + + check = entra_conditional_access_policy_require_mfa_for_management_api() + result = check.execute() + assert len(result) == 1 + assert result[0].status == "FAIL" + assert result[0].subscription == f"Tenant: {DOMAIN}" + assert result[0].resource_name == "Conditional Access Policy" + assert result[0].resource_id == "Conditional Access Policy" + assert ( + result[0].status_extended + == "Conditional Access Policy does not require MFA for management API." + ) + + def test_entra_tenant_policy_no_mfa(self): + entra_client = mock.MagicMock + policy_id = str(uuid4()) + + with mock.patch( + "prowler.providers.azure.services.entra.entra_conditional_access_policy_require_mfa_for_management_api.entra_conditional_access_policy_require_mfa_for_management_api.entra_client", + new=entra_client, + ): + from prowler.providers.azure.services.entra.entra_conditional_access_policy_require_mfa_for_management_api.entra_conditional_access_policy_require_mfa_for_management_api import ( + entra_conditional_access_policy_require_mfa_for_management_api, + ) + from prowler.providers.azure.services.entra.entra_service import ( + ConditionalAccessPolicy, + ) + + policy = ConditionalAccessPolicy( + name="Test Policy", + state="enabled", + users={"include": ["All"]}, + target_resources={"include": ["797f4846-ba00-4fd7-ba43-dac1f8f63013"]}, + access_controls={"grant": ["grant"]}, + ) + + entra_client.conditional_access_policy = {DOMAIN: {policy_id: policy}} + + check = entra_conditional_access_policy_require_mfa_for_management_api() + result = check.execute() + assert len(result) == 1 + assert result[0].status == "FAIL" + assert result[0].subscription == f"Tenant: {DOMAIN}" + assert result[0].resource_name == "Conditional Access Policy" + assert result[0].resource_id == "Conditional Access Policy" + assert ( + result[0].status_extended + == "Conditional Access Policy does not require MFA for management API." + ) + + def test_entra_tenant_policy_mfa(self): + entra_client = mock.MagicMock + policy_id = str(uuid4()) + + with mock.patch( + "prowler.providers.azure.services.entra.entra_conditional_access_policy_require_mfa_for_management_api.entra_conditional_access_policy_require_mfa_for_management_api.entra_client", + new=entra_client, + ): + from prowler.providers.azure.services.entra.entra_conditional_access_policy_require_mfa_for_management_api.entra_conditional_access_policy_require_mfa_for_management_api import ( + entra_conditional_access_policy_require_mfa_for_management_api, + ) + from prowler.providers.azure.services.entra.entra_service import ( + ConditionalAccessPolicy, + ) + + policy = ConditionalAccessPolicy( + name="Test Policy", + state="enabled", + users={"include": ["All"]}, + target_resources={"include": ["797f4846-ba00-4fd7-ba43-dac1f8f63013"]}, + access_controls={"grant": ["grant", "MFA"]}, + ) + + entra_client.conditional_access_policy = {DOMAIN: {policy_id: policy}} + + check = entra_conditional_access_policy_require_mfa_for_management_api() + result = check.execute() + assert len(result) == 1 + assert result[0].status == "PASS" + assert result[0].subscription == f"Tenant: {DOMAIN}" + assert result[0].resource_name == "Test Policy" + assert result[0].resource_id == policy_id + assert ( + result[0].status_extended + == "Conditional Access Policy requires MFA for management API." + ) + + def test_entra_tenant_policy_mfa_disabled(self): + entra_client = mock.MagicMock + policy_id = str(uuid4()) + + with mock.patch( + "prowler.providers.azure.services.entra.entra_conditional_access_policy_require_mfa_for_management_api.entra_conditional_access_policy_require_mfa_for_management_api.entra_client", + new=entra_client, + ): + from prowler.providers.azure.services.entra.entra_conditional_access_policy_require_mfa_for_management_api.entra_conditional_access_policy_require_mfa_for_management_api import ( + entra_conditional_access_policy_require_mfa_for_management_api, + ) + from prowler.providers.azure.services.entra.entra_service import ( + ConditionalAccessPolicy, + ) + + policy = ConditionalAccessPolicy( + name="Test Policy", + state="disabled", + users={"include": ["All"]}, + target_resources={"include": ["797f4846-ba00-4fd7-ba43-dac1f8f63013"]}, + access_controls={"grant": ["grant", "MFA"]}, + ) + + entra_client.conditional_access_policy = {DOMAIN: {policy_id: policy}} + + check = entra_conditional_access_policy_require_mfa_for_management_api() + result = check.execute() + assert len(result) == 1 + assert result[0].status == "FAIL" + assert result[0].subscription == f"Tenant: {DOMAIN}" + assert result[0].resource_name == "Conditional Access Policy" + assert result[0].resource_id == "Conditional Access Policy" + assert ( + result[0].status_extended + == "Conditional Access Policy does not require MFA for management API." + ) + + def test_entra_tenant_policy_mfa_no_target(self): + entra_client = mock.MagicMock + policy_id = str(uuid4()) + + with mock.patch( + "prowler.providers.azure.services.entra.entra_conditional_access_policy_require_mfa_for_management_api.entra_conditional_access_policy_require_mfa_for_management_api.entra_client", + new=entra_client, + ): + from prowler.providers.azure.services.entra.entra_conditional_access_policy_require_mfa_for_management_api.entra_conditional_access_policy_require_mfa_for_management_api import ( + entra_conditional_access_policy_require_mfa_for_management_api, + ) + from prowler.providers.azure.services.entra.entra_service import ( + ConditionalAccessPolicy, + ) + + policy = ConditionalAccessPolicy( + name="Test Policy", + state="enabled", + users={"include": ["All"]}, + target_resources={"include": []}, + access_controls={"grant": ["grant", "MFA"]}, + ) + + entra_client.conditional_access_policy = {DOMAIN: {policy_id: policy}} + + check = entra_conditional_access_policy_require_mfa_for_management_api() + result = check.execute() + assert len(result) == 1 + assert result[0].status == "FAIL" + assert result[0].subscription == f"Tenant: {DOMAIN}" + assert result[0].resource_name == "Conditional Access Policy" + assert result[0].resource_id == "Conditional Access Policy" + assert ( + result[0].status_extended + == "Conditional Access Policy does not require MFA for management API." + ) + + def test_entra_tenant_policy_mfa_no_users(self): + entra_client = mock.MagicMock + policy_id = str(uuid4()) + + with mock.patch( + "prowler.providers.azure.services.entra.entra_conditional_access_policy_require_mfa_for_management_api.entra_conditional_access_policy_require_mfa_for_management_api.entra_client", + new=entra_client, + ): + from prowler.providers.azure.services.entra.entra_conditional_access_policy_require_mfa_for_management_api.entra_conditional_access_policy_require_mfa_for_management_api import ( + entra_conditional_access_policy_require_mfa_for_management_api, + ) + from prowler.providers.azure.services.entra.entra_service import ( + ConditionalAccessPolicy, + ) + + policy = ConditionalAccessPolicy( + name="Test Policy", + state="enabled", + users={"include": []}, + target_resources={"include": ["797f4846-ba00-4fd7-ba43-dac1f8f63013"]}, + access_controls={"grant": ["grant", "MFA"]}, + ) + + entra_client.conditional_access_policy = {DOMAIN: {policy_id: policy}} + + check = entra_conditional_access_policy_require_mfa_for_management_api() + result = check.execute() + assert len(result) == 1 + assert result[0].status == "FAIL" + assert result[0].subscription == f"Tenant: {DOMAIN}" + assert result[0].resource_name == "Conditional Access Policy" + assert result[0].resource_id == "Conditional Access Policy" + assert ( + result[0].status_extended + == "Conditional Access Policy does not require MFA for management API." + ) 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 new file mode 100644 index 0000000000..db1014b1a1 --- /dev/null +++ b/tests/providers/azure/services/entra/entra_non_privileged_user_has_mfa/entra_non_privileged_user_has_mfa_test.py @@ -0,0 +1,170 @@ +from unittest import mock +from uuid import uuid4 + +from tests.providers.azure.azure_fixtures import DOMAIN + + +class Test_entra_non_privileged_user_has_mfa: + def test_entra_no_tenants(self): + entra_client = mock.MagicMock + + with mock.patch( + "prowler.providers.azure.services.entra.entra_non_privileged_user_has_mfa.entra_non_privileged_user_has_mfa.entra_client", + new=entra_client, + ): + from prowler.providers.azure.services.entra.entra_non_privileged_user_has_mfa.entra_non_privileged_user_has_mfa import ( + entra_non_privileged_user_has_mfa, + ) + + entra_client.users = {} + + check = entra_non_privileged_user_has_mfa() + result = check.execute() + assert len(result) == 0 + + def test_entra_tenant_no_users(self): + entra_client = mock.MagicMock + + with mock.patch( + "prowler.providers.azure.services.entra.entra_non_privileged_user_has_mfa.entra_non_privileged_user_has_mfa.entra_client", + new=entra_client, + ): + from prowler.providers.azure.services.entra.entra_non_privileged_user_has_mfa.entra_non_privileged_user_has_mfa import ( + entra_non_privileged_user_has_mfa, + ) + + entra_client.users = {DOMAIN: {}} + + check = entra_non_privileged_user_has_mfa() + result = check.execute() + assert len(result) == 0 + + def test_entra_user_no_privileged_no_mfa(self): + entra_client = mock.MagicMock + user_id = str(uuid4()) + + with mock.patch( + "prowler.providers.azure.services.entra.entra_non_privileged_user_has_mfa.entra_non_privileged_user_has_mfa.entra_client", + new=entra_client, + ): + from prowler.providers.azure.services.entra.entra_non_privileged_user_has_mfa.entra_non_privileged_user_has_mfa import ( + entra_non_privileged_user_has_mfa, + ) + from prowler.providers.azure.services.entra.entra_service import ( + DirectoryRole, + User, + ) + + user = User(id=user_id, name="foo", authentication_methods=["foo"]) + + entra_client.users = {DOMAIN: {f"foo@{DOMAIN}": user}} + entra_client.directory_roles = { + DOMAIN: { + "Global Administrator": DirectoryRole(id=str(uuid4()), members=[]) + } + } + + check = 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].resource_name == f"foo@{DOMAIN}" + assert result[0].resource_id == user_id + assert result[0].subscription == f"Tenant: {DOMAIN}" + + def test_entra_user_no_privileged_mfa(self): + entra_client = mock.MagicMock + user_id = str(uuid4()) + + with mock.patch( + "prowler.providers.azure.services.entra.entra_non_privileged_user_has_mfa.entra_non_privileged_user_has_mfa.entra_client", + new=entra_client, + ): + from prowler.providers.azure.services.entra.entra_non_privileged_user_has_mfa.entra_non_privileged_user_has_mfa import ( + entra_non_privileged_user_has_mfa, + ) + from prowler.providers.azure.services.entra.entra_service import ( + DirectoryRole, + User, + ) + + user = User(id=user_id, name="foo", authentication_methods=["foo", "bar"]) + + entra_client.users = {DOMAIN: {f"foo@{DOMAIN}": user}} + entra_client.directory_roles = { + DOMAIN: { + "Global Administrator": DirectoryRole(id=str(uuid4()), members=[]) + } + } + + check = 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].resource_name == f"foo@{DOMAIN}" + assert result[0].resource_id == user_id + assert result[0].subscription == f"Tenant: {DOMAIN}" + + def test_entra_user_privileged_no_mfa(self): + entra_client = mock.MagicMock + user_id = str(uuid4()) + + with mock.patch( + "prowler.providers.azure.services.entra.entra_non_privileged_user_has_mfa.entra_non_privileged_user_has_mfa.entra_client", + new=entra_client, + ): + from prowler.providers.azure.services.entra.entra_non_privileged_user_has_mfa.entra_non_privileged_user_has_mfa import ( + entra_non_privileged_user_has_mfa, + ) + from prowler.providers.azure.services.entra.entra_service import ( + DirectoryRole, + User, + ) + + user = User(id=user_id, name="foo", authentication_methods=["foo"]) + + entra_client.users = {DOMAIN: {f"foo@{DOMAIN}": user}} + entra_client.directory_roles = { + DOMAIN: { + "Global Administrator": DirectoryRole( + id=str(uuid4()), members=[user] + ) + } + } + + check = entra_non_privileged_user_has_mfa() + result = check.execute() + assert len(result) == 0 + + def test_entra_user_privileged_mfa(self): + entra_client = mock.MagicMock + user_id = str(uuid4()) + + with mock.patch( + "prowler.providers.azure.services.entra.entra_non_privileged_user_has_mfa.entra_non_privileged_user_has_mfa.entra_client", + new=entra_client, + ): + from prowler.providers.azure.services.entra.entra_non_privileged_user_has_mfa.entra_non_privileged_user_has_mfa import ( + entra_non_privileged_user_has_mfa, + ) + from prowler.providers.azure.services.entra.entra_service import ( + DirectoryRole, + User, + ) + + user = User(id=user_id, name="foo", authentication_methods=["foo", "bar"]) + + entra_client.users = {DOMAIN: {f"foo@{DOMAIN}": user}} + entra_client.directory_roles = { + DOMAIN: { + "Global Administrator": DirectoryRole( + id=str(uuid4()), members=[user] + ) + } + } + + check = entra_non_privileged_user_has_mfa() + result = check.execute() + assert len(result) == 0 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 new file mode 100644 index 0000000000..0273449036 --- /dev/null +++ b/tests/providers/azure/services/entra/entra_privileged_user_has_mfa/entra_privileged_user_has_mfa_test.py @@ -0,0 +1,170 @@ +from unittest import mock +from uuid import uuid4 + +from tests.providers.azure.azure_fixtures import DOMAIN + + +class Test_entra_privileged_user_has_mfa: + def test_entra_no_tenants(self): + entra_client = mock.MagicMock + + with mock.patch( + "prowler.providers.azure.services.entra.entra_privileged_user_has_mfa.entra_privileged_user_has_mfa.entra_client", + new=entra_client, + ): + from prowler.providers.azure.services.entra.entra_privileged_user_has_mfa.entra_privileged_user_has_mfa import ( + entra_privileged_user_has_mfa, + ) + + entra_client.users = {} + + check = entra_privileged_user_has_mfa() + result = check.execute() + assert len(result) == 0 + + def test_entra_tenant_no_users(self): + entra_client = mock.MagicMock + + with mock.patch( + "prowler.providers.azure.services.entra.entra_privileged_user_has_mfa.entra_privileged_user_has_mfa.entra_client", + new=entra_client, + ): + from prowler.providers.azure.services.entra.entra_privileged_user_has_mfa.entra_privileged_user_has_mfa import ( + entra_privileged_user_has_mfa, + ) + + entra_client.users = {DOMAIN: {}} + + check = entra_privileged_user_has_mfa() + result = check.execute() + assert len(result) == 0 + + def test_entra_user_no_privileged_no_mfa(self): + entra_client = mock.MagicMock + user_id = str(uuid4()) + + with mock.patch( + "prowler.providers.azure.services.entra.entra_privileged_user_has_mfa.entra_privileged_user_has_mfa.entra_client", + new=entra_client, + ): + from prowler.providers.azure.services.entra.entra_privileged_user_has_mfa.entra_privileged_user_has_mfa import ( + entra_privileged_user_has_mfa, + ) + from prowler.providers.azure.services.entra.entra_service import ( + DirectoryRole, + User, + ) + + user = User(id=user_id, name="foo", authentication_methods=["foo"]) + + entra_client.users = {DOMAIN: {f"foo@{DOMAIN}": user}} + entra_client.directory_roles = { + DOMAIN: { + "Global Administrator": DirectoryRole(id=str(uuid4()), members=[]) + } + } + + check = entra_privileged_user_has_mfa() + result = check.execute() + assert len(result) == 0 + + def test_entra_user_no_privileged_mfa(self): + entra_client = mock.MagicMock + user_id = str(uuid4()) + + with mock.patch( + "prowler.providers.azure.services.entra.entra_privileged_user_has_mfa.entra_privileged_user_has_mfa.entra_client", + new=entra_client, + ): + from prowler.providers.azure.services.entra.entra_privileged_user_has_mfa.entra_privileged_user_has_mfa import ( + entra_privileged_user_has_mfa, + ) + from prowler.providers.azure.services.entra.entra_service import ( + DirectoryRole, + User, + ) + + user = User(id=user_id, name="foo", authentication_methods=["foo", "bar"]) + + entra_client.users = {DOMAIN: {f"foo@{DOMAIN}": user}} + entra_client.directory_roles = { + DOMAIN: { + "Global Administrator": DirectoryRole(id=str(uuid4()), members=[]) + } + } + + check = entra_privileged_user_has_mfa() + result = check.execute() + assert len(result) == 0 + + def test_entra_user_privileged_no_mfa(self): + entra_client = mock.MagicMock + user_id = str(uuid4()) + + with mock.patch( + "prowler.providers.azure.services.entra.entra_privileged_user_has_mfa.entra_privileged_user_has_mfa.entra_client", + new=entra_client, + ): + from prowler.providers.azure.services.entra.entra_privileged_user_has_mfa.entra_privileged_user_has_mfa import ( + entra_privileged_user_has_mfa, + ) + from prowler.providers.azure.services.entra.entra_service import ( + DirectoryRole, + User, + ) + + user = User(id=user_id, name="foo", authentication_methods=["foo"]) + + entra_client.users = {DOMAIN: {f"foo@{DOMAIN}": user}} + entra_client.directory_roles = { + DOMAIN: { + "Global Administrator": DirectoryRole( + id=str(uuid4()), members=[user] + ) + } + } + + check = 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].resource_name == f"foo@{DOMAIN}" + assert result[0].resource_id == user_id + assert result[0].subscription == f"Tenant: {DOMAIN}" + + def test_entra_user_privileged_mfa(self): + entra_client = mock.MagicMock + user_id = str(uuid4()) + + with mock.patch( + "prowler.providers.azure.services.entra.entra_privileged_user_has_mfa.entra_privileged_user_has_mfa.entra_client", + new=entra_client, + ): + from prowler.providers.azure.services.entra.entra_privileged_user_has_mfa.entra_privileged_user_has_mfa import ( + entra_privileged_user_has_mfa, + ) + from prowler.providers.azure.services.entra.entra_service import ( + DirectoryRole, + User, + ) + + user = User(id=user_id, name="foo", authentication_methods=["foo", "bar"]) + + entra_client.users = {DOMAIN: {f"foo@{DOMAIN}": user}} + entra_client.directory_roles = { + DOMAIN: { + "Global Administrator": DirectoryRole( + id=str(uuid4()), members=[user] + ) + } + } + + check = 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].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_service_test.py b/tests/providers/azure/services/entra/entra_service_test.py index 890b066a27..7871213345 100644 --- a/tests/providers/azure/services/entra/entra_service_test.py +++ b/tests/providers/azure/services/entra/entra_service_test.py @@ -2,6 +2,7 @@ from unittest.mock import patch from prowler.providers.azure.services.entra.entra_service import ( AuthorizationPolicy, + ConditionalAccessPolicy, DirectoryRole, Entra, GroupSetting, @@ -78,6 +79,24 @@ async def mock_entra_get_directory_roles(_): } +async def mock_entra_get_conditional_access_policy(_): + return { + DOMAIN: { + "id-1": ConditionalAccessPolicy( + id="id-1", + state="enabled", + name="Test", + users={"include": ["All"], "exclude": []}, + target_resources={ + "include": ["797f4846-ba00-4fd7-ba43-dac1f8f63013"], + "exclude": [], + }, + access_controls={"grant": ["MFA"], "block": []}, + ) + } + } + + @patch( "prowler.providers.azure.services.entra.entra_service.Entra.__get_users__", new=mock_entra_get_users, @@ -102,6 +121,10 @@ async def mock_entra_get_directory_roles(_): "prowler.providers.azure.services.entra.entra_service.Entra.__get_directory_roles__", new=mock_entra_get_directory_roles, ) +@patch( + "prowler.providers.azure.services.entra.entra_service.Entra.__get_conditional_access_policy__", + new=mock_entra_get_conditional_access_policy, +) class Test_Entra_Service: def test__get_client__(self): entra_client = Entra(set_mocked_azure_audit_info()) @@ -116,6 +139,10 @@ class Test_Entra_Service: assert len(entra_client.users) == 1 assert entra_client.users[DOMAIN]["user-1@tenant1.es"].id == "id-1" assert entra_client.users[DOMAIN]["user-1@tenant1.es"].name == "User 1" + assert ( + len(entra_client.users[DOMAIN]["user-1@tenant1.es"].authentication_methods) + == 0 + ) def test__get_authorization_policy__(self): entra_client = Entra(set_mocked_azure_audit_info()) @@ -159,3 +186,36 @@ class Test_Entra_Service: len(entra_client.directory_roles[DOMAIN]["GlobalAdministrator"].members) == 0 ) + + def test__get_conditional_access_policy__(self): + entra_client = Entra(set_mocked_azure_audit_info()) + assert len(entra_client.conditional_access_policy) == 1 + assert len(entra_client.conditional_access_policy[DOMAIN]) == 1 + assert entra_client.conditional_access_policy[DOMAIN]["id-1"] + assert entra_client.conditional_access_policy[DOMAIN]["id-1"].name == "Test" + assert entra_client.conditional_access_policy[DOMAIN]["id-1"].state == "enabled" + assert entra_client.conditional_access_policy[DOMAIN]["id-1"].users[ + "include" + ] == ["All"] + assert ( + entra_client.conditional_access_policy[DOMAIN]["id-1"].users["exclude"] + == [] + ) + assert entra_client.conditional_access_policy[DOMAIN]["id-1"].target_resources[ + "include" + ] == ["797f4846-ba00-4fd7-ba43-dac1f8f63013"] + assert ( + entra_client.conditional_access_policy[DOMAIN]["id-1"].target_resources[ + "exclude" + ] + == [] + ) + assert entra_client.conditional_access_policy[DOMAIN]["id-1"].access_controls[ + "grant" + ] == ["MFA"] + assert ( + entra_client.conditional_access_policy[DOMAIN]["id-1"].access_controls[ + "block" + ] + == [] + ) diff --git a/tests/providers/azure/services/entra/lib/user_privileges_test.py b/tests/providers/azure/services/entra/lib/user_privileges_test.py new file mode 100644 index 0000000000..bf2a75e44c --- /dev/null +++ b/tests/providers/azure/services/entra/lib/user_privileges_test.py @@ -0,0 +1,25 @@ +from unittest import mock +from uuid import uuid4 + +from prowler.providers.azure.services.entra.entra_service import User +from prowler.providers.azure.services.entra.lib.user_privileges import ( + is_privileged_user, +) + + +class Test_user_privileges_test: + def test_user_in_privileged_roles(self): + user_id = str(uuid4()) + privileged_roles = {"admin": mock.MagicMock()} + privileged_roles["admin"].members = [User(id=user_id, name="user1")] + + user = User(id=user_id, name="user1") + assert is_privileged_user(user, privileged_roles) + + def test_user_not_in_privileged_roles(self): + user_id = str(uuid4()) + privileged_roles = {"admin": mock.MagicMock()} + privileged_roles["admin"].members = [User(id=str(uuid4()), name="user2")] + + user = User(id=user_id, name="user1") + assert not is_privileged_user(user, privileged_roles)